Merge branch 'android-client-cert'

Introduces IKEv2 client certificate authentication for the Android App.
This commit is contained in:
Tobias Brunner
2012-09-04 13:58:49 +02:00
26 changed files with 931 additions and 183 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ include $(CLEAR_VARS)
# this is the list of plugins that are built into libstrongswan and charon
# also these plugins are loaded by default (if not changed in strongswan.conf)
strongswan_CHARON_PLUGINS := android-log openssl fips-prf random nonce pubkey \
pkcs1 pem xcbc hmac kernel-netlink socket-default android \
pkcs1 pkcs8 pem xcbc hmac kernel-netlink socket-default android \
stroke eap-identity eap-mschapv2 eap-md5 eap-gtc
ifneq ($(strongswan_BUILD_SCEPCLIENT),)
+1 -1
View File
@@ -2,7 +2,7 @@ LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
strongswan_CHARON_PLUGINS := android-log openssl fips-prf random nonce pubkey \
pkcs1 pem xcbc hmac socket-default \
pkcs1 pkcs8 pem xcbc hmac socket-default \
eap-identity eap-mschapv2 eap-md5 eap-gtc
strongswan_PLUGINS := $(strongswan_CHARON_PLUGINS)
@@ -90,13 +90,16 @@ static inline bool androidjni_exception_occurred(JNIEnv *env)
*/
static inline char *androidjni_convert_jstring(JNIEnv *env, jstring jstr)
{
char *str;
char *str = NULL;
jsize len;
len = (*env)->GetStringUTFLength(env, jstr);
str = malloc(len + 1);
(*env)->GetStringUTFRegion(env, jstr, 0, len, str);
str[len] = '\0';
if (jstr)
{
len = (*env)->GetStringUTFLength(env, jstr);
str = malloc(len + 1);
(*env)->GetStringUTFRegion(env, jstr, 0, len, str);
str[len] = '\0';
}
return str;
}
@@ -49,6 +49,15 @@ struct private_android_creds_t {
bool loaded;
};
/**
* Free allocated DER encoding
*/
static void free_encoding(chunk_t *chunk)
{
chunk_free(chunk);
free(chunk);
}
/**
* Load trusted certificates via charonservice (JNI).
*/
@@ -71,8 +80,7 @@ static void load_trusted_certificates(private_android_creds_t *this)
cert->get_subject(cert));
this->creds->add_cert(this->creds, TRUE, cert);
}
chunk_free(current);
free(current);
free_encoding(current);
}
certs->destroy(certs);
}
@@ -130,6 +138,76 @@ METHOD(credential_set_t, create_shared_enumerator, enumerator_t*,
type, me, other);
}
METHOD(android_creds_t, load_user_certificate, certificate_t*,
private_android_creds_t *this)
{
linked_list_t *encodings;
certificate_t *cert = NULL, *ca_cert;
private_key_t *key = NULL;
chunk_t *current;
encodings = charonservice->get_user_certificate(charonservice);
if (!encodings)
{
return NULL;
}
while (encodings->remove_first(encodings, (void**)&current) == SUCCESS)
{
if (!key)
{ /* the first element is the private key, we assume RSA */
key = lib->creds->create(lib->creds, CRED_PRIVATE_KEY, KEY_RSA,
BUILD_BLOB_ASN1_DER, *current, BUILD_END);
if (key)
{
this->creds->add_key(this->creds, key);
free_encoding(current);
continue;
}
goto failed;
}
if (!cert)
{ /* the next element is the user certificate */
cert = lib->creds->create(lib->creds, CRED_CERTIFICATE, CERT_X509,
BUILD_BLOB_ASN1_DER, *current, BUILD_END);
if (cert)
{
DBG1(DBG_CFG, "loaded user certificate '%Y' and private key",
cert->get_subject(cert));
cert = this->creds->add_cert_ref(this->creds, TRUE, cert);
free_encoding(current);
continue;
}
goto failed;
}
/* the rest are CA certificates, we ignore failures */
ca_cert = lib->creds->create(lib->creds, CRED_CERTIFICATE, CERT_X509,
BUILD_BLOB_ASN1_DER, *current, BUILD_END);
if (ca_cert)
{
DBG1(DBG_CFG, "loaded CA certificate '%Y'",
ca_cert->get_subject(ca_cert));
this->creds->add_cert(this->creds, TRUE, ca_cert);
}
free_encoding(current);
}
encodings->destroy(encodings);
return cert;
failed:
DBG1(DBG_CFG, "failed to load user certificate and private key");
free_encoding(current);
encodings->destroy_function(encodings, (void*)free_encoding);
return NULL;
}
METHOD(credential_set_t, create_private_enumerator, enumerator_t*,
private_android_creds_t *this, key_type_t type, identification_t *id)
{
return this->creds->set.create_private_enumerator(&this->creds->set,
type, id);
}
METHOD(android_creds_t, clear, void,
private_android_creds_t *this)
{
@@ -160,11 +238,12 @@ android_creds_t *android_creds_create()
.set = {
.create_cert_enumerator = _create_cert_enumerator,
.create_shared_enumerator = _create_shared_enumerator,
.create_private_enumerator = (void*)return_null,
.create_private_enumerator = _create_private_enumerator,
.create_cdp_enumerator = (void*)return_null,
.cache_cert = (void*)nop,
},
.add_username_password = _add_username_password,
.load_user_certificate = _load_user_certificate,
.clear = _clear,
.destroy = _destroy,
},
@@ -46,6 +46,13 @@ struct android_creds_t {
void (*add_username_password)(android_creds_t *this, char *username,
char *password);
/**
* Load the user certificate and private key
*
* @preturn loaded client certificate, NULL on failure
*/
certificate_t *(*load_user_certificate)(android_creds_t *this);
/**
* Clear the cached certificates and stored credentials.
*/
@@ -43,11 +43,21 @@ struct private_android_service_t {
*/
android_service_t public;
/**
* credential set
*/
android_creds_t *creds;
/**
* current IKE_SA
*/
ike_sa_t *ike_sa;
/**
* the type of VPN
*/
char *type;
/**
* local ipv4 address
*/
@@ -63,6 +73,11 @@ struct private_android_service_t {
*/
char *username;
/**
* password
*/
char *password;
/**
* lock to safely access the TUN device fd
*/
@@ -445,11 +460,42 @@ static job_requeue_t initiate(private_android_service_t *this)
FALSE, NULL, NULL); /* mediation */
peer_cfg->add_virtual_ip(peer_cfg, host_create_from_string("0.0.0.0", 0));
auth = auth_cfg_create();
auth->add(auth, AUTH_RULE_AUTH_CLASS, AUTH_CLASS_EAP);
user = identification_create_from_string(this->username);
auth->add(auth, AUTH_RULE_IDENTITY, user);
peer_cfg->add_auth_cfg(peer_cfg, auth, TRUE);
/* local auth config */
if (streq("ikev2-eap", this->type))
{
auth = auth_cfg_create();
auth->add(auth, AUTH_RULE_AUTH_CLASS, AUTH_CLASS_EAP);
user = identification_create_from_string(this->username);
auth->add(auth, AUTH_RULE_IDENTITY, user);
this->creds->add_username_password(this->creds, this->username,
this->password);
memwipe(this->password, strlen(this->password));
peer_cfg->add_auth_cfg(peer_cfg, auth, TRUE);
}
else if (streq("ikev2-cert", this->type))
{
certificate_t *cert;
identification_t *id;
cert = this->creds->load_user_certificate(this->creds);
if (!cert)
{
peer_cfg->destroy(peer_cfg);
charonservice->update_status(charonservice,
CHARONSERVICE_GENERIC_ERROR);
return JOB_REQUEUE_NONE;
}
auth = auth_cfg_create();
auth->add(auth, AUTH_RULE_AUTH_CLASS, AUTH_CLASS_PUBKEY);
auth->add(auth, AUTH_RULE_SUBJECT_CERT, cert);
id = cert->get_subject(cert);
auth->add(auth, AUTH_RULE_IDENTITY, id->clone(id));
peer_cfg->add_auth_cfg(peer_cfg, auth, TRUE);
}
/* remote auth config */
auth = auth_cfg_create();
auth->add(auth, AUTH_RULE_AUTH_CLASS, AUTH_CLASS_PUBKEY);
gateway = identification_create_from_string(this->gateway);
@@ -506,17 +552,24 @@ METHOD(android_service_t, destroy, void,
/* make sure the tun device is actually closed */
close_tun_device(this);
this->lock->destroy(this->lock);
free(this->type);
free(this->local_address);
free(this->username);
free(this->gateway);
free(this->username);
if (this->password)
{
memwipe(this->password, strlen(this->password));
free(this->password);
}
free(this);
}
/**
* See header
*/
android_service_t *android_service_create(char *local_address, char *gateway,
char *username)
android_service_t *android_service_create(android_creds_t *creds, char *type,
char *local_address, char *gateway,
char *username, char *password)
{
private_android_service_t *this;
@@ -534,7 +587,10 @@ android_service_t *android_service_create(char *local_address, char *gateway,
.lock = rwlock_create(RWLOCK_TYPE_DEFAULT),
.local_address = local_address,
.username = username,
.password = password,
.gateway = gateway,
.creds = creds,
.type = type,
.tunfd = -1,
);
@@ -51,11 +51,15 @@ struct android_service_t {
* Create an Android service instance. Queues a job that starts initiation of a
* new IKE SA.
*
* @param creds Android specific credential set
* @param type VPN type (see VpnType.java)
* @param local_address local ip address
* @param gateway gateway address
* @param username user name (local identity)
* @param password password (if any)
*/
android_service_t *android_service_create(char *local_address, char *gateway,
char *username);
android_service_t *android_service_create(android_creds_t *creds, char *type,
char *local_address, char *gateway,
char *username, char *password);
#endif /** ANDROID_SERVICE_H_ @}*/
@@ -199,6 +199,33 @@ failed:
return FALSE;
}
/**
* Converts the given Java array of byte arrays (byte[][]) to a linked list
* of chunk_t objects.
*/
static linked_list_t *convert_array_of_byte_arrays(JNIEnv *env,
jobjectArray jarray)
{
linked_list_t *list;
jsize i;
list = linked_list_create();
for (i = 0; i < (*env)->GetArrayLength(env, jarray); ++i)
{
chunk_t *chunk;
jbyteArray jbytearray;
chunk = malloc_thing(chunk_t);
list->insert_last(list, chunk);
jbytearray = (*env)->GetObjectArrayElement(env, jarray, i);
*chunk = chunk_alloc((*env)->GetArrayLength(env, jbytearray));
(*env)->GetByteArrayRegion(env, jbytearray, 0, chunk->len, chunk->ptr);
(*env)->DeleteLocalRef(env, jbytearray);
}
return list;
}
METHOD(charonservice_t, get_trusted_certificates, linked_list_t*,
private_charonservice_t *this)
{
@@ -206,7 +233,6 @@ METHOD(charonservice_t, get_trusted_certificates, linked_list_t*,
jmethodID method_id;
jobjectArray jcerts;
linked_list_t *list;
jsize i;
androidjni_attach_thread(&env);
@@ -222,21 +248,39 @@ METHOD(charonservice_t, get_trusted_certificates, linked_list_t*,
{
goto failed;
}
list = linked_list_create();
for (i = 0; i < (*env)->GetArrayLength(env, jcerts); ++i)
list = convert_array_of_byte_arrays(env, jcerts);
androidjni_detach_thread();
return list;
failed:
androidjni_exception_occurred(env);
androidjni_detach_thread();
return NULL;
}
METHOD(charonservice_t, get_user_certificate, linked_list_t*,
private_charonservice_t *this)
{
JNIEnv *env;
jmethodID method_id;
jobjectArray jencodings;
linked_list_t *list;
androidjni_attach_thread(&env);
method_id = (*env)->GetMethodID(env,
android_charonvpnservice_class,
"getUserCertificate", "()[[B");
if (!method_id)
{
chunk_t *ca_cert;
jbyteArray jcert;
ca_cert = malloc_thing(chunk_t);
list->insert_last(list, ca_cert);
jcert = (*env)->GetObjectArrayElement(env, jcerts, i);
*ca_cert = chunk_alloc((*env)->GetArrayLength(env, jcert));
(*env)->GetByteArrayRegion(env, jcert, 0, ca_cert->len, ca_cert->ptr);
(*env)->DeleteLocalRef(env, jcert);
goto failed;
}
(*env)->DeleteLocalRef(env, jcerts);
jencodings = (*env)->CallObjectMethod(env, this->vpn_service, method_id, NULL);
if (!jencodings)
{
goto failed;
}
list = convert_array_of_byte_arrays(env, jencodings);
androidjni_detach_thread();
return list;
@@ -260,17 +304,15 @@ METHOD(charonservice_t, get_vpnservice_builder, vpnservice_builder_t*,
* @param username username (gets owned)
* @param password password (gets owned)
*/
static void initiate(char *local, char *gateway, char *username, char *password)
static void initiate(char *type, char *local, char *gateway,
char *username, char *password)
{
private_charonservice_t *this = (private_charonservice_t*)charonservice;
this->creds->clear(this->creds);
this->creds->add_username_password(this->creds, username, password);
memwipe(password, strlen(password));
free(password);
DESTROY_IF(this->service);
this->service = android_service_create(local, gateway, username);
this->service = android_service_create(this->creds, type, local, gateway,
username, password);
}
/**
@@ -321,6 +363,7 @@ static void charonservice_init(JNIEnv *env, jobject service, jobject builder)
.update_status = _update_status,
.bypass_socket = _bypass_socket,
.get_trusted_certificates = _get_trusted_certificates,
.get_user_certificate = _get_user_certificate,
.get_vpnservice_builder = _get_vpnservice_builder,
},
.attr = android_attr_create(),
@@ -477,15 +520,16 @@ JNI_METHOD(CharonVpnService, deinitializeCharon, void)
* Initiate SA
*/
JNI_METHOD(CharonVpnService, initiate, void,
jstring jlocal_address, jstring jgateway, jstring jusername,
jstring jtype, jstring jlocal_address, jstring jgateway, jstring jusername,
jstring jpassword)
{
char *local_address, *gateway, *username, *password;
char *type, *local_address, *gateway, *username, *password;
type = androidjni_convert_jstring(env, jtype);
local_address = androidjni_convert_jstring(env, jlocal_address);
gateway = androidjni_convert_jstring(env, jgateway);
username = androidjni_convert_jstring(env, jusername);
password = androidjni_convert_jstring(env, jpassword);
initiate(local_address, gateway, username, password);
initiate(type, local_address, gateway, username, password);
}
@@ -85,6 +85,17 @@ struct charonservice_t {
*/
linked_list_t *(*get_trusted_certificates)(charonservice_t *this);
/**
* Get the configured user certificate chain and private key via JNI
*
* The first item in the returned list is the private key, followed by the
* user certificate and any remaining elements of the certificate chain.
*
* @return list of DER encoded objects (as chunk_t*),
* NULL on failure
*/
linked_list_t *(*get_user_certificate)(charonservice_t *this);
/**
* Get the current vpnservice_builder_t object
*
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012 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.
-->
<TwoLineListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:background="?android:attr/selectableItemBackground"
android:mode="twoLine"
android:padding="10dp" >
<TextView
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@android:id/text2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@android:id/text1"
android:layout_alignLeft="@android:id/text1"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary" />
</TwoLineListItem>
@@ -56,28 +56,67 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/profile_username_label" />
android:text="@string/profile_vpn_type_label" />
<EditText
android:id="@+id/username"
<Spinner
android:id="@+id/vpn_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="textNoSuggestions" />
android:spinnerMode="dropdown"
android:entries="@array/vpn_types" />
<TextView
<LinearLayout
android:id="@+id/username_password_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/profile_password_label" />
android:orientation="vertical" >
<EditText
android:id="@+id/password"
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/profile_username_label" />
<EditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="textNoSuggestions" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/profile_password_label" />
<EditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="textPassword|textNoSuggestions"
android:hint="@string/profile_password_hint" />
</LinearLayout>
<LinearLayout
android:id="@+id/user_certificate_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="textPassword|textNoSuggestions"
android:hint="@string/profile_password_hint" />
android:orientation="vertical" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/profile_user_certificate_label" />
<include
android:id="@+id/select_user_certificate"
layout="@layout/certificate_selector" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
@@ -91,32 +130,9 @@
android:layout_height="wrap_content"
android:text="@string/profile_ca_auto_label" />
<RelativeLayout
<include
android:id="@+id/select_certificate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:background="?android:attr/selectableItemBackground"
android:padding="10dp" >
<TextView
android:id="@+id/select_certificate_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/profile_ca_select_certificate_label" />
<TextView
android:id="@+id/select_certificate_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/select_certificate_title"
android:layout_alignLeft="@id/select_certificate_title"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
android:text="@string/profile_ca_select_certificate" />
</RelativeLayout>
layout="@layout/certificate_selector" />
</LinearLayout>
@@ -46,4 +46,14 @@
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_marginLeft="15dp" />
<TextView
android:id="@+id/profile_item_certificate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?android:textColorSecondary"
android:textAppearance="?android:attr/textAppearanceSmall"
android:singleLine="true"
android:ellipsize="end"
android:layout_marginLeft="15dp" />
</LinearLayout>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012 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.
-->
<resources>
<!-- the order here must match the enum entries in VpnType.java -->
<string-array name="vpn_types">
<item>IKEv2 EAP (Benutzername/Passwort)</item>
<item>IKEv2 Zertifikat</item>
</string-array>
</resources>
@@ -25,6 +25,7 @@
<string name="search">Suchen</string>
<string name="vpn_not_supported_title">VPN nicht unterstützt</string>
<string name="vpn_not_supported">Ihr Gerät unterstützt keine VPN Anwendungen.\nBitte kontaktieren Sie den Hersteller.</string>
<string name="loading">Laden&#8230;</string>
<!-- Log view -->
<string name="log_title">Log</string>
@@ -49,9 +50,13 @@
<string name="profile_name_label">Profilname:</string>
<string name="profile_name_hint">(Gateway-Adresse verwenden)</string>
<string name="profile_gateway_label">Gateway:</string>
<string name="profile_vpn_type_label">Typ:</string>
<string name="profile_username_label">Benutzername:</string>
<string name="profile_password_label">Passwort:</string>
<string name="profile_password_hint">(anfordern wenn benötigt)</string>
<string name="profile_user_certificate_label">Benutzer-Zertifikat:</string>
<string name="profile_user_select_certificate_label">Benutzer-Zertifikat auswählen</string>
<string name="profile_user_select_certificate">Wählen Sie ein bestimmtes Benutzer-Zertifikat</string>
<string name="profile_ca_label">CA-Zertifikat:</string>
<string name="profile_ca_auto_label">Automatisch wählen</string>
<string name="profile_ca_select_certificate_label">CA-Zertifikat auswählen</string>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012 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.
-->
<resources>
<!-- the order here must match the enum entries in VpnType.java -->
<string-array name="vpn_types">
<item>IKEv2 EAP (użytkownik/hasło)</item>
<item>IKEv2 certyfikat</item>
</string-array>
</resources>
@@ -27,6 +27,7 @@
<string name="search">Szukaj</string>
<string name="vpn_not_supported_title">Nie obsługiwany VPN</string>
<string name="vpn_not_supported">Urządzenie nie obsługuje aplikacji VPN.\nProszę skontaktować się z producentem.</string>
<string name="loading">Wczytywanie&#8230;</string>
<!-- Log view -->
<string name="log_title">Log</string>
@@ -51,9 +52,13 @@
<string name="profile_name_label">Nazwa profilu:</string>
<string name="profile_name_hint">(użyj adresu bramki)</string>
<string name="profile_gateway_label">Bramka:</string>
<string name="profile_vpn_type_label">Typ:</string>
<string name="profile_username_label">Użytkownik:</string>
<string name="profile_password_label">Hasło:</string>
<string name="profile_password_hint">(w razie potrzebz zapromptuj)</string>
<string name="profile_password_hint">(w razie potrzeby zapromptuj)</string>
<string name="profile_user_certificate_label">Certyfikat użytkownika:</string>
<string name="profile_user_select_certificate_label">Wybierz certyfikat użytkownika</string>
<string name="profile_user_select_certificate">>Wybierz określony certyfikat użytkownika</string>
<string name="profile_ca_label">Certyfikat CA:</string>
<string name="profile_ca_auto_label">Wybierz automatycznie</string>
<string name="profile_ca_select_certificate_label">Wybierz certyfikat CA</string>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012 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.
-->
<resources>
<!-- the order here must match the enum entries in VpnType.java -->
<string-array name="vpn_types">
<item>IKEv2 EAP (Username/Password)</item>
<item>IKEv2 Certificate</item>
</string-array>
</resources>
@@ -25,6 +25,7 @@
<string name="search">Search</string>
<string name="vpn_not_supported_title">VPN not supported</string>
<string name="vpn_not_supported">Your device does not support VPN applications.\nPlease contact the manufacturer.</string>
<string name="loading">Loading&#8230;</string>
<!-- Log view -->
<string name="log_title">Log</string>
@@ -49,9 +50,13 @@
<string name="profile_name_label">Profile Name:</string>
<string name="profile_name_hint">(use gateway address)</string>
<string name="profile_gateway_label">Gateway:</string>
<string name="profile_vpn_type_label">Type:</string>
<string name="profile_username_label">Username:</string>
<string name="profile_password_label">Password:</string>
<string name="profile_password_hint">(prompt when needed)</string>
<string name="profile_user_certificate_label">User certificate:</string>
<string name="profile_user_select_certificate_label">Select user certificate</string>
<string name="profile_user_select_certificate">Select a specific user certificate</string>
<string name="profile_ca_label">CA certificate:</string>
<string name="profile_ca_auto_label">Select automatically</string>
<string name="profile_ca_select_certificate_label">Select CA certificate</string>
@@ -19,7 +19,8 @@ package org.strongswan.android.data;
public class VpnProfile implements Cloneable
{
private String mName, mGateway, mUsername, mPassword, mCertificate;
private String mName, mGateway, mUsername, mPassword, mCertificate, mUserCertificate;
private VpnType mVpnType;
private long mId = -1;
public long getId()
@@ -52,6 +53,16 @@ public class VpnProfile implements Cloneable
this.mGateway = gateway;
}
public VpnType getVpnType()
{
return mVpnType;
}
public void setVpnType(VpnType type)
{
this.mVpnType = type;
}
public String getUsername()
{
return mUsername;
@@ -77,9 +88,19 @@ public class VpnProfile implements Cloneable
return mCertificate;
}
public void setCertificateAlias(String certificate)
public void setCertificateAlias(String alias)
{
this.mCertificate = certificate;
this.mCertificate = alias;
}
public String getUserCertificateAlias()
{
return mUserCertificate;
}
public void setUserCertificateAlias(String alias)
{
this.mUserCertificate = alias;
}
@Override
@@ -26,6 +26,7 @@ import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.util.Log;
public class VpnProfileDataSource
@@ -34,9 +35,11 @@ public class VpnProfileDataSource
public static final String KEY_ID = "_id";
public static final String KEY_NAME = "name";
public static final String KEY_GATEWAY = "gateway";
public static final String KEY_VPN_TYPE = "vpn_type";
public static final String KEY_USERNAME = "username";
public static final String KEY_PASSWORD = "password";
public static final String KEY_CERTIFICATE = "certificate";
public static final String KEY_USER_CERTIFICATE = "user_certificate";
private DatabaseHelper mDbHelper;
private SQLiteDatabase mDatabase;
@@ -45,24 +48,28 @@ public class VpnProfileDataSource
private static final String DATABASE_NAME = "strongswan.db";
private static final String TABLE_VPNPROFILE = "vpnprofile";
private static final int DATABASE_VERSION = 1;
private static final int DATABASE_VERSION = 4;
public static final String DATABASE_CREATE =
"CREATE TABLE " + TABLE_VPNPROFILE + " (" +
KEY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
KEY_NAME + " TEXT NOT NULL," +
KEY_GATEWAY + " TEXT NOT NULL," +
KEY_USERNAME + " TEXT NOT NULL," +
KEY_VPN_TYPE + " TEXT NOT NULL," +
KEY_USERNAME + " TEXT," +
KEY_PASSWORD + " TEXT," +
KEY_CERTIFICATE + " TEXT" +
KEY_CERTIFICATE + " TEXT," +
KEY_USER_CERTIFICATE + " TEXT" +
");";
private final String[] ALL_COLUMNS = new String[] {
private static final String[] ALL_COLUMNS = new String[] {
KEY_ID,
KEY_NAME,
KEY_GATEWAY,
KEY_VPN_TYPE,
KEY_USERNAME,
KEY_PASSWORD,
KEY_CERTIFICATE
KEY_CERTIFICATE,
KEY_USER_CERTIFICATE,
};
private static class DatabaseHelper extends SQLiteOpenHelper
@@ -82,9 +89,40 @@ public class VpnProfileDataSource
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
{
Log.w(TAG, "Upgrading database from version " + oldVersion +
" to " + newVersion + ", which will destroy all old data");
db.execSQL("DROP TABLE IF EXISTS " + TABLE_VPNPROFILE);
onCreate(db);
" to " + newVersion);
if (oldVersion < 2)
{
db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_USER_CERTIFICATE +
" TEXT;");
}
if (oldVersion < 3)
{
db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_VPN_TYPE +
" TEXT DEFAULT '';");
}
if (oldVersion < 4)
{ /* remove NOT NULL constraint from username column */
updateColumns(db);
}
}
private void updateColumns(SQLiteDatabase db)
{
db.beginTransaction();
try
{
db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " RENAME TO tmp_" + TABLE_VPNPROFILE + ";");
db.execSQL(DATABASE_CREATE);
StringBuilder insert = new StringBuilder("INSERT INTO " + TABLE_VPNPROFILE + " SELECT ");
SQLiteQueryBuilder.appendColumns(insert, ALL_COLUMNS);
db.execSQL(insert.append(" FROM tmp_" + TABLE_VPNPROFILE + ";").toString());
db.execSQL("DROP TABLE tmp_" + TABLE_VPNPROFILE + ";");
db.setTransactionSuccessful();
}
finally
{
db.endTransaction();
}
}
}
@@ -212,9 +250,11 @@ public class VpnProfileDataSource
profile.setId(cursor.getLong(cursor.getColumnIndex(KEY_ID)));
profile.setName(cursor.getString(cursor.getColumnIndex(KEY_NAME)));
profile.setGateway(cursor.getString(cursor.getColumnIndex(KEY_GATEWAY)));
profile.setVpnType(VpnType.fromIdentifier(cursor.getString(cursor.getColumnIndex(KEY_VPN_TYPE))));
profile.setUsername(cursor.getString(cursor.getColumnIndex(KEY_USERNAME)));
profile.setPassword(cursor.getString(cursor.getColumnIndex(KEY_PASSWORD)));
profile.setCertificateAlias(cursor.getString(cursor.getColumnIndex(KEY_CERTIFICATE)));
profile.setUserCertificateAlias(cursor.getString(cursor.getColumnIndex(KEY_USER_CERTIFICATE)));
return profile;
}
@@ -223,9 +263,11 @@ public class VpnProfileDataSource
ContentValues values = new ContentValues();
values.put(KEY_NAME, profile.getName());
values.put(KEY_GATEWAY, profile.getGateway());
values.put(KEY_VPN_TYPE, profile.getVpnType().getIdentifier());
values.put(KEY_USERNAME, profile.getUsername());
values.put(KEY_PASSWORD, profile.getPassword());
values.put(KEY_CERTIFICATE, profile.getCertificateAlias());
values.put(KEY_USER_CERTIFICATE, profile.getUserCertificateAlias());
return values;
}
}
@@ -0,0 +1,88 @@
/*
* Copyright (C) 2012 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.data;
public enum VpnType
{
/* the order here must match the items in R.array.vpn_types */
IKEV2_EAP("ikev2-eap", true, false),
IKEV2_CERT("ikev2-cert", false, true);
private String mIdentifier;
private boolean mCertificate;
private boolean mUsernamePassword;
/**
* Enum which provides additional information about the supported VPN types.
*
* @param id identifier used to store and transmit this specific type
* @param userpass true if username and password are required
* @param certificate true if a client certificate is required
*/
VpnType(String id, boolean userpass, boolean certificate)
{
mIdentifier = id;
mUsernamePassword = userpass;
mCertificate = certificate;
}
/**
* The identifier used to store this value in the database
* @return identifier
*/
public String getIdentifier()
{
return mIdentifier;
}
/**
* Whether username and password are required for this type of VPN.
*
* @return true if username and password are required
*/
public boolean getRequiresUsernamePassword()
{
return mUsernamePassword;
}
/**
* Whether a certificate is required for this type of VPN.
*
* @return true if a certificate is required
*/
public boolean getRequiresCertificate()
{
return mCertificate;
}
/**
* Get the enum entry with the given identifier.
*
* @param identifier get the enum entry with this identifier
* @return the enum entry, or the default if not found
*/
public static VpnType fromIdentifier(String identifier)
{
for (VpnType type : VpnType.values())
{
if (identifier.equals(type.mIdentifier))
{
return type;
}
}
return VpnType.IKEV2_EAP;
}
}
@@ -21,6 +21,7 @@ import java.io.File;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.security.PrivateKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
@@ -42,6 +43,8 @@ import android.net.VpnService;
import android.os.Bundle;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.security.KeyChain;
import android.security.KeyChainException;
import android.util.Log;
public class CharonVpnService extends VpnService implements Runnable
@@ -54,6 +57,7 @@ public class CharonVpnService extends VpnService implements Runnable
private Thread mConnectionHandler;
private VpnProfile mCurrentProfile;
private volatile String mCurrentCertificateAlias;
private volatile String mCurrentUserCertificateAlias;
private VpnProfile mNextProfile;
private volatile boolean mProfileUpdated;
private volatile boolean mTerminate;
@@ -203,6 +207,7 @@ public class CharonVpnService extends VpnService implements Runnable
/* store this in a separate (volatile) variable to avoid
* a possible deadlock during deinitialization */
mCurrentCertificateAlias = mCurrentProfile.getCertificateAlias();
mCurrentUserCertificateAlias = mCurrentProfile.getUserCertificateAlias();
setProfile(mCurrentProfile);
setError(ErrorState.NO_ERROR);
@@ -214,7 +219,8 @@ public class CharonVpnService extends VpnService implements Runnable
Log.i(TAG, "charon started");
String local_address = getLocalIPv4Address();
initiate(local_address != null ? local_address : "0.0.0.0",
initiate(mCurrentProfile.getVpnType().getIdentifier(),
local_address != null ? local_address : "0.0.0.0",
mCurrentProfile.getGateway(), mCurrentProfile.getUsername(),
mCurrentProfile.getPassword());
}
@@ -420,6 +426,41 @@ public class CharonVpnService extends VpnService implements Runnable
return certs.toArray(new byte[certs.size()][]);
}
/**
* Function called via JNI to get a list containing the DER encoded private key
* and DER encoded certificates of the user selected certificate chain (beginning
* with the user certificate).
*
* Since this method is called from a thread of charon's thread pool we are safe
* to call methods on KeyChain directly.
*
* @return list containing the private key and certificates (first element is the key)
* @throws InterruptedException
* @throws KeyChainException
* @throws CertificateEncodingException
*/
private byte[][] getUserCertificate() throws KeyChainException, InterruptedException, CertificateEncodingException
{
ArrayList<byte[]> encodings = new ArrayList<byte[]>();
String alias = mCurrentUserCertificateAlias;
PrivateKey key = KeyChain.getPrivateKey(getApplicationContext(), alias);
if (key == null)
{
return null;
}
encodings.add(key.getEncoded());
X509Certificate[] chain = KeyChain.getCertificateChain(getApplicationContext(), alias);
if (chain == null || chain.length == 0)
{
return null;
}
for (X509Certificate cert : chain)
{
encodings.add(cert.getEncoded());
}
return encodings.toArray(new byte[encodings.size()][]);
}
/**
* Initialization of charon, provided by libandroidbridge.so
*
@@ -436,7 +477,7 @@ public class CharonVpnService extends VpnService implements Runnable
/**
* Initiate VPN, provided by libandroidbridge.so
*/
public native void initiate(String local_address, String gateway,
public native void initiate(String type, String local_address, String gateway,
String username, String password);
/**
@@ -31,7 +31,6 @@ import android.app.AlertDialog.Builder;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.VpnService;
@@ -47,11 +46,9 @@ import android.widget.EditText;
public class MainActivity extends Activity implements OnVpnProfileSelectedListener
{
public static final String CONTACT_EMAIL = "android@strongswan.org";
private static final String SHOW_ERROR_DIALOG = "errordialog";
private static final int PREPARE_VPN_SERVICE = 0;
private VpnProfile activeProfile;
private AlertDialog mErrorDialog;
private Bundle mProfileInfo;
@Override
public void onCreate(Bundle savedInstanceState)
@@ -63,32 +60,10 @@ public class MainActivity extends Activity implements OnVpnProfileSelectedListen
ActionBar bar = getActionBar();
bar.setDisplayShowTitleEnabled(false);
if (savedInstanceState != null && savedInstanceState.getBoolean(SHOW_ERROR_DIALOG))
{
showVpnNotSupportedError();
}
/* load CA certificates in a background task */
new CertificateLoadTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, false);
}
@Override
protected void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
outState.putBoolean(SHOW_ERROR_DIALOG, mErrorDialog != null);
}
@Override
protected void onDestroy()
{
super.onDestroy();
if (mErrorDialog != null)
{ /* avoid any errors about leaked windows */
mErrorDialog.dismiss();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
@@ -116,10 +91,13 @@ public class MainActivity extends Activity implements OnVpnProfileSelectedListen
/**
* Prepare the VpnService. If this succeeds the current VPN profile is
* started.
* @param profileInfo a bundle containing the information about the profile to be started
*/
protected void prepareVpnService()
protected void prepareVpnService(Bundle profileInfo)
{
Intent intent = VpnService.prepare(this);
/* store profile info until the user grants us permission */
mProfileInfo = profileInfo;
if (intent != null)
{
try
@@ -132,11 +110,11 @@ public class MainActivity extends Activity implements OnVpnProfileSelectedListen
* don't have the VPN components built into the system image.
* com.android.vpndialogs/com.android.vpndialogs.ConfirmDialog
* will not be found then */
showVpnNotSupportedError();
new VpnNotSupportedError().show(getFragmentManager(), "ErrorDialog");
}
}
else
{
{ /* user already granted permission to use VpnService */
onActivityResult(PREPARE_VPN_SERVICE, RESULT_OK, null);
}
}
@@ -147,12 +125,10 @@ public class MainActivity extends Activity implements OnVpnProfileSelectedListen
switch (requestCode)
{
case PREPARE_VPN_SERVICE:
if (resultCode == RESULT_OK && activeProfile != null)
if (resultCode == RESULT_OK && mProfileInfo != null)
{
Intent intent = new Intent(this, CharonVpnService.class);
intent.putExtra(VpnProfileDataSource.KEY_ID, activeProfile.getId());
/* submit the password as the profile might not store one */
intent.putExtra(VpnProfileDataSource.KEY_PASSWORD, activeProfile.getPassword());
intent.putExtras(mProfileInfo);
this.startService(intent);
}
break;
@@ -164,36 +140,23 @@ public class MainActivity extends Activity implements OnVpnProfileSelectedListen
@Override
public void onVpnProfileSelected(VpnProfile profile)
{
activeProfile = profile;
if (activeProfile.getPassword() == null)
Bundle profileInfo = new Bundle();
profileInfo.putLong(VpnProfileDataSource.KEY_ID, profile.getId());
profileInfo.putString(VpnProfileDataSource.KEY_USERNAME, profile.getUsername());
if (profile.getVpnType().getRequiresUsernamePassword() &&
profile.getPassword() == null)
{
new LoginDialog().show(getFragmentManager(), "LoginDialog");
LoginDialog login = new LoginDialog();
login.setArguments(profileInfo);
login.show(getFragmentManager(), "LoginDialog");
}
else
{
prepareVpnService();
profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, profile.getPassword());
prepareVpnService(profileInfo);
}
}
/**
* Show an error dialog if case the device lacks VPN support.
*/
private void showVpnNotSupportedError()
{
mErrorDialog = new AlertDialog.Builder(this)
.setTitle(R.string.vpn_not_supported_title)
.setMessage(getString(R.string.vpn_not_supported))
.setCancelable(false)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id)
{
mErrorDialog = null;
dialog.dismiss();
}
}).show();
}
/**
* Class that loads or reloads the cached CA certificates.
*/
@@ -220,28 +183,32 @@ public class MainActivity extends Activity implements OnVpnProfileSelectedListen
}
}
private class LoginDialog extends DialogFragment
/**
* Class that displays a login dialog and initiates the selected VPN
* profile if the user confirms the dialog.
*/
public static class LoginDialog extends DialogFragment
{
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final Bundle profileInfo = getArguments();
LayoutInflater inflater = getActivity().getLayoutInflater();
View view = inflater.inflate(R.layout.login_dialog, null);
EditText username = (EditText)view.findViewById(R.id.username);
username.setText(activeProfile.getUsername());
username.setText(profileInfo.getString(VpnProfileDataSource.KEY_USERNAME));
final EditText password = (EditText)view.findViewById(R.id.password);
Builder adb = new AlertDialog.Builder(MainActivity.this);
Builder adb = new AlertDialog.Builder(getActivity());
adb.setView(view);
adb.setTitle(getString(R.string.login_title));
adb.setPositiveButton(R.string.login_confirm, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int whichButton)
{
/* let's work on a clone of the profile when updating the password */
activeProfile = activeProfile.clone();
activeProfile.setPassword(password.getText().toString().trim());
prepareVpnService();
MainActivity activity = (MainActivity)getActivity();
profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, password.getText().toString().trim());
activity.prepareVpnService(profileInfo);
}
});
adb.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@@ -254,4 +221,27 @@ public class MainActivity extends Activity implements OnVpnProfileSelectedListen
return adb.create();
}
}
/**
* Class representing an error message which is displayed if VpnService is
* not supported on the current device.
*/
public static class VpnNotSupportedError extends DialogFragment
{
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
return new AlertDialog.Builder(getActivity())
.setTitle(R.string.vpn_not_supported_title)
.setMessage(getString(R.string.vpn_not_supported))
.setCancelable(false)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id)
{
dialog.dismiss();
}
}).create();
}
}
}
@@ -23,25 +23,34 @@ 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 android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
import android.security.KeyChainException;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Spinner;
import android.widget.TwoLineListItem;
public class VpnProfileDetailActivity extends Activity
{
@@ -50,16 +59,20 @@ public class VpnProfileDetailActivity extends Activity
private VpnProfileDataSource mDataSource;
private Long mId;
private TrustedCertificateEntry mCertEntry;
private String mUserCertLoading;
private TrustedCertificateEntry mUserCertEntry;
private VpnType mVpnType = VpnType.IKEV2_EAP;
private VpnProfile mProfile;
private EditText mName;
private EditText mGateway;
private Spinner mSelectVpnType;
private ViewGroup mUsernamePassword;
private EditText mUsername;
private EditText mPassword;
private ViewGroup mUserCertificate;
private TwoLineListItem mSelectUserCert;
private CheckBox mCheckAuto;
private RelativeLayout mSelectCert;
private TextView mCertTitle;
private TextView mCertSubtitle;
private TwoLineListItem mSelectCert;
@Override
public void onCreate(Bundle savedInstanceState)
@@ -75,14 +88,36 @@ public class VpnProfileDetailActivity extends Activity
setContentView(R.layout.profile_detail_view);
mName = (EditText)findViewById(R.id.name);
mPassword = (EditText)findViewById(R.id.password);
mGateway = (EditText)findViewById(R.id.gateway);
mSelectVpnType = (Spinner)findViewById(R.id.vpn_type);
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);
mCheckAuto = (CheckBox)findViewById(R.id.ca_auto);
mSelectCert = (RelativeLayout)findViewById(R.id.select_certificate);
mCertTitle = (TextView)findViewById(R.id.select_certificate_title);
mCertSubtitle = (TextView)findViewById(R.id.select_certificate_subtitle);
mSelectCert = (TwoLineListItem)findViewById(R.id.select_certificate);
mSelectVpnType.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
{
mVpnType = VpnType.values()[position];
updateCredentialView();
}
@Override
public void onNothingSelected(AdapterView<?> parent)
{ /* should not happen */
mVpnType = VpnType.IKEV2_EAP;
updateCredentialView();
}
});
mSelectUserCert.setOnClickListener(new SelectUserCertOnClickListener());
mCheckAuto.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
@@ -110,6 +145,7 @@ public class VpnProfileDetailActivity extends Activity
loadProfileData(savedInstanceState);
updateCredentialView();
updateCertificateSelector();
}
@@ -128,6 +164,10 @@ public class VpnProfileDetailActivity extends Activity
{
outState.putLong(VpnProfileDataSource.KEY_ID, mId);
}
if (mUserCertEntry != null)
{
outState.putString(VpnProfileDataSource.KEY_USER_CERTIFICATE, mUserCertEntry.getAlias());
}
if (mCertEntry != null)
{
outState.putString(VpnProfileDataSource.KEY_CERTIFICATE, mCertEntry.getAlias());
@@ -178,6 +218,35 @@ public class VpnProfileDetailActivity extends Activity
}
}
/**
* Update the UI to enter credentials depending on the type of VPN currently selected
*/
private void updateCredentialView()
{
mUsernamePassword.setVisibility(mVpnType.getRequiresUsernamePassword() ? View.VISIBLE : View.GONE);
mUserCertificate.setVisibility(mVpnType.getRequiresCertificate() ? View.VISIBLE : View.GONE);
if (mVpnType.getRequiresCertificate())
{
if (mUserCertLoading != null)
{
mSelectUserCert.getText1().setText(mUserCertLoading);
mSelectUserCert.getText2().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());
}
else
{
mSelectUserCert.getText1().setText(R.string.profile_user_select_certificate_label);
mSelectUserCert.getText2().setText(R.string.profile_user_select_certificate);
}
}
}
/**
* Show an alert in case the previously selected certificate is not found anymore
* or the user did not select a certificate in the spinner.
@@ -210,13 +279,13 @@ public class VpnProfileDetailActivity extends Activity
if (mCertEntry != null)
{
mCertTitle.setText(mCertEntry.getSubjectPrimary());
mCertSubtitle.setText(mCertEntry.getSubjectSecondary());
mSelectCert.getText1().setText(mCertEntry.getSubjectPrimary());
mSelectCert.getText2().setText(mCertEntry.getSubjectSecondary());
}
else
{
mCertTitle.setText(R.string.profile_ca_select_certificate_label);
mCertSubtitle.setText(R.string.profile_ca_select_certificate);
mSelectCert.getText1().setText(R.string.profile_ca_select_certificate_label);
mSelectCert.getText2().setText(R.string.profile_ca_select_certificate);
}
}
else
@@ -262,9 +331,17 @@ public class VpnProfileDetailActivity extends Activity
mGateway.setError(getString(R.string.alert_text_no_input_gateway));
valid = false;
}
if (mUsername.getText().toString().trim().isEmpty())
if (mVpnType.getRequiresUsernamePassword())
{
mUsername.setError(getString(R.string.alert_text_no_input_username));
if (mUsername.getText().toString().trim().isEmpty())
{
mUsername.setError(getString(R.string.alert_text_no_input_username));
valid = false;
}
}
if (mVpnType.getRequiresCertificate() && mUserCertEntry == null)
{ /* let's show an error icon */
mSelectUserCert.getText1().setError("");
valid = false;
}
if (!mCheckAuto.isChecked() && mCertEntry == null)
@@ -285,10 +362,18 @@ public class VpnProfileDetailActivity extends Activity
String gateway = mGateway.getText().toString().trim();
mProfile.setName(name.isEmpty() ? gateway : name);
mProfile.setGateway(gateway);
mProfile.setUsername(mUsername.getText().toString().trim());
String password = mPassword.getText().toString().trim();
password = password.isEmpty() ? null : password;
mProfile.setPassword(password);
mProfile.setVpnType(mVpnType);
if (mVpnType.getRequiresUsernamePassword())
{
mProfile.setUsername(mUsername.getText().toString().trim());
String password = mPassword.getText().toString().trim();
password = password.isEmpty() ? null : password;
mProfile.setPassword(password);
}
if (mVpnType.getRequiresCertificate())
{
mProfile.setUserCertificateAlias(mUserCertEntry.getAlias());
}
String certAlias = mCheckAuto.isChecked() ? null : mCertEntry.getAlias();
mProfile.setCertificateAlias(certAlias);
}
@@ -300,18 +385,20 @@ public class VpnProfileDetailActivity extends Activity
*/
private void loadProfileData(Bundle savedInstanceState)
{
String alias = null;
String useralias = null, alias = null;
getActionBar().setTitle(R.string.add_profile);
if (mId != null)
if (mId != null && mId != 0)
{
mProfile = mDataSource.getVpnProfile(mId);
if (mProfile != null)
{
mName.setText(mProfile.getName());
mGateway.setText(mProfile.getGateway());
mVpnType = mProfile.getVpnType();
mUsername.setText(mProfile.getUsername());
mPassword.setText(mProfile.getPassword());
useralias = mProfile.getUserCertificateAlias();
alias = mProfile.getCertificateAlias();
getActionBar().setTitle(mProfile.getName());
}
@@ -323,7 +410,18 @@ public class VpnProfileDetailActivity extends Activity
}
}
/* check if the user selected a certificate previously */
mSelectVpnType.setSelection(mVpnType.ordinal());
/* check if the user selected a user certificate previously */
useralias = savedInstanceState == null ? useralias: savedInstanceState.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE);
if (useralias != null)
{
UserCertificateLoader loader = new UserCertificateLoader(this, useralias);
mUserCertLoading = useralias;
loader.execute();
}
/* check if the user selected a CA certificate previously */
alias = savedInstanceState == null ? alias : savedInstanceState.getString(VpnProfileDataSource.KEY_CERTIFICATE);
mCheckAuto.setChecked(alias == null);
if (alias != null)
@@ -340,4 +438,102 @@ public class VpnProfileDetailActivity extends Activity
}
}
}
private class SelectUserCertOnClickListener implements OnClickListener, KeyChainAliasCallback
{
@Override
public void onClick(View v)
{
String useralias = mUserCertEntry != null ? mUserCertEntry.getAlias() : null;
KeyChain.choosePrivateKeyAlias(VpnProfileDetailActivity.this, this, new String[] { "RSA" }, null, null, -1, useralias);
}
@Override
public void alias(final String alias)
{
if (alias != null)
{ /* otherwise the dialog was canceled, the request denied */
try
{
final X509Certificate[] chain = KeyChain.getCertificateChain(VpnProfileDetailActivity.this, alias);
/* alias() is not called from our main thread */
runOnUiThread(new Runnable() {
@Override
public void run()
{
if (chain != null && chain.length > 0)
{
mUserCertEntry = new TrustedCertificateEntry(alias, chain[0]);
}
updateCredentialView();
}
});
}
catch (KeyChainException e)
{
e.printStackTrace();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
/**
* Load the selected user certificate asynchronously. This cannot be done
* from the main thread as getCertificateChain() calls back to our main
* thread to bind to the KeyChain service resulting in a deadlock.
*/
private class UserCertificateLoader extends AsyncTask<Void, Void, X509Certificate>
{
private final Context mContext;
private final String mAlias;
public UserCertificateLoader(Context context, String alias)
{
mContext = context;
mAlias = alias;
}
@Override
protected X509Certificate doInBackground(Void... params)
{
X509Certificate[] chain = null;
try
{
chain = KeyChain.getCertificateChain(mContext, mAlias);
}
catch (KeyChainException e)
{
e.printStackTrace();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
if (chain != null && chain.length > 0)
{
return chain[0];
}
return null;
}
@Override
protected void onPostExecute(X509Certificate result)
{
if (result != null)
{
mUserCertEntry = new TrustedCertificateEntry(mAlias, result);
}
else
{ /* previously selected certificate is not here anymore */
mSelectUserCert.getText1().setError("");
mUserCertEntry = null;
}
mUserCertLoading = null;
updateCredentialView();
}
}
}
@@ -64,7 +64,25 @@ public class VpnProfileAdapter extends ArrayAdapter<VpnProfile>
tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_gateway);
tv.setText(getContext().getString(R.string.profile_gateway_label) + " " + profile.getGateway());
tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_username);
tv.setText(getContext().getString(R.string.profile_username_label) + " " + profile.getUsername());
if (profile.getVpnType().getRequiresUsernamePassword())
{ /* if the view is reused we make sure it is visible */
tv.setVisibility(View.VISIBLE);
tv.setText(getContext().getString(R.string.profile_username_label) + " " + profile.getUsername());
}
else
{
tv.setVisibility(View.GONE);
}
tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_certificate);
if (profile.getVpnType().getRequiresCertificate())
{
tv.setText(getContext().getString(R.string.profile_user_certificate_label) + " " + profile.getUserCertificateAlias());
tv.setVisibility(View.VISIBLE);
}
else
{
tv.setVisibility(View.GONE);
}
return vpnProfileView;
}
+2
View File
@@ -67,6 +67,8 @@ LOCAL_SRC_FILES += $(call add_plugin, pem)
LOCAL_SRC_FILES += $(call add_plugin, pkcs1)
LOCAL_SRC_FILES += $(call add_plugin, pkcs8)
LOCAL_SRC_FILES += $(call add_plugin, pkcs11)
LOCAL_SRC_FILES += $(call add_plugin, pubkey)