/*
* Copyright (C) 2012 Google Inc. All rights reserved.
*
* 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.
*/
/*
* URL:https://github.com/google/googletv-android-samples
* クラス:com.example.google.tv.anymotelibrary.connection.KeyStoreManager
* 変更者:NTT DOCOMO, INC.
*/
package org.deviceconnect.server.nanohttpd.util;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyManagementException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.logging.Logger;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import org.deviceconnect.server.nanohttpd.BuildConfig;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.provider.Settings;
import android.util.Log;
import com.google.fix.PRNGFixes;
import com.google.polo.ssl.SslUtil;
/**
* キーストア・マネージャー. クライアントとサーバの証明書を管理する.
*/
public final class KeyStoreManager {
/** タグ. */
private static final String LOG_TAG = "KeyStoreManager";
/**
* キーストアのファイル名.
*/
private static final String KEYSTORE_FILENAME = "keystore.bks";
/**
* キーストアのパスワード.
*
* TODO パスワード文字列について要検証.
*/
private static final char[] KEYSTORE_PASSWORD = "sjdlf'%Rli\"SHglk29ugsld??AfjL+D-".toCharArray();
/**
* Alias for the remote controller (local) identity in the {@link KeyStore}.
*/
private static final String LOCAL_IDENTITY_ALIAS = "Device Connect-remote";
/**
* Alias pattern for Device Connect server identities in the {@link KeyStore}.
*/
private static final String REMOTE_IDENTITY_ALIAS_PATTERN = "Device Connect-server-%X";
/** Context. */
private Context mContext;
/** Key Managers. */
private KeyManager[] mKeyManagers;
/** Trust Managers. */
private TrustManager[] mTrustManagers;
/** Key Store. */
private KeyStore mKeyStore;
/** ロガー. */
private final Logger mLogger = Logger.getLogger("dconnect.manager");
/**
* Verify if local certificate is available.
*
* @return true, if certificate is available.
*/
private boolean hasLocalIdentityAlias() {
try {
if (!mKeyStore.containsAlias(LOCAL_IDENTITY_ALIAS)) {
if (BuildConfig.DEBUG) {
Log.e(LOG_TAG, "Key store missing identity for " + LOCAL_IDENTITY_ALIAS);
}
return false;
}
} catch (KeyStoreException e) {
if (BuildConfig.DEBUG) {
Log.e(LOG_TAG, "Key store exception occurred", e);
}
return false;
}
return true;
}
/**
* Loads or otherwise creates keys for application. This call may take
* substantial amount of time to complete.
*
* @param context Context of the Application
* @param fromFile Generate the key store from the file
* @throws GeneralSecurityException General Security Exception
*/
public void initialize(final Context context, final boolean fromFile) throws GeneralSecurityException {
// Java Cryptography Architectureの乱数種に関するセキュリティ問題への対処.
PRNGFixes.apply();
mContext = context;
if (fromFile) {
loadKeyStore();
if (hasLocalIdentityAlias()) {
generateAppCertificate();
}
} else {
createKeyStore();
generateAppCertificate();
}
collectKeyManagers();
collectTrustManagers();
}
/**
* Create application-specific certificate that will be used to authenticate
* user.
*/
@SuppressLint("TrulyRandom")
private void generateAppCertificate() {
clearKeyStore();
try {
if (BuildConfig.DEBUG) {
Log.v(LOG_TAG, "Generating key pair ...");
}
KeyPairGenerator kg = KeyPairGenerator.getInstance("RSA");
KeyPair keyPair = kg.generateKeyPair();
if (BuildConfig.DEBUG) {
Log.v(LOG_TAG, "Generating certificate ...");
}
String commonName = getCertificateName(getUniqueId());
X509Certificate cert = SslUtil.generateX509V3Certificate(keyPair, commonName);
Certificate[] chain = {cert};
if (BuildConfig.DEBUG) {
Log.v(LOG_TAG, "Adding key to keystore ...");
}
mKeyStore.setKeyEntry(LOCAL_IDENTITY_ALIAS, keyPair.getPrivate(), null, chain);
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "Key added!");
}
} catch (GeneralSecurityException e) {
throw new IllegalStateException("Unable to create identity KeyStore", e);
}
}
/**
* KeyStoreをファイルから読み取る. 失敗した場合は生成する.
*/
public void loadKeyStore() {
KeyStore keyStore;
try {
keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
} catch (KeyStoreException e) {
throw new IllegalStateException("Unable to get default instance of KeyStore", e);
}
try {
FileInputStream fis = mContext.openFileInput(KEYSTORE_FILENAME);
keyStore.load(fis, KEYSTORE_PASSWORD);
} catch (IOException e) {
if (BuildConfig.DEBUG) {
Log.v(LOG_TAG, "Unable open keystore file", e);
}
keyStore = null;
} catch (GeneralSecurityException e) {
if (BuildConfig.DEBUG) {
Log.v(LOG_TAG, "Unable open keystore file", e);
}
keyStore = null;
}
/*
* No keys found: generate.
*/
if (keyStore == null) {
try {
createKeyStore();
return;
} catch (GeneralSecurityException e) {
throw new IllegalStateException("Unable to create identity KeyStore", e);
}
}
mKeyStore = keyStore;
}
/**
* 新たにKeyStoreを生成する.
* @throws GeneralSecurityException General Security Exception
*/
public void createKeyStore() throws GeneralSecurityException {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
try {
keyStore.load(null, KEYSTORE_PASSWORD);
} catch (IOException e) {
throw new GeneralSecurityException("Unable to create empty keyStore", e);
}
mKeyStore = keyStore;
}
/**
* キーストアをファイルに出力する.
*/
public synchronized void storeKeyStore() {
try {
FileOutputStream fos = mContext.openFileOutput(KEYSTORE_FILENAME, Context.MODE_PRIVATE);
mKeyStore.store(fos, KEYSTORE_PASSWORD);
fos.close();
} catch (IOException e) {
throw new IllegalStateException("Unable to store keyStore", e);
} catch (GeneralSecurityException e) {
throw new IllegalStateException("Unable to store keyStore", e);
}
}
/**
* Returns the name that should be used in a new certificate.
* <p>
* The format is:
* "CN=Device Connect-server/PRODUCT/DEVICE/MODEL/unique identifier"
* @param id ID
* @return Certificate Name
*/
private static String getCertificateName(final String id) {
return "CN=Device Connect-server/" + Build.PRODUCT + "/" + Build.DEVICE + "/" + Build.MODEL + "/" + id;
}
/**
* @return key managers loaded for this service.
*/
public KeyManager[] getKeyManagers() {
return mKeyManagers;
}
/**
* Collect Key Manager.
*
* @throws GeneralSecurityException General Security Exception
*/
private synchronized void collectKeyManagers() throws GeneralSecurityException {
if (mKeyStore == null) {
throw new NullPointerException("null mKeyStore");
}
KeyManagerFactory factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
factory.init(mKeyStore, "".toCharArray());
mKeyManagers = factory.getKeyManagers();
}
/**
* @return trust managers loaded for this service.
*/
public TrustManager[] getTrustManagers() {
return mTrustManagers;
}
/**
* Collect Trust Managers.
* @throws GeneralSecurityException General Security Exception
*/
private synchronized void collectTrustManagers() throws GeneralSecurityException {
// Build a new set of TrustManagers based on the KeyStore.
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(mKeyStore);
mTrustManagers = tmf.getTrustManagers();
}
/**
* Stores the remote device certificate in keystore.
*
* @param peerCert Peer Certificate
*/
synchronized void storeCertificate(final Certificate peerCert) {
try {
String alias = String.format(KeyStoreManager.REMOTE_IDENTITY_ALIAS_PATTERN, peerCert.hashCode());
if (mKeyStore.containsAlias(alias)) {
if (BuildConfig.DEBUG) {
Log.w(LOG_TAG, "Deleting existing entry for " + alias);
}
mKeyStore.deleteEntry(alias);
}
if (BuildConfig.DEBUG) {
Log.i(LOG_TAG, "Adding cert to keystore: " + alias);
}
mKeyStore.setCertificateEntry(alias, peerCert);
try {
collectTrustManagers();
} catch (GeneralSecurityException e) {
if (BuildConfig.DEBUG) {
e.printStackTrace();
}
}
} catch (KeyStoreException e) {
if (BuildConfig.DEBUG) {
Log.e(LOG_TAG, "Storing cert failed", e);
}
}
}
/**
* Clear Key Store.
*/
private void clearKeyStore() {
try {
for (Enumeration<String> e = mKeyStore.aliases(); e.hasMoreElements();) {
final String alias = e.nextElement();
if (BuildConfig.DEBUG) {
Log.v(LOG_TAG, "Deleting alias: " + alias);
}
mKeyStore.deleteEntry(alias);
}
} catch (KeyStoreException e) {
if (BuildConfig.DEBUG) {
Log.e(LOG_TAG, "Clearing certificates failed", e);
}
}
}
/**
* Get Unique ID.
* @return Unique ID
*/
private String getUniqueId() {
String id = Settings.Secure.getString(mContext.getContentResolver(), Settings.Secure.ANDROID_ID);
// null ANDROID_ID is possible on emulator
return id != null ? id : "emulator";
}
/**
* 証明書を読み込みFactoryクラスを生成する.
*
* @return 読み込み成功時はSSLServerSocketFactoryを、その他はnullを返す.
*/
public SSLServerSocketFactory getServerSocketFactory() {
SSLServerSocketFactory retval = null;
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(mKeyManagers, mTrustManagers, new SecureRandom());
retval = sslContext.getServerSocketFactory();
} catch (NoSuchAlgorithmException e) {
mLogger.warning("NoSuchAlgorithmException in the"
+ " DConnectServerNanoHttpd#createServerSocketFactory() method. "
+ e.toString());
} catch (KeyManagementException e) {
mLogger.warning("KeyManagementException in the DConnectServerNanoHttpd#createServerSocketFactory() method. "
+ e.toString());
}
return retval;
}
}