android: Migrate to the Gradle build system
authorTobias Brunner <tobias@strongswan.org>
Thu, 22 Oct 2015 15:18:14 +0000 (17:18 +0200)
committerTobias Brunner <tobias@strongswan.org>
Thu, 12 Nov 2015 13:11:37 +0000 (14:11 +0100)
This uses a manual way to trigger the NDK build (the default with
on-the-fly Android.mk files does not work for us).

266 files changed:
src/frontends/android/.classpath [deleted file]
src/frontends/android/.gitignore
src/frontends/android/AndroidManifest.xml [deleted file]
src/frontends/android/README.ndk
src/frontends/android/app/build.gradle [new file with mode: 0644]
src/frontends/android/app/src/main/AndroidManifest.xml [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/data/LogContentProvider.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnType.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/NetworkManager.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/StrongSwanApplication.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/TrustedCertificateManager.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/AndroidImc.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/ImcState.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/RemediationInstruction.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/Attribute.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/AttributeType.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/DeviceIdAttribute.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/InstalledPackagesAttribute.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/PortFilterAttribute.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/PrivateEnterpriseNumber.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/ProductInformationAttribute.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/SettingsAttribute.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/StringVersionAttribute.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/Collector.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/DeviceIdCollector.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/InstalledPackagesCollector.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/PortFilterCollector.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/ProductInformationCollector.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/Protocol.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/SettingsCollector.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/StringVersionCollector.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateKeyStoreProvider.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateKeyStoreSpi.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateStore.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/security/TrustedCertificateEntry.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/CertificateDeleteConfirmationDialog.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/ImcStateFragment.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogActivity.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogFragment.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogScrollView.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionFragment.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsActivity.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsFragment.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedCertificateImportActivity.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedCertificateListFragment.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedCertificatesActivity.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileSelectActivity.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnStateFragment.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/RemediationInstructionAdapter.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/TrustedCertificateAdapter.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/VpnProfileAdapter.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/utils/BufferedByteWriter.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/utils/SettingsWriter.java [new file with mode: 0644]
src/frontends/android/app/src/main/java/org/strongswan/android/utils/Utils.java [new file with mode: 0644]
src/frontends/android/app/src/main/jni/.gitignore [new file with mode: 0644]
src/frontends/android/app/src/main/jni/Android.mk [new file with mode: 0644]
src/frontends/android/app/src/main/jni/Application.mk [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/Android.mk [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/android_jni.c [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/android_jni.h [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_attr.c [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_attr.h [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_creds.c [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_creds.h [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_dns_proxy.c [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_dns_proxy.h [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_private_key.c [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_private_key.h [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_service.c [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_service.h [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/byod/imc_android.c [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/byod/imc_android.h [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/byod/imc_android_state.c [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/byod/imc_android_state.h [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/charonservice.c [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/charonservice.h [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/kernel/android_ipsec.c [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/kernel/android_ipsec.h [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/kernel/android_net.c [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/kernel/android_net.h [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/kernel/network_manager.c [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/kernel/network_manager.h [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/vpnservice_builder.c [new file with mode: 0644]
src/frontends/android/app/src/main/jni/libandroidbridge/vpnservice_builder.h [new file with mode: 0644]
src/frontends/android/app/src/main/res/drawable-hdpi/ic_launcher.png [new file with mode: 0644]
src/frontends/android/app/src/main/res/drawable-mdpi/ic_launcher.png [new file with mode: 0644]
src/frontends/android/app/src/main/res/drawable-xhdpi/ic_launcher.png [new file with mode: 0644]
src/frontends/android/app/src/main/res/drawable/remediation_instruction_background_large.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/drawable/state_background.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/layout-large/remediation_instructions.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/layout/imc_state_fragment.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/layout/log_activity.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/layout/log_fragment.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/layout/login_dialog.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/layout/main.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/layout/profile_detail_view.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/layout/profile_list_fragment.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/layout/profile_list_item.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/layout/remediation_instruction.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/layout/remediation_instruction_item.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/layout/remediation_instructions.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/layout/trusted_certificates_activity.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/layout/trusted_certificates_item.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/layout/two_line_button.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/layout/vpn_profile_select.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/layout/vpn_state_fragment.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/menu/certificates.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/menu/log.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/menu/main.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/menu/profile_edit.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/menu/profile_list.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/menu/profile_list_context.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/values-de/arrays.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/values-de/strings.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/values-pl/arrays.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/values-pl/strings.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/values-ru/arrays.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/values-ru/strings.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/values-ua/arrays.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/values-ua/strings.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/values/arrays.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/values/attrs.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/values/colors.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/values/strings.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/values/styles.xml [new file with mode: 0644]
src/frontends/android/build.gradle [new file with mode: 0644]
src/frontends/android/gradle/wrapper/gradle-wrapper.jar [new file with mode: 0644]
src/frontends/android/gradle/wrapper/gradle-wrapper.properties [new file with mode: 0644]
src/frontends/android/gradlew [new file with mode: 0755]
src/frontends/android/gradlew.bat [new file with mode: 0644]
src/frontends/android/jni/.gitignore [deleted file]
src/frontends/android/jni/Android.mk [deleted file]
src/frontends/android/jni/Application.mk [deleted file]
src/frontends/android/jni/libandroidbridge/Android.mk [deleted file]
src/frontends/android/jni/libandroidbridge/android_jni.c [deleted file]
src/frontends/android/jni/libandroidbridge/android_jni.h [deleted file]
src/frontends/android/jni/libandroidbridge/backend/android_attr.c [deleted file]
src/frontends/android/jni/libandroidbridge/backend/android_attr.h [deleted file]
src/frontends/android/jni/libandroidbridge/backend/android_creds.c [deleted file]
src/frontends/android/jni/libandroidbridge/backend/android_creds.h [deleted file]
src/frontends/android/jni/libandroidbridge/backend/android_dns_proxy.c [deleted file]
src/frontends/android/jni/libandroidbridge/backend/android_dns_proxy.h [deleted file]
src/frontends/android/jni/libandroidbridge/backend/android_private_key.c [deleted file]
src/frontends/android/jni/libandroidbridge/backend/android_private_key.h [deleted file]
src/frontends/android/jni/libandroidbridge/backend/android_service.c [deleted file]
src/frontends/android/jni/libandroidbridge/backend/android_service.h [deleted file]
src/frontends/android/jni/libandroidbridge/byod/imc_android.c [deleted file]
src/frontends/android/jni/libandroidbridge/byod/imc_android.h [deleted file]
src/frontends/android/jni/libandroidbridge/byod/imc_android_state.c [deleted file]
src/frontends/android/jni/libandroidbridge/byod/imc_android_state.h [deleted file]
src/frontends/android/jni/libandroidbridge/charonservice.c [deleted file]
src/frontends/android/jni/libandroidbridge/charonservice.h [deleted file]
src/frontends/android/jni/libandroidbridge/kernel/android_ipsec.c [deleted file]
src/frontends/android/jni/libandroidbridge/kernel/android_ipsec.h [deleted file]
src/frontends/android/jni/libandroidbridge/kernel/android_net.c [deleted file]
src/frontends/android/jni/libandroidbridge/kernel/android_net.h [deleted file]
src/frontends/android/jni/libandroidbridge/kernel/network_manager.c [deleted file]
src/frontends/android/jni/libandroidbridge/kernel/network_manager.h [deleted file]
src/frontends/android/jni/libandroidbridge/vpnservice_builder.c [deleted file]
src/frontends/android/jni/libandroidbridge/vpnservice_builder.h [deleted file]
src/frontends/android/proguard.cfg [deleted file]
src/frontends/android/project.properties [deleted file]
src/frontends/android/res/drawable-hdpi/ic_launcher.png [deleted file]
src/frontends/android/res/drawable-mdpi/ic_launcher.png [deleted file]
src/frontends/android/res/drawable-xhdpi/ic_launcher.png [deleted file]
src/frontends/android/res/drawable/remediation_instruction_background_large.xml [deleted file]
src/frontends/android/res/drawable/state_background.xml [deleted file]
src/frontends/android/res/layout-large/remediation_instructions.xml [deleted file]
src/frontends/android/res/layout/imc_state_fragment.xml [deleted file]
src/frontends/android/res/layout/log_activity.xml [deleted file]
src/frontends/android/res/layout/log_fragment.xml [deleted file]
src/frontends/android/res/layout/login_dialog.xml [deleted file]
src/frontends/android/res/layout/main.xml [deleted file]
src/frontends/android/res/layout/profile_detail_view.xml [deleted file]
src/frontends/android/res/layout/profile_list_fragment.xml [deleted file]
src/frontends/android/res/layout/profile_list_item.xml [deleted file]
src/frontends/android/res/layout/remediation_instruction.xml [deleted file]
src/frontends/android/res/layout/remediation_instruction_item.xml [deleted file]
src/frontends/android/res/layout/remediation_instructions.xml [deleted file]
src/frontends/android/res/layout/trusted_certificates_activity.xml [deleted file]
src/frontends/android/res/layout/trusted_certificates_item.xml [deleted file]
src/frontends/android/res/layout/two_line_button.xml [deleted file]
src/frontends/android/res/layout/vpn_profile_select.xml [deleted file]
src/frontends/android/res/layout/vpn_state_fragment.xml [deleted file]
src/frontends/android/res/menu/certificates.xml [deleted file]
src/frontends/android/res/menu/log.xml [deleted file]
src/frontends/android/res/menu/main.xml [deleted file]
src/frontends/android/res/menu/profile_edit.xml [deleted file]
src/frontends/android/res/menu/profile_list.xml [deleted file]
src/frontends/android/res/menu/profile_list_context.xml [deleted file]
src/frontends/android/res/values-de/arrays.xml [deleted file]
src/frontends/android/res/values-de/strings.xml [deleted file]
src/frontends/android/res/values-pl/arrays.xml [deleted file]
src/frontends/android/res/values-pl/strings.xml [deleted file]
src/frontends/android/res/values-ru/arrays.xml [deleted file]
src/frontends/android/res/values-ru/strings.xml [deleted file]
src/frontends/android/res/values-ua/arrays.xml [deleted file]
src/frontends/android/res/values-ua/strings.xml [deleted file]
src/frontends/android/res/values/arrays.xml [deleted file]
src/frontends/android/res/values/attrs.xml [deleted file]
src/frontends/android/res/values/colors.xml [deleted file]
src/frontends/android/res/values/strings.xml [deleted file]
src/frontends/android/res/values/styles.xml [deleted file]
src/frontends/android/settings.gradle [new file with mode: 0644]
src/frontends/android/src/org/strongswan/android/data/LogContentProvider.java [deleted file]
src/frontends/android/src/org/strongswan/android/data/VpnProfile.java [deleted file]
src/frontends/android/src/org/strongswan/android/data/VpnProfileDataSource.java [deleted file]
src/frontends/android/src/org/strongswan/android/data/VpnType.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/CharonVpnService.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/NetworkManager.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/StrongSwanApplication.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/TrustedCertificateManager.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/VpnStateService.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/AndroidImc.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/ImcState.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/RemediationInstruction.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/attributes/Attribute.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/attributes/AttributeType.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/attributes/DeviceIdAttribute.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/attributes/InstalledPackagesAttribute.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/attributes/PortFilterAttribute.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/attributes/PrivateEnterpriseNumber.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/attributes/ProductInformationAttribute.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/attributes/SettingsAttribute.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/attributes/StringVersionAttribute.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/collectors/Collector.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/collectors/DeviceIdCollector.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/collectors/InstalledPackagesCollector.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/collectors/PortFilterCollector.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/collectors/ProductInformationCollector.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/collectors/Protocol.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/collectors/SettingsCollector.java [deleted file]
src/frontends/android/src/org/strongswan/android/logic/imc/collectors/StringVersionCollector.java [deleted file]
src/frontends/android/src/org/strongswan/android/security/LocalCertificateKeyStoreProvider.java [deleted file]
src/frontends/android/src/org/strongswan/android/security/LocalCertificateKeyStoreSpi.java [deleted file]
src/frontends/android/src/org/strongswan/android/security/LocalCertificateStore.java [deleted file]
src/frontends/android/src/org/strongswan/android/security/TrustedCertificateEntry.java [deleted file]
src/frontends/android/src/org/strongswan/android/ui/CertificateDeleteConfirmationDialog.java [deleted file]
src/frontends/android/src/org/strongswan/android/ui/ImcStateFragment.java [deleted file]
src/frontends/android/src/org/strongswan/android/ui/LogActivity.java [deleted file]
src/frontends/android/src/org/strongswan/android/ui/LogFragment.java [deleted file]
src/frontends/android/src/org/strongswan/android/ui/LogScrollView.java [deleted file]
src/frontends/android/src/org/strongswan/android/ui/MainActivity.java [deleted file]
src/frontends/android/src/org/strongswan/android/ui/RemediationInstructionFragment.java [deleted file]
src/frontends/android/src/org/strongswan/android/ui/RemediationInstructionsActivity.java [deleted file]
src/frontends/android/src/org/strongswan/android/ui/RemediationInstructionsFragment.java [deleted file]
src/frontends/android/src/org/strongswan/android/ui/TrustedCertificateImportActivity.java [deleted file]
src/frontends/android/src/org/strongswan/android/ui/TrustedCertificateListFragment.java [deleted file]
src/frontends/android/src/org/strongswan/android/ui/TrustedCertificatesActivity.java [deleted file]
src/frontends/android/src/org/strongswan/android/ui/VpnProfileDetailActivity.java [deleted file]
src/frontends/android/src/org/strongswan/android/ui/VpnProfileListFragment.java [deleted file]
src/frontends/android/src/org/strongswan/android/ui/VpnProfileSelectActivity.java [deleted file]
src/frontends/android/src/org/strongswan/android/ui/VpnStateFragment.java [deleted file]
src/frontends/android/src/org/strongswan/android/ui/adapter/RemediationInstructionAdapter.java [deleted file]
src/frontends/android/src/org/strongswan/android/ui/adapter/TrustedCertificateAdapter.java [deleted file]
src/frontends/android/src/org/strongswan/android/ui/adapter/VpnProfileAdapter.java [deleted file]
src/frontends/android/src/org/strongswan/android/utils/BufferedByteWriter.java [deleted file]
src/frontends/android/src/org/strongswan/android/utils/SettingsWriter.java [deleted file]
src/frontends/android/src/org/strongswan/android/utils/Utils.java [deleted file]

diff --git a/src/frontends/android/.classpath b/src/frontends/android/.classpath
deleted file mode 100644 (file)
index 5176974..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<classpath>
-       <classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
-       <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
-       <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
-       <classpathentry kind="src" path="src"/>
-       <classpathentry kind="src" path="gen"/>
-       <classpathentry kind="output" path="bin/classes"/>
-</classpath>
index a031dba..7e4bcfd 100644 (file)
@@ -1,4 +1,7 @@
-bin/
-gen/
-libs/
-obj/
+.gradle/
+.idea/
+app/build/
+app/src/main/libs
+app/src/main/obj
+*.iml
+local.properties
diff --git a/src/frontends/android/AndroidManifest.xml b/src/frontends/android/AndroidManifest.xml
deleted file mode 100644 (file)
index 65a8275..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-    Copyright (C) 2012-2015 Tobias Brunner
-    Copyright (C) 2012 Giuliano Grassi
-    Copyright (C) 2012 Ralf Sager
-    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.
--->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="org.strongswan.android"
-    android:versionCode="28"
-    android:versionName="1.5.0" >
-
-    <uses-sdk android:minSdkVersion="15" android:targetSdkVersion="22" />
-
-    <uses-permission android:name="android.permission.INTERNET" />
-    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
-
-    <application
-        android:name=".logic.StrongSwanApplication"
-        android:icon="@drawable/ic_launcher"
-        android:label="@string/app_name"
-        android:theme="@style/ApplicationTheme"
-        android:allowBackup="false" >
-        <activity
-            android:name=".ui.MainActivity"
-            android:label="@string/main_activity_name"
-            android:launchMode="singleTop" >
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-            <intent-filter>
-                <action android:name="org.strongswan.android.action.START_PROFILE" />
-                <category android:name="android.intent.category.DEFAULT" />
-            </intent-filter>
-        </activity>
-        <activity
-            android:name=".ui.VpnProfileDetailActivity" >
-        </activity>
-        <activity
-            android:name=".ui.TrustedCertificatesActivity"
-            android:label="@string/trusted_certs_title" >
-        </activity>
-        <activity
-            android:name=".ui.LogActivity"
-            android:label="@string/log_title" >
-        </activity>
-        <activity
-            android:name=".ui.RemediationInstructionsActivity"
-            android:label="@string/remediation_instructions_title" >
-        </activity>
-        <activity
-            android:name=".ui.VpnProfileSelectActivity"
-            android:label="@string/strongswan_shortcut" >
-            <intent-filter>
-                <action android:name="android.intent.action.CREATE_SHORTCUT" />
-                <category android:name="android.intent.category.DEFAULT" />
-            </intent-filter>
-        </activity>
-        <activity
-            android:name=".ui.TrustedCertificateImportActivity"
-            android:label="@string/import_certificate"
-            android:theme="@android:style/Theme.Holo.Dialog.NoActionBar" >
-            <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="application/x-x509-ca-cert" />
-                <data android:mimeType="application/x-x509-server-cert" />
-                <data android:mimeType="application/x-pem-file" />
-                <data android:mimeType="application/pkix-cert" />
-            </intent-filter>
-        </activity>
-
-        <service
-            android:name=".logic.VpnStateService"
-            android:exported="false" >
-        </service>
-        <service
-            android:name=".logic.CharonVpnService"
-            android:exported="false"
-            android:permission="android.permission.BIND_VPN_SERVICE" >
-            <intent-filter>
-                <action android:name="android.net.VpnService" />
-            </intent-filter>
-        </service>
-
-        <provider
-            android:name=".data.LogContentProvider"
-            android:authorities="org.strongswan.android.content.log"
-            android:exported="true" >
-            <!-- android:grantUriPermissions="true" combined with a custom permission does
-                 not work (probably too many indirections with ACTION_SEND) so we secure
-                 this provider with a custom ticketing system -->
-        </provider>
-    </application>
-
-</manifest>
index 699fa3f..7c8cd30 100644 (file)
@@ -1,5 +1,5 @@
-To build this within the NDK several things have to be added in the jni
-folder:
+To build this within the NDK several things have to be added in the
+app/src/main/jni/ folder:
 
  - strongswan: The strongSwan sources.  This can either be an extracted tarball,
    or a symlink to the Git repository.  To build from the repository the sources
diff --git a/src/frontends/android/app/build.gradle b/src/frontends/android/app/build.gradle
new file mode 100644 (file)
index 0000000..8280f50
--- /dev/null
@@ -0,0 +1,42 @@
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 21
+    buildToolsVersion "22.0.1"
+
+    defaultConfig {
+        applicationId "org.strongswan.android"
+        minSdkVersion 15
+        targetSdkVersion 22
+        versionCode 28
+        versionName "1.5.0"
+    }
+
+    sourceSets.main {
+        jni.srcDirs = [] // disables the default ndk-build call (with on-the-fly Android.mk files)
+        jniLibs.srcDir 'src/main/libs'
+    }
+
+    task buildNative(type: Exec) {
+        workingDir 'src/main/jni'
+        commandLine "${android.ndkDirectory}/ndk-build", '-j', Runtime.runtime.availableProcessors()
+    }
+
+    task cleanNative(type: Exec) {
+        workingDir 'src/main/jni'
+        commandLine "${android.ndkDirectory}/ndk-build", 'clean'
+    }
+
+    tasks.withType(JavaCompile) {
+        compileTask -> compileTask.dependsOn buildNative
+    }
+
+    clean.dependsOn 'cleanNative'
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+        }
+    }
+}
diff --git a/src/frontends/android/app/src/main/AndroidManifest.xml b/src/frontends/android/app/src/main/AndroidManifest.xml
new file mode 100644 (file)
index 0000000..2ab833c
--- /dev/null
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2012-2015 Tobias Brunner
+    Copyright (C) 2012 Giuliano Grassi
+    Copyright (C) 2012 Ralf Sager
+    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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="org.strongswan.android">
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+
+    <application
+        android:name=".logic.StrongSwanApplication"
+        android:icon="@drawable/ic_launcher"
+        android:label="@string/app_name"
+        android:theme="@style/ApplicationTheme"
+        android:allowBackup="false" >
+        <activity
+            android:name=".ui.MainActivity"
+            android:label="@string/main_activity_name"
+            android:launchMode="singleTop" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="org.strongswan.android.action.START_PROFILE" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".ui.VpnProfileDetailActivity" >
+        </activity>
+        <activity
+            android:name=".ui.TrustedCertificatesActivity"
+            android:label="@string/trusted_certs_title" >
+        </activity>
+        <activity
+            android:name=".ui.LogActivity"
+            android:label="@string/log_title" >
+        </activity>
+        <activity
+            android:name=".ui.RemediationInstructionsActivity"
+            android:label="@string/remediation_instructions_title" >
+        </activity>
+        <activity
+            android:name=".ui.VpnProfileSelectActivity"
+            android:label="@string/strongswan_shortcut" >
+            <intent-filter>
+                <action android:name="android.intent.action.CREATE_SHORTCUT" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".ui.TrustedCertificateImportActivity"
+            android:label="@string/import_certificate"
+            android:theme="@android:style/Theme.Holo.Dialog.NoActionBar" >
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="application/x-x509-ca-cert" />
+                <data android:mimeType="application/x-x509-server-cert" />
+                <data android:mimeType="application/x-pem-file" />
+                <data android:mimeType="application/pkix-cert" />
+            </intent-filter>
+        </activity>
+
+        <service
+            android:name=".logic.VpnStateService"
+            android:exported="false" >
+        </service>
+        <service
+            android:name=".logic.CharonVpnService"
+            android:exported="false"
+            android:permission="android.permission.BIND_VPN_SERVICE" >
+            <intent-filter>
+                <action android:name="android.net.VpnService" />
+            </intent-filter>
+        </service>
+
+        <provider
+            android:name=".data.LogContentProvider"
+            android:authorities="org.strongswan.android.content.log"
+            android:exported="true" >
+            <!-- android:grantUriPermissions="true" combined with a custom permission does
+                 not work (probably too many indirections with ACTION_SEND) so we secure
+                 this provider with a custom ticketing system -->
+        </provider>
+    </application>
+
+</manifest>
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/LogContentProvider.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/LogContentProvider.java
new file mode 100644 (file)
index 0000000..370a8d5
--- /dev/null
@@ -0,0 +1,149 @@
+/*
+ * 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;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.strongswan.android.logic.CharonVpnService;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.provider.OpenableColumns;
+
+public class LogContentProvider extends ContentProvider
+{
+       private static final String AUTHORITY = "org.strongswan.android.content.log";
+       /* an Uri is valid for 30 minutes */
+       private static final long URI_VALIDITY = 30 * 60 * 1000;
+       private static ConcurrentHashMap<Uri, Long> mUris = new ConcurrentHashMap<Uri, Long>();
+       private File mLogFile;
+
+       public LogContentProvider()
+       {
+       }
+
+       @Override
+       public boolean onCreate()
+       {
+               mLogFile = new File(getContext().getFilesDir(), CharonVpnService.LOG_FILE);
+               return true;
+       }
+
+       /**
+        * The log file can only be accessed by Uris created with this method
+        * @return null if failed to create the Uri
+        */
+       public static Uri createContentUri()
+       {
+               SecureRandom random;
+               try
+               {
+                       random = SecureRandom.getInstance("SHA1PRNG");
+               }
+               catch (NoSuchAlgorithmException e)
+               {
+                       return null;
+               }
+               Uri uri = Uri.parse("content://" + AUTHORITY + "/" + random.nextLong());
+               mUris.put(uri, SystemClock.uptimeMillis());
+               return uri;
+       }
+
+       @Override
+       public String getType(Uri uri)
+       {
+               /* MIME type for our log file */
+               return "text/plain";
+       }
+
+       @Override
+       public Cursor query(Uri uri, String[] projection, String selection,
+                                               String[] selectionArgs, String sortOrder)
+       {
+               /* this is called by apps to find out the name and size of the file.
+                * since we only provide a single file this is simple to implement */
+               if (projection == null || projection.length < 1)
+               {
+                       return null;
+               }
+               Long timestamp = mUris.get(uri);
+               if (timestamp == null)
+               {       /* don't check the validity as this information is not really private */
+                       return null;
+               }
+               MatrixCursor cursor = new MatrixCursor(projection, 1);
+               if (OpenableColumns.DISPLAY_NAME.equals(cursor.getColumnName(0)))
+               {
+                       cursor.newRow().add(CharonVpnService.LOG_FILE);
+               }
+               else if (OpenableColumns.SIZE.equals(cursor.getColumnName(0)))
+               {
+                       cursor.newRow().add(mLogFile.length());
+               }
+               else
+               {
+                       return null;
+               }
+               return cursor;
+       }
+
+       @Override
+       public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException
+       {
+               Long timestamp = mUris.get(uri);
+               if (timestamp != null)
+               {
+                       long elapsed = SystemClock.uptimeMillis() - timestamp;
+                       if (elapsed > 0 && elapsed < URI_VALIDITY)
+                       {       /* we fail if clock wrapped, should happen rarely though */
+                               return ParcelFileDescriptor.open(mLogFile, ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_ONLY);
+                       }
+                       mUris.remove(uri);
+               }
+               return super.openFile(uri, mode);
+       }
+
+       @Override
+       public Uri insert(Uri uri, ContentValues values)
+       {
+               /* not supported */
+               return null;
+       }
+
+       @Override
+       public int delete(Uri uri, String selection, String[] selectionArgs)
+       {
+               /* not supported */
+               return 0;
+       }
+
+       @Override
+       public int update(Uri uri, ContentValues values, String selection,
+                                         String[] selectionArgs)
+       {
+               /* not supported */
+               return 0;
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java
new file mode 100644 (file)
index 0000000..5c64ad0
--- /dev/null
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2012-2015 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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 class VpnProfile implements Cloneable
+{
+       /* While storing this as EnumSet would be nicer this simplifies storing it in a database */
+       public static final int SPLIT_TUNNELING_BLOCK_IPV4 = 1;
+       public static final int SPLIT_TUNNELING_BLOCK_IPV6 = 2;
+
+       private String mName, mGateway, mUsername, mPassword, mCertificate, mUserCertificate;
+       private Integer mMTU, mPort, mSplitTunneling;
+       private VpnType mVpnType;
+       private long mId = -1;
+
+       public long getId()
+       {
+               return mId;
+       }
+
+       public void setId(long id)
+       {
+               this.mId = id;
+       }
+
+       public String getName()
+       {
+               return mName;
+       }
+
+       public void setName(String name)
+       {
+               this.mName = name;
+       }
+
+       public String getGateway()
+       {
+               return mGateway;
+       }
+
+       public void setGateway(String gateway)
+       {
+               this.mGateway = gateway;
+       }
+
+       public VpnType getVpnType()
+       {
+               return mVpnType;
+       }
+
+       public void setVpnType(VpnType type)
+       {
+               this.mVpnType = type;
+       }
+
+       public String getUsername()
+       {
+               return mUsername;
+       }
+
+       public void setUsername(String username)
+       {
+               this.mUsername = username;
+       }
+
+       public String getPassword()
+       {
+               return mPassword;
+       }
+
+       public void setPassword(String password)
+       {
+               this.mPassword = password;
+       }
+
+       public String getCertificateAlias()
+       {
+               return mCertificate;
+       }
+
+       public void setCertificateAlias(String alias)
+       {
+               this.mCertificate = alias;
+       }
+
+       public String getUserCertificateAlias()
+       {
+               return mUserCertificate;
+       }
+
+       public void setUserCertificateAlias(String alias)
+       {
+               this.mUserCertificate = alias;
+       }
+
+       public Integer getMTU()
+       {
+               return mMTU;
+       }
+
+       public void setMTU(Integer mtu)
+       {
+               this.mMTU = mtu;
+       }
+
+       public Integer getPort()
+       {
+               return mPort;
+       }
+
+       public void setPort(Integer port)
+       {
+               this.mPort = port;
+       }
+
+       public Integer getSplitTunneling()
+       {
+               return mSplitTunneling;
+       }
+
+       public void setSplitTunneling(Integer splitTunneling)
+       {
+               this.mSplitTunneling = splitTunneling;
+       }
+
+       @Override
+       public String toString()
+       {
+               return mName;
+       }
+
+       @Override
+       public boolean equals(Object o)
+       {
+               if (o != null && o instanceof VpnProfile)
+               {
+                       return this.mId == ((VpnProfile)o).getId();
+               }
+               return false;
+       }
+
+       @Override
+       public VpnProfile clone()
+       {
+               try
+               {
+                       return (VpnProfile)super.clone();
+               }
+               catch (CloneNotSupportedException e)
+               {
+                       throw new AssertionError();
+               }
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java
new file mode 100644 (file)
index 0000000..45e9b86
--- /dev/null
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2012-2015 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * 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;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.content.ContentValues;
+import android.content.Context;
+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
+{
+       private static final String TAG = VpnProfileDataSource.class.getSimpleName();
+       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";
+       public static final String KEY_MTU = "mtu";
+       public static final String KEY_PORT = "port";
+       public static final String KEY_SPLIT_TUNNELING = "split_tunneling";
+
+       private DatabaseHelper mDbHelper;
+       private SQLiteDatabase mDatabase;
+       private final Context mContext;
+
+       private static final String DATABASE_NAME = "strongswan.db";
+       private static final String TABLE_VPNPROFILE = "vpnprofile";
+
+       private static final int DATABASE_VERSION = 7;
+
+       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_VPN_TYPE + " TEXT NOT NULL," +
+                                                               KEY_USERNAME + " TEXT," +
+                                                               KEY_PASSWORD + " TEXT," +
+                                                               KEY_CERTIFICATE + " TEXT," +
+                                                               KEY_USER_CERTIFICATE + " TEXT," +
+                                                               KEY_MTU + " INTEGER," +
+                                                               KEY_PORT + " INTEGER," +
+                                                               KEY_SPLIT_TUNNELING + " INTEGER" +
+                                                       ");";
+       private static final String[] ALL_COLUMNS = new String[] {
+                                                               KEY_ID,
+                                                               KEY_NAME,
+                                                               KEY_GATEWAY,
+                                                               KEY_VPN_TYPE,
+                                                               KEY_USERNAME,
+                                                               KEY_PASSWORD,
+                                                               KEY_CERTIFICATE,
+                                                               KEY_USER_CERTIFICATE,
+                                                               KEY_MTU,
+                                                               KEY_PORT,
+                                                               KEY_SPLIT_TUNNELING,
+                                                       };
+
+       private static class DatabaseHelper extends SQLiteOpenHelper
+       {
+               public DatabaseHelper(Context context)
+               {
+                       super(context, DATABASE_NAME, null, DATABASE_VERSION);
+               }
+
+               @Override
+               public void onCreate(SQLiteDatabase database)
+               {
+                       database.execSQL(DATABASE_CREATE);
+               }
+
+               @Override
+               public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
+               {
+                       Log.w(TAG, "Upgrading database from version " + oldVersion +
+                                 " 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);
+                       }
+                       if (oldVersion < 5)
+                       {
+                               db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_MTU +
+                                                  " INTEGER;");
+                       }
+                       if (oldVersion < 6)
+                       {
+                               db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_PORT +
+                                                  " INTEGER;");
+                       }
+                       if (oldVersion < 7)
+                       {
+                               db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_SPLIT_TUNNELING +
+                                                  " INTEGER;");
+                       }
+               }
+
+               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();
+                       }
+               }
+       }
+
+       /**
+        * Construct a new VPN profile data source. The context is used to
+        * open/create the database.
+        * @param context context used to access the database
+        */
+       public VpnProfileDataSource(Context context)
+       {
+               this.mContext = context;
+       }
+
+       /**
+        * Open the VPN profile data source. The database is automatically created
+        * if it does not yet exist. If that fails an exception is thrown.
+        * @return itself (allows to chain initialization calls)
+        * @throws SQLException if the database could not be opened or created
+        */
+       public VpnProfileDataSource open() throws SQLException
+       {
+               if (mDbHelper == null)
+               {
+                       mDbHelper = new DatabaseHelper(mContext);
+                       mDatabase = mDbHelper.getWritableDatabase();
+               }
+               return this;
+       }
+
+       /**
+        * Close the data source.
+        */
+       public void close()
+       {
+               if (mDbHelper != null)
+               {
+                       mDbHelper.close();
+                       mDbHelper = null;
+               }
+       }
+
+       /**
+        * Insert the given VPN profile into the database.  On success the Id of
+        * the object is updated and the object returned.
+        *
+        * @param profile the profile to add
+        * @return the added VPN profile or null, if failed
+        */
+       public VpnProfile insertProfile(VpnProfile profile)
+       {
+               ContentValues values = ContentValuesFromVpnProfile(profile);
+               long insertId = mDatabase.insert(TABLE_VPNPROFILE, null, values);
+               if (insertId == -1)
+               {
+                       return null;
+               }
+               profile.setId(insertId);
+               return profile;
+       }
+
+       /**
+        * Updates the given VPN profile in the database.
+        * @param profile the profile to update
+        * @return true if update succeeded, false otherwise
+        */
+       public boolean updateVpnProfile(VpnProfile profile)
+       {
+               long id = profile.getId();
+               ContentValues values = ContentValuesFromVpnProfile(profile);
+               return mDatabase.update(TABLE_VPNPROFILE, values, KEY_ID + " = " + id, null) > 0;
+       }
+
+       /**
+        * Delete the given VPN profile from the database.
+        * @param profile the profile to delete
+        * @return true if deleted, false otherwise
+        */
+       public boolean deleteVpnProfile(VpnProfile profile)
+       {
+               long id = profile.getId();
+               return mDatabase.delete(TABLE_VPNPROFILE, KEY_ID + " = " + id, null) > 0;
+       }
+
+       /**
+        * Get a single VPN profile from the database.
+        * @param id the ID of the VPN profile
+        * @return the profile or null, if not found
+        */
+       public VpnProfile getVpnProfile(long id)
+       {
+               VpnProfile profile = null;
+               Cursor cursor = mDatabase.query(TABLE_VPNPROFILE, ALL_COLUMNS,
+                                                                               KEY_ID + "=" + id, null, null, null, null);
+               if (cursor.moveToFirst())
+               {
+                       profile = VpnProfileFromCursor(cursor);
+               }
+               cursor.close();
+               return profile;
+       }
+
+       /**
+        * Get a list of all VPN profiles stored in the database.
+        * @return list of VPN profiles
+        */
+       public List<VpnProfile> getAllVpnProfiles()
+       {
+               List<VpnProfile> vpnProfiles = new ArrayList<VpnProfile>();
+
+               Cursor cursor = mDatabase.query(TABLE_VPNPROFILE, ALL_COLUMNS, null, null, null, null, null);
+               cursor.moveToFirst();
+               while (!cursor.isAfterLast())
+               {
+                       VpnProfile vpnProfile = VpnProfileFromCursor(cursor);
+                       vpnProfiles.add(vpnProfile);
+                       cursor.moveToNext();
+               }
+               cursor.close();
+               return vpnProfiles;
+       }
+
+       private VpnProfile VpnProfileFromCursor(Cursor cursor)
+       {
+               VpnProfile profile = new VpnProfile();
+               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)));
+               profile.setMTU(getInt(cursor, cursor.getColumnIndex(KEY_MTU)));
+               profile.setPort(getInt(cursor, cursor.getColumnIndex(KEY_PORT)));
+               profile.setSplitTunneling(getInt(cursor, cursor.getColumnIndex(KEY_SPLIT_TUNNELING)));
+               return profile;
+       }
+
+       private ContentValues ContentValuesFromVpnProfile(VpnProfile profile)
+       {
+               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());
+               values.put(KEY_MTU, profile.getMTU());
+               values.put(KEY_PORT, profile.getPort());
+               values.put(KEY_SPLIT_TUNNELING, profile.getSplitTunneling());
+               return values;
+       }
+
+       private Integer getInt(Cursor cursor, int columnIndex)
+       {
+               return cursor.isNull(columnIndex) ? null : cursor.getInt(columnIndex);
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnType.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnType.java
new file mode 100644 (file)
index 0000000..bb7fd09
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2012-2014 Tobias Brunner
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.data;
+
+import java.util.EnumSet;
+
+public enum VpnType
+{
+       /* the order here must match the items in R.array.vpn_types */
+       IKEV2_EAP("ikev2-eap", EnumSet.of(VpnTypeFeature.USER_PASS)),
+       IKEV2_CERT("ikev2-cert", EnumSet.of(VpnTypeFeature.CERTIFICATE)),
+       IKEV2_CERT_EAP("ikev2-cert-eap", EnumSet.of(VpnTypeFeature.USER_PASS, VpnTypeFeature.CERTIFICATE)),
+       IKEV2_EAP_TLS("ikev2-eap-tls", EnumSet.of(VpnTypeFeature.CERTIFICATE)),
+       IKEV2_BYOD_EAP("ikev2-byod-eap", EnumSet.of(VpnTypeFeature.USER_PASS, VpnTypeFeature.BYOD));
+
+       /**
+        * Features of a VPN type.
+        */
+       public enum VpnTypeFeature
+       {
+               /** client certificate is required */
+               CERTIFICATE,
+               /** username and password are required */
+               USER_PASS,
+               /** enable BYOD features */
+               BYOD;
+       }
+
+       private String mIdentifier;
+       private EnumSet<VpnTypeFeature> mFeatures;
+
+       /**
+        * Enum which provides additional information about the supported VPN types.
+        *
+        * @param id identifier used to store and transmit this specific type
+        * @param features of the given VPN type
+        * @param certificate true if a client certificate is required
+        */
+       VpnType(String id, EnumSet<VpnTypeFeature> features)
+       {
+               mIdentifier = id;
+               mFeatures = features;
+       }
+
+       /**
+        * The identifier used to store this value in the database
+        * @return identifier
+        */
+       public String getIdentifier()
+       {
+               return mIdentifier;
+       }
+
+       /**
+        * Checks whether a feature is supported/required by this type of VPN.
+        *
+        * @return true if the feature is supported/required
+        */
+       public boolean has(VpnTypeFeature feature)
+       {
+               return mFeatures.contains(feature);
+       }
+
+       /**
+        * 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/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java
new file mode 100644 (file)
index 0000000..e5241d5
--- /dev/null
@@ -0,0 +1,857 @@
+/*
+ * Copyright (C) 2012-2015 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic;
+
+import java.io.File;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.PrivateKey;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import org.strongswan.android.data.VpnProfile;
+import org.strongswan.android.data.VpnProfileDataSource;
+import org.strongswan.android.data.VpnType.VpnTypeFeature;
+import org.strongswan.android.logic.VpnStateService.ErrorState;
+import org.strongswan.android.logic.VpnStateService.State;
+import org.strongswan.android.logic.imc.ImcState;
+import org.strongswan.android.logic.imc.RemediationInstruction;
+import org.strongswan.android.ui.MainActivity;
+import org.strongswan.android.utils.SettingsWriter;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.net.VpnService;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.security.KeyChain;
+import android.security.KeyChainException;
+import android.system.OsConstants;
+import android.util.Log;
+
+public class CharonVpnService extends VpnService implements Runnable
+{
+       private static final String TAG = CharonVpnService.class.getSimpleName();
+       public static final String LOG_FILE = "charon.log";
+
+       private String mLogFile;
+       private VpnProfileDataSource mDataSource;
+       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;
+       private volatile boolean mIsDisconnecting;
+       private VpnStateService mService;
+       private final Object mServiceLock = new Object();
+       private final ServiceConnection mServiceConnection = new ServiceConnection() {
+               @Override
+               public void onServiceDisconnected(ComponentName name)
+               {       /* since the service is local this is theoretically only called when the process is terminated */
+                       synchronized (mServiceLock)
+                       {
+                               mService = null;
+                       }
+               }
+
+               @Override
+               public void onServiceConnected(ComponentName name, IBinder service)
+               {
+                       synchronized (mServiceLock)
+                       {
+                               mService = ((VpnStateService.LocalBinder)service).getService();
+                       }
+                       /* we are now ready to start the handler thread */
+                       mConnectionHandler.start();
+               }
+       };
+
+       /**
+        * as defined in charonservice.h
+        */
+       static final int STATE_CHILD_SA_UP = 1;
+       static final int STATE_CHILD_SA_DOWN = 2;
+       static final int STATE_AUTH_ERROR = 3;
+       static final int STATE_PEER_AUTH_ERROR = 4;
+       static final int STATE_LOOKUP_ERROR = 5;
+       static final int STATE_UNREACHABLE_ERROR = 6;
+       static final int STATE_GENERIC_ERROR = 7;
+
+       @Override
+       public int onStartCommand(Intent intent, int flags, int startId)
+       {
+               if (intent != null)
+               {
+                       Bundle bundle = intent.getExtras();
+                       VpnProfile profile = null;
+                       if (bundle != null)
+                       {
+                               profile = mDataSource.getVpnProfile(bundle.getLong(VpnProfileDataSource.KEY_ID));
+                               if (profile != null)
+                               {
+                                       String password = bundle.getString(VpnProfileDataSource.KEY_PASSWORD);
+                                       profile.setPassword(password);
+                               }
+                       }
+                       setNextProfile(profile);
+               }
+               return START_NOT_STICKY;
+       }
+
+       @Override
+       public void onCreate()
+       {
+               mLogFile = getFilesDir().getAbsolutePath() + File.separator + LOG_FILE;
+
+               mDataSource = new VpnProfileDataSource(this);
+               mDataSource.open();
+               /* use a separate thread as main thread for charon */
+               mConnectionHandler = new Thread(this);
+               /* the thread is started when the service is bound */
+               bindService(new Intent(this, VpnStateService.class),
+                                       mServiceConnection, Service.BIND_AUTO_CREATE);
+       }
+
+       @Override
+       public void onRevoke()
+       {       /* the system revoked the rights grated with the initial prepare() call.
+                * called when the user clicks disconnect in the system's VPN dialog */
+               setNextProfile(null);
+       }
+
+       @Override
+       public void onDestroy()
+       {
+               mTerminate = true;
+               setNextProfile(null);
+               try
+               {
+                       mConnectionHandler.join();
+               }
+               catch (InterruptedException e)
+               {
+                       e.printStackTrace();
+               }
+               if (mService != null)
+               {
+                       unbindService(mServiceConnection);
+               }
+               mDataSource.close();
+       }
+
+       /**
+        * Set the profile that is to be initiated next. Notify the handler thread.
+        *
+        * @param profile the profile to initiate
+        */
+       private void setNextProfile(VpnProfile profile)
+       {
+               synchronized (this)
+               {
+                       this.mNextProfile = profile;
+                       mProfileUpdated = true;
+                       notifyAll();
+               }
+       }
+
+       @Override
+       public void run()
+       {
+               while (true)
+               {
+                       synchronized (this)
+                       {
+                               try
+                               {
+                                       while (!mProfileUpdated)
+                                       {
+                                               wait();
+                                       }
+
+                                       mProfileUpdated = false;
+                                       stopCurrentConnection();
+                                       if (mNextProfile == null)
+                                       {
+                                               setState(State.DISABLED);
+                                               if (mTerminate)
+                                               {
+                                                       break;
+                                               }
+                                       }
+                                       else
+                                       {
+                                               mCurrentProfile = mNextProfile;
+                                               mNextProfile = null;
+
+                                               /* store this in a separate (volatile) variable to avoid
+                                                * a possible deadlock during deinitialization */
+                                               mCurrentCertificateAlias = mCurrentProfile.getCertificateAlias();
+                                               mCurrentUserCertificateAlias = mCurrentProfile.getUserCertificateAlias();
+
+                                               startConnection(mCurrentProfile);
+                                               mIsDisconnecting = false;
+
+                                               BuilderAdapter builder = new BuilderAdapter(mCurrentProfile.getName(), mCurrentProfile.getSplitTunneling());
+                                               if (initializeCharon(builder, mLogFile, mCurrentProfile.getVpnType().has(VpnTypeFeature.BYOD)))
+                                               {
+                                                       Log.i(TAG, "charon started");
+                                                       SettingsWriter writer = new SettingsWriter();
+                                                       writer.setValue("global.language", Locale.getDefault().getLanguage());
+                                                       writer.setValue("global.mtu", mCurrentProfile.getMTU());
+                                                       writer.setValue("connection.type", mCurrentProfile.getVpnType().getIdentifier());
+                                                       writer.setValue("connection.server", mCurrentProfile.getGateway());
+                                                       writer.setValue("connection.port", mCurrentProfile.getPort());
+                                                       writer.setValue("connection.username", mCurrentProfile.getUsername());
+                                                       writer.setValue("connection.password", mCurrentProfile.getPassword());
+                                                       initiate(writer.serialize());
+                                               }
+                                               else
+                                               {
+                                                       Log.e(TAG, "failed to start charon");
+                                                       setError(ErrorState.GENERIC_ERROR);
+                                                       setState(State.DISABLED);
+                                                       mCurrentProfile = null;
+                                               }
+                                       }
+                               }
+                               catch (InterruptedException ex)
+                               {
+                                       stopCurrentConnection();
+                                       setState(State.DISABLED);
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Stop any existing connection by deinitializing charon.
+        */
+       private void stopCurrentConnection()
+       {
+               synchronized (this)
+               {
+                       if (mCurrentProfile != null)
+                       {
+                               setState(State.DISCONNECTING);
+                               mIsDisconnecting = true;
+                               deinitializeCharon();
+                               Log.i(TAG, "charon stopped");
+                               mCurrentProfile = null;
+                       }
+               }
+       }
+
+       /**
+        * Notify the state service about a new connection attempt.
+        * Called by the handler thread.
+        *
+        * @param profile currently active VPN profile
+        */
+       private void startConnection(VpnProfile profile)
+       {
+               synchronized (mServiceLock)
+               {
+                       if (mService != null)
+                       {
+                               mService.startConnection(profile);
+                       }
+               }
+       }
+
+       /**
+        * Update the current VPN state on the state service. Called by the handler
+        * thread and any of charon's threads.
+        *
+        * @param state current state
+        */
+       private void setState(State state)
+       {
+               synchronized (mServiceLock)
+               {
+                       if (mService != null)
+                       {
+                               mService.setState(state);
+                       }
+               }
+       }
+
+       /**
+        * Set an error on the state service. Called by the handler thread and any
+        * of charon's threads.
+        *
+        * @param error error state
+        */
+       private void setError(ErrorState error)
+       {
+               synchronized (mServiceLock)
+               {
+                       if (mService != null)
+                       {
+                               mService.setError(error);
+                       }
+               }
+       }
+
+       /**
+        * Set the IMC state on the state service. Called by the handler thread and
+        * any of charon's threads.
+        *
+        * @param state IMC state
+        */
+       private void setImcState(ImcState state)
+       {
+               synchronized (mServiceLock)
+               {
+                       if (mService != null)
+                       {
+                               mService.setImcState(state);
+                       }
+               }
+       }
+
+       /**
+        * Set an error on the state service. Called by the handler thread and any
+        * of charon's threads.
+        *
+        * @param error error state
+        */
+       private void setErrorDisconnect(ErrorState error)
+       {
+               synchronized (mServiceLock)
+               {
+                       if (mService != null)
+                       {
+                               if (!mIsDisconnecting)
+                               {
+                                       mService.setError(error);
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Updates the state of the current connection.
+        * Called via JNI by different threads (but not concurrently).
+        *
+        * @param status new state
+        */
+       public void updateStatus(int status)
+       {
+               switch (status)
+               {
+                       case STATE_CHILD_SA_DOWN:
+                               if (!mIsDisconnecting)
+                               {
+                                       setState(State.CONNECTING);
+                               }
+                               break;
+                       case STATE_CHILD_SA_UP:
+                               setState(State.CONNECTED);
+                               break;
+                       case STATE_AUTH_ERROR:
+                               setErrorDisconnect(ErrorState.AUTH_FAILED);
+                               break;
+                       case STATE_PEER_AUTH_ERROR:
+                               setErrorDisconnect(ErrorState.PEER_AUTH_FAILED);
+                               break;
+                       case STATE_LOOKUP_ERROR:
+                               setErrorDisconnect(ErrorState.LOOKUP_FAILED);
+                               break;
+                       case STATE_UNREACHABLE_ERROR:
+                               setErrorDisconnect(ErrorState.UNREACHABLE);
+                               break;
+                       case STATE_GENERIC_ERROR:
+                               setErrorDisconnect(ErrorState.GENERIC_ERROR);
+                               break;
+                       default:
+                               Log.e(TAG, "Unknown status code received");
+                               break;
+               }
+       }
+
+       /**
+        * Updates the IMC state of the current connection.
+        * Called via JNI by different threads (but not concurrently).
+        *
+        * @param value new state
+        */
+       public void updateImcState(int value)
+       {
+               ImcState state = ImcState.fromValue(value);
+               if (state != null)
+               {
+                       setImcState(state);
+               }
+       }
+
+       /**
+        * Add a remediation instruction to the VPN state service.
+        * Called via JNI by different threads (but not concurrently).
+        *
+        * @param xml XML text
+        */
+       public void addRemediationInstruction(String xml)
+       {
+               for (RemediationInstruction instruction : RemediationInstruction.fromXml(xml))
+               {
+                       synchronized (mServiceLock)
+                       {
+                               if (mService != null)
+                               {
+                                       mService.addRemediationInstruction(instruction);
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Function called via JNI to generate a list of DER encoded CA certificates
+        * as byte array.
+        *
+        * @return a list of DER encoded CA certificates
+        */
+       private byte[][] getTrustedCertificates()
+       {
+               ArrayList<byte[]> certs = new ArrayList<byte[]>();
+               TrustedCertificateManager certman = TrustedCertificateManager.getInstance();
+               try
+               {
+                       String alias = this.mCurrentCertificateAlias;
+                       if (alias != null)
+                       {
+                               X509Certificate cert = certman.getCACertificateFromAlias(alias);
+                               if (cert == null)
+                               {
+                                       return null;
+                               }
+                               certs.add(cert.getEncoded());
+                       }
+                       else
+                       {
+                               for (X509Certificate cert : certman.getAllCACertificates().values())
+                               {
+                                       certs.add(cert.getEncoded());
+                               }
+                       }
+               }
+               catch (CertificateEncodingException e)
+               {
+                       e.printStackTrace();
+                       return null;
+               }
+               return certs.toArray(new byte[certs.size()][]);
+       }
+
+       /**
+        * Function called via JNI to get a list containing the 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 certificates (first element is the user certificate)
+        * @throws InterruptedException
+        * @throws KeyChainException
+        * @throws CertificateEncodingException
+        */
+       private byte[][] getUserCertificate() throws KeyChainException, InterruptedException, CertificateEncodingException
+       {
+               ArrayList<byte[]> encodings = new ArrayList<byte[]>();
+               X509Certificate[] chain = KeyChain.getCertificateChain(getApplicationContext(), mCurrentUserCertificateAlias);
+               if (chain == null || chain.length == 0)
+               {
+                       return null;
+               }
+               for (X509Certificate cert : chain)
+               {
+                       encodings.add(cert.getEncoded());
+               }
+               return encodings.toArray(new byte[encodings.size()][]);
+       }
+
+       /**
+        * Function called via JNI to get the private key the user selected.
+        *
+        * Since this method is called from a thread of charon's thread pool we are safe
+        * to call methods on KeyChain directly.
+        *
+        * @return the private key
+        * @throws InterruptedException
+        * @throws KeyChainException
+        * @throws CertificateEncodingException
+        */
+       private PrivateKey getUserKey() throws KeyChainException, InterruptedException
+       {
+               return KeyChain.getPrivateKey(getApplicationContext(), mCurrentUserCertificateAlias);
+       }
+
+       /**
+        * Initialization of charon, provided by libandroidbridge.so
+        *
+        * @param builder BuilderAdapter for this connection
+        * @param logfile absolute path to the logfile
+        * @param boyd enable BYOD features
+        * @return TRUE if initialization was successful
+        */
+       public native boolean initializeCharon(BuilderAdapter builder, String logfile, boolean byod);
+
+       /**
+        * Deinitialize charon, provided by libandroidbridge.so
+        */
+       public native void deinitializeCharon();
+
+       /**
+        * Initiate VPN, provided by libandroidbridge.so
+        */
+       public native void initiate(String config);
+
+       /**
+        * Adapter for VpnService.Builder which is used to access it safely via JNI.
+        * There is a corresponding C object to access it from native code.
+        */
+       public class BuilderAdapter
+       {
+               private final String mName;
+               private final Integer mSplitTunneling;
+               private VpnService.Builder mBuilder;
+               private BuilderCache mCache;
+               private BuilderCache mEstablishedCache;
+
+               public BuilderAdapter(String name, Integer splitTunneling)
+               {
+                       mName = name;
+                       mSplitTunneling = splitTunneling;
+                       mBuilder = createBuilder(name);
+                       mCache = new BuilderCache(mSplitTunneling);
+               }
+
+               private VpnService.Builder createBuilder(String name)
+               {
+                       VpnService.Builder builder = new CharonVpnService.Builder();
+                       builder.setSession(mName);
+
+                       /* even though the option displayed in the system dialog says "Configure"
+                        * we just use our main Activity */
+                       Context context = getApplicationContext();
+                       Intent intent = new Intent(context, MainActivity.class);
+                       PendingIntent pending = PendingIntent.getActivity(context, 0, intent,
+                                                                                                                         PendingIntent.FLAG_UPDATE_CURRENT);
+                       builder.setConfigureIntent(pending);
+                       return builder;
+               }
+
+               public synchronized boolean addAddress(String address, int prefixLength)
+               {
+                       try
+                       {
+                               mCache.addAddress(address, prefixLength);
+                       }
+                       catch (IllegalArgumentException ex)
+                       {
+                               return false;
+                       }
+                       return true;
+               }
+
+               public synchronized boolean addDnsServer(String address)
+               {
+                       try
+                       {
+                               mBuilder.addDnsServer(address);
+                               mCache.recordAddressFamily(address);
+                       }
+                       catch (IllegalArgumentException ex)
+                       {
+                               return false;
+                       }
+                       return true;
+               }
+
+               public synchronized boolean addRoute(String address, int prefixLength)
+               {
+                       try
+                       {
+                               mCache.addRoute(address, prefixLength);
+                       }
+                       catch (IllegalArgumentException ex)
+                       {
+                               return false;
+                       }
+                       return true;
+               }
+
+               public synchronized boolean addSearchDomain(String domain)
+               {
+                       try
+                       {
+                               mBuilder.addSearchDomain(domain);
+                       }
+                       catch (IllegalArgumentException ex)
+                       {
+                               return false;
+                       }
+                       return true;
+               }
+
+               public synchronized boolean setMtu(int mtu)
+               {
+                       try
+                       {
+                               mCache.setMtu(mtu);
+                       }
+                       catch (IllegalArgumentException ex)
+                       {
+                               return false;
+                       }
+                       return true;
+               }
+
+               public synchronized int establish()
+               {
+                       ParcelFileDescriptor fd;
+                       try
+                       {
+                               mCache.applyData(mBuilder);
+                               fd = mBuilder.establish();
+                       }
+                       catch (Exception ex)
+                       {
+                               ex.printStackTrace();
+                               return -1;
+                       }
+                       if (fd == null)
+                       {
+                               return -1;
+                       }
+                       /* now that the TUN device is created we don't need the current
+                        * builder anymore, but we might need another when reestablishing */
+                       mBuilder = createBuilder(mName);
+                       mEstablishedCache = mCache;
+                       mCache = new BuilderCache(mSplitTunneling);
+                       return fd.detachFd();
+               }
+
+               public synchronized int establishNoDns()
+               {
+                       ParcelFileDescriptor fd;
+
+                       if (mEstablishedCache == null)
+                       {
+                               return -1;
+                       }
+                       try
+                       {
+                               Builder builder = createBuilder(mName);
+                               mEstablishedCache.applyData(builder);
+                               fd = builder.establish();
+                       }
+                       catch (Exception ex)
+                       {
+                               ex.printStackTrace();
+                               return -1;
+                       }
+                       if (fd == null)
+                       {
+                               return -1;
+                       }
+                       return fd.detachFd();
+               }
+       }
+
+       /**
+        * Cache non DNS related information so we can recreate the builder without
+        * that information when reestablishing IKE_SAs
+        */
+       public class BuilderCache
+       {
+               private final List<PrefixedAddress> mAddresses = new ArrayList<PrefixedAddress>();
+               private final List<PrefixedAddress> mRoutesIPv4 = new ArrayList<PrefixedAddress>();
+               private final List<PrefixedAddress> mRoutesIPv6 = new ArrayList<PrefixedAddress>();
+               private final int mSplitTunneling;
+               private int mMtu;
+               private boolean mIPv4Seen, mIPv6Seen;
+
+               public BuilderCache(Integer splitTunneling)
+               {
+                       mSplitTunneling = splitTunneling != null ? splitTunneling : 0;
+               }
+
+               public void addAddress(String address, int prefixLength)
+               {
+                       mAddresses.add(new PrefixedAddress(address, prefixLength));
+                       recordAddressFamily(address);
+               }
+
+               public void addRoute(String address, int prefixLength)
+               {
+                       try
+                       {
+                               if (isIPv6(address))
+                               {
+                                       mRoutesIPv6.add(new PrefixedAddress(address, prefixLength));
+                               }
+                               else
+                               {
+                                       mRoutesIPv4.add(new PrefixedAddress(address, prefixLength));
+                               }
+                       }
+                       catch (UnknownHostException ex)
+                       {
+                               ex.printStackTrace();
+                       }
+               }
+
+               public void setMtu(int mtu)
+               {
+                       mMtu = mtu;
+               }
+
+               public void recordAddressFamily(String address)
+               {
+                       try
+                       {
+                               if (isIPv6(address))
+                               {
+                                       mIPv6Seen = true;
+                               }
+                               else
+                               {
+                                       mIPv4Seen = true;
+                               }
+                       }
+                       catch (UnknownHostException ex)
+                       {
+                               ex.printStackTrace();
+                       }
+               }
+
+               @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+               public void applyData(VpnService.Builder builder)
+               {
+                       for (PrefixedAddress address : mAddresses)
+                       {
+                               builder.addAddress(address.mAddress, address.mPrefix);
+                       }
+                       /* add routes depending on whether split tunneling is allowed or not,
+                        * that is, whether we have to handle and block non-VPN traffic */
+                       if ((mSplitTunneling & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4) == 0)
+                       {
+                               if (mIPv4Seen)
+                               {       /* split tunneling is used depending on the routes */
+                                       for (PrefixedAddress route : mRoutesIPv4)
+                                       {
+                                               builder.addRoute(route.mAddress, route.mPrefix);
+                                       }
+                               }
+                               else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
+                               {       /* allow traffic that would otherwise be blocked to bypass the VPN */
+                                       builder.allowFamily(OsConstants.AF_INET);
+                               }
+                       }
+                       else if (mIPv4Seen)
+                       {       /* only needed if we've seen any addresses.  otherwise, traffic
+                                * is blocked by default (we also install no routes in that case) */
+                               builder.addRoute("0.0.0.0", 0);
+                       }
+                       /* same thing for IPv6 */
+                       if ((mSplitTunneling & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6) == 0)
+                       {
+                               if (mIPv6Seen)
+                               {
+                                       for (PrefixedAddress route : mRoutesIPv6)
+                                       {
+                                               builder.addRoute(route.mAddress, route.mPrefix);
+                                       }
+                               }
+                               else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
+                               {
+                                       builder.allowFamily(OsConstants.AF_INET6);
+                               }
+                       }
+                       else if (mIPv6Seen)
+                       {
+                               builder.addRoute("::", 0);
+                       }
+                       builder.setMtu(mMtu);
+               }
+
+               private boolean isIPv6(String address) throws UnknownHostException
+               {
+                       InetAddress addr = InetAddress.getByName(address);
+                       if (addr instanceof Inet4Address)
+                       {
+                               return false;
+                       }
+                       else if (addr instanceof Inet6Address)
+                       {
+                               return true;
+                       }
+                       return false;
+               }
+
+               private class PrefixedAddress
+               {
+                       public String mAddress;
+                       public int mPrefix;
+
+                       public PrefixedAddress(String address, int prefix)
+                       {
+                               this.mAddress = address;
+                               this.mPrefix = prefix;
+                       }
+               }
+       }
+
+       /*
+        * The libraries are extracted to /data/data/org.strongswan.android/...
+        * during installation.  On newer releases most are loaded in JNI_OnLoad.
+        */
+       static
+       {
+               if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2)
+               {
+                       System.loadLibrary("strongswan");
+
+                       if (MainActivity.USE_BYOD)
+                       {
+                               System.loadLibrary("tncif");
+                               System.loadLibrary("tnccs");
+                               System.loadLibrary("imcv");
+                       }
+
+                       System.loadLibrary("hydra");
+                       System.loadLibrary("charon");
+                       System.loadLibrary("ipsec");
+               }
+               System.loadLibrary("androidbridge");
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/NetworkManager.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/NetworkManager.java
new file mode 100644 (file)
index 0000000..ebe1d00
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2012-2015 Tobias Brunner
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+
+public class NetworkManager extends BroadcastReceiver
+{
+       private final Context mContext;
+       private boolean mRegistered;
+
+       public NetworkManager(Context context)
+       {
+               mContext = context;
+       }
+
+       public void Register()
+       {
+               mContext.registerReceiver(this, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
+       }
+
+       public void Unregister()
+       {
+               mContext.unregisterReceiver(this);
+       }
+
+       public boolean isConnected()
+       {
+               ConnectivityManager cm = (ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+               NetworkInfo info = null;
+               if (cm != null)
+               {
+                       info = cm.getActiveNetworkInfo();
+               }
+               return info != null && info.isConnected();
+       }
+
+       @Override
+       public void onReceive(Context context, Intent intent)
+       {
+               networkChanged(!isConnected());
+       }
+
+       /**
+        * Notify the native parts about a network change
+        *
+        * @param disconnected true if no connection is available at the moment
+        */
+       public native void networkChanged(boolean disconnected);
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/StrongSwanApplication.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/StrongSwanApplication.java
new file mode 100644 (file)
index 0000000..d642b67
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2014 Tobias Brunner
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic;
+
+import java.security.Security;
+
+import org.strongswan.android.security.LocalCertificateKeyStoreProvider;
+
+import android.app.Application;
+import android.content.Context;
+
+public class StrongSwanApplication extends Application
+{
+       private static Context mContext;
+
+       static {
+               Security.addProvider(new LocalCertificateKeyStoreProvider());
+       }
+
+       @Override
+       public void onCreate()
+       {
+               super.onCreate();
+               StrongSwanApplication.mContext = getApplicationContext();
+       }
+
+       /**
+        * Returns the current application context
+        * @return context
+        */
+       public static Context getContext()
+       {
+               return StrongSwanApplication.mContext;
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/TrustedCertificateManager.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/TrustedCertificateManager.java
new file mode 100644 (file)
index 0000000..82a7cbe
--- /dev/null
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2012-2014 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import android.util.Log;
+
+public class TrustedCertificateManager
+{
+       private static final String TAG = TrustedCertificateManager.class.getSimpleName();
+       private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();
+       private Hashtable<String, X509Certificate> mCACerts = new Hashtable<String, X509Certificate>();
+       private volatile boolean mReload;
+       private boolean mLoaded;
+       private final ArrayList<KeyStore> mKeyStores = new ArrayList<KeyStore>();
+
+       public enum TrustedCertificateSource
+       {
+               SYSTEM("system:"),
+               USER("user:"),
+               LOCAL("local:");
+
+               private final String mPrefix;
+
+               private TrustedCertificateSource(String prefix)
+               {
+                       mPrefix = prefix;
+               }
+
+               private String getPrefix()
+               {
+                       return mPrefix;
+               }
+       }
+
+       /**
+        * Private constructor to prevent instantiation from other classes.
+        */
+       private TrustedCertificateManager()
+       {
+               for (String name : new String[] { "LocalCertificateStore", "AndroidCAStore" })
+               {
+                       KeyStore store;
+                       try
+                       {
+                               store = KeyStore.getInstance(name);
+                               store.load(null,null);
+                               mKeyStores.add(store);
+                       }
+                       catch (Exception e)
+                       {
+                               Log.e(TAG, "Unable to load KeyStore: " + name);
+                               e.printStackTrace();
+                       }
+               }
+       }
+
+       /**
+        * This is not instantiated until the first call to getInstance()
+        */
+       private static class Singleton {
+               public static final TrustedCertificateManager mInstance = new TrustedCertificateManager();
+       }
+
+       /**
+        * Get the single instance of the CA certificate manager.
+        * @return CA certificate manager
+        */
+       public static TrustedCertificateManager getInstance()
+       {
+               return Singleton.mInstance;
+       }
+
+       /**
+        * Invalidates the current load state so that the next call to load()
+        * will force a reload of the cached CA certificates.
+        * @return reference to itself
+        */
+       public TrustedCertificateManager reset()
+       {
+               Log.d(TAG, "Force reload of cached CA certificates on next load");
+               this.mReload = true;
+               return this;
+       }
+
+       /**
+        * Ensures that the certificates are loaded but does not force a reload.
+        * As this takes a while if the certificates are not loaded yet it should
+        * be called asynchronously.
+        * @return reference to itself
+        */
+       public TrustedCertificateManager load()
+       {
+               Log.d(TAG, "Ensure cached CA certificates are loaded");
+               this.mLock.writeLock().lock();
+               if (!this.mLoaded || this.mReload)
+               {
+                       this.mReload = false;
+                       loadCertificates();
+               }
+               this.mLock.writeLock().unlock();
+               return this;
+       }
+
+       /**
+        * Opens the CA certificate KeyStore and loads the cached certificates.
+        * The lock must be locked when calling this method.
+        */
+       private void loadCertificates()
+       {
+               Log.d(TAG, "Load cached CA certificates");
+               Hashtable<String, X509Certificate> certs = new Hashtable<String, X509Certificate>();
+               for (KeyStore store : this.mKeyStores)
+               {
+                       fetchCertificates(certs, store);
+               }
+               this.mCACerts = certs;
+               this.mLoaded = true;
+               Log.d(TAG, "Cached CA certificates loaded");
+       }
+
+       /**
+        * Load all X.509 certificates from the given KeyStore.
+        * @param certs Hashtable to store certificates in
+        * @param store KeyStore to load certificates from
+        */
+       private void fetchCertificates(Hashtable<String, X509Certificate> certs, KeyStore store)
+       {
+               try
+               {
+                       Enumeration<String> aliases = store.aliases();
+                       while (aliases.hasMoreElements())
+                       {
+                               String alias = aliases.nextElement();
+                               Certificate cert;
+                               cert = store.getCertificate(alias);
+                               if (cert != null && cert instanceof X509Certificate)
+                               {
+                                       certs.put(alias, (X509Certificate)cert);
+                               }
+                       }
+               }
+               catch (KeyStoreException ex)
+               {
+                       ex.printStackTrace();
+               }
+       }
+
+       /**
+        * Retrieve the CA certificate with the given alias.
+        * @param alias alias of the certificate to get
+        * @return the certificate, null if not found
+        */
+       public X509Certificate getCACertificateFromAlias(String alias)
+       {
+               X509Certificate certificate = null;
+
+               if (this.mLock.readLock().tryLock())
+               {
+                       certificate = this.mCACerts.get(alias);
+                       this.mLock.readLock().unlock();
+               }
+               else
+               {       /* if we cannot get the lock load it directly from the KeyStore,
+                        * should be fast for a single certificate */
+                       for (KeyStore store : this.mKeyStores)
+                       {
+                               try
+                               {
+                                       Certificate cert = store.getCertificate(alias);
+                                       if (cert != null && cert instanceof X509Certificate)
+                                       {
+                                               certificate = (X509Certificate)cert;
+                                               break;
+                                       }
+                               }
+                               catch (KeyStoreException e)
+                               {
+                                       e.printStackTrace();
+                               }
+                       }
+               }
+               return certificate;
+       }
+
+       /**
+        * Get all CA certificates (from all keystores).
+        * @return Hashtable mapping aliases to certificates
+        */
+       @SuppressWarnings("unchecked")
+       public Hashtable<String, X509Certificate> getAllCACertificates()
+       {
+               Hashtable<String, X509Certificate> certs;
+               this.mLock.readLock().lock();
+               certs = (Hashtable<String, X509Certificate>)this.mCACerts.clone();
+               this.mLock.readLock().unlock();
+               return certs;
+       }
+
+       /**
+        * Get all certificates from the given source.
+        * @param source type to filter certificates
+        * @return Hashtable mapping aliases to certificates
+        */
+       public Hashtable<String, X509Certificate> getCACertificates(TrustedCertificateSource source)
+       {
+               Hashtable<String, X509Certificate> certs = new Hashtable<String, X509Certificate>();
+               this.mLock.readLock().lock();
+               for (String alias : this.mCACerts.keySet())
+               {
+                       if (alias.startsWith(source.getPrefix()))
+                       {
+                               certs.put(alias, this.mCACerts.get(alias));
+                       }
+               }
+               this.mLock.readLock().unlock();
+               return certs;
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java
new file mode 100644 (file)
index 0000000..7b40e94
--- /dev/null
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2012-2013 Tobias Brunner
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import org.strongswan.android.data.VpnProfile;
+import org.strongswan.android.logic.imc.ImcState;
+import org.strongswan.android.logic.imc.RemediationInstruction;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+
+public class VpnStateService extends Service
+{
+       private final List<VpnStateListener> mListeners = new ArrayList<VpnStateListener>();
+       private final IBinder mBinder = new LocalBinder();
+       private long mConnectionID = 0;
+       private Handler mHandler;
+       private VpnProfile mProfile;
+       private State mState = State.DISABLED;
+       private ErrorState mError = ErrorState.NO_ERROR;
+       private ImcState mImcState = ImcState.UNKNOWN;
+       private final LinkedList<RemediationInstruction> mRemediationInstructions = new LinkedList<RemediationInstruction>();
+
+       public enum State
+       {
+               DISABLED,
+               CONNECTING,
+               CONNECTED,
+               DISCONNECTING,
+       }
+
+       public enum ErrorState
+       {
+               NO_ERROR,
+               AUTH_FAILED,
+               PEER_AUTH_FAILED,
+               LOOKUP_FAILED,
+               UNREACHABLE,
+               GENERIC_ERROR,
+       }
+
+       /**
+        * Listener interface for bound clients that are interested in changes to
+        * this Service.
+        */
+       public interface VpnStateListener
+       {
+               public void stateChanged();
+       }
+
+       /**
+        * Simple Binder that allows to directly access this Service class itself
+        * after binding to it.
+        */
+       public class LocalBinder extends Binder
+       {
+               public VpnStateService getService()
+               {
+                       return VpnStateService.this;
+               }
+       }
+
+       @Override
+       public void onCreate()
+       {
+               /* this handler allows us to notify listeners from the UI thread and
+                * not from the threads that actually report any state changes */
+               mHandler = new Handler();
+       }
+
+       @Override
+       public IBinder onBind(Intent intent)
+       {
+               return mBinder;
+       }
+
+       @Override
+       public void onDestroy()
+       {
+       }
+
+       /**
+        * Register a listener with this Service. We assume this is called from
+        * the main thread so no synchronization is happening.
+        *
+        * @param listener listener to register
+        */
+       public void registerListener(VpnStateListener listener)
+       {
+               mListeners.add(listener);
+       }
+
+       /**
+        * Unregister a listener from this Service.
+        *
+        * @param listener listener to unregister
+        */
+       public void unregisterListener(VpnStateListener listener)
+       {
+               mListeners.remove(listener);
+       }
+
+       /**
+        * Get the current VPN profile.
+        *
+        * @return profile
+        */
+       public VpnProfile getProfile()
+       {       /* only updated from the main thread so no synchronization needed */
+               return mProfile;
+       }
+
+       /**
+        * Get the current connection ID.  May be used to track which state
+        * changes have already been handled.
+        *
+        * Is increased when startConnection() is called.
+        *
+        * @return connection ID
+        */
+       public long getConnectionID()
+       {       /* only updated from the main thread so no synchronization needed */
+               return mConnectionID;
+       }
+
+       /**
+        * Get the current state.
+        *
+        * @return state
+        */
+       public State getState()
+       {       /* only updated from the main thread so no synchronization needed */
+               return mState;
+       }
+
+       /**
+        * Get the current error, if any.
+        *
+        * @return error
+        */
+       public ErrorState getErrorState()
+       {       /* only updated from the main thread so no synchronization needed */
+               return mError;
+       }
+
+       /**
+        * Get the current IMC state, if any.
+        *
+        * @return imc state
+        */
+       public ImcState getImcState()
+       {       /* only updated from the main thread so no synchronization needed */
+               return mImcState;
+       }
+
+       /**
+        * Get the remediation instructions, if any.
+        *
+        * @return read-only list of instructions
+        */
+       public List<RemediationInstruction> getRemediationInstructions()
+       {       /* only updated from the main thread so no synchronization needed */
+               return Collections.unmodifiableList(mRemediationInstructions);
+       }
+
+       /**
+        * Disconnect any existing connection and shutdown the daemon, the
+        * VpnService is not stopped but it is reset so new connections can be
+        * started.
+        */
+       public void disconnect()
+       {
+               /* as soon as the TUN device is created by calling establish() on the
+                * VpnService.Builder object the system binds to the service and keeps
+                * bound until the file descriptor of the TUN device is closed.  thus
+                * calling stopService() here would not stop (destroy) the service yet,
+                * instead we call startService() with an empty Intent which shuts down
+                * the daemon (and closes the TUN device, if any) */
+               Context context = getApplicationContext();
+               Intent intent = new Intent(context, CharonVpnService.class);
+               context.startService(intent);
+       }
+
+       /**
+        * Update state and notify all listeners about the change. By using a Handler
+        * this is done from the main UI thread and not the initial reporter thread.
+        * Also, in doing the actual state change from the main thread, listeners
+        * see all changes and none are skipped.
+        *
+        * @param change the state update to perform before notifying listeners, returns true if state changed
+        */
+       private void notifyListeners(final Callable<Boolean> change)
+       {
+               mHandler.post(new Runnable() {
+                       @Override
+                       public void run()
+                       {
+                               try
+                               {
+                                       if (change.call())
+                                       {       /* otherwise there is no need to notify the listeners */
+                                               for (VpnStateListener listener : mListeners)
+                                               {
+                                                       listener.stateChanged();
+                                               }
+                                       }
+                               }
+                               catch (Exception e)
+                               {
+                                       e.printStackTrace();
+                               }
+                       }
+               });
+       }
+
+       /**
+        * Called when a connection is started.  Sets the currently active VPN
+        * profile, resets IMC and Error state variables, sets the State to
+        * CONNECTING, increases the connection ID, and notifies all listeners.
+        *
+        * May be called from threads other than the main thread.
+        *
+        * @param profile current profile
+        */
+       public void startConnection(final VpnProfile profile)
+       {
+               notifyListeners(new Callable<Boolean>() {
+                       @Override
+                       public Boolean call() throws Exception
+                       {
+                               VpnStateService.this.mConnectionID++;
+                               VpnStateService.this.mProfile = profile;
+                               VpnStateService.this.mState = State.CONNECTING;
+                               VpnStateService.this.mError = ErrorState.NO_ERROR;
+                               VpnStateService.this.mImcState = ImcState.UNKNOWN;
+                               VpnStateService.this.mRemediationInstructions.clear();
+                               return true;
+                       }
+               });
+       }
+
+       /**
+        * Update the state and notify all listeners, if changed.
+        *
+        * May be called from threads other than the main thread.
+        *
+        * @param state new state
+        */
+       public void setState(final State state)
+       {
+               notifyListeners(new Callable<Boolean>() {
+                       @Override
+                       public Boolean call() throws Exception
+                       {
+                               if (VpnStateService.this.mState != state)
+                               {
+                                       VpnStateService.this.mState = state;
+                                       return true;
+                               }
+                               return false;
+                       }
+               });
+       }
+
+       /**
+        * Set the current error state and notify all listeners, if changed.
+        *
+        * May be called from threads other than the main thread.
+        *
+        * @param error error state
+        */
+       public void setError(final ErrorState error)
+       {
+               notifyListeners(new Callable<Boolean>() {
+                       @Override
+                       public Boolean call() throws Exception
+                       {
+                               if (VpnStateService.this.mError != error)
+                               {
+                                       VpnStateService.this.mError = error;
+                                       return true;
+                               }
+                               return false;
+                       }
+               });
+       }
+
+       /**
+        * Set the current IMC state and notify all listeners, if changed.
+        *
+        * Setting the state to UNKNOWN clears all remediation instructions.
+        *
+        * May be called from threads other than the main thread.
+        *
+        * @param error error state
+        */
+       public void setImcState(final ImcState state)
+       {
+               notifyListeners(new Callable<Boolean>() {
+                       @Override
+                       public Boolean call() throws Exception
+                       {
+                               if (state == ImcState.UNKNOWN)
+                               {
+                                       VpnStateService.this.mRemediationInstructions.clear();
+                               }
+                               if (VpnStateService.this.mImcState != state)
+                               {
+                                       VpnStateService.this.mImcState = state;
+                                       return true;
+                               }
+                               return false;
+                       }
+               });
+       }
+
+       /**
+        * Add the given remediation instruction to the internal list.  Listeners
+        * are not notified.
+        *
+        * Instructions are cleared if the IMC state is set to UNKNOWN.
+        *
+        * May be called from threads other than the main thread.
+        *
+        * @param instruction remediation instruction
+        */
+       public void addRemediationInstruction(final RemediationInstruction instruction)
+       {
+               mHandler.post(new Runnable() {
+                       @Override
+                       public void run()
+                       {
+                               VpnStateService.this.mRemediationInstructions.add(instruction);
+                       }
+               });
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/AndroidImc.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/AndroidImc.java
new file mode 100644 (file)
index 0000000..351fab8
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc;
+
+import org.strongswan.android.logic.imc.attributes.Attribute;
+import org.strongswan.android.logic.imc.attributes.AttributeType;
+import org.strongswan.android.logic.imc.collectors.Collector;
+import org.strongswan.android.logic.imc.collectors.DeviceIdCollector;
+import org.strongswan.android.logic.imc.collectors.InstalledPackagesCollector;
+import org.strongswan.android.logic.imc.collectors.PortFilterCollector;
+import org.strongswan.android.logic.imc.collectors.ProductInformationCollector;
+import org.strongswan.android.logic.imc.collectors.SettingsCollector;
+import org.strongswan.android.logic.imc.collectors.StringVersionCollector;
+
+import android.content.Context;
+
+public class AndroidImc
+{
+       private final Context mContext;
+
+       public AndroidImc(Context context)
+       {
+               mContext = context;
+       }
+
+       /**
+        * Get a measurement (the binary encoding of the requested attribute) for
+        * the given vendor specific attribute type.
+        *
+        * @param vendor vendor ID
+        * @param type vendor specific attribute type
+        * @return encoded attribute, or null if not available or failed
+        */
+       public byte[] getMeasurement(int vendor, int type)
+       {
+               return getMeasurement(vendor, type, null);
+       }
+
+       /**
+        * Get a measurement (the binary encoding of the requested attribute) for
+        * the given vendor specific attribute type.
+        *
+        * @param vendor vendor ID
+        * @param type vendor specific attribute type
+        * @param args optional arguments for a measurement
+        * @return encoded attribute, or null if not available or failed
+        */
+       public byte[] getMeasurement(int vendor, int type, String[] args)
+       {
+               AttributeType attributeType = AttributeType.fromValues(vendor, type);
+               Collector collector = null;
+
+               switch (attributeType)
+               {
+                       case IETF_PRODUCT_INFORMATION:
+                               collector = new ProductInformationCollector();
+                               break;
+                       case IETF_STRING_VERSION:
+                               collector = new StringVersionCollector();
+                               break;
+                       case IETF_PORT_FILTER:
+                               collector = new PortFilterCollector();
+                               break;
+                       case IETF_INSTALLED_PACKAGES:
+                               collector = new InstalledPackagesCollector(mContext);
+                               break;
+                       case ITA_SETTINGS:
+                               collector = new SettingsCollector(mContext, args);
+                               break;
+                       case ITA_DEVICE_ID:
+                               collector = new DeviceIdCollector(mContext);
+                               break;
+                       default:
+                               break;
+               }
+               if (collector != null)
+               {
+                       Attribute attribute = collector.getMeasurement();
+                       if (attribute != null)
+                       {
+                               return attribute.getEncoding();
+                       }
+               }
+               return null;
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/ImcState.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/ImcState.java
new file mode 100644 (file)
index 0000000..4fc3834
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc;
+
+public enum ImcState
+{
+       UNKNOWN(0),
+       ALLOW(1),
+       BLOCK(2),
+       ISOLATE(3);
+
+       private final int mValue;
+
+       private ImcState(int value)
+       {
+               mValue = value;
+       }
+
+       /**
+        * Get the numeric value of the IMC state.
+        * @return numeric value
+        */
+       public int getValue()
+       {
+               return mValue;
+       }
+
+       /**
+        * Get the enum entry from a numeric value, if defined
+        *
+        * @param value numeric value
+        * @return the enum entry or null
+        */
+       public static ImcState fromValue(int value)
+       {
+               for (ImcState state : ImcState.values())
+               {
+                       if (state.mValue == value)
+                       {
+                               return state;
+                       }
+               }
+               return null;
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/RemediationInstruction.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/RemediationInstruction.java
new file mode 100644 (file)
index 0000000..5435ad8
--- /dev/null
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Xml;
+
+public class RemediationInstruction implements Parcelable
+{
+       private String mTitle;
+       private String mDescription;
+       private String mHeader;
+       private final List<String> mItems = new LinkedList<String>();
+
+       @Override
+       public int describeContents()
+       {
+               return 0;
+       }
+
+       @Override
+       public void writeToParcel(Parcel dest, int flags)
+       {
+               dest.writeString(mTitle);
+               dest.writeString(mDescription);
+               dest.writeString(mHeader);
+               dest.writeStringList(mItems);
+       }
+
+       public static final Parcelable.Creator<RemediationInstruction> CREATOR = new Creator<RemediationInstruction>() {
+
+               @Override
+               public RemediationInstruction[] newArray(int size)
+               {
+                       return new RemediationInstruction[size];
+               }
+
+               @Override
+               public RemediationInstruction createFromParcel(Parcel source)
+               {
+                       return new RemediationInstruction(source);
+               }
+       };
+
+       private RemediationInstruction()
+       {
+       }
+
+       private RemediationInstruction(Parcel source)
+       {
+               mTitle = source.readString();
+               mDescription = source.readString();
+               mHeader = source.readString();
+               source.readStringList(mItems);
+       }
+
+       public String getTitle()
+       {
+               return mTitle;
+       }
+
+       private void setTitle(String title)
+       {
+               mTitle = title;
+       }
+
+       public String getDescription()
+       {
+               return mDescription;
+       }
+
+       private void setDescription(String description)
+       {
+               mDescription = description;
+       }
+
+       public String getHeader()
+       {
+               return mHeader;
+       }
+
+       private void setHeader(String header)
+       {
+               mHeader = header;
+       }
+
+       public List<String> getItems()
+       {
+               return Collections.unmodifiableList(mItems);
+       }
+
+       private void addItem(String item)
+       {
+               mItems.add(item);
+       }
+
+       /**
+        * Create a list of RemediationInstruction objects from the given XML data.
+        *
+        * @param xml XML data
+        * @return list of RemediationInstruction objects
+        */
+       public static List<RemediationInstruction> fromXml(String xml)
+       {
+               List<RemediationInstruction> instructions = new LinkedList<RemediationInstruction>();
+               XmlPullParser parser = Xml.newPullParser();
+               try
+               {
+                       parser.setInput(new StringReader(xml));
+                       parser.nextTag();
+                       readInstructions(parser, instructions);
+               }
+               catch (XmlPullParserException e)
+               {
+                       e.printStackTrace();
+               }
+               catch (IOException e)
+               {
+                       e.printStackTrace();
+               }
+               return instructions;
+       }
+
+       /**
+        * Read a &lt;remediationinstructions&gt; element and store the extracted
+        * RemediationInstruction objects in the given list.
+        *
+        * @param parser
+        * @param instructions
+        * @throws XmlPullParserException
+        * @throws IOException
+        */
+       private static void readInstructions(XmlPullParser parser, List<RemediationInstruction> instructions) throws XmlPullParserException, IOException
+       {
+               parser.require(XmlPullParser.START_TAG, null, "remediationinstructions");
+               while (parser.next() != XmlPullParser.END_TAG)
+               {
+                       if (parser.getEventType() != XmlPullParser.START_TAG)
+                       {
+                               continue;
+                       }
+                       if (parser.getName().equals("instruction"))
+                       {
+                               RemediationInstruction instruction = new RemediationInstruction();
+                               readInstruction(parser, instruction);
+                               instructions.add(instruction);
+                       }
+                       else
+                       {
+                               skipTag(parser);
+                       }
+               }
+       }
+
+       /**
+        * Read an &lt;instruction&gt; element and store the information in the
+        * given RemediationInstruction object.
+        *
+        * @param parser
+        * @param instruction
+        * @throws XmlPullParserException
+        * @throws IOException
+        */
+       private static void readInstruction(XmlPullParser parser, RemediationInstruction instruction) throws XmlPullParserException, IOException
+       {
+               parser.require(XmlPullParser.START_TAG, null, "instruction");
+               while (parser.next() != XmlPullParser.END_TAG)
+               {
+                       if (parser.getEventType() != XmlPullParser.START_TAG)
+                       {
+                               continue;
+                       }
+                       String name = parser.getName();
+                       if (name.equals("title"))
+                       {
+                               instruction.setTitle(parser.nextText());
+                       }
+                       else if (name.equals("description"))
+                       {
+                               instruction.setDescription(parser.nextText());
+                       }
+                       else if (name.equals("itemsheader"))
+                       {
+                               instruction.setHeader(parser.nextText());
+                       }
+                       else if (name.equals("items"))
+                       {
+                               readItems(parser, instruction);
+                       }
+                       else
+                       {
+                               skipTag(parser);
+                       }
+               }
+       }
+
+       /**
+        * Read all items of an &lt;items&gt; node and add them to the given
+        * RemediationInstruction object.
+        *
+        * @param parser
+        * @param instruction
+        * @throws XmlPullParserException
+        * @throws IOException
+        */
+       private static void readItems(XmlPullParser parser, RemediationInstruction instruction) throws XmlPullParserException, IOException
+       {
+               while (parser.next() != XmlPullParser.END_TAG)
+               {
+                       if (parser.getEventType() != XmlPullParser.START_TAG)
+                       {
+                               continue;
+                       }
+                       if (parser.getName().equals("item"))
+                       {
+                               instruction.addItem(parser.nextText());
+                       }
+                       else
+                       {
+                               skipTag(parser);
+                       }
+               }
+       }
+
+       /**
+        * Skip the current tag and all child elements.
+        *
+        * @param parser
+        * @throws XmlPullParserException
+        * @throws IOException
+        */
+       private static void skipTag(XmlPullParser parser) throws XmlPullParserException, IOException
+       {
+               int depth = 1;
+
+               parser.require(XmlPullParser.START_TAG, null, null);
+               while (depth != 0)
+               {
+                       switch (parser.next())
+                       {
+                               case XmlPullParser.END_TAG:
+                                       depth--;
+                                       break;
+                               case XmlPullParser.START_TAG:
+                                       depth++;
+                                       break;
+                       }
+               }
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/Attribute.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/Attribute.java
new file mode 100644 (file)
index 0000000..ca75900
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc.attributes;
+
+/**
+ * Interface to be implemented by attribute classes
+ */
+public interface Attribute
+{
+       /**
+        * Returns the binary encoding of the attribute
+        * @return binary encoding
+        */
+       public byte[] getEncoding();
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/AttributeType.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/AttributeType.java
new file mode 100644 (file)
index 0000000..11f1c61
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Copyright (C) 2012 Christoph Buehler
+ * Copyright (C) 2012 Patrick Loetscher
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc.attributes;
+
+public enum AttributeType
+{
+       /* IETF standard PA-TNC attribute types defined by RFC 5792 */
+       IETF_TESTING(PrivateEnterpriseNumber.IETF, 0),
+       IETF_ATTRIBUTE_REQUEST(PrivateEnterpriseNumber.IETF, 1),
+       IETF_PRODUCT_INFORMATION(PrivateEnterpriseNumber.IETF, 2),
+       IETF_NUMERIC_VERSION(PrivateEnterpriseNumber.IETF, 3),
+       IETF_STRING_VERSION(PrivateEnterpriseNumber.IETF, 4),
+       IETF_OPERATIONAL_STATUS(PrivateEnterpriseNumber.IETF, 5),
+       IETF_PORT_FILTER(PrivateEnterpriseNumber.IETF, 6),
+       IETF_INSTALLED_PACKAGES(PrivateEnterpriseNumber.IETF, 7),
+       IETF_PA_TNC_ERROR(PrivateEnterpriseNumber.IETF, 8),
+       IETF_ASSESSMENT_RESULT(PrivateEnterpriseNumber.IETF, 9),
+       IETF_REMEDIATION_INSTRUCTIONS(PrivateEnterpriseNumber.IETF, 10),
+       IETF_FORWARDING_ENABLED(PrivateEnterpriseNumber.IETF, 11),
+       IETF_FACTORY_DEFAULT_PWD_ENABLED(PrivateEnterpriseNumber.IETF, 12),
+       IETF_RESERVED(PrivateEnterpriseNumber.IETF, 0xffffffff),
+       /* ITA attributes */
+       ITA_SETTINGS(PrivateEnterpriseNumber.ITA, 4),
+       ITA_DEVICE_ID(PrivateEnterpriseNumber.ITA, 8);
+
+       private PrivateEnterpriseNumber mVendor;
+       private int mType;
+
+       /**
+        * Enum type for vendor specific attributes (defined in their namespace)
+        *
+        * @param vendor private enterprise number of vendor
+        * @param type vendor specific attribute type
+        */
+       private AttributeType(PrivateEnterpriseNumber vendor, int type)
+       {
+               mVendor = vendor;
+               mType = type;
+       }
+
+       /**
+        * Get private enterprise number of vendor
+        *
+        * @return PEN
+        */
+       public PrivateEnterpriseNumber getVendor()
+       {
+               return mVendor;
+       }
+
+       /**
+        * Get vendor specific type
+        *
+        * @return type
+        */
+       public int getType()
+       {
+               return mType;
+       }
+
+       /**
+        * Get the enum entry from the given numeric values, if defined
+        *
+        * @param vendor vendor id
+        * @param type vendor specific type
+        * @return enum entry or null
+        */
+       public static AttributeType fromValues(int vendor, int type)
+       {
+               PrivateEnterpriseNumber pen = PrivateEnterpriseNumber.fromValue(vendor);
+
+               if (pen == null)
+               {
+                       return null;
+               }
+               for (AttributeType attr : AttributeType.values())
+               {
+                       if (attr.mVendor == pen && attr.mType == type)
+                       {
+                               return attr;
+                       }
+               }
+               return null;
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/DeviceIdAttribute.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/DeviceIdAttribute.java
new file mode 100644 (file)
index 0000000..ecab7db
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc.attributes;
+
+/**
+ * ITA Device ID attribute
+ *
+ *                       1                   2                   3
+ *   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  | Device ID (Variable Length)                                   |
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class DeviceIdAttribute implements Attribute
+{
+       private String mDeviceId;
+
+       /**
+        * Set the device ID
+        * @param version version number
+        */
+       public void setDeviceId(String deviceId)
+       {
+               this.mDeviceId = deviceId;
+       }
+
+       @Override
+       public byte[] getEncoding()
+       {
+               return mDeviceId.getBytes();
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/InstalledPackagesAttribute.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/InstalledPackagesAttribute.java
new file mode 100644 (file)
index 0000000..dd1ad72
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Copyright (C) 2012 Christoph Buehler
+ * Copyright (C) 2012 Patrick Loetscher
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc.attributes;
+
+import java.util.LinkedList;
+
+import org.strongswan.android.utils.BufferedByteWriter;
+
+import android.util.Pair;
+
+/**
+ * PA-TNC Installed Packages attribute (see section 4.2.7 of RFC 5792)
+ *
+ *                       1                   2                   3
+ *   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  |          Reserved             |         Package Count         |
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  | Pkg Name Len  |        Package Name (Variable Length)         |
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  |  Version Len  |    Package Version Number (Variable Length)   |
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class InstalledPackagesAttribute implements Attribute
+{
+       private final short RESERVED = 0;
+       private final LinkedList<Pair<String, String>> mPackages = new LinkedList<Pair<String, String>>();
+
+       /**
+        * Add an installed package to this attribute.
+        * @param name name of the package
+        * @param version version number of the package
+        */
+       public void addPackage(String name, String version)
+       {
+               mPackages.add(new Pair<String, String>(name, version));
+       }
+
+       @Override
+       public byte[] getEncoding()
+       {
+               BufferedByteWriter writer = new BufferedByteWriter();
+               writer.put16(RESERVED);
+               writer.put16((short)mPackages.size());
+               for (Pair<String, String> pair : mPackages)
+               {
+                       writer.putLen8(pair.first.getBytes());
+                       writer.putLen8(pair.second.getBytes());
+               }
+               return writer.toByteArray();
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/PortFilterAttribute.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/PortFilterAttribute.java
new file mode 100644 (file)
index 0000000..191690b
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Copyright (C) 2012 Christoph Buehler
+ * Copyright (C) 2012 Patrick Loetscher
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc.attributes;
+
+import java.util.LinkedList;
+
+import org.strongswan.android.logic.imc.collectors.Protocol;
+import org.strongswan.android.utils.BufferedByteWriter;
+
+import android.util.Pair;
+
+/**
+ * PA-TNC Port Filter attribute (see section 4.2.6 of RFC 5792)
+ *
+ *                       1                   2                   3
+ *   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  |   Reserved  |B|    Protocol   |         Port Number           |
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  |   Reserved  |B|    Protocol   |         Port Number           |
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class PortFilterAttribute implements Attribute
+{
+       private final LinkedList<Pair<Protocol, Short>> mPorts = new LinkedList<Pair<Protocol, Short>>();
+
+       /**
+        * Add an open port with the given protocol and port number
+        * @param protocol transport protocol
+        * @param port port number
+        */
+       public void addPort(Protocol protocol, short port)
+       {
+               mPorts.add(new Pair<Protocol, Short>(protocol, port));
+       }
+
+       @Override
+       public byte[] getEncoding()
+       {
+               BufferedByteWriter writer = new BufferedByteWriter();
+               for (Pair<Protocol, Short> port : mPorts)
+               {
+                       /* we report open ports, so the BLOCKED flag is not set */
+                       writer.put((byte)0);
+                       writer.put(port.first.getValue());
+                       writer.put16(port.second);
+               }
+               return writer.toByteArray();
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/PrivateEnterpriseNumber.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/PrivateEnterpriseNumber.java
new file mode 100644 (file)
index 0000000..9db702e
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc.attributes;
+
+public enum PrivateEnterpriseNumber
+{
+       IETF(0x000000),
+       GOOGLE(0x002B79),
+       ITA(0x00902a),
+       UNASSIGNED(0xfffffe),
+       RESERVED(0xffffff);
+
+       private int mValue;
+
+       /**
+        * Enum for private enterprise numbers (PEN) as allocated by IANA
+        *
+        * @param value numeric value
+        */
+       private PrivateEnterpriseNumber(int value)
+       {
+               mValue = value;
+       }
+
+       /**
+        * Get the numeric value of a PEN
+        *
+        * @return numeric value
+        */
+       public int getValue()
+       {
+               return mValue;
+       }
+
+       /**
+        * Get the enum entry from a numeric value, if defined
+        *
+        * @param value numeric value
+        * @return the enum entry or null
+        */
+       public static PrivateEnterpriseNumber fromValue(int value)
+       {
+               for (PrivateEnterpriseNumber pen : PrivateEnterpriseNumber.values())
+               {
+                       if (pen.mValue == value)
+                       {
+                               return pen;
+                       }
+               }
+               return null;
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/ProductInformationAttribute.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/ProductInformationAttribute.java
new file mode 100644 (file)
index 0000000..cace18d
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Copyright (C) 2012 Christoph Buehler
+ * Copyright (C) 2012 Patrick Loetscher
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc.attributes;
+
+import org.strongswan.android.utils.BufferedByteWriter;
+
+/**
+ * PA-TNC Product Information attribute (see section 4.2.2 of RFC 5792)
+ *
+ *                       1                   2                   3
+ *   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  |               Product Vendor ID               |  Product ID   |
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  |  Product ID   |         Product Name (Variable Length)        |
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class ProductInformationAttribute implements Attribute
+{
+       private final String PRODUCT_NAME = "Android";
+       private final short PRODUCT_ID = 0;
+
+       @Override
+       public byte[] getEncoding()
+       {
+               BufferedByteWriter writer = new BufferedByteWriter();
+               writer.put24(PrivateEnterpriseNumber.GOOGLE.getValue());
+               writer.put16(PRODUCT_ID);
+               writer.put(PRODUCT_NAME.getBytes());
+               return writer.toByteArray();
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/SettingsAttribute.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/SettingsAttribute.java
new file mode 100644 (file)
index 0000000..37d8201
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Copyright (C) 2012 Christoph Buehler
+ * Copyright (C) 2012 Patrick Loetscher
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc.attributes;
+
+import java.util.LinkedList;
+
+import org.strongswan.android.utils.BufferedByteWriter;
+
+import android.util.Pair;
+
+/**
+ * ITA Settings attribute
+ *
+ *                                        1                               2                               3
+ *   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  |                         Settings Count                        |
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  |        Name Length            |  Name (Variable Length)       ~
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  ~                      Name (Variable Length)                   ~
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  |        Value Length           |  Value (Variable Length)      ~
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  ~                      Value (Variable Length)                  ~
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  |        Name Length            |  Name (Variable Length)       ~
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  ~                      Name (Variable Length)                   ~
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  |        Value Length           |  Value (Variable Length)      ~
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  ~                      Value (Variable Length)                  ~
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *                                      ...........................
+ */
+public class SettingsAttribute implements Attribute
+{
+       private final LinkedList<Pair<String, String>> mSettings = new LinkedList<Pair<String, String>>();
+
+       /**
+        * Add a setting to this attribute.
+        * @param name name of the setting
+        * @param value value of the setting
+        */
+       public void addSetting(String name, String value)
+       {
+               mSettings.add(new Pair<String, String>(name, value));
+       }
+
+       @Override
+       public byte[] getEncoding()
+       {
+               BufferedByteWriter writer = new BufferedByteWriter();
+               writer.put32(mSettings.size());
+               for (Pair<String, String> pair : mSettings)
+               {
+                       writer.putLen16(pair.first.getBytes());
+                       writer.putLen16(pair.second.getBytes());
+               }
+               return writer.toByteArray();
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/StringVersionAttribute.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/attributes/StringVersionAttribute.java
new file mode 100644 (file)
index 0000000..4b6f2bc
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Copyright (C) 2012 Christoph Buehler
+ * Copyright (C) 2012 Patrick Loetscher
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc.attributes;
+
+import org.strongswan.android.utils.BufferedByteWriter;
+
+/**
+ * PA-TNC String Version attribute (see section 4.2.4 of RFC 5792)
+ *
+ *                       1                   2                   3
+ *   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  |  Version Len  |   Product Version Number (Variable Length)    |
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  | Build Num Len |   Internal Build Number (Variable Length)     |
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *  |  Config. Len  | Configuration Version Number (Variable Length)|
+ *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class StringVersionAttribute implements Attribute
+{
+       private String mVersionNumber;
+       private String mBuildNumber;
+
+       /**
+        * Set the product version number
+        * @param version version number
+        */
+       public void setProductVersionNumber(String version)
+       {
+               this.mVersionNumber = version;
+       }
+
+       /**
+        * Set the internal build number
+        * @param build build number
+        */
+       public void setInternalBuildNumber(String build)
+       {
+               this.mBuildNumber = build;
+       }
+
+       @Override
+       public byte[] getEncoding()
+       {
+               BufferedByteWriter writer = new BufferedByteWriter();
+               writer.putLen8(mVersionNumber.getBytes());
+               writer.putLen8(mBuildNumber.getBytes());
+               /* we don't provide a configuration number */
+               writer.put((byte)0);
+               return writer.toByteArray();
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/Collector.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/Collector.java
new file mode 100644 (file)
index 0000000..a686f13
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc.collectors;
+
+import org.strongswan.android.logic.imc.attributes.Attribute;
+
+/**
+ * Interface for measurement collectors
+ */
+public interface Collector
+{
+       /**
+        * This method shall return the result of a measurement, if available
+        * @return attribute or null
+        */
+       public abstract Attribute getMeasurement();
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/DeviceIdCollector.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/DeviceIdCollector.java
new file mode 100644 (file)
index 0000000..ebe9e10
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc.collectors;
+
+import org.strongswan.android.logic.imc.attributes.Attribute;
+import org.strongswan.android.logic.imc.attributes.DeviceIdAttribute;
+
+import android.content.ContentResolver;
+import android.content.Context;
+
+public class DeviceIdCollector implements Collector
+{
+       private final ContentResolver mContentResolver;
+
+       public DeviceIdCollector(Context context)
+       {
+               mContentResolver = context.getContentResolver();
+       }
+
+       @Override
+       public Attribute getMeasurement()
+       {
+               String id = android.provider.Settings.Secure.getString(mContentResolver, "android_id");
+               if (id != null)
+               {
+                       DeviceIdAttribute attribute = new DeviceIdAttribute();
+                       attribute.setDeviceId(id);
+                       return attribute;
+               }
+               return null;
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/InstalledPackagesCollector.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/InstalledPackagesCollector.java
new file mode 100644 (file)
index 0000000..caa5170
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Copyright (C) 2012 Christoph Buehler
+ * Copyright (C) 2012 Patrick Loetscher
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc.collectors;
+
+import java.util.List;
+
+import org.strongswan.android.logic.imc.attributes.Attribute;
+import org.strongswan.android.logic.imc.attributes.InstalledPackagesAttribute;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+
+public class InstalledPackagesCollector implements Collector
+{
+       private final PackageManager mPackageManager;
+
+       public InstalledPackagesCollector(Context context)
+       {
+               mPackageManager = context.getPackageManager();
+       }
+
+       @Override
+       public Attribute getMeasurement()
+       {
+               InstalledPackagesAttribute attribute = new InstalledPackagesAttribute();
+               List<PackageInfo> packages = mPackageManager.getInstalledPackages(0);
+               for (PackageInfo info : packages)
+               {
+                       if ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 ||
+                               info.packageName == null || info.versionName == null)
+                       {       /* ignore packages installed in the system image */
+                               continue;
+                       }
+                       attribute.addPackage(info.packageName, info.versionName);
+               }
+               return attribute;
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/PortFilterCollector.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/PortFilterCollector.java
new file mode 100644 (file)
index 0000000..ed86686
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Copyright (C) 2012 Christoph Buehler
+ * Copyright (C) 2012 Patrick Loetscher
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc.collectors;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.strongswan.android.logic.imc.attributes.Attribute;
+import org.strongswan.android.logic.imc.attributes.PortFilterAttribute;
+
+public class PortFilterCollector implements Collector
+{
+       private static Pattern LISTEN = Pattern.compile("\\bLISTEN\\b");
+       private static Pattern PROTOCOL = Pattern.compile("\\b(tcp|udp)6?\\b");
+       private static Pattern PORT = Pattern.compile("[:]{1,3}(\\d{1,5})\\b(?!\\.)");
+
+       @Override
+       public Attribute getMeasurement()
+       {
+               PortFilterAttribute attribute = null;
+               try
+               {
+                       Process netstat = Runtime.getRuntime().exec("netstat -n");
+                       try
+                       {
+                               BufferedReader reader = new BufferedReader(new InputStreamReader(netstat.getInputStream()));
+                               String line;
+                               attribute = new PortFilterAttribute();
+                               while ((line = reader.readLine()) != null)
+                               {
+                                       if (!LISTEN.matcher(line).find())
+                                       {
+                                               continue;
+                                       }
+                                       Matcher protocolMatcher = PROTOCOL.matcher(line);
+                                       Matcher portMatcher = PORT.matcher(line);
+                                       if (protocolMatcher.find() && portMatcher.find())
+                                       {
+                                               Protocol protocol = Protocol.fromName(protocolMatcher.group());
+                                               if (protocol == null)
+                                               {
+                                                       continue;
+                                               }
+                                               int port = Integer.parseInt(portMatcher.group(1));
+                                               attribute.addPort(protocol, (short)port);
+                                       }
+                               }
+                       }
+                       finally
+                       {
+                               netstat.destroy();
+                       }
+               }
+               catch (IOException e)
+               {
+                       e.printStackTrace();
+               }
+               return attribute;
+       }
+
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/ProductInformationCollector.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/ProductInformationCollector.java
new file mode 100644 (file)
index 0000000..c377e90
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Copyright (C) 2012 Christoph Buehler
+ * Copyright (C) 2012 Patrick Loetscher
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc.collectors;
+
+import org.strongswan.android.logic.imc.attributes.Attribute;
+import org.strongswan.android.logic.imc.attributes.ProductInformationAttribute;
+
+public class ProductInformationCollector implements Collector
+{
+       @Override
+       public Attribute getMeasurement()
+       {       /* this is currently hardcoded in the attribute */
+               return new ProductInformationAttribute();
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/Protocol.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/Protocol.java
new file mode 100644 (file)
index 0000000..7320652
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc.collectors;
+
+public enum Protocol
+{
+       TCP((byte)6, "tcp", "tcp6"),
+       UDP((byte)17, "udp", "udp6");
+
+       private final byte mValue;
+       private String[] mNames;
+
+       private Protocol(byte value, String... names)
+       {
+               mValue = value;
+               mNames = names;
+       }
+
+       /**
+        * Get the numeric value of the protocol.
+        * @return numeric value
+        */
+       public byte getValue()
+       {
+               return mValue;
+       }
+
+       /**
+        * Get the protocol from the given protocol name, if found.
+        * @param name protocol name (e.g. "udp" or "tcp")
+        * @return enum entry or null
+        */
+       public static Protocol fromName(String name)
+       {
+               for (Protocol protocol : Protocol.values())
+               {
+                       for (String keyword : protocol.mNames)
+                       {
+                               if (keyword.equalsIgnoreCase(name))
+                               {
+                                       return protocol;
+                               }
+                       }
+               }
+               return null;
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/SettingsCollector.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/SettingsCollector.java
new file mode 100644 (file)
index 0000000..658c2da
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Copyright (C) 2012 Christoph Buehler
+ * Copyright (C) 2012 Patrick Loetscher
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc.collectors;
+
+import java.util.Locale;
+
+import org.strongswan.android.logic.imc.attributes.Attribute;
+import org.strongswan.android.logic.imc.attributes.SettingsAttribute;
+
+import android.content.ContentResolver;
+import android.content.Context;
+
+public class SettingsCollector implements Collector
+{
+       private final ContentResolver mContentResolver;
+       private final String[] mSettings;
+
+       public SettingsCollector(Context context, String[] args)
+       {
+               mContentResolver = context.getContentResolver();
+               mSettings = args;
+       }
+
+       @Override
+       public Attribute getMeasurement()
+       {
+               if (mSettings == null || mSettings.length == 0)
+               {
+                       return null;
+               }
+               SettingsAttribute attribute = new SettingsAttribute();
+               for (String name : mSettings)
+               {
+                       String value = android.provider.Settings.Secure.getString(mContentResolver, name.toLowerCase(Locale.US));
+                       if (value == null)
+                       {
+                               value = android.provider.Settings.System.getString(mContentResolver, name.toLowerCase(Locale.US));
+                       }
+                       if (value != null)
+                       {
+                               attribute.addSetting(name, value);
+                       }
+               }
+               return attribute;
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/StringVersionCollector.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/collectors/StringVersionCollector.java
new file mode 100644 (file)
index 0000000..6e0df94
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Copyright (C) 2012 Christoph Buehler
+ * Copyright (C) 2012 Patrick Loetscher
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.imc.collectors;
+
+import org.strongswan.android.logic.imc.attributes.Attribute;
+import org.strongswan.android.logic.imc.attributes.StringVersionAttribute;
+
+public class StringVersionCollector implements Collector
+{
+       @Override
+       public Attribute getMeasurement()
+       {
+               StringVersionAttribute attribute = new StringVersionAttribute();
+               attribute.setProductVersionNumber(android.os.Build.VERSION.RELEASE);
+               attribute.setInternalBuildNumber(android.os.Build.DISPLAY);
+               return attribute;
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateKeyStoreProvider.java b/src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateKeyStoreProvider.java
new file mode 100644 (file)
index 0000000..c49b104
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2014 Tobias Brunner
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.security;
+
+import java.security.Provider;
+
+public class LocalCertificateKeyStoreProvider extends Provider
+{
+       private static final long serialVersionUID = 3515038332469843219L;
+
+       public LocalCertificateKeyStoreProvider()
+       {
+               super("LocalCertificateKeyStoreProvider", 1.0, "KeyStore provider for local certificates");
+               put("KeyStore.LocalCertificateStore", LocalCertificateKeyStoreSpi.class.getName());
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateKeyStoreSpi.java b/src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateKeyStoreSpi.java
new file mode 100644 (file)
index 0000000..64a48a9
--- /dev/null
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2014 Tobias Brunner
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.security;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.Key;
+import java.security.KeyStoreException;
+import java.security.KeyStoreSpi;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+
+public class LocalCertificateKeyStoreSpi extends KeyStoreSpi
+{
+       private final LocalCertificateStore mStore = new LocalCertificateStore();
+
+       @Override
+       public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException, UnrecoverableKeyException
+       {
+               return null;
+       }
+
+       @Override
+       public Certificate[] engineGetCertificateChain(String alias)
+       {
+               return null;
+       }
+
+       @Override
+       public Certificate engineGetCertificate(String alias)
+       {
+               return mStore.getCertificate(alias);
+       }
+
+       @Override
+       public Date engineGetCreationDate(String alias)
+       {
+               return mStore.getCreationDate(alias);
+       }
+
+       @Override
+       public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain) throws KeyStoreException
+       {
+               throw new UnsupportedOperationException();
+       }
+
+       @Override
+       public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) throws KeyStoreException
+       {
+               throw new UnsupportedOperationException();
+       }
+
+       @Override
+       public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException
+       {
+               /* we ignore the given alias as the store calculates it on its own,
+                * duplicates are replaced */
+               if (!mStore.addCertificate(cert))
+               {
+                       throw new KeyStoreException();
+               }
+       }
+
+       @Override
+       public void engineDeleteEntry(String alias) throws KeyStoreException
+       {
+               mStore.deleteCertificate(alias);
+       }
+
+       @Override
+       public Enumeration<String> engineAliases()
+       {
+               return Collections.enumeration(mStore.aliases());
+       }
+
+       @Override
+       public boolean engineContainsAlias(String alias)
+       {
+               return mStore.containsAlias(alias);
+       }
+
+       @Override
+       public int engineSize()
+       {
+               return mStore.aliases().size();
+       }
+
+       @Override
+       public boolean engineIsKeyEntry(String alias)
+       {
+               return false;
+       }
+
+       @Override
+       public boolean engineIsCertificateEntry(String alias)
+       {
+               return engineContainsAlias(alias);
+       }
+
+       @Override
+       public String engineGetCertificateAlias(Certificate cert)
+       {
+               return mStore.getCertificateAlias(cert);
+       }
+
+       @Override
+       public void engineStore(OutputStream stream, char[] password) throws IOException, NoSuchAlgorithmException, CertificateException
+       {
+               throw new UnsupportedOperationException();
+       }
+
+       @Override
+       public void engineLoad(InputStream stream, char[] password) throws IOException, NoSuchAlgorithmException, CertificateException
+       {
+               if (stream != null)
+               {
+                       throw new UnsupportedOperationException();
+               }
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateStore.java b/src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateStore.java
new file mode 100644 (file)
index 0000000..cec5c60
--- /dev/null
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2014 Tobias Brunner
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.security;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+import org.strongswan.android.logic.StrongSwanApplication;
+import org.strongswan.android.utils.Utils;
+
+import android.content.Context;
+
+public class LocalCertificateStore
+{
+       private static final String FILE_PREFIX = "certificate-";
+       private static final String ALIAS_PREFIX = "local:";
+       private static final Pattern ALIAS_PATTERN = Pattern.compile("^" + ALIAS_PREFIX + "[0-9a-f]{40}$");
+
+       /**
+        * Add the given certificate to the store
+        * @param cert the certificate to add
+        * @return true if successful
+        */
+       public boolean addCertificate(Certificate cert)
+       {
+               if (!(cert instanceof X509Certificate))
+               {       /* only accept X.509 certificates */
+                       return false;
+               }
+               String keyid = getKeyId(cert);
+               if (keyid == null)
+               {
+                       return false;
+               }
+               FileOutputStream out;
+               try
+               {
+                       /* we replace any existing file with the same alias */
+                       out = StrongSwanApplication.getContext().openFileOutput(FILE_PREFIX + keyid, Context.MODE_PRIVATE);
+                       try
+                       {
+                               out.write(cert.getEncoded());
+                               return true;
+                       }
+                       catch (CertificateEncodingException e)
+                       {
+                               e.printStackTrace();
+                       }
+                       catch (IOException e)
+                       {
+                               e.printStackTrace();
+                       }
+                       finally
+                       {
+                               try
+                               {
+                                       out.close();
+                               }
+                               catch (IOException e)
+                               {
+                                       e.printStackTrace();
+                               }
+                       }
+               }
+               catch (FileNotFoundException e)
+               {
+                       e.printStackTrace();
+               }
+               return false;
+       }
+
+       /**
+        * Delete the certificate with the given alias
+        * @param alias a certificate's alias
+        */
+       public void deleteCertificate(String alias)
+       {
+               if (ALIAS_PATTERN.matcher(alias).matches())
+               {
+                       alias = alias.substring(ALIAS_PREFIX.length());
+                       StrongSwanApplication.getContext().deleteFile(FILE_PREFIX + alias);
+               }
+       }
+
+       /**
+        * Retrieve the certificate with the given alias
+        * @param alias a certificate's alias
+        * @return certificate object or null
+        */
+       public X509Certificate getCertificate(String alias)
+       {
+               if (!ALIAS_PATTERN.matcher(alias).matches())
+               {
+                       return null;
+               }
+               alias = alias.substring(ALIAS_PREFIX.length());
+               try
+               {
+                       FileInputStream in = StrongSwanApplication.getContext().openFileInput(FILE_PREFIX + alias);
+                       try
+                       {
+                               CertificateFactory factory = CertificateFactory.getInstance("X.509");
+                               X509Certificate certificate = (X509Certificate)factory.generateCertificate(in);
+                               return certificate;
+                       }
+                       catch (CertificateException e)
+                       {
+                               e.printStackTrace();
+                       }
+                       finally
+                       {
+                               try
+                               {
+                                       in.close();
+                               }
+                               catch (IOException e)
+                               {
+                                       e.printStackTrace();
+                               }
+                       }
+               }
+               catch (FileNotFoundException e)
+               {
+                       e.printStackTrace();
+               }
+               return null;
+       }
+
+       /**
+        * Returns the creation date of the certificate with the given alias
+        * @param alias certificate alias
+        * @return creation date or null if not found
+        */
+       public Date getCreationDate(String alias)
+       {
+               if (!ALIAS_PATTERN.matcher(alias).matches())
+               {
+                       return null;
+               }
+               alias = alias.substring(ALIAS_PREFIX.length());
+               File file = StrongSwanApplication.getContext().getFileStreamPath(FILE_PREFIX + alias);
+               return file.exists() ? new Date(file.lastModified()) : null;
+       }
+
+       /**
+        * Returns a list of all known certificate aliases
+        * @return list of aliases
+        */
+       public ArrayList<String> aliases()
+       {
+               ArrayList<String> list = new ArrayList<String>();
+               for (String file : StrongSwanApplication.getContext().fileList())
+               {
+                       if (file.startsWith(FILE_PREFIX))
+                       {
+                               list.add(ALIAS_PREFIX + file.substring(FILE_PREFIX.length()));
+                       }
+               }
+               return list;
+       }
+
+       /**
+        * Check if the store contains a certificate with the given alias
+        * @param alias certificate alias
+        * @return true if the store contains the certificate
+        */
+       public boolean containsAlias(String alias)
+       {
+               return getCreationDate(alias) != null;
+       }
+
+       /**
+        * Returns a certificate alias based on a SHA-1 hash of the public key.
+        *
+        * @param cert certificate to get an alias for
+        * @return hex encoded alias, or null if failed
+        */
+       public String getCertificateAlias(Certificate cert)
+       {
+               String keyid = getKeyId(cert);
+               return keyid != null ? ALIAS_PREFIX + keyid : null;
+       }
+
+       /**
+        * Calculates the SHA-1 hash of the public key of the given certificate.
+        * @param cert certificate to get the key ID from
+        * @return hex encoded SHA-1 hash of the public key or null if failed
+        */
+       private String getKeyId(Certificate cert)
+       {
+               MessageDigest md;
+               try
+               {
+                       md = java.security.MessageDigest.getInstance("SHA1");
+                       byte[] hash = md.digest(cert.getPublicKey().getEncoded());
+                       return Utils.bytesToHex(hash);
+               }
+               catch (NoSuchAlgorithmException e)
+               {
+                       e.printStackTrace();
+               }
+               return null;
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/security/TrustedCertificateEntry.java b/src/frontends/android/app/src/main/java/org/strongswan/android/security/TrustedCertificateEntry.java
new file mode 100644 (file)
index 0000000..143741f
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+ * 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.security;
+
+import java.security.cert.X509Certificate;
+
+import android.net.http.SslCertificate;
+
+public class TrustedCertificateEntry implements Comparable<TrustedCertificateEntry>
+{
+       private final X509Certificate mCert;
+       private final String mAlias;
+       private String mSubjectPrimary;
+       private String mSubjectSecondary = "";
+       private String mString;
+
+       /**
+        * Create an entry for certificate lists.
+        *
+        * @param alias alias of the certificate (as used in the KeyStore)
+        * @param cert certificate associated with that alias
+        */
+       public TrustedCertificateEntry(String alias, X509Certificate cert)
+       {
+               mCert = cert;
+               mAlias = alias;
+
+               SslCertificate ssl = new SslCertificate(mCert);
+               String o = ssl.getIssuedTo().getOName();
+               String ou = ssl.getIssuedTo().getUName();
+               String cn = ssl.getIssuedTo().getCName();
+               if (!o.isEmpty())
+               {
+                       mSubjectPrimary = o;
+                       if (!cn.isEmpty())
+                       {
+                               mSubjectSecondary = cn;
+                       }
+                       else if (!ou.isEmpty())
+                       {
+                               mSubjectSecondary = ou;
+                       }
+               }
+               else if (!cn.isEmpty())
+               {
+                       mSubjectPrimary = cn;
+               }
+               else
+               {
+                       mSubjectPrimary = ssl.getIssuedTo().getDName();
+               }
+       }
+
+       /**
+        * The main subject of this certificate (O, CN or the complete DN, whatever
+        * is found first).
+        *
+        * @return the main subject
+        */
+       public String getSubjectPrimary()
+       {
+               return mSubjectPrimary;
+       }
+
+       /**
+        * Get the secondary subject of this certificate (either CN or OU if primary
+        * subject is O, empty otherwise)
+        *
+        * @return the secondary subject
+        */
+       public String getSubjectSecondary()
+       {
+               return mSubjectSecondary;
+       }
+
+       /**
+        * The alias associated with this certificate.
+        *
+        * @return KeyStore alias of this certificate
+        */
+       public String getAlias()
+       {
+               return mAlias;
+       }
+
+       /**
+        * The certificate.
+        *
+        * @return certificate
+        */
+       public X509Certificate getCertificate()
+       {
+               return mCert;
+       }
+
+       @Override
+       public String toString()
+       {       /* combination of both subject lines, used for filtering lists */
+               if (mString == null)
+               {
+                       mString = mSubjectPrimary;
+                       if (!mSubjectSecondary.isEmpty())
+                       {
+                               mString += ", " + mSubjectSecondary;
+                       }
+               }
+               return mString;
+       }
+
+       @Override
+       public int compareTo(TrustedCertificateEntry another)
+       {
+               int diff = mSubjectPrimary.compareToIgnoreCase(another.mSubjectPrimary);
+               if (diff == 0)
+               {
+                       diff = mSubjectSecondary.compareToIgnoreCase(another.mSubjectSecondary);
+               }
+               return diff;
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/CertificateDeleteConfirmationDialog.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/CertificateDeleteConfirmationDialog.java
new file mode 100644 (file)
index 0000000..c381900
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2014 Tobias Brunner
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.ui;
+
+import org.strongswan.android.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.os.Bundle;
+
+/**
+ * Class that displays a confirmation dialog to delete a selected local
+ * certificate.
+ */
+public class CertificateDeleteConfirmationDialog extends DialogFragment
+{
+       public static final String ALIAS = "alias";
+       OnCertificateDeleteListener mListener;
+
+       /**
+        * Interface that can be implemented by parent activities to get the
+        * alias of the certificate to delete, if the user confirms the deletion.
+        */
+       public interface OnCertificateDeleteListener
+       {
+               public void onDelete(String alias);
+       }
+
+       @Override
+       public void onAttach(Activity activity)
+       {
+               super.onAttach(activity);
+               if (activity instanceof OnCertificateDeleteListener)
+               {
+                       mListener = (OnCertificateDeleteListener)activity;
+               }
+       }
+
+       @Override
+       public Dialog onCreateDialog(Bundle savedInstanceState)
+       {
+               return new AlertDialog.Builder(getActivity())
+                       .setIcon(android.R.drawable.ic_dialog_alert)
+                       .setTitle(R.string.delete_certificate_question)
+                       .setMessage(R.string.delete_certificate)
+                       .setPositiveButton(R.string.delete_profile, new DialogInterface.OnClickListener() {
+                               @Override
+                               public void onClick(DialogInterface dialog, int whichButton)
+                               {
+                                       if (mListener != null)
+                                       {
+                                               mListener.onDelete(getArguments().getString(ALIAS));
+                                       }
+                               }
+                       })
+                       .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+                               @Override
+                               public void onClick(DialogInterface dialog, int which)
+                               {
+                                       dismiss();
+                               }
+                       }).create();
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/ImcStateFragment.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/ImcStateFragment.java
new file mode 100644 (file)
index 0000000..5b17997
--- /dev/null
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2013 Tobias Brunner
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.ui;
+
+import java.util.ArrayList;
+
+import org.strongswan.android.R;
+import org.strongswan.android.logic.VpnStateService;
+import org.strongswan.android.logic.VpnStateService.VpnStateListener;
+import org.strongswan.android.logic.imc.ImcState;
+import org.strongswan.android.logic.imc.RemediationInstruction;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.view.GestureDetector;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class ImcStateFragment extends Fragment implements VpnStateListener
+{
+       private TextView mStateView;
+       private TextView mAction;
+       private LinearLayout mButton;
+       private VpnStateService mService;
+       private final ServiceConnection mServiceConnection = new ServiceConnection() {
+               @Override
+               public void onServiceDisconnected(ComponentName name)
+               {
+                       mService = null;
+               }
+
+               @Override
+               public void onServiceConnected(ComponentName name, IBinder service)
+               {
+                       mService = ((VpnStateService.LocalBinder)service).getService();
+                       mService.registerListener(ImcStateFragment.this);
+                       updateView();
+               }
+       };
+
+       @Override
+       public void onCreate(Bundle savedInstanceState)
+       {
+               super.onCreate(savedInstanceState);
+
+               /* bind to the service only seems to work from the ApplicationContext */
+               Context context = getActivity().getApplicationContext();
+               context.bindService(new Intent(context, VpnStateService.class),
+                                                       mServiceConnection, Service.BIND_AUTO_CREATE);
+               /* hide it initially */
+               getFragmentManager().beginTransaction().hide(this).commit();
+       }
+
+       @Override
+       public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                                                        Bundle savedInstanceState)
+       {
+               View view = inflater.inflate(R.layout.imc_state_fragment, container, false);
+
+               mButton = (LinearLayout)view.findViewById(R.id.imc_state_button);
+               mButton.setOnClickListener(new OnClickListener() {
+                       @Override
+                       public void onClick(View v)
+                       {
+                               Intent intent;
+                               if (mService != null && !mService.getRemediationInstructions().isEmpty())
+                               {
+                                       intent = new Intent(getActivity(), RemediationInstructionsActivity.class);
+                                       intent.putParcelableArrayListExtra(RemediationInstructionsFragment.EXTRA_REMEDIATION_INSTRUCTIONS,
+                                                                                                          new ArrayList<RemediationInstruction>(mService.getRemediationInstructions()));
+                               }
+                               else
+                               {
+                                       intent = new Intent(getActivity(), LogActivity.class);
+                               }
+                               startActivity(intent);
+                       }
+               });
+               final GestureDetector gestures = new GestureDetector(getActivity(), new GestureDetector.SimpleOnGestureListener() {
+                       /* a better value would be getScaledTouchExplorationTapSlop() but that is hidden */
+                       private final int mMinDistance = ViewConfiguration.get(getActivity()).getScaledTouchSlop() * 4;
+
+                       @Override
+                       public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
+                       {
+                               if (Math.abs(e1.getX() - e2.getX()) >= mMinDistance)
+                               {       /* only if the user swiped a minimum horizontal distance */
+                                       if (mService != null)
+                                       {
+                                               mService.setImcState(ImcState.UNKNOWN);
+                                       }
+                                       return true;
+                               }
+                               return false;
+                       }
+               });
+               mButton.setOnTouchListener(new OnTouchListener() {
+                       @Override
+                       public boolean onTouch(View v, MotionEvent event)
+                       {
+                               return gestures.onTouchEvent(event);
+                       }
+               });
+
+               mStateView = (TextView)view.findViewById(R.id.imc_state);
+               mAction = (TextView)view.findViewById(R.id.action);
+
+               return view;
+       }
+
+       @Override
+       public void onStart()
+       {
+               super.onStart();
+               if (mService != null)
+               {
+                       mService.registerListener(this);
+                       updateView();
+               }
+       }
+
+       @Override
+       public void onStop()
+       {
+               super.onStop();
+               if (mService != null)
+               {
+                       mService.unregisterListener(this);
+               }
+       }
+
+       @Override
+       public void onDestroy()
+       {
+               super.onDestroy();
+               if (mService != null)
+               {
+                       getActivity().getApplicationContext().unbindService(mServiceConnection);
+               }
+       }
+
+       @Override
+       public void stateChanged()
+       {
+               updateView();
+       }
+
+       public void updateView()
+       {
+               FragmentManager fm = getFragmentManager();
+               if (fm == null)
+               {
+                       return;
+               }
+               FragmentTransaction ft = fm.beginTransaction();
+
+               switch (mService.getImcState())
+               {
+                       case UNKNOWN:
+                       case ALLOW:
+                               ft.hide(this);
+                               break;
+                       case ISOLATE:
+                               mStateView.setText(R.string.imc_state_isolate);
+                               mStateView.setTextColor(getResources().getColor(R.color.warning_text));
+                               ft.show(this);
+                               break;
+                       case BLOCK:
+                               mStateView.setText(R.string.imc_state_block);
+                               mStateView.setTextColor(getResources().getColor(R.color.error_text));
+                               ft.show(this);
+                               break;
+               }
+               ft.commit();
+
+               mAction.setText(mService.getRemediationInstructions().isEmpty() ? R.string.show_log
+                                                                                                                                               : R.string.show_remediation_instructions);
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogActivity.java
new file mode 100644 (file)
index 0000000..a5efecc
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * 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.ui;
+
+import java.io.File;
+
+import org.strongswan.android.R;
+import org.strongswan.android.data.LogContentProvider;
+import org.strongswan.android.logic.CharonVpnService;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+public class LogActivity extends Activity
+{
+       @Override
+       public void onCreate(Bundle savedInstanceState)
+       {
+               super.onCreate(savedInstanceState);
+               setContentView(R.layout.log_activity);
+
+               getActionBar().setDisplayHomeAsUpEnabled(true);
+       }
+
+       @Override
+       public boolean onCreateOptionsMenu(Menu menu)
+       {
+               getMenuInflater().inflate(R.menu.log, menu);
+               return true;
+       }
+
+       @Override
+       public boolean onOptionsItemSelected(MenuItem item)
+       {
+               switch (item.getItemId())
+               {
+                       case android.R.id.home:
+                               finish();
+                               return true;
+                       case R.id.menu_send_log:
+                               File logfile = new File(getFilesDir(), CharonVpnService.LOG_FILE);
+                               if (!logfile.exists() || logfile.length() == 0)
+                               {
+                                       Toast.makeText(this, getString(R.string.empty_log), Toast.LENGTH_SHORT).show();
+                                       return true;
+                               }
+
+                               String version = "";
+                               try
+                               {
+                                       version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
+                               }
+                               catch (NameNotFoundException e)
+                               {
+                                       e.printStackTrace();
+                               }
+
+                               Intent intent = new Intent(Intent.ACTION_SEND);
+                               intent.putExtra(Intent.EXTRA_EMAIL, new String[] { MainActivity.CONTACT_EMAIL });
+                               intent.putExtra(Intent.EXTRA_SUBJECT, String.format(getString(R.string.log_mail_subject), version));
+                               intent.setType("text/plain");
+                               intent.putExtra(Intent.EXTRA_STREAM, LogContentProvider.createContentUri());
+                               startActivity(Intent.createChooser(intent, getString(R.string.send_log)));
+                               return true;
+               }
+               return super.onOptionsItemSelected(item);
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogFragment.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogFragment.java
new file mode 100644 (file)
index 0000000..8740e0c
--- /dev/null
@@ -0,0 +1,227 @@
+/*
+ * 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.ui;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.StringReader;
+
+import org.strongswan.android.R;
+import org.strongswan.android.logic.CharonVpnService;
+
+import android.app.Fragment;
+import android.os.Bundle;
+import android.os.FileObserver;
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+public class LogFragment extends Fragment implements Runnable
+{
+       private String mLogFilePath;
+       private Handler mLogHandler;
+       private TextView mLogView;
+       private LogScrollView mScrollView;
+       private BufferedReader mReader;
+       private Thread mThread;
+       private volatile boolean mRunning;
+       private FileObserver mDirectoryObserver;
+
+       @Override
+       public void onCreate(Bundle savedInstanceState)
+       {
+               super.onCreate(savedInstanceState);
+
+               mLogFilePath = getActivity().getFilesDir() + File.separator + CharonVpnService.LOG_FILE;
+               /* use a handler to update the log view */
+               mLogHandler = new Handler();
+
+               mDirectoryObserver = new LogDirectoryObserver(getActivity().getFilesDir().getAbsolutePath());
+       }
+
+       @Override
+       public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
+       {
+               View view = inflater.inflate(R.layout.log_fragment, null);
+               mLogView = (TextView)view.findViewById(R.id.log_view);
+               mScrollView = (LogScrollView)view.findViewById(R.id.scroll_view);
+               return view;
+       }
+
+       @Override
+       public void onStart()
+       {
+               super.onStart();
+               startLogReader();
+               mDirectoryObserver.startWatching();
+       }
+
+       @Override
+       public void onStop()
+       {
+               super.onStop();
+               mDirectoryObserver.stopWatching();
+               stopLogReader();
+       }
+
+       /**
+        * Start reading from the log file
+        */
+       private void startLogReader()
+       {
+               try
+               {
+                       mReader = new BufferedReader(new FileReader(mLogFilePath));
+               }
+               catch (FileNotFoundException e)
+               {
+                       mReader = new BufferedReader(new StringReader(""));
+               }
+
+               mLogView.setText("");
+               mRunning = true;
+               mThread = new Thread(this);
+               mThread.start();
+       }
+
+       /**
+        * Stop reading from the log file
+        */
+       private void stopLogReader()
+       {
+               try
+               {
+                       mRunning = false;
+                       mThread.interrupt();
+                       mThread.join();
+               }
+               catch (InterruptedException e)
+               {
+               }
+       }
+
+       /**
+        * Write the given log line to the TextView. We strip the prefix off to save
+        * some space (it is not that helpful for regular users anyway).
+        *
+        * @param line log line to log
+        */
+       public void logLine(final String line)
+       {
+               mLogHandler.post(new Runnable() {
+                       @Override
+                       public void run()
+                       {
+                               /* strip off prefix (month=3, day=2, time=8, thread=2, spaces=3) */
+                               mLogView.append((line.length() > 18 ? line.substring(18) : line) + '\n');
+                               /* calling autoScroll() directly does not work, probably because content
+                                * is not yet updated, so we post this to be done later */
+                               mScrollView.post(new Runnable() {
+                                       @Override
+                                       public void run()
+                                       {
+                                               mScrollView.autoScroll();
+                                       }
+                               });
+                       }
+               });
+       }
+
+       @Override
+       public void run()
+       {
+               while (mRunning)
+               {
+                       try
+                       {       /* this works as long as the file is not truncated */
+                               String line = mReader.readLine();
+                               if (line == null)
+                               {       /* wait until there is more to log */
+                                       Thread.sleep(1000);
+                               }
+                               else
+                               {
+                                       logLine(line);
+                               }
+                       }
+                       catch (Exception e)
+                       {
+                               break;
+                       }
+               }
+       }
+
+       /**
+        * FileObserver that checks for changes regarding the log file. Since charon
+        * truncates it (for which there is no explicit event) we check for any modification
+        * to the file, keep track of the file size and reopen it if it got smaller.
+        */
+       private class LogDirectoryObserver extends FileObserver
+       {
+               private final File mFile;
+               private long mSize;
+
+               public LogDirectoryObserver(String path)
+               {
+                       super(path, FileObserver.CREATE | FileObserver.MODIFY | FileObserver.DELETE);
+                       mFile = new File(mLogFilePath);
+                       mSize = mFile.length();
+               }
+
+               @Override
+               public void onEvent(int event, String path)
+               {
+                       if (path == null || !path.equals(CharonVpnService.LOG_FILE))
+                       {
+                               return;
+                       }
+                       switch (event)
+                       {       /* even though we only subscribed for these we check them,
+                                * as strange events are sometimes received */
+                               case FileObserver.CREATE:
+                               case FileObserver.DELETE:
+                                       restartLogReader();
+                                       break;
+                               case FileObserver.MODIFY:
+                                       /* if the size got smaller reopen the log file, as it was probably truncated */
+                                       long size = mFile.length();
+                                       if (size < mSize)
+                                       {
+                                               restartLogReader();
+                                       }
+                                       mSize = size;
+                                       break;
+                       }
+               }
+
+               private void restartLogReader()
+               {
+                       /* we are called from a separate thread, so we use the handler */
+                       mLogHandler.post(new Runnable() {
+                               @Override
+                               public void run()
+                               {
+                                       stopLogReader();
+                                       startLogReader();
+                               }
+                       });
+               }
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogScrollView.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogScrollView.java
new file mode 100644 (file)
index 0000000..7eee820
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ScrollView;
+
+public class LogScrollView extends ScrollView
+{
+       private boolean mAutoScroll = true;
+
+       public LogScrollView(Context context)
+       {
+               super(context);
+       }
+
+       public LogScrollView(Context context, AttributeSet attrs)
+       {
+               super(context, attrs);
+       }
+
+       public LogScrollView(Context context, AttributeSet attrs, int defStyle)
+       {
+               super(context, attrs, defStyle);
+       }
+
+       @Override
+       public boolean onTouchEvent(MotionEvent ev)
+       {
+               /* disable auto-scrolling when the user starts scrolling around */
+               if (ev.getActionMasked() == MotionEvent.ACTION_DOWN)
+               {
+                       mAutoScroll = false;
+               }
+               return super.onTouchEvent(ev);
+       }
+
+       /**
+        * Call this to move newly added content into view by scrolling to the bottom.
+        * Nothing happens if auto-scrolling is disabled.
+        */
+       public void autoScroll()
+       {
+               if (mAutoScroll)
+               {
+                       fullScroll(View.FOCUS_DOWN);
+               }
+       }
+
+       @Override
+       protected void onScrollChanged(int l, int t, int oldl, int oldt)
+       {
+               super.onScrollChanged(l, t, oldl, oldt);
+               /* if the user scrolls to the bottom we enable auto-scrolling again */
+               if (t == getChildAt(getChildCount() - 1).getHeight() - getHeight())
+               {
+                       mAutoScroll = true;
+               }
+       }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java
new file mode 100644 (file)
index 0000000..e1b3e07
--- /dev/null
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2012 Tobias Brunner
+ * Copyright (C) 2012 Giuliano Grassi
+ * Copyright (C) 2012 Ralf Sager
+ * Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.ui;
+
+import org.strongswan.android.R;
+import org.strongswan.android.data.VpnProfile;
+import org.strongswan.android.data.VpnProfileDataSource;
+import org.strongswan.android.data.VpnType.VpnTypeFeature;
+import org.strongswan.android.logic.CharonVpnService;
+import org.strongswan.android.logic.TrustedCertificateManager;
+import org.strongswan.android.logic.VpnStateService;
+import org.strongswan.android.logic.VpnStateService.State;
+import org.strongswan.android.ui.VpnProfileListFragment.OnVpnProfileSelectedListener;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.app.Service;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.net.VpnService;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.widget.EditText;
+import android.widget.Toast;
+
+public class MainActivity extends Activity implements OnVpnProfileSelectedListener
+{
+       public static final String CONTACT_EMAIL = "android@strongswan.org";
+       public static final String START_PROFILE = "org.strongswan.android.action.START_PROFILE";
+       public static final String EXTRA_VPN_PROFILE_ID = "org.strongswan.android.VPN_PROFILE_ID";
+       /** Use "bring your own device" (BYOD) features */
+       public static final boolean USE_BYOD = true;
+       private static final int PREPARE_VPN_SERVICE = 0;
+       private static final String PROFILE_NAME = "org.strongswan.android.MainActivity.PROFILE_NAME";
+       private static final String PROFILE_REQUIRES_PASSWORD = "org.strongswan.android.MainActivity.REQUIRES_PASSWORD";
+       private static final String PROFILE_RECONNECT = "org.strongswan.android.MainActivity.RECONNECT";
+       private static final String DIALOG_TAG = "Dialog";
+
+       private Bundle mProfileInfo;
+       private VpnStateService mService;
+       private final ServiceConnection mServiceConnection = new ServiceConnection() {
+               @Override
+               public void onServiceDisconnected(ComponentName name)
+               {
+                       mService = null;
+               }
+
+               @Override
+               public void onServiceConnected(ComponentName name, IBinder service)
+               {
+                       mService = ((VpnStateService.LocalBinder)service).getService();
+
+                       if (START_PROFILE.equals(getIntent().getAction()))
+                       {
+                               startVpnProfile(getIntent());
+                       }
+               }
+       };
+
+       @Override
+       public void onCreate(Bundle savedInstanceState)
+       {
+               super.onCreate(savedInstanceState);
+               requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+               setContentView(R.layout.main);
+
+               this.bindService(new Intent(this, VpnStateService.class),
+                                                mServiceConnection, Service.BIND_AUTO_CREATE);
+
+               ActionBar bar = getActionBar();
+               bar.setDisplayShowTitleEnabled(false);
+
+               /* load CA certificates in a background task */
+               new LoadCertificatesTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+       }
+
+       @Override
+       protected void onDestroy()
+       {
+               super.onDestroy();
+               if (mService != null)
+               {
+                       this.unbindService(mServiceConnection);
+               }
+       }
+
+       /**
+        * Due to launchMode=singleTop this is called if the Activity already exists
+        */
+       @Override
+       protected void onNewIntent(Intent intent)
+       {
+               super.onNewIntent(intent);
+
+               if (START_PROFILE.equals(intent.getAction()))
+               {
+                       startVpnProfile(intent);
+               }
+       }
+
+       @Override
+       public boolean onCreateOptionsMenu(Menu menu)
+       {
+               getMenuInflater().inflate(R.menu.main, menu);
+               return true;
+       }
+
+       @Override
+       public boolean onOptionsItemSelected(MenuItem item)
+       {
+               switch (item.getItemId())
+               {
+                       case R.id.menu_manage_certs:
+                               Intent certIntent = new Intent(this, TrustedCertificatesActivity.class);
+                               startActivity(certIntent);
+                               return true;
+                       case R.id.menu_show_log:
+                               Intent logIntent = new Intent(this, LogActivity.class);
+                               startActivity(logIntent);
+                               return true;
+                       default:
+                               return super.onOptionsItemSelected(item);
+               }
+       }
+
+       /**
+        * 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(Bundle profileInfo)
+       {
+               Intent intent;
+               try
+               {
+                       intent = VpnService.prepare(this);
+               }
+               catch (IllegalStateException ex)
+               {
+                       /* this happens if the always-on VPN feature (Android 4.2+) is activated */
+                       VpnNotSupportedError.showWithMessage(this, R.string.vpn_not_supported_during_lockdown);
+                       return;
+               }
+               /* store profile info until the user grants us permission */
+               mProfileInfo = profileInfo;
+               if (intent != null)
+               {
+                       try
+                       {
+                               startActivityForResult(intent, PREPARE_VPN_SERVICE);
+                       }
+                       catch (ActivityNotFoundException ex)
+                       {
+                               /* it seems some devices, even though they come with Android 4,
+                                * don't have the VPN components built into the system image.
+                                * com.android.vpndialogs/com.android.vpndialogs.ConfirmDialog
+                                * will not be found then */
+                               VpnNotSupportedError.showWithMessage(this, R.string.vpn_not_supported);
+                       }
+               }
+               else
+               {       /* user already granted permission to use VpnService */
+                       onActivityResult(PREPARE_VPN_SERVICE, RESULT_OK, null);
+               }
+       }
+
+       @Override
+       protected void onActivityResult(int requestCode, int resultCode, Intent data)
+       {
+               switch (requestCode)
+               {
+                       case PREPARE_VPN_SERVICE:
+                               if (resultCode == RESULT_OK && mProfileInfo != null)
+                               {
+                                       Intent intent = new Intent(this, CharonVpnService.class);
+                                       intent.putExtras(mProfileInfo);
+                                       this.startService(intent);
+                               }
+                               break;
+                       default:
+                               super.onActivityResult(requestCode, resultCode, data);
+               }
+       }
+
+       @Override
+       public void onVpnProfileSelected(VpnProfile profile)
+       {
+               Bundle profileInfo = new Bundle();
+               profileInfo.putLong(VpnProfileDataSource.KEY_ID, profile.getId());
+               profileInfo.putString(VpnProfileDataSource.KEY_USERNAME, profile.getUsername());
+               profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, profile.getPassword());
+               profileInfo.putBoolean(PROFILE_REQUIRES_PASSWORD, profile.getVpnType().has(VpnTypeFeature.USER_PASS));
+               profileInfo.putString(PROFILE_NAME, profile.getName());
+
+               removeFragmentByTag(DIALOG_TAG);
+
+               if (mService != null && mService.getState() == State.CONNECTED)
+               {
+                       profileInfo.putBoolean(PROFILE_RECONNECT, mService.getProfile().getId() == profile.getId());
+
+                       ConfirmationDialog dialog = new ConfirmationDialog();
+                       dialog.setArguments(profileInfo);
+                       dialog.show(this.getFragmentManager(), DIALOG_TAG);
+                       return;
+               }
+               startVpnProfile(profileInfo);
+       }
+
+       /**
+        * Start the given VPN profile asking the user for a password if required.
+        * @param profileInfo data about the profile
+        */
+       private void startVpnProfile(Bundle profileInfo)
+       {
+               if (profileInfo.getBoolean(PROFILE_REQUIRES_PASSWORD) &&
+                       profileInfo.getString(VpnProfileDataSource.KEY_PASSWORD) == null)
+               {
+                       LoginDialog login = new LoginDialog();
+                       login.setArguments(profileInfo);
+                       login.show(getFragmentManager(), DIALOG_TAG);
+                       return;
+               }
+               prepareVpnService(profileInfo);
+       }
+
+       /**
+        * Start the VPN profile referred to by the given intent. Displays an error
+        * if the profile doesn't exist.
+        * @param intent Intent that caused us to start this
+        */
+       private void startVpnProfile(Intent intent)
+       {
+               long profileId = intent.getLongExtra(EXTRA_VPN_PROFILE_ID, 0);
+               if (profileId <= 0)
+               {       /* invalid invocation */
+                       return;
+               }
+               VpnProfileDataSource dataSource = new VpnProfileDataSource(this);
+               dataSource.open();
+               VpnProfile profile = dataSource.getVpnProfile(profileId);
+               dataSource.close();
+
+               if (profile != null)
+               {
+                       onVpnProfileSelected(profile);
+               }
+               else
+               {
+                       Toast.makeText(this, R.string.profile_not_found, Toast.LENGTH_LONG).show();
+               }
+       }
+
+       /**
+        * Class that loads the cached CA certificates.
+        */
+       private class LoadCertificatesTask extends AsyncTask<Void, Void, TrustedCertificateManager>
+       {
+               @Override
+               protected void onPreExecute()
+               {
+                       setProgressBarIndeterminateVisibility(true);
+               }
+               @Override
+               protected TrustedCertificateManager doInBackground(Void... params)
+               {
+                       return TrustedCertificateManager.getInstance().load();
+               }
+               @Override
+               protected void onPostExecute(TrustedCertificateManager result)
+               {
+                       setProgressBarIndeterminateVisibility(false);
+               }
+       }
+
+       /**
+        * Dismiss dialog if shown
+        */
+       public void removeFragmentByTag(String tag)
+       {
+               FragmentManager fm = getFragmentManager();
+               Fragment login = fm.findFragmentByTag(tag);
+               if (login != null)
+               {
+                       FragmentTransaction ft = fm.beginTransaction();
+                       ft.remove(login);
+                       ft.commit();
+               }
+       }
+
+       /**
+        * Class that displays a confirmation dialog if a VPN profile is already connected
+        * and then initiates the selected VPN profile if the user confirms the dialog.
+        */
+       public static class ConfirmationDialog extends DialogFragment
+       {
+               @Override
+               public Dialog onCreateDialog(Bundle savedInstanceState)
+               {
+                       final Bundle profileInfo = getArguments();
+                       int icon = android.R.drawable.ic_dialog_alert;
+                       int title = R.string.connect_profile_question;
+                       int message = R.string.replaces_active_connection;
+                       int button = R.string.connect;
+
+                       if (profileInfo.getBoolean(PROFILE_RECONNECT))
+                       {
+                               icon = android.R.drawable.ic_dialog_info;
+                               title = R.string.vpn_connected;
+                               message = R.string.vpn_profile_connected;
+                               button = R.string.reconnect;
+                       }
+
+                       return new AlertDialog.Builder(getActivity())
+                               .setIcon(icon)
+                               .setTitle(String.format(getString(title), profileInfo.getString(PROFILE_NAME)))
+                               .setMessage(message)
+                               .setPositiveButton(button, new DialogInterface.OnClickListener() {
+                                       @Override
+                                       public void onClick(DialogInterface dialog, int whichButton)
+                                       {
+                                               MainActivity activity = (MainActivity)getActivity();
+                                               activity.startVpnProfile(profileInfo);
+                                       }
+                               })
+                               .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+                                       @Override
+                                       public void onClick(DialogInterface dialog, int which)
+                                       {
+                                               dismiss();
+                                       }
+                               }).create();
+               }
+       }
+
+       /**
+        * 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)
+               {
+                       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(profileInfo.getString(VpnProfileDataSource.KEY_USERNAME));
+                       final EditText password = (EditText)view.findViewById(R.id.password);
+
+                       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)
+                               {
+                                       MainActivity activity = (MainActivity)getActivity();
+                                       profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, password.getText().toString().trim());
+                                       activity.prepareVpnService(profileInfo);
+                   &