diff options
7 files changed, 316 insertions, 1 deletions
diff --git a/app/build.gradle b/app/build.gradle index 995229d5..794a01d6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -80,6 +80,7 @@ ext { // If you choose to upgrade to minSDK 24 then you should also disable Jetifier from // gradle.properties. zxingEmbeddedVersion = '3.6.0' + eddsaVersion = '0.3.0' } dependencies { @@ -94,6 +95,7 @@ dependencies { implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion" implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion" implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion" + implementation "net.i2p.crypto:eddsa:$eddsaVersion" } tasks.withType(JavaCompile) { diff --git a/app/src/main/java/com/wireguard/android/Application.java b/app/src/main/java/com/wireguard/android/Application.java index 1ffaa250..744986e8 100644 --- a/app/src/main/java/com/wireguard/android/Application.java +++ b/app/src/main/java/com/wireguard/android/Application.java @@ -22,6 +22,7 @@ import com.wireguard.android.backend.WgQuickBackend; import com.wireguard.android.configStore.FileConfigStore; import com.wireguard.android.model.TunnelManager; import com.wireguard.android.util.AsyncWorker; +import com.wireguard.android.util.ModuleLoader; import com.wireguard.android.util.RootShell; import com.wireguard.android.util.ToolsInstaller; @@ -38,6 +39,7 @@ public class Application extends android.app.Application { @SuppressWarnings("NullableProblems") private RootShell rootShell; @SuppressWarnings("NullableProblems") private SharedPreferences sharedPreferences; @SuppressWarnings("NullableProblems") private ToolsInstaller toolsInstaller; + @SuppressWarnings("NullableProblems") private ModuleLoader moduleLoader; @SuppressWarnings("NullableProblems") private TunnelManager tunnelManager; public Application() { @@ -57,9 +59,19 @@ public class Application extends android.app.Application { synchronized (app.futureBackend) { if (app.backend == null) { Backend backend = null; - if (new File("/sys/module/wireguard").exists()) { + boolean didStartRootShell = false; + if (!app.moduleLoader.isModuleLoaded() && app.moduleLoader.moduleMightExist()) { try { app.rootShell.start(); + didStartRootShell = true; + app.moduleLoader.loadModule(); + } catch (final Exception ignored) { + } + } + if (app.moduleLoader.isModuleLoaded()) { + try { + if (!didStartRootShell) + app.rootShell.start(); backend = new WgQuickBackend(app.getApplicationContext()); } catch (final Exception ignored) { } @@ -87,6 +99,9 @@ public class Application extends android.app.Application { public static ToolsInstaller getToolsInstaller() { return get().toolsInstaller; } + public static ModuleLoader getModuleLoader() { + return get().moduleLoader; + } public static TunnelManager getTunnelManager() { return get().tunnelManager; @@ -113,6 +128,7 @@ public class Application extends android.app.Application { asyncWorker = new AsyncWorker(AsyncTask.SERIAL_EXECUTOR, new Handler(Looper.getMainLooper())); rootShell = new RootShell(getApplicationContext()); toolsInstaller = new ToolsInstaller(getApplicationContext()); + moduleLoader = new ModuleLoader(getApplicationContext()); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { diff --git a/app/src/main/java/com/wireguard/android/activity/SettingsActivity.java b/app/src/main/java/com/wireguard/android/activity/SettingsActivity.java index 89ba0c12..442c93e6 100644 --- a/app/src/main/java/com/wireguard/android/activity/SettingsActivity.java +++ b/app/src/main/java/com/wireguard/android/activity/SettingsActivity.java @@ -110,6 +110,19 @@ public class SettingsActivity extends ThemeChangeAwareActivity { screen.removePreference(pref); } }); + + final Preference moduleInstaller = getPreferenceManager().findPreference("module_downloader"); + moduleInstaller.setVisible(false); + if (Application.getModuleLoader().isModuleLoaded()) { + screen.removePreference(moduleInstaller); + } else { + Application.getAsyncWorker().runAsync(Application.getRootShell()::start).whenComplete((v, e) -> { + if (e == null) + moduleInstaller.setVisible(true); + else + screen.removePreference(moduleInstaller); + }); + } } } } diff --git a/app/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.java b/app/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.java new file mode 100644 index 00000000..a04bed76 --- /dev/null +++ b/app/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.java @@ -0,0 +1,91 @@ +/* + * Copyright © 2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.preference; + +import android.content.Context; +import android.content.Intent; +import android.system.OsConstants; +import android.util.AttributeSet; +import android.widget.Toast; + +import com.wireguard.android.Application; +import com.wireguard.android.R; +import com.wireguard.android.util.ModuleLoader; +import com.wireguard.android.util.ToolsInstaller; + +import androidx.annotation.Nullable; +import androidx.preference.Preference; + +public class ModuleDownloaderPreference extends Preference { + private State state = State.INITIAL; + + public ModuleDownloaderPreference(final Context context, final AttributeSet attrs) { + super(context, attrs); + } + + @Override + public CharSequence getSummary() { + return getContext().getString(state.messageResourceId); + } + + @Override + public CharSequence getTitle() { + return getContext().getString(R.string.module_installer_title); + } + + @Override + protected void onClick() { + setState(State.WORKING); + Application.getAsyncWorker().supplyAsync(Application.getModuleLoader()::download).whenComplete(this::onDownloadResult); + } + + private void onDownloadResult(final Integer result, @Nullable final Throwable throwable) { + if (throwable != null) { + setState(State.FAILURE); + Toast.makeText(getContext(), throwable.getMessage(), Toast.LENGTH_LONG).show(); + } else if (result == OsConstants.ENOENT) + setState(State.NOTFOUND); + else if (result == OsConstants.EXIT_SUCCESS) { + setState(State.SUCCESS); + Application.getAsyncWorker().runAsync(() -> { + Thread.sleep(1000 * 5); + Intent i = getContext().getPackageManager().getLaunchIntentForPackage(getContext().getPackageName()); + if (i == null) + return; + i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Application.get().startActivity(i); + System.exit(0); + }); + } else + setState(State.FAILURE); + } + + private void setState(final State state) { + if (this.state == state) + return; + this.state = state; + if (isEnabled() != state.shouldEnableView) + setEnabled(state.shouldEnableView); + notifyChanged(); + } + + private enum State { + INITIAL(R.string.module_installer_initial, true), + FAILURE(R.string.module_installer_error, true), + WORKING(R.string.module_installer_working, false), + SUCCESS(R.string.module_installer_success, false), + NOTFOUND(R.string.module_installer_not_found, false); + + private final int messageResourceId; + private final boolean shouldEnableView; + + State(final int messageResourceId, final boolean shouldEnableView) { + this.messageResourceId = messageResourceId; + this.shouldEnableView = shouldEnableView; + } + } +} diff --git a/app/src/main/java/com/wireguard/android/util/ModuleLoader.java b/app/src/main/java/com/wireguard/android/util/ModuleLoader.java new file mode 100644 index 00000000..21ff9c77 --- /dev/null +++ b/app/src/main/java/com/wireguard/android/util/ModuleLoader.java @@ -0,0 +1,186 @@ +/* + * Copyright © 2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.util; + +import android.content.Context; +import android.system.OsConstants; +import android.util.Base64; + +import com.wireguard.android.Application; +import com.wireguard.android.BuildConfig; +import com.wireguard.android.util.RootShell.NoRootException; + +import net.i2p.crypto.eddsa.EdDSAEngine; +import net.i2p.crypto.eddsa.EdDSAPublicKey; +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; +import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec; +import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.InvalidParameterException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +public class ModuleLoader { + private static final String MODULE_PUBLIC_KEY_BASE64 = "RWRmHuT9PSqtwfsLtEx+QS06BJtLgFYteL9WCNjH7yuyu5Y1DieSN7If"; + private static final String MODULE_LIST_URL = "https://download.wireguard.com/android-module/modules.txt.sig"; + private static final String MODULE_URL = "https://download.wireguard.com/android-module/%s"; + private static final String MODULE_NAME = "wireguard-%s.ko"; + + private final File moduleDir; + private final File tmpDir; + + public ModuleLoader(final Context context) { + moduleDir = new File(context.getCacheDir(), "kmod"); + tmpDir = new File(context.getCacheDir(), "tmp"); + } + + public boolean moduleMightExist() { + return moduleDir.exists() && moduleDir.isDirectory(); + } + + public void loadModule() throws IOException, NoRootException { + Application.getRootShell().run(null, String.format("insmod \"%s/wireguard-$(sha256sum /proc/version|cut -d ' ' -f 1).ko\"", moduleDir.getAbsolutePath())); + } + + public boolean isModuleLoaded() { + return new File("/sys/module/wireguard").exists(); + } + + private static final class Sha256Digest { + private byte[] bytes; + private Sha256Digest(final String hex) { + if (hex.length() != 64) + throw new InvalidParameterException("SHA256 hashes must be 32 bytes long"); + bytes = new byte[32]; + for (int i = 0; i < 32; ++i) + bytes[i] = (byte)Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16); + } + } + + @Nullable + private Map<String, Sha256Digest> verifySignedHashes(final String signifyDigest) { + final byte[] publicKeyBytes = Base64.decode(MODULE_PUBLIC_KEY_BASE64, Base64.DEFAULT); + + if (publicKeyBytes == null || publicKeyBytes.length != 32 + 10 || publicKeyBytes[0] != 'E' || publicKeyBytes[1] != 'd') + return null; + + final String[] lines = signifyDigest.split("\n", 3); + if (lines.length != 3) + return null; + if (!lines[0].startsWith("untrusted comment: ")) + return null; + + final byte[] signatureBytes = Base64.decode(lines[1], Base64.DEFAULT); + if (signatureBytes == null || signatureBytes.length != 64 + 10) + return null; + for (int i = 0; i < 10; ++i) { + if (signatureBytes[i] != publicKeyBytes[i]) + return null; + } + + try { + EdDSAParameterSpec parameterSpec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519); + Signature signature = new EdDSAEngine(MessageDigest.getInstance(parameterSpec.getHashAlgorithm())); + byte[] rawPublicKeyBytes = new byte[32]; + System.arraycopy(publicKeyBytes, 10, rawPublicKeyBytes, 0, 32); + signature.initVerify(new EdDSAPublicKey(new EdDSAPublicKeySpec(rawPublicKeyBytes, parameterSpec))); + signature.update(lines[2].getBytes(StandardCharsets.UTF_8)); + if (!signature.verify(signatureBytes, 10, 64)) + return null; + } catch (final Exception ignored) { + return null; + } + + Map<String, Sha256Digest> hashes = new HashMap<>(); + for (final String line : lines[2].split("\n")) { + final String[] components = line.split(" ", 2); + if (components.length != 2) + return null; + try { + hashes.put(components[1], new Sha256Digest(components[0])); + } catch (final Exception ignored) { + return null; + } + } + return hashes; + } + + public Integer download() throws IOException, NoRootException, NoSuchAlgorithmException { + final List<String> output = new ArrayList<>(); + Application.getRootShell().run(output, "sha256sum /proc/version|cut -d ' ' -f 1"); + if (output.size() != 1 || output.get(0).length() != 64) + throw new InvalidParameterException("Invalid sha256 of /proc/version"); + final String moduleName = String.format(MODULE_NAME, output.get(0)); + final String userAgent = String.format("WireGuard/%s (Android)", BuildConfig.VERSION_NAME); //TODO: expand a bit + + HttpURLConnection connection = (HttpURLConnection)new URL(MODULE_LIST_URL).openConnection(); + connection.setRequestProperty("User-Agent", userAgent); + connection.connect(); + if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) + throw new IOException("Hash list could not be found"); + byte[] input = new byte[1024 * 1024 * 3 /* 3MiB */]; + int len; + try (final InputStream inputStream = connection.getInputStream()) { + len = inputStream.read(input); + } + if (len <= 0) + throw new IOException("Hash list was empty"); + final Map<String, Sha256Digest> modules = verifySignedHashes(new String(input, 0, len, StandardCharsets.UTF_8)); + if (modules == null) + throw new InvalidParameterException("The signature did not verify or invalid hash list format"); + if (!modules.containsKey(moduleName)) + return OsConstants.ENOENT; + connection = (HttpURLConnection)new URL(String.format(MODULE_URL, moduleName)).openConnection(); + connection.setRequestProperty("User-Agent", userAgent); + connection.connect(); + if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) + throw new IOException("Module file could not be found, despite being on hash list"); + + tmpDir.mkdirs(); + moduleDir.mkdir(); + File tempFile = null; + try { + tempFile = File.createTempFile("UNVERIFIED-", null, tmpDir); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + try (final InputStream inputStream = connection.getInputStream(); + final OutputStream outputStream = new FileOutputStream(tempFile)) { + int total = 0; + while ((len = inputStream.read(input)) > 0) { + total += len; + if (total > 1024 * 1024 * 15 /* 15 MiB */) + throw new IOException("File too big"); + outputStream.write(input, 0, len); + digest.update(input, 0, len); + } + } + if (!Arrays.equals(digest.digest(), modules.get(moduleName).bytes)) + throw new IOException("Incorrect file hash"); + + if (!tempFile.renameTo(new File(moduleDir, moduleName))) + throw new IOException("Unable to rename to final destination"); + } finally { + if (tempFile != null) + tempFile.delete(); + } + return OsConstants.EXIT_SUCCESS; + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7b5dcf70..6cc50f2d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -99,6 +99,12 @@ <string name="log_export_title">Export log file</string> <string name="logcat_error">Unable to run logcat: </string> <string name="module_version_error">Unable to determine kernel module version</string> + <string name="module_installer_not_found">No modules are available for your device</string> + <string name="module_installer_initial">The experimental kernel module can improve performance</string> + <string name="module_installer_success">Success. The application will restart in 5 seconds</string> + <string name="module_installer_title">Download and install kernel module</string> + <string name="module_installer_working">Downloading and installing…</string> + <string name="module_installer_error">Something went wrong. Please try again</string> <string name="mtu">MTU</string> <string name="multiple_tunnels_error">Only one userspace tunnel can run at a time</string> <string name="name">Name</string> diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index ce96a4e5..e27b607d 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -6,6 +6,7 @@ android:key="restore_on_boot" android:summary="@string/restore_on_boot_summary" android:title="@string/restore_on_boot_title" /> + <com.wireguard.android.preference.ModuleDownloaderPreference android:key="module_downloader" /> <com.wireguard.android.preference.ToolsInstallerPreference android:key="tools_installer" /> <com.wireguard.android.preference.ZipExporterPreference /> <com.wireguard.android.preference.LogExporterPreference /> |