summaryrefslogtreecommitdiffhomepage
path: root/app/src/main/java/com
diff options
context:
space:
mode:
authorJason A. Donenfeld <Jason@zx2c4.com>2019-10-13 20:48:09 +0200
committerJason A. Donenfeld <Jason@zx2c4.com>2019-10-14 00:03:39 +0200
commit17c00af121da6bdd8ab70341811107178289c1a5 (patch)
tree60c2cdc0268f994eda4b341fba60ccc6228c5339 /app/src/main/java/com
parente060d85863f5582562633f18ee3312de222db699 (diff)
Download modules after verifying signify signature
Diffstat (limited to 'app/src/main/java/com')
-rw-r--r--app/src/main/java/com/wireguard/android/Application.java18
-rw-r--r--app/src/main/java/com/wireguard/android/activity/SettingsActivity.java13
-rw-r--r--app/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.java91
-rw-r--r--app/src/main/java/com/wireguard/android/util/ModuleLoader.java186
4 files changed, 307 insertions, 1 deletions
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;
+ }
+}