From 4466103d770c353cfb8d4ea08093560ba28d58b8 Mon Sep 17 00:00:00 2001 From: Arne Schwabe Date: Thu, 16 Dec 2021 16:51:27 +0100 Subject: Update OpenVPN and add support for the new xkey infrastructure in OpenVPN Signed-off-by: Arne Schwabe --- main/src/main/cpp/CMakeLists.txt | 7 +- main/src/main/cpp/openvpn | 2 +- main/src/main/cpp/openvpn-config/config.h | 4 +- main/src/main/cpp/ovpnutil/rsapss.cpp | 146 +++++++++++++ .../main/java/de/blinkt/openvpn/VpnProfile.java | 233 +++++++++++++-------- .../java/de/blinkt/openvpn/core/NativeUtils.java | 18 +- .../openvpn/fragments/Settings_Connections.kt | 4 +- 7 files changed, 320 insertions(+), 94 deletions(-) create mode 100644 main/src/main/cpp/ovpnutil/rsapss.cpp diff --git a/main/src/main/cpp/CMakeLists.txt b/main/src/main/cpp/CMakeLists.txt index 1a1176bd..04520267 100644 --- a/main/src/main/cpp/CMakeLists.txt +++ b/main/src/main/cpp/CMakeLists.txt @@ -104,6 +104,9 @@ target_compile_definitions(ovpnutil PRIVATE -DTARGET_ARCH_ABI=\"${ANDROID_ABI}\" ) target_link_libraries(ovpnutil log) +add_library(rsapss SHARED ovpnutil/rsapss.cpp) +target_link_libraries(rsapss log crypto ssl) + if (NOT ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} MATCHES "build/intermediates/cmake/.*skeleton.*/") add_library(osslspeedtest SHARED ovpnutil/sslspeed.c) target_link_libraries(osslspeedtest log crypto ssl) @@ -195,8 +198,8 @@ set(openvpn_srcs src/openvpn/tls_crypt.c src/openvpn/tun.c src/openvpn/vlan.c - #src/openvpn/xkey_helper.c - #src/openvpn/xkey_provider.c + src/openvpn/xkey_helper.c + src/openvpn/xkey_provider.c src/openvpn/comp-lz4.c src/openvpn/comp.c src/openvpn/compstub.c diff --git a/main/src/main/cpp/openvpn b/main/src/main/cpp/openvpn index e3831a62..cc435973 160000 --- a/main/src/main/cpp/openvpn +++ b/main/src/main/cpp/openvpn @@ -1 +1 @@ -Subproject commit e3831a62798d0324eb1e1badc1f199269a5baca7 +Subproject commit cc43597350cb6f4ce89f23d2053dc58e01174569 diff --git a/main/src/main/cpp/openvpn-config/config.h b/main/src/main/cpp/openvpn-config/config.h index 69ed58d3..7959c147 100644 --- a/main/src/main/cpp/openvpn-config/config.h +++ b/main/src/main/cpp/openvpn-config/config.h @@ -681,6 +681,4 @@ int res_init(); #define HAVE_EVP_CIPHER_CTX_RESET -#define _SOCKLEN_T_DECLARED 1 - -//#define HAVE_XKEY_PROVIDER 1 \ No newline at end of file +#define _SOCKLEN_T_DECLARED 1 \ No newline at end of file diff --git a/main/src/main/cpp/ovpnutil/rsapss.cpp b/main/src/main/cpp/ovpnutil/rsapss.cpp new file mode 100644 index 00000000..d6346811 --- /dev/null +++ b/main/src/main/cpp/ovpnutil/rsapss.cpp @@ -0,0 +1,146 @@ +/* Adapted from OpenSSL's rsa_pss.c from OpenSSL 3.0.1 */ + +/* + * Copyright 2005-2021 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the Apache License 2.0 (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ +#include "jni.h" + +#include +#include +#include + +#include + +static const unsigned char zeroes[] = { 0, 0, 0, 0, 0, 0, 0, 0 }; + +static char opensslerr[1024]; +extern "C" jbyteArray Java_de_blinkt_openvpn_core_NativeUtils_rsapss(JNIEnv *env, + jclass, + jint hashtype, + jint MSBits, + jint rsa_size, + jbyteArray from) { + + /* + unsigned char *EM, + const unsigned char *mHash, + const EVP_MD *Hash, const EVP_MD *mgf1Hash, + int sLen) +*/ + + jbyte *data = env->GetByteArrayElements(from, NULL); + int datalen = env->GetArrayLength(from); + + const auto *mHash = reinterpret_cast(data); + + const EVP_MD *Hash; + + if (hashtype == 0) { + Hash = EVP_md5(); + } else if (hashtype == 1) { + Hash = EVP_sha1(); + } else if (hashtype == 2) { + Hash = EVP_sha224(); + } else if (hashtype == 3) { + Hash = EVP_sha256(); + } else if (hashtype == 4) { + Hash = EVP_sha384(); + } else if (hashtype == 5) { + Hash = EVP_sha512(); + } + + const EVP_MD *mgf1Hash = Hash; + + int ret = 0; + int maskedDBLen, emLen; + unsigned char *H, *salt = nullptr, *p; + EVP_MD_CTX *ctx = nullptr; + + int hLen = EVP_MD_get_size(Hash); + int sLen = hLen; /* RSA_PSS_SALTLEN_DIGEST */ + + std::array buf{}; + unsigned char *EM = buf.data(); + + if (hLen < 0) + goto err; + + emLen = rsa_size; + if (MSBits == 0) { + *EM++ = 0; + emLen--; + } + if (emLen < hLen + 2) { + goto err; + } + if (sLen == RSA_PSS_SALTLEN_MAX) { + sLen = emLen - hLen - 2; + } else if (sLen > emLen - hLen - 2) { + goto err; + } + + if (sLen > 0) { + salt = (unsigned char *) OPENSSL_malloc(sLen); + if (salt == nullptr) { + goto err; + } + if (RAND_bytes_ex(nullptr, salt, sLen, 0) <= 0) + goto err; + } + maskedDBLen = emLen - hLen - 1; + H = EM + maskedDBLen; + ctx = EVP_MD_CTX_new(); + if (ctx == nullptr) + goto err; + if (!EVP_DigestInit_ex(ctx, Hash, nullptr) + || !EVP_DigestUpdate(ctx, zeroes, sizeof(zeroes)) + || !EVP_DigestUpdate(ctx, mHash, hLen)) + goto err; + if (sLen && !EVP_DigestUpdate(ctx, salt, sLen)) + goto err; + if (!EVP_DigestFinal_ex(ctx, H, nullptr)) + goto err; + + /* Generate dbMask in place then perform XOR on it */ + if (PKCS1_MGF1(EM, maskedDBLen, H, hLen, mgf1Hash)) + goto err; + + p = EM; + + /* + * Initial PS XORs with all zeroes which is a NOP so just update pointer. + * Note from a test above this value is guaranteed to be non-negative. + */ + p += emLen - sLen - hLen - 2; + *p++ ^= 0x1; + if (sLen > 0) { + for (int i = 0; i < sLen; i++) + *p++ ^= salt[i]; + } + if (MSBits) + EM[0] &= 0xFF >> (8 - MSBits); + + /* H is already in place so just set final 0xbc */ + + EM[emLen - 1] = 0xbc; + + ret = 1; + + err: + EVP_MD_CTX_free(ctx); + OPENSSL_clear_free(salt, (size_t) sLen); /* salt != NULL implies sLen > 0 */ + + + jbyteArray jb; + + jb = env->NewByteArray(emLen); + + env->SetByteArrayRegion(jb, 0, emLen, (jbyte *) EM); + + return jb; +} \ No newline at end of file diff --git a/main/src/main/java/de/blinkt/openvpn/VpnProfile.java b/main/src/main/java/de/blinkt/openvpn/VpnProfile.java index bac5939d..af85eeb1 100644 --- a/main/src/main/java/de/blinkt/openvpn/VpnProfile.java +++ b/main/src/main/java/de/blinkt/openvpn/VpnProfile.java @@ -41,6 +41,8 @@ import java.security.*; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; import java.security.spec.MGF1ParameterSpec; import java.security.spec.PSSParameterSpec; import java.util.Collection; @@ -89,6 +91,9 @@ public class VpnProfile implements Serializable, Cloneable { private static final int AUTH_RETRY_NONE_KEEP = 1; private static final int AUTH_RETRY_INTERACT = 3; private static final String EXTRA_RSA_PADDING_TYPE = "de.blinkt.openvpn.api.RSA_PADDING_TYPE"; + private static final String EXTRA_SALTLEN = "de.blinkt.openvpn.api.SALTLEN"; + private static final String EXTRA_NEEDS_DIGEST = "de.blinkt.openvpn.api.NEEDS_DIGEST"; + private static final String EXTRA_DIGEST = "de.blinkt.openvpn.api.DIGEST"; public static String DEFAULT_DNS1 = "9.9.9.9"; public static String DEFAULT_DNS2 = "2620:fe::fe"; // variable named wrong and should haven beeen transient @@ -302,7 +307,7 @@ public class VpnProfile implements Serializable, Cloneable { public void upgradeProfile() { /* Fallthrough is intended here */ - switch(mProfileVersion) { + switch (mProfileVersion) { case 0: case 1: /* default to the behaviour the OS used */ @@ -335,8 +340,7 @@ public class VpnProfile implements Serializable, Cloneable { } case 9: if (!TextUtils.isEmpty(mDataCiphers) && - mDataCiphers.toUpperCase(Locale.ROOT).contains("BF-CBC")) - { + mDataCiphers.toUpperCase(Locale.ROOT).contains("BF-CBC")) { mUseLegacyProvider = true; } default: @@ -507,8 +511,7 @@ public class VpnProfile implements Serializable, Cloneable { if (!TextUtils.isEmpty(ks[1])) cfg.append("\n").append(ks[1]).append("\n\n"); cfg.append("\n").append(ks[2]).append("\n\n"); - cfg.append("management-external-key nopadding pkcs1\n"); - // for xkey branch:" management-external-key pss digest + cfg.append("management-external-key nopadding pkcs1 pss digest\n"); } else { cfg.append(context.getString(R.string.keychain_access)).append("\n"); if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) @@ -527,8 +530,7 @@ public class VpnProfile implements Serializable, Cloneable { } } - if (mCheckPeerFingerprint) - { + if (mCheckPeerFingerprint) { cfg.append("\n").append(mPeerFingerPrints).append("\n\n"); } @@ -672,10 +674,9 @@ public class VpnProfile implements Serializable, Cloneable { cfg.append("data-ciphers ").append(mDataCiphers).append("\n"); } - if (mCompatMode > 0) - { - int major = mCompatMode/10000; - int minor = mCompatMode % 10000/100; + if (mCompatMode > 0) { + int major = mCompatMode / 10000; + int minor = mCompatMode % 10000 / 100; int patch = mCompatMode % 100; cfg.append(String.format(Locale.US, "compat-mode %d.%d.%d\n", major, minor, patch)); @@ -1067,9 +1068,8 @@ public class VpnProfile implements Serializable, Cloneable { if (!mUseLegacyProvider && (dataciphers.contains("BF-CBC") - || (mCompatMode > 0 && mCompatMode < 20500) - && cipher.equals("BF-CBC"))) - { + || (mCompatMode > 0 && mCompatMode < 20500) + && cipher.equals("BF-CBC"))) { return R.string.bf_cbc_requires_legacy; } @@ -1198,12 +1198,7 @@ public class VpnProfile implements Serializable, Cloneable { byte[] data = Base64.decode(b64data, Base64.DEFAULT); byte[] signed_bytes; if (mAuthenticationType == TYPE_EXTERNAL_APP) { - /* TODO: FIXME */ - RsaPaddingType paddingType = (padding == OpenVPNManagement.SignaturePadding.RSA_PKCS1_PADDING) ? RsaPaddingType.PKCS1_PADDING : RsaPaddingType.NO_PADDING; - Bundle extra = new Bundle(); - extra.putInt(EXTRA_RSA_PADDING_TYPE, paddingType.ordinal()); - - signed_bytes = getExtAppSignedData(c, data, extra); + signed_bytes = getExtAppSignedData(c, data, padding, saltlen, hashalg, needDigest); } else { signed_bytes = getKeyChainSignedData(data, padding, saltlen, hashalg, needDigest); } @@ -1214,7 +1209,30 @@ public class VpnProfile implements Serializable, Cloneable { return null; } - private byte[] getExtAppSignedData(Context c, byte[] data, Bundle extra) { + private byte[] getExtAppSignedData(Context c, byte[] data, OpenVPNManagement.SignaturePadding padding, String saltlen, String hashalg, boolean needDigest) + { + + Bundle extra = new Bundle(); + RsaPaddingType paddingType; + switch (padding) { + case RSA_PKCS1_PADDING: + paddingType = RsaPaddingType.PKCS1_PADDING; + break; + case NO_PADDING: + paddingType = RsaPaddingType.NO_PADDING; + break; + case RSA_PKCS1_PSS_PADDING: + paddingType = RsaPaddingType.RSAPSS_PADDING; + break; + default: + paddingType = RsaPaddingType.NO_PADDING; + } + + extra.putInt(EXTRA_RSA_PADDING_TYPE, paddingType.ordinal()); + extra.putString(EXTRA_SALTLEN, saltlen); + extra.putString(EXTRA_DIGEST, hashalg); + extra.putBoolean(EXTRA_NEEDS_DIGEST, needDigest); + if (TextUtils.isEmpty(mExternalAuthenticator)) return null; try { @@ -1227,12 +1245,6 @@ public class VpnProfile implements Serializable, Cloneable { private byte[] getKeyChainSignedData(byte[] data, OpenVPNManagement.SignaturePadding padding, String saltlen, String hashalg, boolean needDigest) { PrivateKey privkey = getKeystoreKey(); - // The Jelly Bean *evil* Hack - // 4.2 implements the RSA/ECB/PKCS1PADDING in the OpenSSLprovider - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) { - /* TODO: really fix this?! */ - return processSignJellyBeans(privkey, data, padding); - } try { @@ -1243,17 +1255,25 @@ public class VpnProfile implements Serializable, Cloneable { if (needDigest || keyalgorithm.equals("EC")) { return doDigestSign(privkey, data, padding, hashalg, saltlen); } else { - /* ECB is perfectly fine in this special case, since we are using it for - the public/private part in the TLS exchange - */ - Cipher signer; - if (padding == OpenVPNManagement.SignaturePadding.RSA_PKCS1_PADDING) - signer = Cipher.getInstance("RSA/ECB/PKCS1PADDING"); - else if (padding == OpenVPNManagement.SignaturePadding.NO_PADDING) - signer = Cipher.getInstance("RSA/ECB/NoPadding"); + // The Jelly Bean *evil* Hack + // 4.2 implements the RSA/ECB/PKCS1PADDING in the OpenSSLprovider + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) { + return processSignJellyBeans(privkey, data, padding); + } - else - throw new NoSuchPaddingException("Unknown padding used for signature"); + /* ECB is perfectly fine in this special case, since we are using it for + the public/private part in the TLS exchange */ + Cipher signer = null; + switch (padding) { + case RSA_PKCS1_PADDING: + signer = Cipher.getInstance("RSA/ECB/PKCS1PADDING"); + break; + case NO_PADDING: + signer = Cipher.getInstance("RSA/ECB/NoPadding"); + break; + case RSA_PKCS1_PSS_PADDING: + throw new NoSuchPaddingException("Cannot do PKCS1 PSS padding without also doing the digest"); + } signer.init(Cipher.ENCRYPT_MODE, privkey); @@ -1261,13 +1281,52 @@ public class VpnProfile implements Serializable, Cloneable { return signed_bytes; } - } catch - (NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException | SignatureException | InvalidAlgorithmParameterException - e){ - VpnStatus.logError(R.string.error_rsa_sign, e.getClass().toString(), e.getLocalizedMessage()); - return null; - } + } catch + (NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException | SignatureException | InvalidAlgorithmParameterException + e) { + VpnStatus.logError(R.string.error_rsa_sign, e.getClass().toString(), e.getLocalizedMessage()); + return null; } + } + + private byte[] addPSSPadding(PrivateKey privkey, String digest, byte[] data) throws NoSuchAlgorithmException { + /* For < API 23, add padding ourselves */ + int hashtype = getHashtype(digest); + + MessageDigest msgDigest = MessageDigest.getInstance(digest); + byte[] hash = msgDigest.digest(data); + + /* MSBits = (BN_num_bits(rsa->n) - 1) & 0x7; */ + int numbits = ((RSAPrivateKey) privkey).getModulus().bitLength(); + + int MSBits = (numbits - 1) & 0x7; + + return NativeUtils.addRssPssPadding(hashtype, MSBits, numbits/8, hash); + } + + private int getHashtype(String digest) throws NoSuchAlgorithmException { + int hashtype = 0; + switch (digest) { + case "SHA1": + hashtype = 1; + break; + case "SHA224": + hashtype = 2; + break; + case "SHA256": + hashtype = 3; + break; + case "SHA384": + hashtype = 4; + break; + case "SHA512": + hashtype = 5; + break; + default: + throw new NoSuchAlgorithmException("Unknown digest algorithm: " + digest); + } + return hashtype; + } private byte[] doDigestSign(PrivateKey privkey, byte[] data, OpenVPNManagement.SignaturePadding padding, String hashalg, String saltlen) throws SignatureException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException { /* RSA */ @@ -1284,6 +1343,11 @@ public class VpnProfile implements Serializable, Cloneable { if (!"digest".equals(saltlen)) throw new SignatureException("PSS signing requires saltlen=digest"); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + data = addPSSPadding(privkey, hashalg, data); + return getKeyChainSignedData(data, OpenVPNManagement.SignaturePadding.NO_PADDING, "none", "none", false); + } + sig = Signature.getInstance(hashalg + "withRSA/PSS"); PSSParameterSpec pssspec = null; @@ -1308,62 +1372,63 @@ public class VpnProfile implements Serializable, Cloneable { return sig.sign(); } - private byte[] processSignJellyBeans (PrivateKey privkey, byte[] data, OpenVPNManagement.SignaturePadding padding){ - try { - boolean pkcs1padding=false; - if (padding == OpenVPNManagement.SignaturePadding.RSA_PKCS1_PADDING) - pkcs1padding = true; - else if (padding != OpenVPNManagement.SignaturePadding.NO_PADDING) - throw new IllegalAccessException("Unsuppoirted padding for jelly bean native signing"); + private byte[] processSignJellyBeans(PrivateKey privkey, byte[] data, OpenVPNManagement.SignaturePadding padding) { + try { + boolean pkcs1padding = false; + if (padding == OpenVPNManagement.SignaturePadding.RSA_PKCS1_PADDING) + pkcs1padding = true; + else if (padding != OpenVPNManagement.SignaturePadding.NO_PADDING) + throw new IllegalAccessException("Unsuppoirted padding for jelly bean native signing"); - Method getKey = privkey.getClass().getSuperclass().getDeclaredMethod("getOpenSSLKey"); - getKey.setAccessible(true); + Method getKey = privkey.getClass().getSuperclass().getDeclaredMethod("getOpenSSLKey"); + getKey.setAccessible(true); - // Real object type is OpenSSLKey - Object opensslkey = getKey.invoke(privkey); + // Real object type is OpenSSLKey + Object opensslkey = getKey.invoke(privkey); - getKey.setAccessible(false); + getKey.setAccessible(false); - Method getPkeyContext = opensslkey.getClass().getDeclaredMethod("getPkeyContext"); + Method getPkeyContext = opensslkey.getClass().getDeclaredMethod("getPkeyContext"); - // integer pointer to EVP_pkey - getPkeyContext.setAccessible(true); - int pkey = (Integer) getPkeyContext.invoke(opensslkey); - getPkeyContext.setAccessible(false); + // integer pointer to EVP_pkey + getPkeyContext.setAccessible(true); + int pkey = (Integer) getPkeyContext.invoke(opensslkey); + getPkeyContext.setAccessible(false); - // 112 with TLS 1.2 (172 back with 4.3), 36 with TLS 1.0 - return NativeUtils.rsasign(data, pkey, pkcs1padding); + // 112 with TLS 1.2 (172 back with 4.3), 36 with TLS 1.0 + return NativeUtils.rsasign(data, pkey, pkcs1padding); - } catch (NoSuchMethodException | InvalidKeyException | InvocationTargetException | IllegalAccessException | IllegalArgumentException e) { - VpnStatus.logError(R.string.error_rsa_sign, e.getClass().toString(), e.getLocalizedMessage()); - return null; - } + } catch (NoSuchMethodException | InvalidKeyException | InvocationTargetException | IllegalAccessException | IllegalArgumentException e) { + VpnStatus.logError(R.string.error_rsa_sign, e.getClass().toString(), e.getLocalizedMessage()); + return null; } + } - private boolean usesExtraProxyOptions () { - if (mUseCustomConfig && mCustomConfigOptions != null && mCustomConfigOptions.contains("http-proxy-option ")) + private boolean usesExtraProxyOptions() { + if (mUseCustomConfig && mCustomConfigOptions != null && mCustomConfigOptions.contains("http-proxy-option ")) + return true; + for (Connection c : mConnections) + if (c.usesExtraProxyOptions()) return true; - for (Connection c : mConnections) - if (c.usesExtraProxyOptions()) - return true; - return false; - } + return false; + } - /** - * The order of elements is important! - */ - private enum RsaPaddingType { - NO_PADDING, - PKCS1_PADDING - } + /** + * The order of elements is important! + */ + private enum RsaPaddingType { + NO_PADDING, + PKCS1_PADDING, + RSAPSS_PADDING + } - static class NoCertReturnedException extends Exception { - public NoCertReturnedException(String msg) { - super(msg); - } + static class NoCertReturnedException extends Exception { + public NoCertReturnedException(String msg) { + super(msg); } } +} diff --git a/main/src/main/java/de/blinkt/openvpn/core/NativeUtils.java b/main/src/main/java/de/blinkt/openvpn/core/NativeUtils.java index ecfa86ff..470ec6d6 100644 --- a/main/src/main/java/de/blinkt/openvpn/core/NativeUtils.java +++ b/main/src/main/java/de/blinkt/openvpn/core/NativeUtils.java @@ -17,8 +17,7 @@ public class NativeUtils { static native void jniclose(int fdint); - public static String getNativeAPI() - { + public static String getNativeAPI() { if (isRoboUnitTest()) return "ROBO"; else @@ -31,6 +30,20 @@ public class NativeUtils { public static native String getOpenVPN3GitVersion(); + static boolean rsspssloaded = false; + + public static byte[] addRssPssPadding(int hashtype, int MSBits, int rsa_size, byte[] from) + { + if (!rsspssloaded) { + rsspssloaded = true; + System.loadLibrary("rsapss"); + } + + return rsapss(hashtype, MSBits, rsa_size, from); + } + + private static native byte[] rsapss(int hashtype, int MSBits, int rsa_size, byte[] from); + public final static int[] openSSLlengths = { 16, 64, 256, 1024, 1500, 8 * 1024, 16 * 1024 }; @@ -43,6 +56,7 @@ public class NativeUtils { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) System.loadLibrary("jbcrypto"); + if (!BuildConfig.FLAVOR.equals("skeleton")) { System.loadLibrary("osslspeedtest"); } diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Connections.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Connections.kt index 75b87157..8ed9d9ef 100644 --- a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Connections.kt +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Connections.kt @@ -50,8 +50,8 @@ class Settings_Connections : Settings_Fragment(), View.OnClickListener { mRecyclerView.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false) mRecyclerView.adapter = mConnectionsAdapter - val fab_button = v.findViewById(R.id.add_new_remote) as ImageButton - fab_button.setOnClickListener(this) + val fab_button = v.findViewById(R.id.add_new_remote) as ImageButton? + fab_button?.setOnClickListener(this) mUseRandomRemote = v.findViewById(R.id.remote_random) as Checkable mUseRandomRemote.isChecked = mProfile.mRemoteRandom mConnectionsAdapter.displayWarningIfNoneEnabled() -- cgit v1.2.3