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" >
-
+
-
+
+
+
+
+
+
+
+
+
+
+
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
+#include
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.
-->
-
-
+
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.
-->
-
-
+
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 @@
+
+
+
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 @@
strongSwan VPN Client
strongSwan
- CA-Zertifikate neu laden
Log anzeigen
Suchen
VPN nicht unterstützt
@@ -76,8 +75,15 @@
CA-Zertifikate
Keine Zertifikate
+ CA-Zertifikate neu laden
System
Benutzer
+ Importiert
+ Zertifikat löschen?
+ Das Zertifikat wird permanent entfernt!
+ Zertifikat importieren
+ Zertifikat erfolgreich importiert
+ Zertifikat-Import fehlgeschlagen
Status:
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 @@
strongSwan klient VPN
strongSwan
- Przeładuj certyfikaty CA
Pokaż log
Szukaj
Nie obsługiwany VPN
@@ -76,8 +75,15 @@
Certyfikaty CA
Brak certyfikatów
+ Przeładuj certyfikaty CA
System
Użytkownik
+ Imported
+ Delete certificate?
+ The certificate will be permanently removed!
+ Import certificate
+ Certificate successfully imported
+ Failed to import certificate
Status:
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 @@
Клиент strongSwan VPN
strongSwan
- Обновить сертификат CA
Журнал
Поиск
VPN не поддерживается
@@ -73,8 +72,15 @@
Сертификаты CA
Нет доступных сертификатов
+ Обновить сертификат CA
Система
Пользователь
+ Imported
+ Delete certificate?
+ The certificate will be permanently removed!
+ Import certificate
+ Certificate successfully imported
+ Failed to import certificate
Статус:
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 @@
strongSwan VPN клієнт
strongSwan
- Перезавантажити CA сертифікати
Перегляд журналу
Пошук
VPN не підтримуеться
@@ -74,8 +73,15 @@
Сертифікати CA
Немає сертифікатів
+ Перезавантажити CA сертифікати
Система
Користувач
+ Imported
+ Delete certificate?
+ The certificate will be permanently removed!
+ Import certificate
+ Certificate successfully imported
+ Failed to import certificate
Статус:
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 @@
strongSwan VPN Client
strongSwan
- Reload CA certificates
View log
Search
VPN not supported
@@ -76,8 +75,15 @@
CA certificates
No certificates
+ Reload CA certificates
System
User
+ Imported
+ Delete certificate?
+ The certificate will be permanently removed!
+ Import certificate
+ Certificate successfully imported
+ Failed to import certificate
Status:
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,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 certs = new ArrayList();
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)
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 .
+ *
+ * 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 mCACerts = new Hashtable();
+ private volatile boolean mReload;
private boolean mLoaded;
+ private final ArrayList mKeyStores = new ArrayList();
+
+ 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 certs = new Hashtable();
+ 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();
+ 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 fetchCertificates(KeyStore store)
+ private void fetchCertificates(Hashtable certs, KeyStore store)
{
- Hashtable certs = new Hashtable();
try
{
Enumeration 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 getSystemCACertificates()
+ public Hashtable getCACertificates(TrustedCertificateSource source)
{
Hashtable certs = new Hashtable();
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 getUserCACertificates()
- {
- Hashtable certs = new Hashtable();
- 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 .
+ *
+ * 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 .
+ *
+ * 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 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 .
+ *
+ * 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 aliases()
+ {
+ ArrayList list = new ArrayList();
+ 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
similarity index 98%
rename from src/frontends/android/src/org/strongswan/android/data/TrustedCertificateEntry.java
rename to 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 .
+ *
+ * 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
+ private class LoadCertificatesTask extends AsyncTask
{
@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 .
+ *
+ * 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>, 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> 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>
{
private List 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 loadInBackground()
{
TrustedCertificateManager certman = TrustedCertificateManager.getInstance().load();
- Hashtable certificates;
+ Hashtable certificates = certman.getCACertificates(mSource);
List selected;
- certificates = mUser ? certman.getUserCACertificates() : certman.getSystemCACertificates();
selected = new ArrayList();
for (Entry 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
@@ -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();
+ }
+ }
}
}
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 .
+ *
+ * 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);
+ }
+}