Merge branch 'android-cert-import'

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

Also adds support for ECDSA keys on recent Android systems.
This commit is contained in:
Tobias Brunner
2014-07-22 10:51:32 +02:00
28 changed files with 1243 additions and 186 deletions
+16 -2
View File
@@ -20,12 +20,13 @@
android:versionCode="20" android:versionCode="20"
android:versionName="1.3.4" > 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.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:name=".logic.StrongSwanApplication"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/ApplicationTheme" android:theme="@style/ApplicationTheme"
@@ -63,7 +64,20 @@
android:label="@string/strongswan_shortcut" > android:label="@string/strongswan_shortcut" >
<intent-filter> <intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" /> <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> </intent-filter>
</activity> </activity>
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2012 Tobias Brunner * Copyright (C) 2012-2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil * Hochschule fuer Technik Rapperswil
* *
* This program is free software; you can redistribute it and/or modify it * This program is free software; you can redistribute it and/or modify it
@@ -17,6 +17,7 @@
#include "../android_jni.h" #include "../android_jni.h"
#include <utils/debug.h> #include <utils/debug.h>
#include <asn1/asn1.h>
typedef struct private_private_key_t private_private_key_t; typedef struct private_private_key_t private_private_key_t;
@@ -57,35 +58,62 @@ METHOD(private_key_t, sign, bool,
{ {
JNIEnv *env; JNIEnv *env;
jmethodID method_id; jmethodID method_id;
const char *method; const char *method = NULL;
jstring jmethod; jstring jmethod;
jobject jsignature; jobject jsignature;
jbyteArray jdata, jsigarray; jbyteArray jdata, jsigarray;
switch (scheme) switch (this->pubkey->get_type(this->pubkey))
{ {
case SIGN_RSA_EMSA_PKCS1_MD5: case KEY_RSA:
method = "MD5withRSA"; 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; break;
case SIGN_RSA_EMSA_PKCS1_SHA1: case KEY_ECDSA:
method = "SHA1withRSA"; switch (scheme)
break; {
case SIGN_RSA_EMSA_PKCS1_SHA224: case SIGN_ECDSA_256:
method = "SHA224withRSA"; method = "SHA256withECDSA";
break; break;
case SIGN_RSA_EMSA_PKCS1_SHA256: case SIGN_ECDSA_384:
method = "SHA256withRSA"; method = "SHA384withECDSA";
break; break;
case SIGN_RSA_EMSA_PKCS1_SHA384: case SIGN_ECDSA_521:
method = "SHA384withRSA"; method = "SHA512withECDSA";
break; break;
case SIGN_RSA_EMSA_PKCS1_SHA512: default:
method = "SHA512withRSA"; break;
}
break; break;
default: default:
DBG1(DBG_LIB, "signature scheme %N not supported via JNI", break;
signature_scheme_names, scheme); }
return FALSE; if (!method)
{
DBG1(DBG_LIB, "signature scheme %N not supported via JNI",
signature_scheme_names, scheme);
return FALSE;
} }
androidjni_attach_thread(&env); androidjni_attach_thread(&env);
@@ -142,7 +170,54 @@ METHOD(private_key_t, sign, bool,
{ {
goto failed; 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(); androidjni_detach_thread();
return TRUE; return TRUE;
@@ -157,7 +232,7 @@ failed:
METHOD(private_key_t, get_type, key_type_t, METHOD(private_key_t, get_type, key_type_t,
private_private_key_t *this) private_private_key_t *this)
{ {
return KEY_RSA; return this->pubkey->get_type(this->pubkey);
} }
METHOD(private_key_t, decrypt, bool, METHOD(private_key_t, decrypt, bool,
@@ -299,12 +299,12 @@ METHOD(charonservice_t, get_trusted_certificates, linked_list_t*,
method_id = (*env)->GetMethodID(env, method_id = (*env)->GetMethodID(env,
android_charonvpnservice_class, android_charonvpnservice_class,
"getTrustedCertificates", "(Ljava/lang/String;)[[B"); "getTrustedCertificates", "()[[B");
if (!method_id) if (!method_id)
{ {
goto failed; 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)) if (!jcerts || androidjni_exception_occurred(env))
{ {
goto failed; goto failed;
+1 -1
View File
@@ -8,4 +8,4 @@
# project structure. # project structure.
# Project target. # Project target.
target=android-14 target=android-19
@@ -13,7 +13,7 @@
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details. 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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="8dip" android:paddingBottom="8dip"
@@ -44,4 +44,4 @@
android:ellipsize="end" android:ellipsize="end"
android:textIsSelectable="false" /> android:textIsSelectable="false" />
</TwoLineListItem> </RelativeLayout>
@@ -13,7 +13,7 @@
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details. 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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight" android:minHeight="?android:attr/listPreferredItemHeight"
@@ -36,4 +36,4 @@
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary" /> android:textColor="?android:attr/textColorSecondary" />
</TwoLineListItem> </RelativeLayout>
@@ -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>
+3 -3
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
Copyright (C) 2012 Tobias Brunner Copyright (C) 2012-2014 Tobias Brunner
Hochschule fuer Technik Rapperswil Hochschule fuer Technik Rapperswil
This program is free software; you can redistribute it and/or modify it 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" > <menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item <item
android:id="@+id/menu_reload_certs" android:id="@+id/menu_manage_certs"
android:title="@string/reload_trusted_certs" android:title="@string/trusted_certs_title"
android:showAsAction="withText" /> android:showAsAction="withText" />
<item <item
@@ -20,7 +20,6 @@
<!-- Application --> <!-- Application -->
<string name="app_name">strongSwan VPN Client</string> <string name="app_name">strongSwan VPN Client</string>
<string name="main_activity_name">strongSwan</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="show_log">Log anzeigen</string>
<string name="search">Suchen</string> <string name="search">Suchen</string>
<string name="vpn_not_supported_title">VPN nicht unterstützt</string> <string name="vpn_not_supported_title">VPN nicht unterstützt</string>
@@ -76,8 +75,15 @@
<!-- Trusted certificate selection --> <!-- Trusted certificate selection -->
<string name="trusted_certs_title">CA-Zertifikate</string> <string name="trusted_certs_title">CA-Zertifikate</string>
<string name="no_certificates">Keine 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="system_tab">System</string>
<string name="user_tab">Benutzer</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 --> <!-- VPN state fragment -->
<string name="state_label">Status:</string> <string name="state_label">Status:</string>
@@ -20,7 +20,6 @@
<!-- Application --> <!-- Application -->
<string name="app_name">strongSwan klient VPN</string> <string name="app_name">strongSwan klient VPN</string>
<string name="main_activity_name">strongSwan</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="show_log">Pokaż log</string>
<string name="search">Szukaj</string> <string name="search">Szukaj</string>
<string name="vpn_not_supported_title">Nie obsługiwany VPN</string> <string name="vpn_not_supported_title">Nie obsługiwany VPN</string>
@@ -76,8 +75,15 @@
<!-- Trusted certificate selection --> <!-- Trusted certificate selection -->
<string name="trusted_certs_title">Certyfikaty CA</string> <string name="trusted_certs_title">Certyfikaty CA</string>
<string name="no_certificates">Brak certyfikatów</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="system_tab">System</string>
<string name="user_tab">Użytkownik</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 --> <!-- VPN state fragment -->
<string name="state_label">Status:</string> <string name="state_label">Status:</string>
@@ -17,7 +17,6 @@
<!-- Application --> <!-- Application -->
<string name="app_name">Клиент strongSwan VPN</string> <string name="app_name">Клиент strongSwan VPN</string>
<string name="main_activity_name">strongSwan</string> <string name="main_activity_name">strongSwan</string>
<string name="reload_trusted_certs">Обновить сертификат CA</string>
<string name="show_log">Журнал</string> <string name="show_log">Журнал</string>
<string name="search">Поиск</string> <string name="search">Поиск</string>
<string name="vpn_not_supported_title">VPN не поддерживается</string> <string name="vpn_not_supported_title">VPN не поддерживается</string>
@@ -73,8 +72,15 @@
<!-- Trusted certificate selection --> <!-- Trusted certificate selection -->
<string name="trusted_certs_title">Сертификаты CA</string> <string name="trusted_certs_title">Сертификаты CA</string>
<string name="no_certificates">Нет доступных сертификатов</string> <string name="no_certificates">Нет доступных сертификатов</string>
<string name="reload_trusted_certs">Обновить сертификат CA</string>
<string name="system_tab">Система</string> <string name="system_tab">Система</string>
<string name="user_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 --> <!-- VPN state fragment -->
<string name="state_label">Статус:</string> <string name="state_label">Статус:</string>
@@ -18,7 +18,6 @@
<!-- Application --> <!-- Application -->
<string name="app_name">strongSwan VPN клієнт</string> <string name="app_name">strongSwan VPN клієнт</string>
<string name="main_activity_name">strongSwan</string> <string name="main_activity_name">strongSwan</string>
<string name="reload_trusted_certs">Перезавантажити CA сертифікати</string>
<string name="show_log">Перегляд журналу</string> <string name="show_log">Перегляд журналу</string>
<string name="search">Пошук</string> <string name="search">Пошук</string>
<string name="vpn_not_supported_title">VPN не підтримуеться</string> <string name="vpn_not_supported_title">VPN не підтримуеться</string>
@@ -74,8 +73,15 @@
<!-- Trusted certificate selection --> <!-- Trusted certificate selection -->
<string name="trusted_certs_title">Сертифікати CA</string> <string name="trusted_certs_title">Сертифікати CA</string>
<string name="no_certificates">Немає сертифікатів</string> <string name="no_certificates">Немає сертифікатів</string>
<string name="reload_trusted_certs">Перезавантажити CA сертифікати</string>
<string name="system_tab">Система</string> <string name="system_tab">Система</string>
<string name="user_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 --> <!-- VPN state fragment -->
<string name="state_label">Статус:</string> <string name="state_label">Статус:</string>
+7 -1
View File
@@ -20,7 +20,6 @@
<!-- Application --> <!-- Application -->
<string name="app_name">strongSwan VPN Client</string> <string name="app_name">strongSwan VPN Client</string>
<string name="main_activity_name">strongSwan</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="show_log">View log</string>
<string name="search">Search</string> <string name="search">Search</string>
<string name="vpn_not_supported_title">VPN not supported</string> <string name="vpn_not_supported_title">VPN not supported</string>
@@ -76,8 +75,15 @@
<!-- Trusted certificate selection --> <!-- Trusted certificate selection -->
<string name="trusted_certs_title">CA certificates</string> <string name="trusted_certs_title">CA certificates</string>
<string name="no_certificates">No 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="system_tab">System</string>
<string name="user_tab">User</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 --> <!-- VPN state fragment -->
<string name="state_label">Status:</string> <string name="state_label">Status:</string>
@@ -419,25 +419,19 @@ public class CharonVpnService extends VpnService implements Runnable
* Function called via JNI to generate a list of DER encoded CA certificates * Function called via JNI to generate a list of DER encoded CA certificates
* as byte array. * as byte array.
* *
* @param hash optional alias (only hash part), if given matching certificates are returned
* @return a list of DER encoded CA certificates * @return a list of DER encoded CA certificates
*/ */
private byte[][] getTrustedCertificates(String hash) private byte[][] getTrustedCertificates()
{ {
ArrayList<byte[]> certs = new ArrayList<byte[]>(); ArrayList<byte[]> certs = new ArrayList<byte[]>();
TrustedCertificateManager certman = TrustedCertificateManager.getInstance(); TrustedCertificateManager certman = TrustedCertificateManager.getInstance();
try try
{ {
if (hash != null) String alias = this.mCurrentCertificateAlias;
if (alias != null)
{ {
String alias = "user:" + hash + ".0";
X509Certificate cert = certman.getCACertificateFromAlias(alias); X509Certificate cert = certman.getCACertificateFromAlias(alias);
if (cert == null) if (cert == null)
{
alias = "system:" + hash + ".0";
cert = certman.getCACertificateFromAlias(alias);
}
if (cert == null)
{ {
return null; return null;
} }
@@ -445,23 +439,10 @@ public class CharonVpnService extends VpnService implements Runnable
} }
else else
{ {
String alias = this.mCurrentCertificateAlias; for (X509Certificate cert : certman.getAllCACertificates().values())
if (alias != null)
{ {
X509Certificate cert = certman.getCACertificateFromAlias(alias);
if (cert == null)
{
return null;
}
certs.add(cert.getEncoded()); certs.add(cert.getEncoded());
} }
else
{
for (X509Certificate cert : certman.getAllCACertificates().values())
{
certs.add(cert.getEncoded());
}
}
} }
} }
catch (CertificateEncodingException e) catch (CertificateEncodingException e)
@@ -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;
}
}
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2012 Tobias Brunner * Copyright (C) 2012-2014 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi * Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager * Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil * Hochschule fuer Technik Rapperswil
@@ -21,6 +21,7 @@ import java.security.KeyStore;
import java.security.KeyStoreException; import java.security.KeyStoreException;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -32,13 +33,49 @@ public class TrustedCertificateManager
private static final String TAG = TrustedCertificateManager.class.getSimpleName(); private static final String TAG = TrustedCertificateManager.class.getSimpleName();
private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock(); private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();
private Hashtable<String, X509Certificate> mCACerts = new Hashtable<String, X509Certificate>(); private Hashtable<String, X509Certificate> mCACerts = new Hashtable<String, X509Certificate>();
private volatile boolean mReload;
private boolean mLoaded; 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 constructor to prevent instantiation from other classes.
*/ */
private TrustedCertificateManager() 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. * Invalidates the current load state so that the next call to load()
* As this takes a while it should be called asynchronously. * will force a reload of the cached CA certificates.
* @return reference to itself * @return reference to itself
*/ */
public TrustedCertificateManager reload() public TrustedCertificateManager reset()
{ {
Log.d(TAG, "Force reload of cached CA certificates"); Log.d(TAG, "Force reload of cached CA certificates on next load");
this.mLock.writeLock().lock(); this.mReload = true;
loadCertificates();
this.mLock.writeLock().unlock();
return this; return this;
} }
@@ -81,8 +116,9 @@ public class TrustedCertificateManager
{ {
Log.d(TAG, "Ensure cached CA certificates are loaded"); Log.d(TAG, "Ensure cached CA certificates are loaded");
this.mLock.writeLock().lock(); this.mLock.writeLock().lock();
if (!this.mLoaded) if (!this.mLoaded || this.mReload)
{ {
this.mReload = false;
loadCertificates(); loadCertificates();
} }
this.mLock.writeLock().unlock(); this.mLock.writeLock().unlock();
@@ -96,29 +132,23 @@ public class TrustedCertificateManager
private void loadCertificates() private void loadCertificates()
{ {
Log.d(TAG, "Load cached CA certificates"); Log.d(TAG, "Load cached CA certificates");
try Hashtable<String, X509Certificate> certs = new Hashtable<String, X509Certificate>();
for (KeyStore store : this.mKeyStores)
{ {
KeyStore store = KeyStore.getInstance("AndroidCAStore"); fetchCertificates(certs, store);
store.load(null, null);
this.mCACerts = fetchCertificates(store);
this.mLoaded = true;
Log.d(TAG, "Cached CA certificates loaded");
}
catch (Exception ex)
{
ex.printStackTrace();
this.mCACerts = new Hashtable<String, X509Certificate>();
} }
this.mCACerts = certs;
this.mLoaded = true;
Log.d(TAG, "Cached CA certificates loaded");
} }
/** /**
* Load all X.509 certificates from the given KeyStore. * Load all X.509 certificates from the given KeyStore.
* @param certs Hashtable to store certificates in
* @param store KeyStore to load certificates from * @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 try
{ {
Enumeration<String> aliases = store.aliases(); Enumeration<String> aliases = store.aliases();
@@ -137,7 +167,6 @@ public class TrustedCertificateManager
{ {
ex.printStackTrace(); ex.printStackTrace();
} }
return certs;
} }
/** /**
@@ -157,27 +186,28 @@ public class TrustedCertificateManager
else else
{ /* if we cannot get the lock load it directly from the KeyStore, { /* if we cannot get the lock load it directly from the KeyStore,
* should be fast for a single certificate */ * should be fast for a single certificate */
try for (KeyStore store : this.mKeyStores)
{ {
KeyStore store = KeyStore.getInstance("AndroidCAStore"); try
store.load(null, null);
Certificate cert = store.getCertificate(alias);
if (cert != null && cert instanceof X509Certificate)
{ {
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; 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 * @return Hashtable mapping aliases to certificates
*/ */
@SuppressWarnings("unchecked") @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 * @return Hashtable mapping aliases to certificates
*/ */
public Hashtable<String, X509Certificate> getSystemCACertificates() public Hashtable<String, X509Certificate> getCACertificates(TrustedCertificateSource source)
{ {
Hashtable<String, X509Certificate> certs = new Hashtable<String, X509Certificate>(); Hashtable<String, X509Certificate> certs = new Hashtable<String, X509Certificate>();
this.mLock.readLock().lock(); this.mLock.readLock().lock();
for (String alias : this.mCACerts.keySet()) for (String alias : this.mCACerts.keySet())
{ {
if (alias.startsWith("system:")) if (alias.startsWith(source.getPrefix()))
{
certs.put(alias, this.mCACerts.get(alias));
}
}
this.mLock.readLock().unlock();
return certs;
}
/**
* Get only the CA certificates installed by the user.
* @return Hashtable mapping aliases to certificates
*/
public Hashtable<String, X509Certificate> getUserCACertificates()
{
Hashtable<String, X509Certificate> certs = new Hashtable<String, X509Certificate>();
this.mLock.readLock().lock();
for (String alias : this.mCACerts.keySet())
{
if (alias.startsWith("user:"))
{ {
certs.put(alias, this.mCACerts.get(alias)); certs.put(alias, this.mCACerts.get(alias));
} }
@@ -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());
}
}
@@ -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();
}
}
}
@@ -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;
}
}
@@ -13,7 +13,7 @@
* for more details. * for more details.
*/ */
package org.strongswan.android.data; package org.strongswan.android.security;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
@@ -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();
}
}
@@ -101,7 +101,7 @@ public class MainActivity extends Activity implements OnVpnProfileSelectedListen
bar.setDisplayShowTitleEnabled(false); bar.setDisplayShowTitleEnabled(false);
/* load CA certificates in a background task */ /* load CA certificates in a background task */
new CertificateLoadTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, false); new LoadCertificatesTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
@Override @Override
@@ -140,8 +140,9 @@ public class MainActivity extends Activity implements OnVpnProfileSelectedListen
{ {
switch (item.getItemId()) switch (item.getItemId())
{ {
case R.id.menu_reload_certs: case R.id.menu_manage_certs:
new CertificateLoadTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, true); Intent certIntent = new Intent(this, TrustedCertificatesActivity.class);
startActivity(certIntent);
return true; return true;
case R.id.menu_show_log: case R.id.menu_show_log:
Intent logIntent = new Intent(this, LogActivity.class); 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 @Override
protected void onPreExecute() protected void onPreExecute()
@@ -290,12 +291,8 @@ public class MainActivity extends Activity implements OnVpnProfileSelectedListen
setProgressBarIndeterminateVisibility(true); setProgressBarIndeterminateVisibility(true);
} }
@Override @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(); return TrustedCertificateManager.getInstance().load();
} }
@Override @Override
@@ -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();
}
}
}
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2012 Tobias Brunner * Copyright (C) 2012-2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil * Hochschule fuer Technik Rapperswil
* *
* This program is free software; you can redistribute it and/or modify it * 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 java.util.Map.Entry;
import org.strongswan.android.R; import org.strongswan.android.R;
import org.strongswan.android.data.TrustedCertificateEntry;
import org.strongswan.android.logic.TrustedCertificateManager; 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 org.strongswan.android.ui.adapter.TrustedCertificateAdapter;
import android.app.Activity; import android.app.Activity;
@@ -45,9 +46,10 @@ import android.widget.SearchView.OnQueryTextListener;
public class TrustedCertificateListFragment extends ListFragment implements LoaderCallbacks<List<TrustedCertificateEntry>>, OnQueryTextListener public class TrustedCertificateListFragment extends ListFragment implements LoaderCallbacks<List<TrustedCertificateEntry>>, OnQueryTextListener
{ {
public static final String EXTRA_CERTIFICATE_SOURCE = "certificate_source";
private OnTrustedCertificateSelectedListener mListener; private OnTrustedCertificateSelectedListener mListener;
private TrustedCertificateAdapter mAdapter; private TrustedCertificateAdapter mAdapter;
private boolean mUser; private TrustedCertificateSource mSource = TrustedCertificateSource.SYSTEM;
/** /**
* The activity containing this fragment should implement this interface * The activity containing this fragment should implement this interface
@@ -69,8 +71,11 @@ public class TrustedCertificateListFragment extends ListFragment implements Load
setListShown(false); setListShown(false);
/* non empty arguments mean we list user certificate */ Bundle arguments = getArguments();
mUser = getArguments() != null; if (arguments != null)
{
mSource = (TrustedCertificateSource)arguments.getSerializable(EXTRA_CERTIFICATE_SOURCE);
}
getLoaderManager().initLoader(0, null, this); getLoaderManager().initLoader(0, null, this);
} }
@@ -118,10 +123,22 @@ public class TrustedCertificateListFragment extends ListFragment implements Load
return true; return true;
} }
/**
* Reset the loader of this list fragment
*/
public void reset()
{
if (isResumed())
{
setListShown(false);
}
getLoaderManager().restartLoader(0, null, this);
}
@Override @Override
public Loader<List<TrustedCertificateEntry>> onCreateLoader(int id, Bundle args) public Loader<List<TrustedCertificateEntry>> onCreateLoader(int id, Bundle args)
{ /* we don't need the id as we have only one loader */ { /* we don't need the id as we have only one loader */
return new CertificateListLoader(getActivity(), mUser); return new CertificateListLoader(getActivity(), mSource);
} }
@Override @Override
@@ -157,22 +174,21 @@ public class TrustedCertificateListFragment extends ListFragment implements Load
public static class CertificateListLoader extends AsyncTaskLoader<List<TrustedCertificateEntry>> public static class CertificateListLoader extends AsyncTaskLoader<List<TrustedCertificateEntry>>
{ {
private List<TrustedCertificateEntry> mData; 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); super(context);
mUser = user; mSource = source;
} }
@Override @Override
public List<TrustedCertificateEntry> loadInBackground() public List<TrustedCertificateEntry> loadInBackground()
{ {
TrustedCertificateManager certman = TrustedCertificateManager.getInstance().load(); TrustedCertificateManager certman = TrustedCertificateManager.getInstance().load();
Hashtable<String,X509Certificate> certificates; Hashtable<String,X509Certificate> certificates = certman.getCACertificates(mSource);
List<TrustedCertificateEntry> selected; List<TrustedCertificateEntry> selected;
certificates = mUser ? certman.getUserCACertificates() : certman.getSystemCACertificates();
selected = new ArrayList<TrustedCertificateEntry>(); selected = new ArrayList<TrustedCertificateEntry>();
for (Entry<String, X509Certificate> entry : certificates.entrySet()) for (Entry<String, X509Certificate> entry : certificates.entrySet())
{ {
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2012 Tobias Brunner * Copyright (C) 2012-2014 Tobias Brunner
* Hochschule fuer Technik Rapperswil * Hochschule fuer Technik Rapperswil
* *
* This program is free software; you can redistribute it and/or modify it * This program is free software; you can redistribute it and/or modify it
@@ -15,9 +15,14 @@
package org.strongswan.android.ui; package org.strongswan.android.ui;
import java.security.KeyStore;
import org.strongswan.android.R; import org.strongswan.android.R;
import org.strongswan.android.data.TrustedCertificateEntry;
import org.strongswan.android.data.VpnProfileDataSource; 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;
import android.app.ActionBar.Tab; import android.app.ActionBar.Tab;
@@ -25,11 +30,18 @@ import android.app.Activity;
import android.app.Fragment; import android.app.Fragment;
import android.app.FragmentTransaction; import android.app.FragmentTransaction;
import android.content.Intent; import android.content.Intent;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem; 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 @Override
public void onCreate(Bundle savedInstanceState) public void onCreate(Bundle savedInstanceState)
{ {
@@ -40,19 +52,31 @@ public class TrustedCertificatesActivity extends Activity implements TrustedCert
actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
TrustedCertificatesTabListener listener;
listener = new TrustedCertificatesTabListener(this, "system", TrustedCertificateSource.SYSTEM);
actionBar.addTab(actionBar actionBar.addTab(actionBar
.newTab() .newTab()
.setText(R.string.system_tab) .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 actionBar.addTab(actionBar
.newTab() .newTab()
.setText(R.string.user_tab) .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) if (savedInstanceState != null)
{ {
actionBar.setSelectedNavigationItem(savedInstanceState.getInt("tab", 0)); actionBar.setSelectedNavigationItem(savedInstanceState.getInt("tab", 0));
} }
mSelect = SELECT_CERTIFICATE.equals(getIntent().getAction());
} }
@Override @Override
@@ -62,6 +86,23 @@ public class TrustedCertificatesActivity extends Activity implements TrustedCert
outState.putInt("tab", getActionBar().getSelectedNavigationIndex()); 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 @Override
public boolean onOptionsItemSelected(MenuItem item) public boolean onOptionsItemSelected(MenuItem item)
{ {
@@ -70,30 +111,95 @@ public class TrustedCertificatesActivity extends Activity implements TrustedCert
case android.R.id.home: case android.R.id.home:
finish(); finish();
return true; 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); 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 @Override
public void onTrustedCertificateSelected(TrustedCertificateEntry selected) public void onTrustedCertificateSelected(TrustedCertificateEntry selected)
{ {
/* the user selected a certificate, return to calling activity */ if (mSelect)
Intent intent = new Intent(); {
intent.putExtra(VpnProfileDataSource.KEY_CERTIFICATE, selected.getAlias()); /* the user selected a certificate, return to calling activity */
setResult(Activity.RESULT_OK, intent); Intent intent = new Intent();
finish(); 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 public static class TrustedCertificatesTabListener implements ActionBar.TabListener
{ {
private final String mTag; private final String mTag;
private final boolean mUser; private final TrustedCertificateSource mSource;
private Fragment mFragment; private Fragment mFragment;
public TrustedCertificatesTabListener(Activity activity, String tag, boolean user) public TrustedCertificatesTabListener(Activity activity, String tag, TrustedCertificateSource source)
{ {
mTag = tag; mTag = tag;
mUser = user; mSource = source;
/* check to see if we already have a fragment for this tab, probably /* check to see if we already have a fragment for this tab, probably
* from a previously saved state. if so, deactivate it, because the * from a previously saved state. if so, deactivate it, because the
* initial state is that no tab is shown */ * initial state is that no tab is shown */
@@ -112,10 +218,9 @@ public class TrustedCertificatesActivity extends Activity implements TrustedCert
if (mFragment == null) if (mFragment == null)
{ {
mFragment = new TrustedCertificateListFragment(); mFragment = new TrustedCertificateListFragment();
if (mUser) Bundle args = new Bundle();
{ /* use non empty arguments to indicate this */ args.putSerializable(TrustedCertificateListFragment.EXTRA_CERTIFICATE_SOURCE, mSource);
mFragment.setArguments(new Bundle()); mFragment.setArguments(args);
}
ft.add(android.R.id.content, mFragment, mTag); ft.add(android.R.id.content, mFragment, mTag);
} }
else else
@@ -138,5 +243,13 @@ public class TrustedCertificatesActivity extends Activity implements TrustedCert
{ {
/* nothing to be done */ /* nothing to be done */
} }
public void reset()
{
if (mFragment != null)
{
((TrustedCertificateListFragment)mFragment).reset();
}
}
} }
} }
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2012 Tobias Brunner * Copyright (C) 2012-2014 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi * Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager * Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil * Hochschule fuer Technik Rapperswil
@@ -20,11 +20,11 @@ package org.strongswan.android.ui;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import org.strongswan.android.R; import org.strongswan.android.R;
import org.strongswan.android.data.TrustedCertificateEntry;
import org.strongswan.android.data.VpnProfile; import org.strongswan.android.data.VpnProfile;
import org.strongswan.android.data.VpnProfileDataSource; import org.strongswan.android.data.VpnProfileDataSource;
import org.strongswan.android.data.VpnType; import org.strongswan.android.data.VpnType;
import org.strongswan.android.logic.TrustedCertificateManager; import org.strongswan.android.logic.TrustedCertificateManager;
import org.strongswan.android.security.TrustedCertificateEntry;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
@@ -52,8 +52,9 @@ import android.widget.CheckBox;
import android.widget.CompoundButton; import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText; import android.widget.EditText;
import android.widget.RelativeLayout;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TwoLineListItem; import android.widget.TextView;
public class VpnProfileDetailActivity extends Activity public class VpnProfileDetailActivity extends Activity
{ {
@@ -73,10 +74,10 @@ public class VpnProfileDetailActivity extends Activity
private EditText mUsername; private EditText mUsername;
private EditText mPassword; private EditText mPassword;
private ViewGroup mUserCertificate; private ViewGroup mUserCertificate;
private TwoLineListItem mSelectUserCert; private RelativeLayout mSelectUserCert;
private CheckBox mCheckAuto; private CheckBox mCheckAuto;
private TwoLineListItem mSelectCert; private RelativeLayout mSelectCert;
private TwoLineListItem mTncNotice; private RelativeLayout mTncNotice;
@Override @Override
public void onCreate(Bundle savedInstanceState) public void onCreate(Bundle savedInstanceState)
@@ -94,17 +95,17 @@ public class VpnProfileDetailActivity extends Activity
mName = (EditText)findViewById(R.id.name); mName = (EditText)findViewById(R.id.name);
mGateway = (EditText)findViewById(R.id.gateway); mGateway = (EditText)findViewById(R.id.gateway);
mSelectVpnType = (Spinner)findViewById(R.id.vpn_type); 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); mUsernamePassword = (ViewGroup)findViewById(R.id.username_password_group);
mUsername = (EditText)findViewById(R.id.username); mUsername = (EditText)findViewById(R.id.username);
mPassword = (EditText)findViewById(R.id.password); mPassword = (EditText)findViewById(R.id.password);
mUserCertificate = (ViewGroup)findViewById(R.id.user_certificate_group); 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); 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() { mSelectVpnType.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override @Override
@@ -122,8 +123,8 @@ public class VpnProfileDetailActivity extends Activity
} }
}); });
mTncNotice.getText1().setText(R.string.tnc_notice_title); ((TextView)mTncNotice.findViewById(android.R.id.text1)).setText(R.string.tnc_notice_title);
mTncNotice.getText2().setText(R.string.tnc_notice_subtitle); ((TextView)mTncNotice.findViewById(android.R.id.text2)).setText(R.string.tnc_notice_subtitle);
mTncNotice.setOnClickListener(new OnClickListener() { mTncNotice.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View v) public void onClick(View v)
@@ -147,6 +148,7 @@ public class VpnProfileDetailActivity extends Activity
public void onClick(View v) public void onClick(View v)
{ {
Intent intent = new Intent(VpnProfileDetailActivity.this, TrustedCertificatesActivity.class); Intent intent = new Intent(VpnProfileDetailActivity.this, TrustedCertificatesActivity.class);
intent.setAction(TrustedCertificatesActivity.SELECT_CERTIFICATE);
startActivityForResult(intent, SELECT_TRUSTED_CERTIFICATE); startActivityForResult(intent, SELECT_TRUSTED_CERTIFICATE);
} }
}); });
@@ -246,19 +248,19 @@ public class VpnProfileDetailActivity extends Activity
{ {
if (mUserCertLoading != null) if (mUserCertLoading != null)
{ {
mSelectUserCert.getText1().setText(mUserCertLoading); ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertLoading);
mSelectUserCert.getText2().setText(R.string.loading); ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.loading);
} }
else if (mUserCertEntry != null) else if (mUserCertEntry != null)
{ /* clear any errors and set the new data */ { /* clear any errors and set the new data */
mSelectUserCert.getText1().setError(null); ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError(null);
mSelectUserCert.getText1().setText(mUserCertEntry.getAlias()); ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertEntry.getAlias());
mSelectUserCert.getText2().setText(mUserCertEntry.getCertificate().getSubjectDN().toString()); ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(mUserCertEntry.getCertificate().getSubjectDN().toString());
} }
else else
{ {
mSelectUserCert.getText1().setText(R.string.profile_user_select_certificate_label); ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(R.string.profile_user_select_certificate_label);
mSelectUserCert.getText2().setText(R.string.profile_user_select_certificate); ((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) if (mCertEntry != null)
{ {
mSelectCert.getText1().setText(mCertEntry.getSubjectPrimary()); ((TextView)mSelectCert.findViewById(android.R.id.text1)).setText(mCertEntry.getSubjectPrimary());
mSelectCert.getText2().setText(mCertEntry.getSubjectSecondary()); ((TextView)mSelectCert.findViewById(android.R.id.text2)).setText(mCertEntry.getSubjectSecondary());
} }
else else
{ {
mSelectCert.getText1().setText(R.string.profile_ca_select_certificate_label); ((TextView)mSelectCert.findViewById(android.R.id.text1)).setText(R.string.profile_ca_select_certificate_label);
mSelectCert.getText2().setText(R.string.profile_ca_select_certificate); ((TextView)mSelectCert.findViewById(android.R.id.text2)).setText(R.string.profile_ca_select_certificate);
} }
} }
else else
@@ -357,7 +359,7 @@ public class VpnProfileDetailActivity extends Activity
} }
if (mVpnType.getRequiresCertificate() && mUserCertEntry == null) if (mVpnType.getRequiresCertificate() && mUserCertEntry == null)
{ /* let's show an error icon */ { /* let's show an error icon */
mSelectUserCert.getText1().setError(""); ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
valid = false; valid = false;
} }
if (!mCheckAuto.isChecked() && mCertEntry == null) if (!mCheckAuto.isChecked() && mCertEntry == null)
@@ -545,7 +547,7 @@ public class VpnProfileDetailActivity extends Activity
} }
else else
{ /* previously selected certificate is not here anymore */ { /* previously selected certificate is not here anymore */
mSelectUserCert.getText1().setError(""); ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
mUserCertEntry = null; mUserCertEntry = null;
} }
mUserCertLoading = null; mUserCertLoading = null;
@@ -18,7 +18,7 @@ package org.strongswan.android.ui.adapter;
import java.util.List; import java.util.List;
import org.strongswan.android.R; import org.strongswan.android.R;
import org.strongswan.android.data.TrustedCertificateEntry; import org.strongswan.android.security.TrustedCertificateEntry;
import android.content.Context; import android.content.Context;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -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);
}
}