Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>master
@@ -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) { | |||
@@ -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) { | |||
@@ -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); | |||
}); | |||
} | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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> | |||
@@ -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 /> | |||