Merge branch 'android-cert-import'

Adds support to import CA and server certificate directly in the app.
On Android 4.4 and newer the SAF allows users to easily browse for such
files, on older systems they have to open them from file manager or the
download app (only works if the MIME type is correctly detected).

Also adds support for ECDSA keys on recent Android systems.
This commit is contained in:
Tobias Brunner 2014-07-22 10:51:32 +02:00
commit 94124456f2
28 changed files with 1243 additions and 186 deletions

View File

@ -20,12 +20,13 @@
android:versionCode="20"
android:versionName="1.3.4" >
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="18" />
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="19" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".logic.StrongSwanApplication"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/ApplicationTheme"
@ -63,7 +64,20 @@
android:label="@string/strongswan_shortcut" >
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" />
<action android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ui.TrustedCertificateImportActivity"
android:label="@string/import_certificate"
android:theme="@android:style/Theme.Holo.Dialog.NoActionBar" >
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/x-x509-ca-cert" />
<data android:mimeType="application/x-x509-server-cert" />
<data android:mimeType="application/x-pem-file" />
<data android:mimeType="application/pkix-cert" />
</intent-filter>
</activity>

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Copyright (C) 2012-2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
@ -17,6 +17,7 @@
#include "../android_jni.h"
#include <utils/debug.h>
#include <asn1/asn1.h>
typedef struct private_private_key_t private_private_key_t;
@ -57,35 +58,62 @@ METHOD(private_key_t, sign, bool,
{
JNIEnv *env;
jmethodID method_id;
const char *method;
const char *method = NULL;
jstring jmethod;
jobject jsignature;
jbyteArray jdata, jsigarray;
switch (scheme)
switch (this->pubkey->get_type(this->pubkey))
{
case SIGN_RSA_EMSA_PKCS1_MD5:
method = "MD5withRSA";
case KEY_RSA:
switch (scheme)
{
case SIGN_RSA_EMSA_PKCS1_MD5:
method = "MD5withRSA";
break;
case SIGN_RSA_EMSA_PKCS1_SHA1:
method = "SHA1withRSA";
break;
case SIGN_RSA_EMSA_PKCS1_SHA224:
method = "SHA224withRSA";
break;
case SIGN_RSA_EMSA_PKCS1_SHA256:
method = "SHA256withRSA";
break;
case SIGN_RSA_EMSA_PKCS1_SHA384:
method = "SHA384withRSA";
break;
case SIGN_RSA_EMSA_PKCS1_SHA512:
method = "SHA512withRSA";
break;
default:
break;
}
break;
case SIGN_RSA_EMSA_PKCS1_SHA1:
method = "SHA1withRSA";
break;
case SIGN_RSA_EMSA_PKCS1_SHA224:
method = "SHA224withRSA";
break;
case SIGN_RSA_EMSA_PKCS1_SHA256:
method = "SHA256withRSA";
break;
case SIGN_RSA_EMSA_PKCS1_SHA384:
method = "SHA384withRSA";
break;
case SIGN_RSA_EMSA_PKCS1_SHA512:
method = "SHA512withRSA";
case KEY_ECDSA:
switch (scheme)
{
case SIGN_ECDSA_256:
method = "SHA256withECDSA";
break;
case SIGN_ECDSA_384:
method = "SHA384withECDSA";
break;
case SIGN_ECDSA_521:
method = "SHA512withECDSA";
break;
default:
break;
}
break;
default:
DBG1(DBG_LIB, "signature scheme %N not supported via JNI",
signature_scheme_names, scheme);
return FALSE;
break;
}
if (!method)
{
DBG1(DBG_LIB, "signature scheme %N not supported via JNI",
signature_scheme_names, scheme);
return FALSE;
}
androidjni_attach_thread(&env);
@ -142,7 +170,54 @@ METHOD(private_key_t, sign, bool,
{
goto failed;
}
*signature = chunk_from_byte_array(env, jsigarray);
if (this->pubkey->get_type(this->pubkey) == KEY_ECDSA)
{
chunk_t encoded, parse, r, s;
size_t len = 0;
switch (scheme)
{
case SIGN_ECDSA_256:
len = 32;
break;
case SIGN_ECDSA_384:
len = 48;
break;
case SIGN_ECDSA_521:
len = 66;
break;
default:
break;
}
/* we get an ASN.1 encoded sequence of integers r and s */
parse = encoded = chunk_from_byte_array(env, jsigarray);
if (asn1_unwrap(&parse, &parse) != ASN1_SEQUENCE ||
asn1_unwrap(&parse, &r) != ASN1_INTEGER ||
asn1_unwrap(&parse, &s) != ASN1_INTEGER)
{
chunk_free(&encoded);
goto failed;
}
r = chunk_skip_zero(r);
s = chunk_skip_zero(s);
if (r.len > len || s.len > len)
{
chunk_free(&encoded);
goto failed;
}
/* concatenate r and s (forced to the defined length) */
*signature = chunk_alloc(2*len);
memset(signature->ptr, 0, signature->len);
memcpy(signature->ptr + (len - r.len), r.ptr, r.len);
memcpy(signature->ptr + len + (len - s.len), s.ptr, s.len);
chunk_free(&encoded);
}
else
{
*signature = chunk_from_byte_array(env, jsigarray);
}
androidjni_detach_thread();
return TRUE;
@ -157,7 +232,7 @@ failed:
METHOD(private_key_t, get_type, key_type_t,
private_private_key_t *this)
{
return KEY_RSA;
return this->pubkey->get_type(this->pubkey);
}
METHOD(private_key_t, decrypt, bool,

View File

@ -299,12 +299,12 @@ METHOD(charonservice_t, get_trusted_certificates, linked_list_t*,
method_id = (*env)->GetMethodID(env,
android_charonvpnservice_class,
"getTrustedCertificates", "(Ljava/lang/String;)[[B");
"getTrustedCertificates", "()[[B");
if (!method_id)
{
goto failed;
}
jcerts = (*env)->CallObjectMethod(env, this->vpn_service, method_id, NULL);
jcerts = (*env)->CallObjectMethod(env, this->vpn_service, method_id);
if (!jcerts || androidjni_exception_occurred(env))
{
goto failed;

View File

@ -8,4 +8,4 @@
# project structure.
# Project target.
target=android-14
target=android-19

View File

@ -13,7 +13,7 @@
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
-->
<TwoLineListItem xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dip"
@ -44,4 +44,4 @@
android:ellipsize="end"
android:textIsSelectable="false" />
</TwoLineListItem>
</RelativeLayout>

View File

@ -13,7 +13,7 @@
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
-->
<TwoLineListItem xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
@ -36,4 +36,4 @@
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary" />
</TwoLineListItem>
</RelativeLayout>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2014 Tobias Brunner
Hochschule fuer Technik Rapperswil
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at your
option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/menu_import_certificate"
android:title="@string/import_certificate"
android:showAsAction="withText" />
<item
android:id="@+id/menu_reload_certs"
android:title="@string/reload_trusted_certs"
android:showAsAction="withText" />
</menu>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012 Tobias Brunner
Copyright (C) 2012-2014 Tobias Brunner
Hochschule fuer Technik Rapperswil
This program is free software; you can redistribute it and/or modify it
@ -16,8 +16,8 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/menu_reload_certs"
android:title="@string/reload_trusted_certs"
android:id="@+id/menu_manage_certs"
android:title="@string/trusted_certs_title"
android:showAsAction="withText" />
<item

View File

@ -20,7 +20,6 @@
<!-- Application -->
<string name="app_name">strongSwan VPN Client</string>
<string name="main_activity_name">strongSwan</string>
<string name="reload_trusted_certs">CA-Zertifikate neu laden</string>
<string name="show_log">Log anzeigen</string>
<string name="search">Suchen</string>
<string name="vpn_not_supported_title">VPN nicht unterstützt</string>
@ -76,8 +75,15 @@
<!-- Trusted certificate selection -->
<string name="trusted_certs_title">CA-Zertifikate</string>
<string name="no_certificates">Keine Zertifikate</string>
<string name="reload_trusted_certs">CA-Zertifikate neu laden</string>
<string name="system_tab">System</string>
<string name="user_tab">Benutzer</string>
<string name="local_tab">Importiert</string>
<string name="delete_certificate_question">Zertifikat löschen?</string>
<string name="delete_certificate">Das Zertifikat wird permanent entfernt!</string>
<string name="import_certificate">Zertifikat importieren</string>
<string name="cert_imported_successfully">Zertifikat erfolgreich importiert</string>
<string name="cert_import_failed">Zertifikat-Import fehlgeschlagen</string>
<!-- VPN state fragment -->
<string name="state_label">Status:</string>

View File

@ -20,7 +20,6 @@
<!-- Application -->
<string name="app_name">strongSwan klient VPN</string>
<string name="main_activity_name">strongSwan</string>
<string name="reload_trusted_certs">Przeładuj certyfikaty CA</string>
<string name="show_log">Pokaż log</string>
<string name="search">Szukaj</string>
<string name="vpn_not_supported_title">Nie obsługiwany VPN</string>
@ -76,8 +75,15 @@
<!-- Trusted certificate selection -->
<string name="trusted_certs_title">Certyfikaty CA</string>
<string name="no_certificates">Brak certyfikatów</string>
<string name="reload_trusted_certs">Przeładuj certyfikaty CA</string>
<string name="system_tab">System</string>
<string name="user_tab">Użytkownik</string>
<string name="local_tab">Imported</string>
<string name="delete_certificate_question">Delete certificate?</string>
<string name="delete_certificate">The certificate will be permanently removed!</string>
<string name="import_certificate">Import certificate</string>
<string name="cert_imported_successfully">Certificate successfully imported</string>
<string name="cert_import_failed">Failed to import certificate</string>
<!-- VPN state fragment -->
<string name="state_label">Status:</string>

View File

@ -17,7 +17,6 @@
<!-- Application -->
<string name="app_name">Клиент strongSwan VPN</string>
<string name="main_activity_name">strongSwan</string>
<string name="reload_trusted_certs">Обновить сертификат CA</string>
<string name="show_log">Журнал</string>
<string name="search">Поиск</string>
<string name="vpn_not_supported_title">VPN не поддерживается</string>
@ -73,8 +72,15 @@
<!-- Trusted certificate selection -->
<string name="trusted_certs_title">Сертификаты CA</string>
<string name="no_certificates">Нет доступных сертификатов</string>
<string name="reload_trusted_certs">Обновить сертификат CA</string>
<string name="system_tab">Система</string>
<string name="user_tab">Пользователь</string>
<string name="local_tab">Imported</string>
<string name="delete_certificate_question">Delete certificate?</string>
<string name="delete_certificate">The certificate will be permanently removed!</string>
<string name="import_certificate">Import certificate</string>
<string name="cert_imported_successfully">Certificate successfully imported</string>
<string name="cert_import_failed">Failed to import certificate</string>
<!-- VPN state fragment -->
<string name="state_label">Статус:</string>

View File

@ -18,7 +18,6 @@
<!-- Application -->
<string name="app_name">strongSwan VPN клієнт</string>
<string name="main_activity_name">strongSwan</string>
<string name="reload_trusted_certs">Перезавантажити CA сертифікати</string>
<string name="show_log">Перегляд журналу</string>
<string name="search">Пошук</string>
<string name="vpn_not_supported_title">VPN не підтримуеться</string>
@ -74,8 +73,15 @@
<!-- Trusted certificate selection -->
<string name="trusted_certs_title">Сертифікати CA</string>
<string name="no_certificates">Немає сертифікатів</string>
<string name="reload_trusted_certs">Перезавантажити CA сертифікати</string>
<string name="system_tab">Система</string>
<string name="user_tab">Користувач</string>
<string name="local_tab">Imported</string>
<string name="delete_certificate_question">Delete certificate?</string>
<string name="delete_certificate">The certificate will be permanently removed!</string>
<string name="import_certificate">Import certificate</string>
<string name="cert_imported_successfully">Certificate successfully imported</string>
<string name="cert_import_failed">Failed to import certificate</string>
<!-- VPN state fragment -->
<string name="state_label">Статус:</string>

View File

@ -20,7 +20,6 @@
<!-- Application -->
<string name="app_name">strongSwan VPN Client</string>
<string name="main_activity_name">strongSwan</string>
<string name="reload_trusted_certs">Reload CA certificates</string>
<string name="show_log">View log</string>
<string name="search">Search</string>
<string name="vpn_not_supported_title">VPN not supported</string>
@ -76,8 +75,15 @@
<!-- Trusted certificate selection -->
<string name="trusted_certs_title">CA certificates</string>
<string name="no_certificates">No certificates</string>
<string name="reload_trusted_certs">Reload CA certificates</string>
<string name="system_tab">System</string>
<string name="user_tab">User</string>
<string name="local_tab">Imported</string>
<string name="delete_certificate_question">Delete certificate?</string>
<string name="delete_certificate">The certificate will be permanently removed!</string>
<string name="import_certificate">Import certificate</string>
<string name="cert_imported_successfully">Certificate successfully imported</string>
<string name="cert_import_failed">Failed to import certificate</string>
<!-- VPN state fragment -->
<string name="state_label">Status:</string>

View File

@ -419,25 +419,19 @@ public class CharonVpnService extends VpnService implements Runnable
* Function called via JNI to generate a list of DER encoded CA certificates
* as byte array.
*
* @param hash optional alias (only hash part), if given matching certificates are returned
* @return a list of DER encoded CA certificates
*/
private byte[][] getTrustedCertificates(String hash)
private byte[][] getTrustedCertificates()
{
ArrayList<byte[]> certs = new ArrayList<byte[]>();
TrustedCertificateManager certman = TrustedCertificateManager.getInstance();
try
{
if (hash != null)
String alias = this.mCurrentCertificateAlias;
if (alias != null)
{
String alias = "user:" + hash + ".0";
X509Certificate cert = certman.getCACertificateFromAlias(alias);
if (cert == null)
{
alias = "system:" + hash + ".0";
cert = certman.getCACertificateFromAlias(alias);
}
if (cert == null)
{
return null;
}
@ -445,23 +439,10 @@ public class CharonVpnService extends VpnService implements Runnable
}
else
{
String alias = this.mCurrentCertificateAlias;
if (alias != null)
for (X509Certificate cert : certman.getAllCACertificates().values())
{
X509Certificate cert = certman.getCACertificateFromAlias(alias);
if (cert == null)
{
return null;
}
certs.add(cert.getEncoded());
}
else
{
for (X509Certificate cert : certman.getAllCACertificates().values())
{
certs.add(cert.getEncoded());
}
}
}
}
catch (CertificateEncodingException e)

View File

@ -0,0 +1,48 @@
/*
* Copyright (C) 2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic;
import java.security.Security;
import org.strongswan.android.security.LocalCertificateKeyStoreProvider;
import android.app.Application;
import android.content.Context;
public class StrongSwanApplication extends Application
{
private static Context mContext;
static {
Security.addProvider(new LocalCertificateKeyStoreProvider());
}
@Override
public void onCreate()
{
super.onCreate();
StrongSwanApplication.mContext = getApplicationContext();
}
/**
* Returns the current application context
* @return context
*/
public static Context getContext()
{
return StrongSwanApplication.mContext;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Copyright (C) 2012-2014 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
@ -21,6 +21,7 @@ import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@ -32,13 +33,49 @@ public class TrustedCertificateManager
private static final String TAG = TrustedCertificateManager.class.getSimpleName();
private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();
private Hashtable<String, X509Certificate> mCACerts = new Hashtable<String, X509Certificate>();
private volatile boolean mReload;
private boolean mLoaded;
private final ArrayList<KeyStore> mKeyStores = new ArrayList<KeyStore>();
public enum TrustedCertificateSource
{
SYSTEM("system:"),
USER("user:"),
LOCAL("local:");
private final String mPrefix;
private TrustedCertificateSource(String prefix)
{
mPrefix = prefix;
}
private String getPrefix()
{
return mPrefix;
}
}
/**
* Private constructor to prevent instantiation from other classes.
*/
private TrustedCertificateManager()
{
for (String name : new String[] { "LocalCertificateStore", "AndroidCAStore" })
{
KeyStore store;
try
{
store = KeyStore.getInstance(name);
store.load(null,null);
mKeyStores.add(store);
}
catch (Exception e)
{
Log.e(TAG, "Unable to load KeyStore: " + name);
e.printStackTrace();
}
}
}
/**
@ -58,16 +95,14 @@ public class TrustedCertificateManager
}
/**
* Forces a load/reload of the cached CA certificates.
* As this takes a while it should be called asynchronously.
* Invalidates the current load state so that the next call to load()
* will force a reload of the cached CA certificates.
* @return reference to itself
*/
public TrustedCertificateManager reload()
public TrustedCertificateManager reset()
{
Log.d(TAG, "Force reload of cached CA certificates");
this.mLock.writeLock().lock();
loadCertificates();
this.mLock.writeLock().unlock();
Log.d(TAG, "Force reload of cached CA certificates on next load");
this.mReload = true;
return this;
}
@ -81,8 +116,9 @@ public class TrustedCertificateManager
{
Log.d(TAG, "Ensure cached CA certificates are loaded");
this.mLock.writeLock().lock();
if (!this.mLoaded)
if (!this.mLoaded || this.mReload)
{
this.mReload = false;
loadCertificates();
}
this.mLock.writeLock().unlock();
@ -96,29 +132,23 @@ public class TrustedCertificateManager
private void loadCertificates()
{
Log.d(TAG, "Load cached CA certificates");
try
Hashtable<String, X509Certificate> certs = new Hashtable<String, X509Certificate>();
for (KeyStore store : this.mKeyStores)
{
KeyStore store = KeyStore.getInstance("AndroidCAStore");
store.load(null, null);
this.mCACerts = fetchCertificates(store);
this.mLoaded = true;
Log.d(TAG, "Cached CA certificates loaded");
}
catch (Exception ex)
{
ex.printStackTrace();
this.mCACerts = new Hashtable<String, X509Certificate>();
fetchCertificates(certs, store);
}
this.mCACerts = certs;
this.mLoaded = true;
Log.d(TAG, "Cached CA certificates loaded");
}
/**
* Load all X.509 certificates from the given KeyStore.
* @param certs Hashtable to store certificates in
* @param store KeyStore to load certificates from
* @return Hashtable mapping aliases to certificates
*/
private Hashtable<String, X509Certificate> fetchCertificates(KeyStore store)
private void fetchCertificates(Hashtable<String, X509Certificate> certs, KeyStore store)
{
Hashtable<String, X509Certificate> certs = new Hashtable<String, X509Certificate>();
try
{
Enumeration<String> aliases = store.aliases();
@ -137,7 +167,6 @@ public class TrustedCertificateManager
{
ex.printStackTrace();
}
return certs;
}
/**
@ -157,27 +186,28 @@ public class TrustedCertificateManager
else
{ /* if we cannot get the lock load it directly from the KeyStore,
* should be fast for a single certificate */
try
for (KeyStore store : this.mKeyStores)
{
KeyStore store = KeyStore.getInstance("AndroidCAStore");
store.load(null, null);
Certificate cert = store.getCertificate(alias);
if (cert != null && cert instanceof X509Certificate)
try
{
certificate = (X509Certificate)cert;
Certificate cert = store.getCertificate(alias);
if (cert != null && cert instanceof X509Certificate)
{
certificate = (X509Certificate)cert;
break;
}
}
catch (KeyStoreException e)
{
e.printStackTrace();
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
return certificate;
}
/**
* Get all CA certificates (from the system and user keystore).
* Get all CA certificates (from all keystores).
* @return Hashtable mapping aliases to certificates
*/
@SuppressWarnings("unchecked")
@ -191,35 +221,17 @@ public class TrustedCertificateManager
}
/**
* Get only the system-wide CA certificates.
* Get all certificates from the given source.
* @param source type to filter certificates
* @return Hashtable mapping aliases to certificates
*/
public Hashtable<String, X509Certificate> getSystemCACertificates()
public Hashtable<String, X509Certificate> getCACertificates(TrustedCertificateSource source)
{
Hashtable<String, X509Certificate> certs = new Hashtable<String, X509Certificate>();
this.mLock.readLock().lock();
for (String alias : this.mCACerts.keySet())
{
if (alias.startsWith("system:"))
{
certs.put(alias, this.mCACerts.get(alias));
}
}
this.mLock.readLock().unlock();
return certs;
}
/**
* Get only the CA certificates installed by the user.
* @return Hashtable mapping aliases to certificates
*/
public Hashtable<String, X509Certificate> getUserCACertificates()
{
Hashtable<String, X509Certificate> certs = new Hashtable<String, X509Certificate>();
this.mLock.readLock().lock();
for (String alias : this.mCACerts.keySet())
{
if (alias.startsWith("user:"))
if (alias.startsWith(source.getPrefix()))
{
certs.put(alias, this.mCACerts.get(alias));
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.security;
import java.security.Provider;
public class LocalCertificateKeyStoreProvider extends Provider
{
private static final long serialVersionUID = 3515038332469843219L;
public LocalCertificateKeyStoreProvider()
{
super("LocalCertificateKeyStoreProvider", 1.0, "KeyStore provider for local certificates");
put("KeyStore.LocalCertificateStore", LocalCertificateKeyStoreSpi.class.getName());
}
}

View File

@ -0,0 +1,139 @@
/*
* Copyright (C) 2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.security;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.Key;
import java.security.KeyStoreException;
import java.security.KeyStoreSpi;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
public class LocalCertificateKeyStoreSpi extends KeyStoreSpi
{
private final LocalCertificateStore mStore = new LocalCertificateStore();
@Override
public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException, UnrecoverableKeyException
{
return null;
}
@Override
public Certificate[] engineGetCertificateChain(String alias)
{
return null;
}
@Override
public Certificate engineGetCertificate(String alias)
{
return mStore.getCertificate(alias);
}
@Override
public Date engineGetCreationDate(String alias)
{
return mStore.getCreationDate(alias);
}
@Override
public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain) throws KeyStoreException
{
throw new UnsupportedOperationException();
}
@Override
public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) throws KeyStoreException
{
throw new UnsupportedOperationException();
}
@Override
public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException
{
/* we ignore the given alias as the store calculates it on its own,
* duplicates are replaced */
if (!mStore.addCertificate(cert))
{
throw new KeyStoreException();
}
}
@Override
public void engineDeleteEntry(String alias) throws KeyStoreException
{
mStore.deleteCertificate(alias);
}
@Override
public Enumeration<String> engineAliases()
{
return Collections.enumeration(mStore.aliases());
}
@Override
public boolean engineContainsAlias(String alias)
{
return mStore.containsAlias(alias);
}
@Override
public int engineSize()
{
return mStore.aliases().size();
}
@Override
public boolean engineIsKeyEntry(String alias)
{
return false;
}
@Override
public boolean engineIsCertificateEntry(String alias)
{
return engineContainsAlias(alias);
}
@Override
public String engineGetCertificateAlias(Certificate cert)
{
return mStore.getCertificateAlias(cert);
}
@Override
public void engineStore(OutputStream stream, char[] password) throws IOException, NoSuchAlgorithmException, CertificateException
{
throw new UnsupportedOperationException();
}
@Override
public void engineLoad(InputStream stream, char[] password) throws IOException, NoSuchAlgorithmException, CertificateException
{
if (stream != null)
{
throw new UnsupportedOperationException();
}
}
}

View File

@ -0,0 +1,230 @@
/*
* Copyright (C) 2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.security;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Date;
import java.util.regex.Pattern;
import org.strongswan.android.logic.StrongSwanApplication;
import org.strongswan.android.utils.Utils;
import android.content.Context;
public class LocalCertificateStore
{
private static final String FILE_PREFIX = "certificate-";
private static final String ALIAS_PREFIX = "local:";
private static final Pattern ALIAS_PATTERN = Pattern.compile("^" + ALIAS_PREFIX + "[0-9a-f]{40}$");
/**
* Add the given certificate to the store
* @param cert the certificate to add
* @return true if successful
*/
public boolean addCertificate(Certificate cert)
{
if (!(cert instanceof X509Certificate))
{ /* only accept X.509 certificates */
return false;
}
String keyid = getKeyId(cert);
if (keyid == null)
{
return false;
}
FileOutputStream out;
try
{
/* we replace any existing file with the same alias */
out = StrongSwanApplication.getContext().openFileOutput(FILE_PREFIX + keyid, Context.MODE_PRIVATE);
try
{
out.write(cert.getEncoded());
return true;
}
catch (CertificateEncodingException e)
{
e.printStackTrace();
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
try
{
out.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
return false;
}
/**
* Delete the certificate with the given alias
* @param alias a certificate's alias
*/
public void deleteCertificate(String alias)
{
if (ALIAS_PATTERN.matcher(alias).matches())
{
alias = alias.substring(ALIAS_PREFIX.length());
StrongSwanApplication.getContext().deleteFile(FILE_PREFIX + alias);
}
}
/**
* Retrieve the certificate with the given alias
* @param alias a certificate's alias
* @return certificate object or null
*/
public X509Certificate getCertificate(String alias)
{
if (!ALIAS_PATTERN.matcher(alias).matches())
{
return null;
}
alias = alias.substring(ALIAS_PREFIX.length());
try
{
FileInputStream in = StrongSwanApplication.getContext().openFileInput(FILE_PREFIX + alias);
try
{
CertificateFactory factory = CertificateFactory.getInstance("X.509");
X509Certificate certificate = (X509Certificate)factory.generateCertificate(in);
return certificate;
}
catch (CertificateException e)
{
e.printStackTrace();
}
finally
{
try
{
in.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
return null;
}
/**
* Returns the creation date of the certificate with the given alias
* @param alias certificate alias
* @return creation date or null if not found
*/
public Date getCreationDate(String alias)
{
if (!ALIAS_PATTERN.matcher(alias).matches())
{
return null;
}
alias = alias.substring(ALIAS_PREFIX.length());
File file = StrongSwanApplication.getContext().getFileStreamPath(FILE_PREFIX + alias);
return file.exists() ? new Date(file.lastModified()) : null;
}
/**
* Returns a list of all known certificate aliases
* @return list of aliases
*/
public ArrayList<String> aliases()
{
ArrayList<String> list = new ArrayList<String>();
for (String file : StrongSwanApplication.getContext().fileList())
{
if (file.startsWith(FILE_PREFIX))
{
list.add(ALIAS_PREFIX + file.substring(FILE_PREFIX.length()));
}
}
return list;
}
/**
* Check if the store contains a certificate with the given alias
* @param alias certificate alias
* @return true if the store contains the certificate
*/
public boolean containsAlias(String alias)
{
return getCreationDate(alias) != null;
}
/**
* Returns a certificate alias based on a SHA-1 hash of the public key.
*
* @param cert certificate to get an alias for
* @return hex encoded alias, or null if failed
*/
public String getCertificateAlias(Certificate cert)
{
String keyid = getKeyId(cert);
return keyid != null ? ALIAS_PREFIX + keyid : null;
}
/**
* Calculates the SHA-1 hash of the public key of the given certificate.
* @param cert certificate to get the key ID from
* @return hex encoded SHA-1 hash of the public key or null if failed
*/
private String getKeyId(Certificate cert)
{
MessageDigest md;
try
{
md = java.security.MessageDigest.getInstance("SHA1");
byte[] hash = md.digest(cert.getPublicKey().getEncoded());
return Utils.bytesToHex(hash);
}
catch (NoSuchAlgorithmException e)
{
e.printStackTrace();
}
return null;
}
}

View File

@ -13,7 +13,7 @@
* for more details.
*/
package org.strongswan.android.data;
package org.strongswan.android.security;
import java.security.cert.X509Certificate;

View File

@ -0,0 +1,80 @@
/*
* Copyright (C) 2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui;
import org.strongswan.android.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.os.Bundle;
/**
* Class that displays a confirmation dialog to delete a selected local
* certificate.
*/
public class CertificateDeleteConfirmationDialog extends DialogFragment
{
public static final String ALIAS = "alias";
OnCertificateDeleteListener mListener;
/**
* Interface that can be implemented by parent activities to get the
* alias of the certificate to delete, if the user confirms the deletion.
*/
public interface OnCertificateDeleteListener
{
public void onDelete(String alias);
}
@Override
public void onAttach(Activity activity)
{
super.onAttach(activity);
if (activity instanceof OnCertificateDeleteListener)
{
mListener = (OnCertificateDeleteListener)activity;
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
return new AlertDialog.Builder(getActivity())
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.delete_certificate_question)
.setMessage(R.string.delete_certificate)
.setPositiveButton(R.string.delete_profile, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int whichButton)
{
if (mListener != null)
{
mListener.onDelete(getArguments().getString(ALIAS));
}
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which)
{
dismiss();
}
}).create();
}
}

View File

@ -101,7 +101,7 @@ public class MainActivity extends Activity implements OnVpnProfileSelectedListen
bar.setDisplayShowTitleEnabled(false);
/* load CA certificates in a background task */
new CertificateLoadTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, false);
new LoadCertificatesTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@Override
@ -140,8 +140,9 @@ public class MainActivity extends Activity implements OnVpnProfileSelectedListen
{
switch (item.getItemId())
{
case R.id.menu_reload_certs:
new CertificateLoadTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, true);
case R.id.menu_manage_certs:
Intent certIntent = new Intent(this, TrustedCertificatesActivity.class);
startActivity(certIntent);
return true;
case R.id.menu_show_log:
Intent logIntent = new Intent(this, LogActivity.class);
@ -280,9 +281,9 @@ public class MainActivity extends Activity implements OnVpnProfileSelectedListen
}
/**
* Class that loads or reloads the cached CA certificates.
* Class that loads the cached CA certificates.
*/
private class CertificateLoadTask extends AsyncTask<Boolean, Void, TrustedCertificateManager>
private class LoadCertificatesTask extends AsyncTask<Void, Void, TrustedCertificateManager>
{
@Override
protected void onPreExecute()
@ -290,12 +291,8 @@ public class MainActivity extends Activity implements OnVpnProfileSelectedListen
setProgressBarIndeterminateVisibility(true);
}
@Override
protected TrustedCertificateManager doInBackground(Boolean... params)
protected TrustedCertificateManager doInBackground(Void... params)
{
if (params.length > 0 && params[0])
{ /* force a reload of the certificates */
return TrustedCertificateManager.getInstance().reload();
}
return TrustedCertificateManager.getInstance().load();
}
@Override

View File

@ -0,0 +1,223 @@
/*
* Copyright (C) 2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.ui;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import org.strongswan.android.R;
import org.strongswan.android.data.VpnProfileDataSource;
import org.strongswan.android.logic.TrustedCertificateManager;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.FragmentTransaction;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.widget.Toast;
public class TrustedCertificateImportActivity extends Activity
{
private static final int OPEN_DOCUMENT = 0;
private static final String DIALOG_TAG = "Dialog";
/* same as those listed in the manifest */
private static final String[] ACCEPTED_MIME_TYPES = {
"application/x-x509-ca-cert",
"application/x-x509-server-cert",
"application/x-pem-file",
"application/pkix-cert"
};
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
if (savedInstanceState != null)
{ /* do nothing when we are restoring */
return;
}
Intent intent = getIntent();
String action = intent.getAction();
if (Intent.ACTION_VIEW.equals(action))
{
importCertificate(intent.getData());
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
{
Intent openIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
openIntent.setType("*/*");
openIntent.putExtra(Intent.EXTRA_MIME_TYPES, ACCEPTED_MIME_TYPES);
startActivityForResult(openIntent, OPEN_DOCUMENT);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
switch (requestCode)
{
case OPEN_DOCUMENT:
if (resultCode == Activity.RESULT_OK && data != null)
{
importCertificate(data.getData());
return;
}
finish();
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
/**
* Import the file pointed to by the given URI as a certificate.
* @param uri
*/
private void importCertificate(Uri uri)
{
X509Certificate certificate = parseCertificate(uri);
if (certificate == null)
{
Toast.makeText(this, R.string.cert_import_failed, Toast.LENGTH_LONG).show();
finish();
return;
}
/* Ask the user whether to import the certificate. This is particularly
* necessary because the import activity can be triggered by any app on
* the system. Also, if our app is the only one that is registered to
* open certificate files by MIME type the user would have no idea really
* where the file was imported just by reading the Toast we display. */
ConfirmImportDialog dialog = new ConfirmImportDialog();
Bundle args = new Bundle();
args.putSerializable(VpnProfileDataSource.KEY_CERTIFICATE, certificate);
dialog.setArguments(args);
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.add(dialog, DIALOG_TAG);
ft.commit();
}
/**
* Load the file from the given URI and try to parse it as X.509 certificate.
* @param uri
* @return certificate or null
*/
private X509Certificate parseCertificate(Uri uri)
{
X509Certificate certificate = null;
try
{
CertificateFactory factory = CertificateFactory.getInstance("X.509");
InputStream in = getContentResolver().openInputStream(uri);
certificate = (X509Certificate)factory.generateCertificate(in);
/* we don't check whether it's actually a CA certificate or not */
}
catch (CertificateException e)
{
e.printStackTrace();
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
return certificate;
}
/**
* Try to store the given certificate in the KeyStore.
* @param certificate
* @return whether it was successfully stored
*/
private boolean storeCertificate(X509Certificate certificate)
{
try
{
KeyStore store = KeyStore.getInstance("LocalCertificateStore");
store.load(null, null);
store.setCertificateEntry(null, certificate);
TrustedCertificateManager.getInstance().reset();
return true;
}
catch (Exception e)
{
e.printStackTrace();
return false;
}
}
/**
* Class that displays a confirmation dialog when a certificate should get
* imported. If the user confirms the import we try to store it.
*/
public static class ConfirmImportDialog extends DialogFragment
{
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
final X509Certificate certificate;
certificate = (X509Certificate)getArguments().getSerializable(VpnProfileDataSource.KEY_CERTIFICATE);
return new AlertDialog.Builder(getActivity())
.setIcon(R.drawable.ic_launcher)
.setTitle(R.string.import_certificate)
.setMessage(certificate.getSubjectDN().toString())
.setPositiveButton(R.string.import_certificate, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int whichButton)
{
TrustedCertificateImportActivity activity = (TrustedCertificateImportActivity)getActivity();
if (activity.storeCertificate(certificate))
{
Toast.makeText(getActivity(), R.string.cert_imported_successfully, Toast.LENGTH_LONG).show();
getActivity().setResult(Activity.RESULT_OK);
}
else
{
Toast.makeText(getActivity(), R.string.cert_import_failed, Toast.LENGTH_LONG).show();
}
getActivity().finish();
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which)
{
getActivity().finish();
}
}).create();
}
@Override
public void onCancel(DialogInterface dialog)
{
getActivity().finish();
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Copyright (C) 2012-2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
@ -23,8 +23,9 @@ import java.util.List;
import java.util.Map.Entry;
import org.strongswan.android.R;
import org.strongswan.android.data.TrustedCertificateEntry;
import org.strongswan.android.logic.TrustedCertificateManager;
import org.strongswan.android.logic.TrustedCertificateManager.TrustedCertificateSource;
import org.strongswan.android.security.TrustedCertificateEntry;
import org.strongswan.android.ui.adapter.TrustedCertificateAdapter;
import android.app.Activity;
@ -45,9 +46,10 @@ import android.widget.SearchView.OnQueryTextListener;
public class TrustedCertificateListFragment extends ListFragment implements LoaderCallbacks<List<TrustedCertificateEntry>>, OnQueryTextListener
{
public static final String EXTRA_CERTIFICATE_SOURCE = "certificate_source";
private OnTrustedCertificateSelectedListener mListener;
private TrustedCertificateAdapter mAdapter;
private boolean mUser;
private TrustedCertificateSource mSource = TrustedCertificateSource.SYSTEM;
/**
* The activity containing this fragment should implement this interface
@ -69,8 +71,11 @@ public class TrustedCertificateListFragment extends ListFragment implements Load
setListShown(false);
/* non empty arguments mean we list user certificate */
mUser = getArguments() != null;
Bundle arguments = getArguments();
if (arguments != null)
{
mSource = (TrustedCertificateSource)arguments.getSerializable(EXTRA_CERTIFICATE_SOURCE);
}
getLoaderManager().initLoader(0, null, this);
}
@ -118,10 +123,22 @@ public class TrustedCertificateListFragment extends ListFragment implements Load
return true;
}
/**
* Reset the loader of this list fragment
*/
public void reset()
{
if (isResumed())
{
setListShown(false);
}
getLoaderManager().restartLoader(0, null, this);
}
@Override
public Loader<List<TrustedCertificateEntry>> onCreateLoader(int id, Bundle args)
{ /* we don't need the id as we have only one loader */
return new CertificateListLoader(getActivity(), mUser);
return new CertificateListLoader(getActivity(), mSource);
}
@Override
@ -157,22 +174,21 @@ public class TrustedCertificateListFragment extends ListFragment implements Load
public static class CertificateListLoader extends AsyncTaskLoader<List<TrustedCertificateEntry>>
{
private List<TrustedCertificateEntry> mData;
private final boolean mUser;
private final TrustedCertificateSource mSource;
public CertificateListLoader(Context context, boolean user)
public CertificateListLoader(Context context, TrustedCertificateSource source)
{
super(context);
mUser = user;
mSource = source;
}
@Override
public List<TrustedCertificateEntry> loadInBackground()
{
TrustedCertificateManager certman = TrustedCertificateManager.getInstance().load();
Hashtable<String,X509Certificate> certificates;
Hashtable<String,X509Certificate> certificates = certman.getCACertificates(mSource);
List<TrustedCertificateEntry> selected;
certificates = mUser ? certman.getUserCACertificates() : certman.getSystemCACertificates();
selected = new ArrayList<TrustedCertificateEntry>();
for (Entry<String, X509Certificate> entry : certificates.entrySet())
{

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Copyright (C) 2012-2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
@ -15,9 +15,14 @@
package org.strongswan.android.ui;
import java.security.KeyStore;
import org.strongswan.android.R;
import org.strongswan.android.data.TrustedCertificateEntry;
import org.strongswan.android.data.VpnProfileDataSource;
import org.strongswan.android.logic.TrustedCertificateManager;
import org.strongswan.android.logic.TrustedCertificateManager.TrustedCertificateSource;
import org.strongswan.android.security.TrustedCertificateEntry;
import org.strongswan.android.ui.CertificateDeleteConfirmationDialog.OnCertificateDeleteListener;
import android.app.ActionBar;
import android.app.ActionBar.Tab;
@ -25,11 +30,18 @@ import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
public class TrustedCertificatesActivity extends Activity implements TrustedCertificateListFragment.OnTrustedCertificateSelectedListener
public class TrustedCertificatesActivity extends Activity implements TrustedCertificateListFragment.OnTrustedCertificateSelectedListener, OnCertificateDeleteListener
{
public static final String SELECT_CERTIFICATE = "org.strongswan.android.action.SELECT_CERTIFICATE";
private static final String DIALOG_TAG = "Dialog";
private static final int IMPORT_CERTIFICATE = 0;
private boolean mSelect;
@Override
public void onCreate(Bundle savedInstanceState)
{
@ -40,19 +52,31 @@ public class TrustedCertificatesActivity extends Activity implements TrustedCert
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
TrustedCertificatesTabListener listener;
listener = new TrustedCertificatesTabListener(this, "system", TrustedCertificateSource.SYSTEM);
actionBar.addTab(actionBar
.newTab()
.setText(R.string.system_tab)
.setTabListener(new TrustedCertificatesTabListener(this, "system", false)));
.setTag(listener)
.setTabListener(listener));
listener = new TrustedCertificatesTabListener(this, "user", TrustedCertificateSource.USER);
actionBar.addTab(actionBar
.newTab()
.setText(R.string.user_tab)
.setTabListener(new TrustedCertificatesTabListener(this, "user", true)));
.setTag(listener)
.setTabListener(listener));
listener = new TrustedCertificatesTabListener(this, "local", TrustedCertificateSource.LOCAL);
actionBar.addTab(actionBar
.newTab()
.setText(R.string.local_tab)
.setTag(listener)
.setTabListener(listener));
if (savedInstanceState != null)
{
actionBar.setSelectedNavigationItem(savedInstanceState.getInt("tab", 0));
}
mSelect = SELECT_CERTIFICATE.equals(getIntent().getAction());
}
@Override
@ -62,6 +86,23 @@ public class TrustedCertificatesActivity extends Activity implements TrustedCert
outState.putInt("tab", getActionBar().getSelectedNavigationIndex());
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
getMenuInflater().inflate(R.menu.certificates, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu)
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)
{
menu.removeItem(R.id.menu_import_certificate);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
@ -70,30 +111,95 @@ public class TrustedCertificatesActivity extends Activity implements TrustedCert
case android.R.id.home:
finish();
return true;
case R.id.menu_reload_certs:
reloadCertificates();
return true;
case R.id.menu_import_certificate:
Intent intent = new Intent(this, TrustedCertificateImportActivity.class);
startActivityForResult(intent, IMPORT_CERTIFICATE);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
switch (requestCode)
{
case IMPORT_CERTIFICATE:
if (resultCode == Activity.RESULT_OK)
{
reloadCertificates();
}
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
public void onTrustedCertificateSelected(TrustedCertificateEntry selected)
{
/* the user selected a certificate, return to calling activity */
Intent intent = new Intent();
intent.putExtra(VpnProfileDataSource.KEY_CERTIFICATE, selected.getAlias());
setResult(Activity.RESULT_OK, intent);
finish();
if (mSelect)
{
/* the user selected a certificate, return to calling activity */
Intent intent = new Intent();
intent.putExtra(VpnProfileDataSource.KEY_CERTIFICATE, selected.getAlias());
setResult(Activity.RESULT_OK, intent);
finish();
}
else
{
TrustedCertificatesTabListener listener;
listener = (TrustedCertificatesTabListener)getActionBar().getSelectedTab().getTag();
if (listener.mTag == "local")
{
Bundle args = new Bundle();
args.putString(CertificateDeleteConfirmationDialog.ALIAS, selected.getAlias());
CertificateDeleteConfirmationDialog dialog = new CertificateDeleteConfirmationDialog();
dialog.setArguments(args);
dialog.show(this.getFragmentManager(), DIALOG_TAG);
}
}
}
@Override
public void onDelete(String alias)
{
try
{
KeyStore store = KeyStore.getInstance("LocalCertificateStore");
store.load(null, null);
store.deleteEntry(alias);
reloadCertificates();
}
catch (Exception e)
{
e.printStackTrace();
}
}
private void reloadCertificates()
{
TrustedCertificateManager.getInstance().reset();
for (int i = 0; i < getActionBar().getTabCount(); i++)
{
Tab tab = getActionBar().getTabAt(i);
TrustedCertificatesTabListener listener = (TrustedCertificatesTabListener)tab.getTag();
listener.reset();
}
}
public static class TrustedCertificatesTabListener implements ActionBar.TabListener
{
private final String mTag;
private final boolean mUser;
private final TrustedCertificateSource mSource;
private Fragment mFragment;
public TrustedCertificatesTabListener(Activity activity, String tag, boolean user)
public TrustedCertificatesTabListener(Activity activity, String tag, TrustedCertificateSource source)
{
mTag = tag;
mUser = user;
mSource = source;
/* check to see if we already have a fragment for this tab, probably
* from a previously saved state. if so, deactivate it, because the
* initial state is that no tab is shown */
@ -112,10 +218,9 @@ public class TrustedCertificatesActivity extends Activity implements TrustedCert
if (mFragment == null)
{
mFragment = new TrustedCertificateListFragment();
if (mUser)
{ /* use non empty arguments to indicate this */
mFragment.setArguments(new Bundle());
}
Bundle args = new Bundle();
args.putSerializable(TrustedCertificateListFragment.EXTRA_CERTIFICATE_SOURCE, mSource);
mFragment.setArguments(args);
ft.add(android.R.id.content, mFragment, mTag);
}
else
@ -138,5 +243,13 @@ public class TrustedCertificatesActivity extends Activity implements TrustedCert
{
/* nothing to be done */
}
public void reset()
{
if (mFragment != null)
{
((TrustedCertificateListFragment)mFragment).reset();
}
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2012 Tobias Brunner
* Copyright (C) 2012-2014 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
@ -20,11 +20,11 @@ package org.strongswan.android.ui;
import java.security.cert.X509Certificate;
import org.strongswan.android.R;
import org.strongswan.android.data.TrustedCertificateEntry;
import org.strongswan.android.data.VpnProfile;
import org.strongswan.android.data.VpnProfileDataSource;
import org.strongswan.android.data.VpnType;
import org.strongswan.android.logic.TrustedCertificateManager;
import org.strongswan.android.security.TrustedCertificateEntry;
import android.app.Activity;
import android.app.AlertDialog;
@ -52,8 +52,9 @@ import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.RelativeLayout;
import android.widget.Spinner;
import android.widget.TwoLineListItem;
import android.widget.TextView;
public class VpnProfileDetailActivity extends Activity
{
@ -73,10 +74,10 @@ public class VpnProfileDetailActivity extends Activity
private EditText mUsername;
private EditText mPassword;
private ViewGroup mUserCertificate;
private TwoLineListItem mSelectUserCert;
private RelativeLayout mSelectUserCert;
private CheckBox mCheckAuto;
private TwoLineListItem mSelectCert;
private TwoLineListItem mTncNotice;
private RelativeLayout mSelectCert;
private RelativeLayout mTncNotice;
@Override
public void onCreate(Bundle savedInstanceState)
@ -94,17 +95,17 @@ public class VpnProfileDetailActivity extends Activity
mName = (EditText)findViewById(R.id.name);
mGateway = (EditText)findViewById(R.id.gateway);
mSelectVpnType = (Spinner)findViewById(R.id.vpn_type);
mTncNotice = (TwoLineListItem)findViewById(R.id.tnc_notice);
mTncNotice = (RelativeLayout)findViewById(R.id.tnc_notice);
mUsernamePassword = (ViewGroup)findViewById(R.id.username_password_group);
mUsername = (EditText)findViewById(R.id.username);
mPassword = (EditText)findViewById(R.id.password);
mUserCertificate = (ViewGroup)findViewById(R.id.user_certificate_group);
mSelectUserCert = (TwoLineListItem)findViewById(R.id.select_user_certificate);
mSelectUserCert = (RelativeLayout)findViewById(R.id.select_user_certificate);
mCheckAuto = (CheckBox)findViewById(R.id.ca_auto);
mSelectCert = (TwoLineListItem)findViewById(R.id.select_certificate);
mSelectCert = (RelativeLayout)findViewById(R.id.select_certificate);
mSelectVpnType.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
@ -122,8 +123,8 @@ public class VpnProfileDetailActivity extends Activity
}
});
mTncNotice.getText1().setText(R.string.tnc_notice_title);
mTncNotice.getText2().setText(R.string.tnc_notice_subtitle);
((TextView)mTncNotice.findViewById(android.R.id.text1)).setText(R.string.tnc_notice_title);
((TextView)mTncNotice.findViewById(android.R.id.text2)).setText(R.string.tnc_notice_subtitle);
mTncNotice.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v)
@ -147,6 +148,7 @@ public class VpnProfileDetailActivity extends Activity
public void onClick(View v)
{
Intent intent = new Intent(VpnProfileDetailActivity.this, TrustedCertificatesActivity.class);
intent.setAction(TrustedCertificatesActivity.SELECT_CERTIFICATE);
startActivityForResult(intent, SELECT_TRUSTED_CERTIFICATE);
}
});
@ -246,19 +248,19 @@ public class VpnProfileDetailActivity extends Activity
{
if (mUserCertLoading != null)
{
mSelectUserCert.getText1().setText(mUserCertLoading);
mSelectUserCert.getText2().setText(R.string.loading);
((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertLoading);
((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.loading);
}
else if (mUserCertEntry != null)
{ /* clear any errors and set the new data */
mSelectUserCert.getText1().setError(null);
mSelectUserCert.getText1().setText(mUserCertEntry.getAlias());
mSelectUserCert.getText2().setText(mUserCertEntry.getCertificate().getSubjectDN().toString());
((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError(null);
((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertEntry.getAlias());
((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(mUserCertEntry.getCertificate().getSubjectDN().toString());
}
else
{
mSelectUserCert.getText1().setText(R.string.profile_user_select_certificate_label);
mSelectUserCert.getText2().setText(R.string.profile_user_select_certificate);
((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(R.string.profile_user_select_certificate_label);
((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.profile_user_select_certificate);
}
}
}
@ -295,13 +297,13 @@ public class VpnProfileDetailActivity extends Activity
if (mCertEntry != null)
{
mSelectCert.getText1().setText(mCertEntry.getSubjectPrimary());
mSelectCert.getText2().setText(mCertEntry.getSubjectSecondary());
((TextView)mSelectCert.findViewById(android.R.id.text1)).setText(mCertEntry.getSubjectPrimary());
((TextView)mSelectCert.findViewById(android.R.id.text2)).setText(mCertEntry.getSubjectSecondary());
}
else
{
mSelectCert.getText1().setText(R.string.profile_ca_select_certificate_label);
mSelectCert.getText2().setText(R.string.profile_ca_select_certificate);
((TextView)mSelectCert.findViewById(android.R.id.text1)).setText(R.string.profile_ca_select_certificate_label);
((TextView)mSelectCert.findViewById(android.R.id.text2)).setText(R.string.profile_ca_select_certificate);
}
}
else
@ -357,7 +359,7 @@ public class VpnProfileDetailActivity extends Activity
}
if (mVpnType.getRequiresCertificate() && mUserCertEntry == null)
{ /* let's show an error icon */
mSelectUserCert.getText1().setError("");
((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
valid = false;
}
if (!mCheckAuto.isChecked() && mCertEntry == null)
@ -545,7 +547,7 @@ public class VpnProfileDetailActivity extends Activity
}
else
{ /* previously selected certificate is not here anymore */
mSelectUserCert.getText1().setError("");
((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
mUserCertEntry = null;
}
mUserCertLoading = null;

View File

@ -18,7 +18,7 @@ package org.strongswan.android.ui.adapter;
import java.util.List;
import org.strongswan.android.R;
import org.strongswan.android.data.TrustedCertificateEntry;
import org.strongswan.android.security.TrustedCertificateEntry;
import android.content.Context;
import android.view.LayoutInflater;

View File

@ -0,0 +1,40 @@
/*
* Copyright (C) 2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.utils;
public class Utils
{
static final char[] HEXDIGITS = "0123456789abcdef".toCharArray();
/**
* Converts the given byte array to a hexadecimal string encoding.
*
* @param bytes byte array to convert
* @return hex string
*/
public static String bytesToHex(byte[] bytes)
{
char[] hex = new char[bytes.length * 2];
for (int i = 0; i < bytes.length; i++)
{
int value = bytes[i];
hex[i*2] = HEXDIGITS[(value & 0xf0) >> 4];
hex[i*2+1] = HEXDIGITS[ value & 0x0f];
}
return new String(hex);
}
}