diff options
Diffstat (limited to 'src')
28 files changed, 1243 insertions, 186 deletions
diff --git a/src/frontends/android/AndroidManifest.xml b/src/frontends/android/AndroidManifest.xml index e3e7ec631..1a5af0d15 100644 --- a/src/frontends/android/AndroidManifest.xml +++ b/src/frontends/android/AndroidManifest.xml @@ -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> diff --git a/src/frontends/android/jni/libandroidbridge/backend/android_private_key.c b/src/frontends/android/jni/libandroidbridge/backend/android_private_key.c index 1aeabac2f..1985f0e98 100644 --- a/src/frontends/android/jni/libandroidbridge/backend/android_private_key.c +++ b/src/frontends/android/jni/libandroidbridge/backend/android_private_key.c @@ -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, diff --git a/src/frontends/android/jni/libandroidbridge/charonservice.c b/src/frontends/android/jni/libandroidbridge/charonservice.c index 707bb3df0..32bf28f09 100644 --- a/src/frontends/android/jni/libandroidbridge/charonservice.c +++ b/src/frontends/android/jni/libandroidbridge/charonservice.c @@ -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; diff --git a/src/frontends/android/project.properties b/src/frontends/android/project.properties index 730e911f2..a5578ba09 100644 --- a/src/frontends/android/project.properties +++ b/src/frontends/android/project.properties @@ -8,4 +8,4 @@ # project structure. # Project target. -target=android-14 +target=android-19 diff --git a/src/frontends/android/res/layout/remediation_instruction_item.xml b/src/frontends/android/res/layout/remediation_instruction_item.xml index 30dfb2219..c25e6c123 100644 --- a/src/frontends/android/res/layout/remediation_instruction_item.xml +++ b/src/frontends/android/res/layout/remediation_instruction_item.xml @@ -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> diff --git a/src/frontends/android/res/layout/two_line_button.xml b/src/frontends/android/res/layout/two_line_button.xml index c8c25811b..89d095295 100644 --- a/src/frontends/android/res/layout/two_line_button.xml +++ b/src/frontends/android/res/layout/two_line_button.xml @@ -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> diff --git a/src/frontends/android/res/menu/certificates.xml b/src/frontends/android/res/menu/certificates.xml new file mode 100644 index 000000000..6066cab60 --- /dev/null +++ b/src/frontends/android/res/menu/certificates.xml @@ -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> diff --git a/src/frontends/android/res/menu/main.xml b/src/frontends/android/res/menu/main.xml index 4063110da..3dde5227e 100644 --- a/src/frontends/android/res/menu/main.xml +++ b/src/frontends/android/res/menu/main.xml @@ -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 diff --git a/src/frontends/android/res/values-de/strings.xml b/src/frontends/android/res/values-de/strings.xml index db7698135..491fe8a8a 100644 --- a/src/frontends/android/res/values-de/strings.xml +++ b/src/frontends/android/res/values-de/strings.xml @@ -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> diff --git a/src/frontends/android/res/values-pl/strings.xml b/src/frontends/android/res/values-pl/strings.xml index 7aa9c51a7..d0cfa48f1 100644 --- a/src/frontends/android/res/values-pl/strings.xml +++ b/src/frontends/android/res/values-pl/strings.xml @@ -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> diff --git a/src/frontends/android/res/values-ru/strings.xml b/src/frontends/android/res/values-ru/strings.xml index 3838485af..eb69183db 100644 --- a/src/frontends/android/res/values-ru/strings.xml +++ b/src/frontends/android/res/values-ru/strings.xml @@ -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> diff --git a/src/frontends/android/res/values-ua/strings.xml b/src/frontends/android/res/values-ua/strings.xml index df016ff8f..e23b9b9b2 100644 --- a/src/frontends/android/res/values-ua/strings.xml +++ b/src/frontends/android/res/values-ua/strings.xml @@ -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> diff --git a/src/frontends/android/res/values/strings.xml b/src/frontends/android/res/values/strings.xml index 180948969..933a80aff 100644 --- a/src/frontends/android/res/values/strings.xml +++ b/src/frontends/android/res/values/strings.xml @@ -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> diff --git a/src/frontends/android/src/org/strongswan/android/logic/CharonVpnService.java b/src/frontends/android/src/org/strongswan/android/logic/CharonVpnService.java index e45a7d9bd..31172ab44 100644 --- a/src/frontends/android/src/org/strongswan/android/logic/CharonVpnService.java +++ b/src/frontends/android/src/org/strongswan/android/logic/CharonVpnService.java @@ -419,49 +419,30 @@ 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; } certs.add(cert.getEncoded()); } 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) diff --git a/src/frontends/android/src/org/strongswan/android/logic/StrongSwanApplication.java b/src/frontends/android/src/org/strongswan/android/logic/StrongSwanApplication.java new file mode 100644 index 000000000..d642b67b3 --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/logic/StrongSwanApplication.java @@ -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; + } +} diff --git a/src/frontends/android/src/org/strongswan/android/logic/TrustedCertificateManager.java b/src/frontends/android/src/org/strongswan/android/logic/TrustedCertificateManager.java index 95fdecf14..82a7cbe4e 100644 --- a/src/frontends/android/src/org/strongswan/android/logic/TrustedCertificateManager.java +++ b/src/frontends/android/src/org/strongswan/android/logic/TrustedCertificateManager.java @@ -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 - { - 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) + Hashtable<String, X509Certificate> certs = new Hashtable<String, X509Certificate>(); + for (KeyStore store : this.mKeyStores) { - 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. - * @return Hashtable mapping aliases to certificates - */ - public Hashtable<String, X509Certificate> getSystemCACertificates() - { - 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. + * Get all certificates from the given source. + * @param source type to filter certificates * @return Hashtable mapping aliases to certificates */ - public Hashtable<String, X509Certificate> getUserCACertificates() + 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("user:")) + if (alias.startsWith(source.getPrefix())) { certs.put(alias, this.mCACerts.get(alias)); } diff --git a/src/frontends/android/src/org/strongswan/android/security/LocalCertificateKeyStoreProvider.java b/src/frontends/android/src/org/strongswan/android/security/LocalCertificateKeyStoreProvider.java new file mode 100644 index 000000000..c49b1044f --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/security/LocalCertificateKeyStoreProvider.java @@ -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()); + } +} diff --git a/src/frontends/android/src/org/strongswan/android/security/LocalCertificateKeyStoreSpi.java b/src/frontends/android/src/org/strongswan/android/security/LocalCertificateKeyStoreSpi.java new file mode 100644 index 000000000..64a48a9bb --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/security/LocalCertificateKeyStoreSpi.java @@ -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(); + } + } +} diff --git a/src/frontends/android/src/org/strongswan/android/security/LocalCertificateStore.java b/src/frontends/android/src/org/strongswan/android/security/LocalCertificateStore.java new file mode 100644 index 000000000..cec5c603d --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/security/LocalCertificateStore.java @@ -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; + } +} diff --git a/src/frontends/android/src/org/strongswan/android/data/TrustedCertificateEntry.java b/src/frontends/android/src/org/strongswan/android/security/TrustedCertificateEntry.java index de7ea32b4..143741faf 100644 --- a/src/frontends/android/src/org/strongswan/android/data/TrustedCertificateEntry.java +++ b/src/frontends/android/src/org/strongswan/android/security/TrustedCertificateEntry.java @@ -13,7 +13,7 @@ * for more details. */ -package org.strongswan.android.data; +package org.strongswan.android.security; import java.security.cert.X509Certificate; diff --git a/src/frontends/android/src/org/strongswan/android/ui/CertificateDeleteConfirmationDialog.java b/src/frontends/android/src/org/strongswan/android/ui/CertificateDeleteConfirmationDialog.java new file mode 100644 index 000000000..c381900c6 --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/ui/CertificateDeleteConfirmationDialog.java @@ -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(); + } +} diff --git a/src/frontends/android/src/org/strongswan/android/ui/MainActivity.java b/src/frontends/android/src/org/strongswan/android/ui/MainActivity.java index 3cf395054..a2ad80e82 100644 --- a/src/frontends/android/src/org/strongswan/android/ui/MainActivity.java +++ b/src/frontends/android/src/org/strongswan/android/ui/MainActivity.java @@ -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 diff --git a/src/frontends/android/src/org/strongswan/android/ui/TrustedCertificateImportActivity.java b/src/frontends/android/src/org/strongswan/android/ui/TrustedCertificateImportActivity.java new file mode 100644 index 000000000..61bd2c9a2 --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/ui/TrustedCertificateImportActivity.java @@ -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(); + } + } +} diff --git a/src/frontends/android/src/org/strongswan/android/ui/TrustedCertificateListFragment.java b/src/frontends/android/src/org/strongswan/android/ui/TrustedCertificateListFragment.java index 4e8e0ddeb..8bd39c435 100644 --- a/src/frontends/android/src/org/strongswan/android/ui/TrustedCertificateListFragment.java +++ b/src/frontends/android/src/org/strongswan/android/ui/TrustedCertificateListFragment.java @@ -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()) { diff --git a/src/frontends/android/src/org/strongswan/android/ui/TrustedCertificatesActivity.java b/src/frontends/android/src/org/strongswan/android/ui/TrustedCertificatesActivity.java index 967d25a02..663950c16 100644 --- a/src/frontends/android/src/org/strongswan/android/ui/TrustedCertificatesActivity.java +++ b/src/frontends/android/src/org/strongswan/android/ui/TrustedCertificatesActivity.java @@ -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 @@ -63,6 +87,23 @@ public class TrustedCertificatesActivity extends Activity implements TrustedCert } @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) { switch (item.getItemId()) @@ -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(); + } + } } } diff --git a/src/frontends/android/src/org/strongswan/android/ui/VpnProfileDetailActivity.java b/src/frontends/android/src/org/strongswan/android/ui/VpnProfileDetailActivity.java index baad9611d..39d37005d 100644 --- a/src/frontends/android/src/org/strongswan/android/ui/VpnProfileDetailActivity.java +++ b/src/frontends/android/src/org/strongswan/android/ui/VpnProfileDetailActivity.java @@ -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; diff --git a/src/frontends/android/src/org/strongswan/android/ui/adapter/TrustedCertificateAdapter.java b/src/frontends/android/src/org/strongswan/android/ui/adapter/TrustedCertificateAdapter.java index a97360d58..3795bb199 100644 --- a/src/frontends/android/src/org/strongswan/android/ui/adapter/TrustedCertificateAdapter.java +++ b/src/frontends/android/src/org/strongswan/android/ui/adapter/TrustedCertificateAdapter.java @@ -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; diff --git a/src/frontends/android/src/org/strongswan/android/utils/Utils.java b/src/frontends/android/src/org/strongswan/android/utils/Utils.java new file mode 100644 index 000000000..b5c447f31 --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/utils/Utils.java @@ -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); + } +} |