From 4989dad67b68b38e75416916df406dcec908b399 Mon Sep 17 00:00:00 2001 From: pacien Date: Thu, 7 Sep 2017 14:27:37 +0200 Subject: Implement encrypted private keys support --- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 2 +- .../pacien/tincapp/activities/ConfigureActivity.kt | 38 ++++++++++--- .../pacien/tincapp/activities/LaunchActivity.kt | 50 ++++++++++++++--- .../java/org/pacien/tincapp/commands/TincApp.kt | 24 +++++++++ .../main/java/org/pacien/tincapp/commands/Tincd.kt | 6 ++- .../java/org/pacien/tincapp/context/AppPaths.kt | 7 +++ .../org/pacien/tincapp/data/TincConfiguration.kt | 26 +++++++++ .../tincapp/extensions/ApacheConfiguration.kt | 4 +- .../org/pacien/tincapp/service/TincVpnService.kt | 41 +++++++++++--- .../main/java/org/pacien/tincapp/utils/PemUtils.kt | 62 ++++++++++++++++++++++ app/src/main/res/layout/dialog_decrypt_keys.xml | 17 ++++++ .../res/layout/dialog_encrypt_decrypt_keys.xml | 30 +++++++++++ .../main/res/layout/dialog_network_generate.xml | 7 +++ app/src/main/res/layout/dialog_network_join.xml | 7 +++ app/src/main/res/layout/page_configure.xml | 16 ++++-- app/src/main/res/values/strings.xml | 13 ++++- 17 files changed, 323 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/org/pacien/tincapp/data/TincConfiguration.kt create mode 100644 app/src/main/java/org/pacien/tincapp/utils/PemUtils.kt create mode 100644 app/src/main/res/layout/dialog_decrypt_keys.xml create mode 100644 app/src/main/res/layout/dialog_encrypt_decrypt_keys.xml diff --git a/app/build.gradle b/app/build.gradle index 881bfdf..2605544 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -52,6 +52,8 @@ dependencies { exclude group: 'commons-logging', module: 'commons-logging' } + compile 'org.bouncycastle:bcpkix-jdk15on:1.57' + compile 'net.sourceforge.streamsupport:streamsupport-cfuture:1.5.5' compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 489245d..3ccc9e6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -41,7 +41,7 @@ + android:theme="@style/AppTheme.NoActionBar"> diff --git a/app/src/main/java/org/pacien/tincapp/activities/ConfigureActivity.kt b/app/src/main/java/org/pacien/tincapp/activities/ConfigureActivity.kt index 9027b14..6c29a53 100644 --- a/app/src/main/java/org/pacien/tincapp/activities/ConfigureActivity.kt +++ b/app/src/main/java/org/pacien/tincapp/activities/ConfigureActivity.kt @@ -10,6 +10,7 @@ import android.support.v7.app.AlertDialog import android.view.View import java8.util.concurrent.CompletableFuture import kotlinx.android.synthetic.main.base.* +import kotlinx.android.synthetic.main.dialog_encrypt_decrypt_keys.view.* import kotlinx.android.synthetic.main.dialog_network_generate.view.* import kotlinx.android.synthetic.main.dialog_network_join.view.* import kotlinx.android.synthetic.main.page_configure.* @@ -62,7 +63,10 @@ class ConfigureActivity : BaseActivity() { val genDialog = layoutInflater.inflate(R.layout.dialog_network_generate, main_content, false) AlertDialog.Builder(this).setTitle(R.string.title_new_network).setView(genDialog) .setPositiveButton(R.string.action_create) { _, _ -> - generateConf(genDialog.new_net_name.text.toString(), genDialog.new_node_name.text.toString()) + generateConf( + genDialog.new_net_name.text.toString(), + genDialog.new_node_name.text.toString(), + genDialog.new_passphrase.text.toString()) }.setNegativeButton(R.string.action_cancel, App.dismissAction).show() } @@ -70,7 +74,21 @@ class ConfigureActivity : BaseActivity() { joinDialog = layoutInflater.inflate(R.layout.dialog_network_join, main_content, false) AlertDialog.Builder(this).setTitle(R.string.title_join_network).setView(joinDialog) .setPositiveButton(R.string.action_join) { _, _ -> - joinNetwork(joinDialog!!.net_name.text.toString(), joinDialog!!.invitation_url.text.toString()) + joinNetwork( + joinDialog!!.net_name.text.toString(), + joinDialog!!.invitation_url.text.toString(), + joinDialog!!.join_passphrase.text.toString()) + }.setNegativeButton(R.string.action_cancel, App.dismissAction).show() + } + + fun openEncryptDecryptPrivateKeyDialog(@Suppress("UNUSED_PARAMETER") v: View) { + val encryptDecryptDialog = layoutInflater.inflate(R.layout.dialog_encrypt_decrypt_keys, main_content, false) + AlertDialog.Builder(this).setTitle(R.string.title_private_keys_encryption).setView(encryptDecryptDialog) + .setPositiveButton(R.string.action_apply) { _, _ -> + encryptDecryptPrivateKeys( + encryptDecryptDialog!!.enc_dec_net_name.text.toString(), + encryptDecryptDialog.enc_dec_current_passphrase.text.toString(), + encryptDecryptDialog.enc_dec_new_passphrase.text.toString()) }.setNegativeButton(R.string.action_cancel, App.dismissAction).show() } @@ -80,22 +98,28 @@ class ConfigureActivity : BaseActivity() { text_tinc_binary.text = AppPaths.tinc().absolutePath } - private fun generateConf(netName: String, nodeName: String) = execAction( + private fun generateConf(netName: String, nodeName: String, passphrase: String? = null) = execAction( R.string.message_generating_configuration, Tinc.init(netName, nodeName) - .thenCompose { TincApp.removeScripts(netName) }) + .thenCompose { TincApp.removeScripts(netName) } + .thenCompose { TincApp.setPassphrase(netName, newPassphrase = passphrase) }) - private fun joinNetwork(netName: String, url: String) = execAction( + private fun joinNetwork(netName: String, url: String, passphrase: String? = null) = execAction( R.string.message_joining_network, Tinc.join(netName, url) .thenCompose { TincApp.removeScripts(netName) } - .thenCompose { TincApp.generateIfaceCfg(netName) }) + .thenCompose { TincApp.generateIfaceCfg(netName) } + .thenCompose { TincApp.setPassphrase(netName, newPassphrase = passphrase) }) + + private fun encryptDecryptPrivateKeys(netName: String, currentPassphrase: String, newPassphrase: String) = execAction( + R.string.message_encrypting_decrypting_private_keys, + TincApp.setPassphrase(netName, currentPassphrase, newPassphrase)) private fun execAction(@StringRes label: Int, action: CompletableFuture) { showProgressDialog(label).let { progressDialog -> action .whenComplete { _, _ -> progressDialog.dismiss() } - .thenAccept { notify(R.string.message_network_configuration_created) } + .thenAccept { notify(R.string.message_network_configuration_written) } .exceptionallyAccept { runOnUiThread { showErrorDialog(it.cause!!.localizedMessage) } } } } diff --git a/app/src/main/java/org/pacien/tincapp/activities/LaunchActivity.kt b/app/src/main/java/org/pacien/tincapp/activities/LaunchActivity.kt index 6eb630d..74a059b 100644 --- a/app/src/main/java/org/pacien/tincapp/activities/LaunchActivity.kt +++ b/app/src/main/java/org/pacien/tincapp/activities/LaunchActivity.kt @@ -1,20 +1,28 @@ package org.pacien.tincapp.activities +import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.net.Uri import android.net.VpnService import android.os.Bundle +import android.support.v7.app.AlertDialog +import android.support.v7.app.AppCompatActivity +import kotlinx.android.synthetic.main.dialog_decrypt_keys.view.* +import org.pacien.tincapp.R +import org.pacien.tincapp.commands.TincApp import org.pacien.tincapp.context.App import org.pacien.tincapp.intent.action.ACTION_CONNECT import org.pacien.tincapp.intent.action.ACTION_DISCONNECT import org.pacien.tincapp.intent.action.TINC_SCHEME import org.pacien.tincapp.service.TincVpnService +import org.pacien.tincapp.utils.PemUtils +import java.io.FileNotFoundException /** * @author pacien */ -class LaunchActivity : Activity() { +class LaunchActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -26,15 +34,41 @@ class LaunchActivity : Activity() { } override fun onActivityResult(request: Int, result: Int, data: Intent?) { - if (result == Activity.RESULT_OK) TincVpnService.startVpn(intent.data.schemeSpecificPart) - finish() + if (request == PERMISSION_REQUEST_CODE && result == Activity.RESULT_OK) askPassphrase() } private fun requestPerm() = VpnService.prepare(this).let { if (it != null) - startActivityForResult(it, 0) + startActivityForResult(it, PERMISSION_REQUEST_CODE) else - onActivityResult(0, Activity.RESULT_OK, null) + onActivityResult(PERMISSION_REQUEST_CODE, Activity.RESULT_OK, null) + } + + @SuppressLint("InflateParams") + private fun askPassphrase() { + val netName = intent.data.schemeSpecificPart + + if (needPassphrase(netName) && intent.data.fragment == null) { + val dialog = layoutInflater.inflate(R.layout.dialog_decrypt_keys, null, false) + AlertDialog.Builder(this) + .setTitle(R.string.title_unlock_private_keys).setView(dialog) + .setPositiveButton(R.string.action_unlock) { _, _ -> connect(netName, dialog.passphrase.text.toString()) } + .setNegativeButton(R.string.action_cancel, { _, _ -> finish() }) + .show() + } else { + connect(netName, intent.data.fragment) + } + } + + private fun needPassphrase(netName: String) = try { + TincApp.listPrivateKeys(netName).filter { it.exists() }.any { PemUtils.isEncrypted(PemUtils.read(it)) } + } catch (e: FileNotFoundException) { + false + } + + private fun connect(netName: String, passphrase: String? = null) { + TincVpnService.startVpn(netName, passphrase) + finish() } private fun disconnect() { @@ -44,11 +78,13 @@ class LaunchActivity : Activity() { companion object { - fun connect(netName: String) { + private val PERMISSION_REQUEST_CODE = 0 + + fun connect(netName: String, passphrase: String? = null) { App.getContext().startActivity(Intent(App.getContext(), LaunchActivity::class.java) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .setAction(ACTION_CONNECT) - .setData(Uri.Builder().scheme(TINC_SCHEME).opaquePart(netName).build())) + .setData(Uri.Builder().scheme(TINC_SCHEME).opaquePart(netName).fragment(passphrase).build())) } fun disconnect() { diff --git a/app/src/main/java/org/pacien/tincapp/commands/TincApp.kt b/app/src/main/java/org/pacien/tincapp/commands/TincApp.kt index b18a39b..b564b6a 100644 --- a/app/src/main/java/org/pacien/tincapp/commands/TincApp.kt +++ b/app/src/main/java/org/pacien/tincapp/commands/TincApp.kt @@ -1,9 +1,14 @@ package org.pacien.tincapp.commands import java8.util.concurrent.CompletableFuture +import org.pacien.tincapp.R import org.pacien.tincapp.commands.Executor.runAsyncTask +import org.pacien.tincapp.context.App import org.pacien.tincapp.context.AppPaths +import org.pacien.tincapp.data.TincConfiguration import org.pacien.tincapp.data.VpnInterfaceConfiguration +import org.pacien.tincapp.utils.PemUtils +import java.io.FileNotFoundException /** * @author pacien @@ -16,6 +21,16 @@ object TincApp { private fun listScripts(netName: String) = AppPaths.confDir(netName).listFiles { f -> f.name in STATIC_SCRIPTS } + AppPaths.hostsDir(netName).listFiles { f -> SCRIPT_SUFFIXES.any { f.name.endsWith(it) } } + fun listPrivateKeys(netName: String) = try { + TincConfiguration.fromTincConfiguration(AppPaths.existing(AppPaths.tincConfFile(netName))).let { + listOf( + it.privateKeyFile ?: AppPaths.defaultRsaPrivateKeyFile(netName), + it.ed25519PrivateKeyFile ?: AppPaths.defaultEd25519PrivateKeyFile(netName)) + } + } catch (e: FileNotFoundException) { + throw FileNotFoundException(App.getResources().getString(R.string.message_network_config_not_found_format, e.message!!)) + } + fun removeScripts(netName: String): CompletableFuture = runAsyncTask { listScripts(netName).forEach { it.delete() } } @@ -26,4 +41,13 @@ object TincApp { .write(AppPaths.netConfFile(netName)) } + fun setPassphrase(netName: String, currentPassphrase: String? = null, newPassphrase: String?): CompletableFuture = runAsyncTask { + listPrivateKeys(netName) + .filter { it.exists() } + .map { Pair(PemUtils.read(it), it) } + .map { Pair(PemUtils.decrypt(it.first, currentPassphrase), it.second) } + .map { Pair(if (newPassphrase?.isNotEmpty() == true) PemUtils.encrypt(it.first, newPassphrase) else it.first, it.second) } + .forEach { PemUtils.write(it.first, it.second.writer()) } + } + } diff --git a/app/src/main/java/org/pacien/tincapp/commands/Tincd.kt b/app/src/main/java/org/pacien/tincapp/commands/Tincd.kt index db113cc..d44d930 100644 --- a/app/src/main/java/org/pacien/tincapp/commands/Tincd.kt +++ b/app/src/main/java/org/pacien/tincapp/commands/Tincd.kt @@ -7,14 +7,16 @@ import org.pacien.tincapp.context.AppPaths */ object Tincd { - fun start(netName: String, fd: Int) { + fun start(netName: String, deviceFd: Int, ed25519PrivateKeyFd: Int? = null, rsaPrivateKeyFd: Int? = null) { Executor.forkExec(Command(AppPaths.tincd().absolutePath) .withOption("no-detach") .withOption("config", AppPaths.confDir(netName).absolutePath) .withOption("pidfile", AppPaths.pidFile(netName).absolutePath) .withOption("logfile", AppPaths.logFile(netName).absolutePath) .withOption("option", "DeviceType=fd") - .withOption("option", "Device=" + fd)) + .withOption("option", "Device=" + deviceFd) + .apply { if (ed25519PrivateKeyFd != null) withOption("option", "Ed25519PrivateKeyFile=/proc/self/fd/$ed25519PrivateKeyFd") } + .apply { if (rsaPrivateKeyFd != null) withOption("option", "PrivateKeyFile=/proc/self/fd/$rsaPrivateKeyFd") }) } } diff --git a/app/src/main/java/org/pacien/tincapp/context/AppPaths.kt b/app/src/main/java/org/pacien/tincapp/context/AppPaths.kt index 673faa7..94780cc 100644 --- a/app/src/main/java/org/pacien/tincapp/context/AppPaths.kt +++ b/app/src/main/java/org/pacien/tincapp/context/AppPaths.kt @@ -17,8 +17,11 @@ object AppPaths { private val PIDFILE_FORMAT = "tinc.%s.pid" private val NET_CONF_FILE = "network.conf" + private val NET_TINC_CONF_FILE = "tinc.conf" private val NET_HOSTS_DIR = "hosts" private val NET_INVITATION_FILE = "invitation-data" + private val NET_DEFAULT_ED25519_PRIVATE_KEY_FILE = "ed25519_key.priv" + private val NET_DEFAULT_RSA_PRIVATE_KEY_FILE = "rsa_key.priv" fun cacheDir() = App.getContext().externalCacheDir!! fun confDir() = App.getContext().getExternalFilesDir(null)!! @@ -27,12 +30,16 @@ object AppPaths { fun confDir(netName: String) = File(confDir(), netName) fun hostsDir(netName: String) = File(confDir(netName), NET_HOSTS_DIR) fun netConfFile(netName: String) = File(confDir(netName), NET_CONF_FILE) + fun tincConfFile(netName: String) = File(confDir(netName), NET_TINC_CONF_FILE) fun invitationFile(netName: String) = File(confDir(netName), NET_INVITATION_FILE) fun logFile(netName: String) = File(cacheDir(), String.format(LOGFILE_FORMAT, netName)) fun pidFile(netName: String) = File(App.getContext().cacheDir, String.format(PIDFILE_FORMAT, netName)) fun existing(f: File) = f.apply { if (!exists()) throw FileNotFoundException(f.absolutePath) } + fun defaultEd25519PrivateKeyFile(netName: String) = File(confDir(netName), NET_DEFAULT_ED25519_PRIVATE_KEY_FILE) + fun defaultRsaPrivateKeyFile(netName: String) = File(confDir(netName), NET_DEFAULT_RSA_PRIVATE_KEY_FILE) + fun tincd() = File(binDir(), TINCD_BIN) fun tinc() = File(binDir(), TINC_BIN) diff --git a/app/src/main/java/org/pacien/tincapp/data/TincConfiguration.kt b/app/src/main/java/org/pacien/tincapp/data/TincConfiguration.kt new file mode 100644 index 0000000..66496c4 --- /dev/null +++ b/app/src/main/java/org/pacien/tincapp/data/TincConfiguration.kt @@ -0,0 +1,26 @@ +package org.pacien.tincapp.data + +import org.apache.commons.configuration2.Configuration +import org.apache.commons.configuration2.builder.fluent.Configurations +import org.pacien.tincapp.extensions.ApacheConfiguration.getFile +import java.io.File + +/** + * @author pacien + */ +data class TincConfiguration(val ed25519PrivateKeyFile: File? = null, + val privateKeyFile: File? = null) { + + companion object { + + private val KEY_ED25519_PRIVATE_KEY_FILE = "Ed25519PrivateKeyFile" + private val KEY_PRIVATE_KEY_FILE = "PrivateKeyFile" + + fun fromTincConfiguration(f: File) = fromTincConfiguration(Configurations().properties(f)) + fun fromTincConfiguration(c: Configuration) = TincConfiguration( + c.getFile(KEY_ED25519_PRIVATE_KEY_FILE), + c.getFile(KEY_PRIVATE_KEY_FILE)) + + } + +} diff --git a/app/src/main/java/org/pacien/tincapp/extensions/ApacheConfiguration.kt b/app/src/main/java/org/pacien/tincapp/extensions/ApacheConfiguration.kt index eb4bb47..367a280 100644 --- a/app/src/main/java/org/pacien/tincapp/extensions/ApacheConfiguration.kt +++ b/app/src/main/java/org/pacien/tincapp/extensions/ApacheConfiguration.kt @@ -1,8 +1,8 @@ package org.pacien.tincapp.extensions - import org.apache.commons.configuration2.Configuration import org.pacien.tincapp.data.CidrAddress +import java.io.File /** * @author pacien @@ -13,4 +13,6 @@ object ApacheConfiguration { fun Configuration.getCidrList(key: String): List = getStringList(key).map { CidrAddress.fromSlashSeparated(it) } fun Configuration.getIntList(key: String): List = getList(Int::class.java, key, emptyList()) + fun Configuration.getFile(key: String): File? = getString(key)?.let { File(it) } + } diff --git a/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt b/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt index cd1dd74..3db8dce 100644 --- a/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt +++ b/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt @@ -7,16 +7,20 @@ import android.net.VpnService import android.os.ParcelFileDescriptor import android.util.Log import org.apache.commons.configuration2.ex.ConversionException +import org.bouncycastle.openssl.PEMException import org.pacien.tincapp.BuildConfig import org.pacien.tincapp.R import org.pacien.tincapp.commands.Tinc import org.pacien.tincapp.commands.Tincd import org.pacien.tincapp.context.App import org.pacien.tincapp.context.AppPaths +import org.pacien.tincapp.data.TincConfiguration import org.pacien.tincapp.data.VpnInterfaceConfiguration import org.pacien.tincapp.extensions.Java.applyIgnoringException import org.pacien.tincapp.extensions.VpnServiceBuilder.applyCfg import org.pacien.tincapp.intent.action.TINC_SCHEME +import org.pacien.tincapp.utils.PemUtils +import java.io.File import java.io.FileNotFoundException import java.io.IOException @@ -32,11 +36,11 @@ class TincVpnService : VpnService() { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { if (isConnected()) stopVpn() - startVpn(intent.data.schemeSpecificPart) + startVpn(intent.data.schemeSpecificPart, intent.data.fragment) return Service.START_REDELIVER_INTENT } - private fun startVpn(netName: String) { + private fun startVpn(netName: String, passphrase: String? = null) { if (netName.isBlank()) return reportError(resources.getString(R.string.message_no_network_name_provided), docTopic = "intent-api") @@ -53,7 +57,7 @@ class TincVpnService : VpnService() { return reportError(resources.getString(R.string.message_network_config_invalid_format, e.message!!), e, "network-interface") } - val fd = try { + val deviceFd = try { Builder().setSession(netName) .applyCfg(interfaceCfg) .also { applyIgnoringException(it::addDisallowedApplication, BuildConfig.APPLICATION_ID) } @@ -62,11 +66,34 @@ class TincVpnService : VpnService() { return reportError(resources.getString(R.string.message_network_config_invalid_format, e.message!!), e, "network-interface") } - Tincd.start(netName, fd!!.fd) - setState(true, netName, interfaceCfg, fd) + val privateKeys = try { + val tincCfg = TincConfiguration.fromTincConfiguration(AppPaths.existing(AppPaths.tincConfFile(netName))) + + Pair( + openPrivateKey(tincCfg.ed25519PrivateKeyFile ?: AppPaths.defaultEd25519PrivateKeyFile(netName), passphrase), + openPrivateKey(tincCfg.privateKeyFile ?: AppPaths.defaultRsaPrivateKeyFile(netName), passphrase) + ) + } catch (e: FileNotFoundException) { + Pair(null, null) + } catch (e: PEMException) { + return reportError(resources.getString(R.string.message_could_not_decrypt_private_keys_format, e.message!!)) + } + + Tincd.start(netName, deviceFd!!.fd, privateKeys.first?.fd, privateKeys.second?.fd) + setState(true, netName, interfaceCfg, deviceFd) Log.i(TAG, "tinc daemon started.") } + private fun openPrivateKey(f: File?, passphrase: String?): ParcelFileDescriptor? { + if (f == null || !f.exists() || passphrase == null) return null + + val pipe = ParcelFileDescriptor.createPipe() + val decryptedKey = PemUtils.decrypt(PemUtils.read(f), passphrase) + val outputStream = ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]) + PemUtils.write(decryptedKey, outputStream.writer()) + return pipe[0] + } + private fun reportError(msg: String, e: Throwable? = null, docTopic: String? = null) { if (e != null) Log.e(TAG, msg, e) @@ -93,9 +120,9 @@ class TincVpnService : VpnService() { TincVpnService.fd = fd } - fun startVpn(netName: String) { + fun startVpn(netName: String, passphrase: String? = null) { App.getContext().startService(Intent(App.getContext(), TincVpnService::class.java) - .setData(Uri.Builder().scheme(TINC_SCHEME).opaquePart(netName).build())) + .setData(Uri.Builder().scheme(TINC_SCHEME).opaquePart(netName).fragment(passphrase).build())) } fun stopVpn() { diff --git a/app/src/main/java/org/pacien/tincapp/utils/PemUtils.kt b/app/src/main/java/org/pacien/tincapp/utils/PemUtils.kt new file mode 100644 index 0000000..3d59476 --- /dev/null +++ b/app/src/main/java/org/pacien/tincapp/utils/PemUtils.kt @@ -0,0 +1,62 @@ +package org.pacien.tincapp.utils + +import org.bouncycastle.openssl.PEMException +import org.bouncycastle.openssl.PEMParser +import org.bouncycastle.openssl.jcajce.JcaPEMWriter +import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder +import org.bouncycastle.openssl.jcajce.JcePEMEncryptorBuilder +import org.bouncycastle.util.encoders.Hex +import org.bouncycastle.util.io.pem.PemHeader +import org.bouncycastle.util.io.pem.PemObject +import java.io.File +import java.io.FileReader +import java.io.Writer + +/** + * @author pacien + */ +object PemUtils { + + private val PROVIDER = org.bouncycastle.jce.provider.BouncyCastleProvider() + private val ENCRYPTED_PROC_TYPE_HEADER = PemHeader("Proc-Type", "4,ENCRYPTED") + private val DEK_INFO_HEADER_KEY = "DEK-Info" + private val ALGO = "AES-256-CBC" + + private class DekInfo(val algName: String, val iv: ByteArray) + + private fun dekInfoHeader(iv: ByteArray) = PemHeader(DEK_INFO_HEADER_KEY, "$ALGO,${Hex.toHexString(iv)}") + private fun PemObject.getPemHeaders() = headers.map { it as PemHeader } + + fun read(f: File): PemObject = PEMParser(FileReader(f)).readPemObject() + fun write(obj: PemObject, out: Writer) = JcaPEMWriter(out).apply { writeObject(obj) }.apply { close() } + fun isEncrypted(obj: PemObject) = obj.headers.contains(ENCRYPTED_PROC_TYPE_HEADER) + + fun encrypt(obj: PemObject, passPhrase: String) = + JcePEMEncryptorBuilder(ALGO) + .setProvider(PROVIDER) + .build(passPhrase.toCharArray()) + .let { PemObject(obj.type, listOf(ENCRYPTED_PROC_TYPE_HEADER, dekInfoHeader(it.iv)), it.encrypt(obj.content)) } + + fun decrypt(obj: PemObject, passPhrase: String?) = + if (isEncrypted(obj)) { + val dekInfo = try { + obj.getPemHeaders() + .find { it.name == DEK_INFO_HEADER_KEY }!! + .value!! + .split(',') + .let { DekInfo(it[0], Hex.decode(it[1])) } + } catch (e: Exception) { + throw PEMException("Malformed DEK-Info header.", e) + } + + JcePEMDecryptorProviderBuilder() + .setProvider(PROVIDER) + .build(passPhrase?.toCharArray()) + .get(dekInfo.algName) + .decrypt(obj.content, dekInfo.iv) + .let { PemObject(obj.type, it) } + } else { + obj + } + +} diff --git a/app/src/main/res/layout/dialog_decrypt_keys.xml b/app/src/main/res/layout/dialog_decrypt_keys.xml new file mode 100644 index 0000000..b8a3586 --- /dev/null +++ b/app/src/main/res/layout/dialog_decrypt_keys.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/layout/dialog_encrypt_decrypt_keys.xml b/app/src/main/res/layout/dialog_encrypt_decrypt_keys.xml new file mode 100644 index 0000000..1f20d9b --- /dev/null +++ b/app/src/main/res/layout/dialog_encrypt_decrypt_keys.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_network_generate.xml b/app/src/main/res/layout/dialog_network_generate.xml index cc51b7b..d849ca1 100644 --- a/app/src/main/res/layout/dialog_network_generate.xml +++ b/app/src/main/res/layout/dialog_network_generate.xml @@ -19,4 +19,11 @@ android:layout_height="match_parent" android:hint="@string/field_node_name"/> + + diff --git a/app/src/main/res/layout/dialog_network_join.xml b/app/src/main/res/layout/dialog_network_join.xml index 2d23d4b..7bc8804 100644 --- a/app/src/main/res/layout/dialog_network_join.xml +++ b/app/src/main/res/layout/dialog_network_join.xml @@ -34,4 +34,11 @@ + + diff --git a/app/src/main/res/layout/page_configure.xml b/app/src/main/res/layout/page_configure.xml index e094f7e..4f92b07 100644 --- a/app/src/main/res/layout/page_configure.xml +++ b/app/src/main/res/layout/page_configure.xml @@ -26,7 +26,7 @@ + android:textIsSelectable="true"/> @@ -39,7 +39,7 @@ + android:textIsSelectable="true"/> @@ -52,7 +52,7 @@ + android:textIsSelectable="true"/> @@ -84,6 +84,16 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c909933..dcb415b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,6 +18,10 @@ Tinc network name Name of this node Invitation URL + Current passphrase\n(leave empty if none) + New passphrase\n(leave empty to decrypt) + Passphrase (optional) + Passphrase Connect to network Request VPN permissions @@ -41,6 +45,8 @@ New network Join network Unable to start tinc + Private keys encryption + Unlock tinc private keys Close Cancel @@ -52,17 +58,22 @@ Open manual Scan QR code Install + Unlock + Apply + Encrypt or decrypt private keys No network configuration has been found. No known node Generating node configuration… Joining network… - Network configuration successfully created. + Network configuration successfully written. No network name has been provided. No configuration has been found for network \"%1$s\". Network configuration file not found at \"%1$s\". Invalid network configuration:\n\n%1$s No scanner could be started.\n\nInstall Barcode Scanner? + Encrypting/decrypting private keys + Could not decrypt private keys:\n\n%1$s none yes -- cgit v1.2.3