diff options
Diffstat (limited to 'src')
25 files changed, 929 insertions, 181 deletions
diff --git a/src/frontends/android/jni/Android.mk b/src/frontends/android/jni/Android.mk index a603a733e..326565f2b 100644 --- a/src/frontends/android/jni/Android.mk +++ b/src/frontends/android/jni/Android.mk @@ -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) diff --git a/src/frontends/android/jni/libandroidbridge/android_jni.h b/src/frontends/android/jni/libandroidbridge/android_jni.h index 774d37d7e..bafd6b72e 100644 --- a/src/frontends/android/jni/libandroidbridge/android_jni.h +++ b/src/frontends/android/jni/libandroidbridge/android_jni.h @@ -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; } diff --git a/src/frontends/android/jni/libandroidbridge/backend/android_creds.c b/src/frontends/android/jni/libandroidbridge/backend/android_creds.c index 27023d721..931f22316 100644 --- a/src/frontends/android/jni/libandroidbridge/backend/android_creds.c +++ b/src/frontends/android/jni/libandroidbridge/backend/android_creds.c @@ -50,6 +50,15 @@ struct private_android_creds_t { }; /** + * Free allocated DER encoding + */ +static void free_encoding(chunk_t *chunk) +{ + chunk_free(chunk); + free(chunk); +} + +/** * Load trusted certificates via charonservice (JNI). */ static void load_trusted_certificates(private_android_creds_t *this) @@ -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**)¤t) == 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, }, diff --git a/src/frontends/android/jni/libandroidbridge/backend/android_creds.h b/src/frontends/android/jni/libandroidbridge/backend/android_creds.h index 33de838c1..a3ecddde4 100644 --- a/src/frontends/android/jni/libandroidbridge/backend/android_creds.h +++ b/src/frontends/android/jni/libandroidbridge/backend/android_creds.h @@ -47,6 +47,13 @@ struct android_creds_t { 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. */ void (*clear)(android_creds_t *this); diff --git a/src/frontends/android/jni/libandroidbridge/backend/android_service.c b/src/frontends/android/jni/libandroidbridge/backend/android_service.c index d1769a99a..f62aea0e8 100644 --- a/src/frontends/android/jni/libandroidbridge/backend/android_service.c +++ b/src/frontends/android/jni/libandroidbridge/backend/android_service.c @@ -44,11 +44,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 */ char *local_address; @@ -64,6 +74,11 @@ struct private_android_service_t { char *username; /** + * password + */ + char *password; + + /** * lock to safely access the TUN device fd */ rwlock_t *lock; @@ -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, ); diff --git a/src/frontends/android/jni/libandroidbridge/backend/android_service.h b/src/frontends/android/jni/libandroidbridge/backend/android_service.h index a7bd8b059..52c3dc5c8 100644 --- a/src/frontends/android/jni/libandroidbridge/backend/android_service.h +++ b/src/frontends/android/jni/libandroidbridge/backend/android_service.h @@ -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_ @}*/ diff --git a/src/frontends/android/jni/libandroidbridge/charonservice.c b/src/frontends/android/jni/libandroidbridge/charonservice.c index fab99ac10..59ec62fc7 100644 --- a/src/frontends/android/jni/libandroidbridge/charonservice.c +++ b/src/frontends/android/jni/libandroidbridge/charonservice.c @@ -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) - { - chunk_t *ca_cert; - jbyteArray jcert; + list = convert_array_of_byte_arrays(env, jcerts); + androidjni_detach_thread(); + return list; - ca_cert = malloc_thing(chunk_t); - list->insert_last(list, ca_cert); +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); - 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); + method_id = (*env)->GetMethodID(env, + android_charonvpnservice_class, + "getUserCertificate", "()[[B"); + if (!method_id) + { + 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); } diff --git a/src/frontends/android/jni/libandroidbridge/charonservice.h b/src/frontends/android/jni/libandroidbridge/charonservice.h index 706eaa220..507010bad 100644 --- a/src/frontends/android/jni/libandroidbridge/charonservice.h +++ b/src/frontends/android/jni/libandroidbridge/charonservice.h @@ -86,6 +86,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 * * @return VpnService.Builder instance diff --git a/src/frontends/android/res/layout/certificate_selector.xml b/src/frontends/android/res/layout/certificate_selector.xml new file mode 100644 index 000000000..c8c25811b --- /dev/null +++ b/src/frontends/android/res/layout/certificate_selector.xml @@ -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> diff --git a/src/frontends/android/res/layout/profile_detail_view.xml b/src/frontends/android/res/layout/profile_detail_view.xml index d9ccca546..39c94348b 100644 --- a/src/frontends/android/res/layout/profile_detail_view.xml +++ b/src/frontends/android/res/layout/profile_detail_view.xml @@ -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> diff --git a/src/frontends/android/res/layout/profile_list_item.xml b/src/frontends/android/res/layout/profile_list_item.xml index f55c8357a..93df7b649 100644 --- a/src/frontends/android/res/layout/profile_list_item.xml +++ b/src/frontends/android/res/layout/profile_list_item.xml @@ -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> diff --git a/src/frontends/android/res/values-de/arrays.xml b/src/frontends/android/res/values-de/arrays.xml new file mode 100644 index 000000000..efa4bcb03 --- /dev/null +++ b/src/frontends/android/res/values-de/arrays.xml @@ -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>
\ No newline at end of file diff --git a/src/frontends/android/res/values-de/strings.xml b/src/frontends/android/res/values-de/strings.xml index 9f3f637a8..a04da7208 100644 --- a/src/frontends/android/res/values-de/strings.xml +++ b/src/frontends/android/res/values-de/strings.xml @@ -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…</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> diff --git a/src/frontends/android/res/values-pl/arrays.xml b/src/frontends/android/res/values-pl/arrays.xml new file mode 100644 index 000000000..3e1af5f82 --- /dev/null +++ b/src/frontends/android/res/values-pl/arrays.xml @@ -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>
\ No newline at end of file diff --git a/src/frontends/android/res/values-pl/strings.xml b/src/frontends/android/res/values-pl/strings.xml index e7d4670d9..54f4259ae 100644 --- a/src/frontends/android/res/values-pl/strings.xml +++ b/src/frontends/android/res/values-pl/strings.xml @@ -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…</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> diff --git a/src/frontends/android/res/values/arrays.xml b/src/frontends/android/res/values/arrays.xml new file mode 100644 index 000000000..21576f22c --- /dev/null +++ b/src/frontends/android/res/values/arrays.xml @@ -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>
\ No newline at end of file diff --git a/src/frontends/android/res/values/strings.xml b/src/frontends/android/res/values/strings.xml index bc7fa4a1d..3e4b746fd 100644 --- a/src/frontends/android/res/values/strings.xml +++ b/src/frontends/android/res/values/strings.xml @@ -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…</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> diff --git a/src/frontends/android/src/org/strongswan/android/data/VpnProfile.java b/src/frontends/android/src/org/strongswan/android/data/VpnProfile.java index 053f91555..8323826d2 100644 --- a/src/frontends/android/src/org/strongswan/android/data/VpnProfile.java +++ b/src/frontends/android/src/org/strongswan/android/data/VpnProfile.java @@ -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 = alias; + } + + public String getUserCertificateAlias() + { + return mUserCertificate; + } + + public void setUserCertificateAlias(String alias) { - this.mCertificate = certificate; + this.mUserCertificate = alias; } @Override diff --git a/src/frontends/android/src/org/strongswan/android/data/VpnProfileDataSource.java b/src/frontends/android/src/org/strongswan/android/data/VpnProfileDataSource.java index 18632ad6f..6fd68d0c8 100644 --- a/src/frontends/android/src/org/strongswan/android/data/VpnProfileDataSource.java +++ b/src/frontends/android/src/org/strongswan/android/data/VpnProfileDataSource.java @@ -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; } } diff --git a/src/frontends/android/src/org/strongswan/android/data/VpnType.java b/src/frontends/android/src/org/strongswan/android/data/VpnType.java new file mode 100644 index 000000000..44a4fa6b4 --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/data/VpnType.java @@ -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; + } +} diff --git a/src/frontends/android/src/org/strongswan/android/logic/CharonVpnService.java b/src/frontends/android/src/org/strongswan/android/logic/CharonVpnService.java index c9c1ad02a..9b502e89a 100644 --- a/src/frontends/android/src/org/strongswan/android/logic/CharonVpnService.java +++ b/src/frontends/android/src/org/strongswan/android/logic/CharonVpnService.java @@ -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()); } @@ -421,6 +427,41 @@ public class CharonVpnService extends VpnService implements Runnable } /** + * 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 * * @param builder BuilderAdapter for this connection @@ -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); /** diff --git a/src/frontends/android/src/org/strongswan/android/ui/MainActivity.java b/src/frontends/android/src/org/strongswan/android/ui/MainActivity.java index bc5030ea5..6ebfdcafc 100644 --- a/src/frontends/android/src/org/strongswan/android/ui/MainActivity.java +++ b/src/frontends/android/src/org/strongswan/android/ui/MainActivity.java @@ -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,33 +60,11 @@ 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) { getMenuInflater().inflate(R.menu.main, 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,37 +140,24 @@ 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. */ private class CertificateLoadTask extends AsyncTask<Boolean, Void, TrustedCertificateManager> @@ -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(); + } + } } diff --git a/src/frontends/android/src/org/strongswan/android/ui/VpnProfileDetailActivity.java b/src/frontends/android/src/org/strongswan/android/ui/VpnProfileDetailActivity.java index 73365b40c..91e521cf4 100644 --- a/src/frontends/android/src/org/strongswan/android/ui/VpnProfileDetailActivity.java +++ b/src/frontends/android/src/org/strongswan/android/ui/VpnProfileDetailActivity.java @@ -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()); @@ -179,6 +219,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(); + } + } } diff --git a/src/frontends/android/src/org/strongswan/android/ui/adapter/VpnProfileAdapter.java b/src/frontends/android/src/org/strongswan/android/ui/adapter/VpnProfileAdapter.java index 39e3e586a..85dc8370a 100644 --- a/src/frontends/android/src/org/strongswan/android/ui/adapter/VpnProfileAdapter.java +++ b/src/frontends/android/src/org/strongswan/android/ui/adapter/VpnProfileAdapter.java @@ -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; } diff --git a/src/libstrongswan/Android.mk b/src/libstrongswan/Android.mk index 3b2d7eaaa..9125079a4 100644 --- a/src/libstrongswan/Android.mk +++ b/src/libstrongswan/Android.mk @@ -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) |