Native android service implemented
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.annotation.RestrictTo.Scope;
|
||||
|
||||
public final class SharedLibraryLoader {
|
||||
private static final String TAG = "WireGuard/SharedLibraryLoader";
|
||||
|
||||
private SharedLibraryLoader() {}
|
||||
|
||||
public static boolean extractLibrary(
|
||||
final Context context, final String libName, final File destination) throws IOException {
|
||||
final Collection<String> apks = new HashSet<>();
|
||||
Log.d(TAG, "Loading Lib ->" + libName);
|
||||
if (context.getApplicationInfo().sourceDir != null)
|
||||
apks.add(context.getApplicationInfo().sourceDir);
|
||||
if (context.getApplicationInfo().splitSourceDirs != null)
|
||||
apks.addAll(Arrays.asList(context.getApplicationInfo().splitSourceDirs));
|
||||
|
||||
for (final String abi : Build.SUPPORTED_ABIS) {
|
||||
for (final String apk : apks) {
|
||||
try (final ZipFile zipFile = new ZipFile(new File(apk), ZipFile.OPEN_READ)) {
|
||||
final String mappedLibName = System.mapLibraryName(libName);
|
||||
final String libZipPath =
|
||||
"lib" + File.separatorChar + abi + File.separatorChar + mappedLibName;
|
||||
final ZipEntry zipEntry = zipFile.getEntry(libZipPath);
|
||||
if (zipEntry == null)
|
||||
continue;
|
||||
Log.d(TAG, "Extracting apk:/" + libZipPath + " to " + destination.getAbsolutePath());
|
||||
try (final FileOutputStream out = new FileOutputStream(destination);
|
||||
final InputStream in = zipFile.getInputStream(zipEntry)) {
|
||||
int len;
|
||||
final byte[] buffer = new byte[1024 * 32];
|
||||
while ((len = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, len);
|
||||
}
|
||||
out.getFD().sync();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void loadSharedLibrary(final Context context, final String libName) {
|
||||
Throwable noAbiException;
|
||||
try {
|
||||
System.loadLibrary(libName);
|
||||
return;
|
||||
} catch (final UnsatisfiedLinkError e) {
|
||||
Log.d(TAG, "Failed to load library normally, so attempting to extract from apk", e);
|
||||
noAbiException = e;
|
||||
}
|
||||
File f = null;
|
||||
try {
|
||||
f = File.createTempFile("lib", ".so", context.getCodeCacheDir());
|
||||
if (extractLibrary(context, libName, f)) {
|
||||
System.load(f.getAbsolutePath());
|
||||
return;
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
Log.d(TAG, "Failed to load library apk:/" + libName, e);
|
||||
noAbiException = e;
|
||||
} finally {
|
||||
if (f != null)
|
||||
// noinspection ResultOfMethodCallIgnored
|
||||
f.delete();
|
||||
}
|
||||
if (noAbiException instanceof RuntimeException)
|
||||
throw(RuntimeException) noAbiException;
|
||||
throw new RuntimeException(noAbiException);
|
||||
}
|
||||
}
|
||||
57
client/android/src/com/wireguard/config/Attribute.java
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class Attribute {
|
||||
private static final Pattern LINE_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*([^\\s#][^#]*)");
|
||||
private static final Pattern LIST_SEPARATOR = Pattern.compile("\\s*,\\s*");
|
||||
|
||||
private final String key;
|
||||
private final String value;
|
||||
|
||||
private Attribute(final String key, final String value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static String join(final Iterable<?> values) {
|
||||
final Iterator<?> it = values.iterator();
|
||||
if (!it.hasNext()) {
|
||||
return "";
|
||||
}
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append(it.next());
|
||||
while (it.hasNext()) {
|
||||
sb.append(", ");
|
||||
sb.append(it.next());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static Optional<Attribute> parse(final CharSequence line) {
|
||||
final Matcher matcher = LINE_PATTERN.matcher(line);
|
||||
if (!matcher.matches())
|
||||
return Optional.empty();
|
||||
return Optional.of(new Attribute(matcher.group(1), matcher.group(2)));
|
||||
}
|
||||
|
||||
public static String[] split(final CharSequence value) {
|
||||
return LIST_SEPARATOR.split(value);
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
116
client/android/src/com/wireguard/config/BadConfigException.java
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import com.wireguard.crypto.KeyFormatException;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class BadConfigException extends Exception {
|
||||
private final Location location;
|
||||
private final Reason reason;
|
||||
private final Section section;
|
||||
@Nullable private final CharSequence text;
|
||||
|
||||
private BadConfigException(final Section section, final Location location, final Reason reason,
|
||||
@Nullable final CharSequence text, @Nullable final Throwable cause) {
|
||||
super(cause);
|
||||
this.section = section;
|
||||
this.location = location;
|
||||
this.reason = reason;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public BadConfigException(final Section section, final Location location, final Reason reason,
|
||||
@Nullable final CharSequence text) {
|
||||
this(section, location, reason, text, null);
|
||||
}
|
||||
|
||||
public BadConfigException(
|
||||
final Section section, final Location location, final KeyFormatException cause) {
|
||||
this(section, location, Reason.INVALID_KEY, null, cause);
|
||||
}
|
||||
|
||||
public BadConfigException(final Section section, final Location location,
|
||||
@Nullable final CharSequence text, final NumberFormatException cause) {
|
||||
this(section, location, Reason.INVALID_NUMBER, text, cause);
|
||||
}
|
||||
|
||||
public BadConfigException(
|
||||
final Section section, final Location location, final ParseException cause) {
|
||||
this(section, location, Reason.INVALID_VALUE, cause.getText(), cause);
|
||||
}
|
||||
|
||||
public Location getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public Reason getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public Section getSection() {
|
||||
return section;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public CharSequence getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public enum Location {
|
||||
TOP_LEVEL(""),
|
||||
ADDRESS("Address"),
|
||||
ALLOWED_IPS("AllowedIPs"),
|
||||
DNS("DNS"),
|
||||
ENDPOINT("Endpoint"),
|
||||
EXCLUDED_APPLICATIONS("ExcludedApplications"),
|
||||
INCLUDED_APPLICATIONS("IncludedApplications"),
|
||||
LISTEN_PORT("ListenPort"),
|
||||
MTU("MTU"),
|
||||
PERSISTENT_KEEPALIVE("PersistentKeepalive"),
|
||||
PRE_SHARED_KEY("PresharedKey"),
|
||||
PRIVATE_KEY("PrivateKey"),
|
||||
PUBLIC_KEY("PublicKey");
|
||||
|
||||
private final String name;
|
||||
|
||||
Location(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Reason {
|
||||
INVALID_KEY,
|
||||
INVALID_NUMBER,
|
||||
INVALID_VALUE,
|
||||
MISSING_ATTRIBUTE,
|
||||
MISSING_SECTION,
|
||||
SYNTAX_ERROR,
|
||||
UNKNOWN_ATTRIBUTE,
|
||||
UNKNOWN_SECTION
|
||||
}
|
||||
|
||||
public enum Section {
|
||||
CONFIG("Config"),
|
||||
INTERFACE("Interface"),
|
||||
PEER("Peer");
|
||||
|
||||
private final String name;
|
||||
|
||||
Section(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
218
client/android/src/com/wireguard/config/Config.java
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import com.wireguard.config.BadConfigException.Location;
|
||||
import com.wireguard.config.BadConfigException.Reason;
|
||||
import com.wireguard.config.BadConfigException.Section;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Represents the contents of a wg-quick configuration file, made up of one or more "Interface"
|
||||
* sections (combined together), and zero or more "Peer" sections (treated individually).
|
||||
* <p>
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
|
||||
public final class Config {
|
||||
private final Interface interfaze;
|
||||
private final List<Peer> peers;
|
||||
|
||||
private Config(final Builder builder) {
|
||||
interfaze = Objects.requireNonNull(builder.interfaze, "An [Interface] section is required");
|
||||
// Defensively copy to ensure immutability even if the Builder is reused.
|
||||
peers = Collections.unmodifiableList(new ArrayList<>(builder.peers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an series of "Interface" and "Peer" sections into a {@code Config}. Throws
|
||||
* {@link BadConfigException} if the input is not well-formed or contains data that cannot
|
||||
* be parsed.
|
||||
*
|
||||
* @param stream a stream of UTF-8 text that is interpreted as a WireGuard configuration
|
||||
* @return a {@code Config} instance representing the supplied configuration
|
||||
*/
|
||||
public static Config parse(final InputStream stream) throws IOException, BadConfigException {
|
||||
return parse(new BufferedReader(new InputStreamReader(stream)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an series of "Interface" and "Peer" sections into a {@code Config}. Throws
|
||||
* {@link BadConfigException} if the input is not well-formed or contains data that cannot
|
||||
* be parsed.
|
||||
*
|
||||
* @param reader a BufferedReader of UTF-8 text that is interpreted as a WireGuard configuration
|
||||
* @return a {@code Config} instance representing the supplied configuration
|
||||
*/
|
||||
public static Config parse(final BufferedReader reader) throws IOException, BadConfigException {
|
||||
final Builder builder = new Builder();
|
||||
final Collection<String> interfaceLines = new ArrayList<>();
|
||||
final Collection<String> peerLines = new ArrayList<>();
|
||||
boolean inInterfaceSection = false;
|
||||
boolean inPeerSection = false;
|
||||
boolean seenInterfaceSection = false;
|
||||
@Nullable String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
final int commentIndex = line.indexOf('#');
|
||||
if (commentIndex != -1)
|
||||
line = line.substring(0, commentIndex);
|
||||
line = line.trim();
|
||||
if (line.isEmpty())
|
||||
continue;
|
||||
if (line.startsWith("[")) {
|
||||
// Consume all [Peer] lines read so far.
|
||||
if (inPeerSection) {
|
||||
builder.parsePeer(peerLines);
|
||||
peerLines.clear();
|
||||
}
|
||||
if ("[Interface]".equalsIgnoreCase(line)) {
|
||||
inInterfaceSection = true;
|
||||
inPeerSection = false;
|
||||
seenInterfaceSection = true;
|
||||
} else if ("[Peer]".equalsIgnoreCase(line)) {
|
||||
inInterfaceSection = false;
|
||||
inPeerSection = true;
|
||||
} else {
|
||||
throw new BadConfigException(
|
||||
Section.CONFIG, Location.TOP_LEVEL, Reason.UNKNOWN_SECTION, line);
|
||||
}
|
||||
} else if (inInterfaceSection) {
|
||||
interfaceLines.add(line);
|
||||
} else if (inPeerSection) {
|
||||
peerLines.add(line);
|
||||
} else {
|
||||
throw new BadConfigException(
|
||||
Section.CONFIG, Location.TOP_LEVEL, Reason.UNKNOWN_SECTION, line);
|
||||
}
|
||||
}
|
||||
if (inPeerSection)
|
||||
builder.parsePeer(peerLines);
|
||||
if (!seenInterfaceSection)
|
||||
throw new BadConfigException(
|
||||
Section.CONFIG, Location.TOP_LEVEL, Reason.MISSING_SECTION, null);
|
||||
// Combine all [Interface] sections in the file.
|
||||
builder.parseInterface(interfaceLines);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (!(obj instanceof Config))
|
||||
return false;
|
||||
final Config other = (Config) obj;
|
||||
return interfaze.equals(other.interfaze) && peers.equals(other.peers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the interface section of the configuration.
|
||||
*
|
||||
* @return the interface configuration
|
||||
*/
|
||||
public Interface getInterface() {
|
||||
return interfaze;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of the configuration's peer sections.
|
||||
*
|
||||
* @return a list of {@link Peer}s
|
||||
*/
|
||||
public List<Peer> getPeers() {
|
||||
return peers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 31 * interfaze.hashCode() + peers.hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@code Config} into a string suitable for debugging purposes. The {@code Config}
|
||||
* is identified by its interface's public key and the number of peers it has.
|
||||
*
|
||||
* @return a concise single-line identifier for the {@code Config}
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "(Config " + interfaze + " (" + peers.size() + " peers))";
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@code Config} into a string suitable for use as a {@code wg-quick}
|
||||
* configuration file.
|
||||
*
|
||||
* @return the {@code Config} represented as one [Interface] and zero or more [Peer] sections
|
||||
*/
|
||||
public String toWgQuickString() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append("[Interface]\n").append(interfaze.toWgQuickString());
|
||||
for (final Peer peer : peers) sb.append("\n[Peer]\n").append(peer.toWgQuickString());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the {@code Config} for use with the WireGuard cross-platform userspace API.
|
||||
*
|
||||
* @return the {@code Config} represented as a series of "key=value" lines
|
||||
*/
|
||||
public String toWgUserspaceString() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append(interfaze.toWgUserspaceString());
|
||||
sb.append("replace_peers=true\n");
|
||||
for (final Peer peer : peers) sb.append(peer.toWgUserspaceString());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public static final class Builder {
|
||||
// Defaults to an empty set.
|
||||
private final ArrayList<Peer> peers = new ArrayList<>();
|
||||
// No default; must be provided before building.
|
||||
@Nullable private Interface interfaze;
|
||||
|
||||
public Builder addPeer(final Peer peer) {
|
||||
peers.add(peer);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addPeers(final Collection<Peer> peers) {
|
||||
this.peers.addAll(peers);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Config build() {
|
||||
if (interfaze == null)
|
||||
throw new IllegalArgumentException("An [Interface] section is required");
|
||||
return new Config(this);
|
||||
}
|
||||
|
||||
public Builder parseInterface(final Iterable<? extends CharSequence> lines)
|
||||
throws BadConfigException {
|
||||
return setInterface(Interface.parse(lines));
|
||||
}
|
||||
|
||||
public Builder parsePeer(final Iterable<? extends CharSequence> lines)
|
||||
throws BadConfigException {
|
||||
return addPeer(Peer.parse(lines));
|
||||
}
|
||||
|
||||
public Builder setInterface(final Interface interfaze) {
|
||||
this.interfaze = interfaze;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
73
client/android/src/com/wireguard/config/InetAddresses.java
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Utility methods for creating instances of {@link InetAddress}.
|
||||
*/
|
||||
|
||||
public final class InetAddresses {
|
||||
@Nullable private static final Method PARSER_METHOD;
|
||||
private static final Pattern WONT_TOUCH_RESOLVER = Pattern.compile(
|
||||
"^(((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?)|((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$");
|
||||
|
||||
static {
|
||||
Method m = null;
|
||||
try {
|
||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.Q)
|
||||
// noinspection JavaReflectionMemberAccess
|
||||
m = InetAddress.class.getMethod("parseNumericAddress", String.class);
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
PARSER_METHOD = m;
|
||||
}
|
||||
|
||||
private InetAddresses() {}
|
||||
|
||||
/**
|
||||
* Parses a numeric IPv4 or IPv6 address without performing any DNS lookups.
|
||||
*
|
||||
* @param address a string representing the IP address
|
||||
* @return an instance of {@link Inet4Address} or {@link Inet6Address}, as appropriate
|
||||
*/
|
||||
public static InetAddress parse(final String address) throws ParseException {
|
||||
if (address.isEmpty())
|
||||
throw new ParseException(InetAddress.class, address, "Empty address");
|
||||
try {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q)
|
||||
return android.net.InetAddresses.parseNumericAddress(address);
|
||||
else if (PARSER_METHOD != null)
|
||||
return (InetAddress) PARSER_METHOD.invoke(null, address);
|
||||
else
|
||||
throw new NoSuchMethodException("parseNumericAddress");
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new ParseException(InetAddress.class, address, e);
|
||||
} catch (final Exception e) {
|
||||
final Throwable cause = e.getCause();
|
||||
// Re-throw parsing exceptions with the original type, as callers might try to catch
|
||||
// them. On the other hand, callers cannot be expected to handle reflection failures.
|
||||
if (cause instanceof IllegalArgumentException)
|
||||
throw new ParseException(InetAddress.class, address, cause);
|
||||
try {
|
||||
if (WONT_TOUCH_RESOLVER.matcher(address).matches())
|
||||
return InetAddress.getByName(address);
|
||||
else
|
||||
throw new ParseException(InetAddress.class, address, "Not an IP address");
|
||||
} catch (final UnknownHostException f) {
|
||||
throw new ParseException(InetAddress.class, address, f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
123
client/android/src/com/wireguard/config/InetEndpoint.java
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* An external endpoint (host and port) used to connect to a WireGuard {@link Peer}.
|
||||
* <p>
|
||||
* Instances of this class are externally immutable.
|
||||
*/
|
||||
|
||||
public final class InetEndpoint {
|
||||
private static final Pattern BARE_IPV6 = Pattern.compile("^[^\\[\\]]*:[^\\[\\]]*");
|
||||
private static final Pattern FORBIDDEN_CHARACTERS = Pattern.compile("[/?#]");
|
||||
|
||||
private final String host;
|
||||
private final boolean isResolved;
|
||||
private final Object lock = new Object();
|
||||
private final int port;
|
||||
private Instant lastResolution = Instant.EPOCH;
|
||||
@Nullable private InetEndpoint resolved;
|
||||
|
||||
private InetEndpoint(final String host, final boolean isResolved, final int port) {
|
||||
this.host = host;
|
||||
this.isResolved = isResolved;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public static InetEndpoint parse(final String endpoint) throws ParseException {
|
||||
if (FORBIDDEN_CHARACTERS.matcher(endpoint).find())
|
||||
throw new ParseException(InetEndpoint.class, endpoint, "Forbidden characters");
|
||||
final URI uri;
|
||||
try {
|
||||
uri = new URI("wg://" + endpoint);
|
||||
} catch (final URISyntaxException e) {
|
||||
throw new ParseException(InetEndpoint.class, endpoint, e);
|
||||
}
|
||||
if (uri.getPort() < 0 || uri.getPort() > 65535)
|
||||
throw new ParseException(InetEndpoint.class, endpoint, "Missing/invalid port number");
|
||||
try {
|
||||
InetAddresses.parse(uri.getHost());
|
||||
// Parsing ths host as a numeric address worked, so we don't need to do DNS lookups.
|
||||
return new InetEndpoint(uri.getHost(), true, uri.getPort());
|
||||
} catch (final ParseException ignored) {
|
||||
// Failed to parse the host as a numeric address, so it must be a DNS hostname/FQDN.
|
||||
return new InetEndpoint(uri.getHost(), false, uri.getPort());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (!(obj instanceof InetEndpoint))
|
||||
return false;
|
||||
final InetEndpoint other = (InetEndpoint) obj;
|
||||
return host.equals(other.host) && port == other.port;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an {@code InetEndpoint} instance with the same port and the host resolved using DNS
|
||||
* to a numeric address. If the host is already numeric, the existing instance may be returned.
|
||||
* Because this function may perform network I/O, it must not be called from the main thread.
|
||||
*
|
||||
* @return the resolved endpoint, or {@link Optional#empty()}
|
||||
*/
|
||||
public Optional<InetEndpoint> getResolved() {
|
||||
if (isResolved)
|
||||
return Optional.of(this);
|
||||
synchronized (lock) {
|
||||
// TODO(zx2c4): Implement a real timeout mechanism using DNS TTL
|
||||
if (Duration.between(lastResolution, Instant.now()).toMinutes() > 1) {
|
||||
try {
|
||||
// Prefer v4 endpoints over v6 to work around DNS64 and IPv6 NAT issues.
|
||||
final InetAddress[] candidates = InetAddress.getAllByName(host);
|
||||
InetAddress address = candidates[0];
|
||||
for (final InetAddress candidate : candidates) {
|
||||
if (candidate instanceof Inet4Address) {
|
||||
address = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
resolved = new InetEndpoint(address.getHostAddress(), true, port);
|
||||
lastResolution = Instant.now();
|
||||
} catch (final UnknownHostException e) {
|
||||
resolved = null;
|
||||
}
|
||||
}
|
||||
return Optional.ofNullable(resolved);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return host.hashCode() ^ port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final boolean isBareIpv6 = isResolved && BARE_IPV6.matcher(host).matches();
|
||||
return (isBareIpv6 ? '[' + host + ']' : host) + ':' + port;
|
||||
}
|
||||
}
|
||||
77
client/android/src/com/wireguard/config/InetNetwork.java
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
|
||||
/**
|
||||
* An Internet network, denoted by its address and netmask
|
||||
* <p>
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
|
||||
public final class InetNetwork {
|
||||
private final InetAddress address;
|
||||
private final int mask;
|
||||
|
||||
private InetNetwork(final InetAddress address, final int mask) {
|
||||
this.address = address;
|
||||
this.mask = mask;
|
||||
}
|
||||
|
||||
public static InetNetwork parse(final String network) throws ParseException {
|
||||
final int slash = network.lastIndexOf('/');
|
||||
final String maskString;
|
||||
final int rawMask;
|
||||
final String rawAddress;
|
||||
if (slash >= 0) {
|
||||
maskString = network.substring(slash + 1);
|
||||
try {
|
||||
rawMask = Integer.parseInt(maskString, 10);
|
||||
} catch (final NumberFormatException ignored) {
|
||||
throw new ParseException(Integer.class, maskString);
|
||||
}
|
||||
rawAddress = network.substring(0, slash);
|
||||
} else {
|
||||
maskString = "";
|
||||
rawMask = -1;
|
||||
rawAddress = network;
|
||||
}
|
||||
final InetAddress address = InetAddresses.parse(rawAddress);
|
||||
final int maxMask = (address instanceof Inet4Address) ? 32 : 128;
|
||||
if (rawMask > maxMask)
|
||||
throw new ParseException(InetNetwork.class, maskString, "Invalid network mask");
|
||||
final int mask = rawMask >= 0 ? rawMask : maxMask;
|
||||
return new InetNetwork(address, mask);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (!(obj instanceof InetNetwork))
|
||||
return false;
|
||||
final InetNetwork other = (InetNetwork) obj;
|
||||
return address.equals(other.address) && mask == other.mask;
|
||||
}
|
||||
|
||||
public InetAddress getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public int getMask() {
|
||||
return mask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return address.hashCode() ^ mask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return address.getHostAddress() + '/' + mask;
|
||||
}
|
||||
}
|
||||
394
client/android/src/com/wireguard/config/Interface.java
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import com.wireguard.config.BadConfigException.Location;
|
||||
import com.wireguard.config.BadConfigException.Reason;
|
||||
import com.wireguard.config.BadConfigException.Section;
|
||||
import com.wireguard.crypto.Key;
|
||||
import com.wireguard.crypto.KeyFormatException;
|
||||
import com.wireguard.crypto.KeyPair;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Represents the configuration for a WireGuard interface (an [Interface] block). Interfaces must
|
||||
* have a private key (used to initialize a {@code KeyPair}), and may optionally have several other
|
||||
* attributes.
|
||||
* <p>
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
|
||||
public final class Interface {
|
||||
private static final int MAX_UDP_PORT = 65535;
|
||||
private static final int MIN_UDP_PORT = 0;
|
||||
|
||||
private final Set<InetNetwork> addresses;
|
||||
private final Set<InetAddress> dnsServers;
|
||||
private final Set<String> excludedApplications;
|
||||
private final Set<String> includedApplications;
|
||||
private final KeyPair keyPair;
|
||||
private final Optional<Integer> listenPort;
|
||||
private final Optional<Integer> mtu;
|
||||
|
||||
private Interface(final Builder builder) {
|
||||
// Defensively copy to ensure immutability even if the Builder is reused.
|
||||
addresses = Collections.unmodifiableSet(new LinkedHashSet<>(builder.addresses));
|
||||
dnsServers = Collections.unmodifiableSet(new LinkedHashSet<>(builder.dnsServers));
|
||||
excludedApplications =
|
||||
Collections.unmodifiableSet(new LinkedHashSet<>(builder.excludedApplications));
|
||||
includedApplications =
|
||||
Collections.unmodifiableSet(new LinkedHashSet<>(builder.includedApplications));
|
||||
keyPair = Objects.requireNonNull(builder.keyPair, "Interfaces must have a private key");
|
||||
listenPort = builder.listenPort;
|
||||
mtu = builder.mtu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an series of "KEY = VALUE" lines into an {@code Interface}. Throws
|
||||
* {@link ParseException} if the input is not well-formed or contains unknown attributes.
|
||||
*
|
||||
* @param lines An iterable sequence of lines, containing at least a private key attribute
|
||||
* @return An {@code Interface} with all of the attributes from {@code lines} set
|
||||
*/
|
||||
public static Interface parse(final Iterable<? extends CharSequence> lines)
|
||||
throws BadConfigException {
|
||||
final Builder builder = new Builder();
|
||||
for (final CharSequence line : lines) {
|
||||
final Attribute attribute =
|
||||
Attribute.parse(line).orElseThrow(()
|
||||
-> new BadConfigException(Section.INTERFACE,
|
||||
Location.TOP_LEVEL, Reason.SYNTAX_ERROR, line));
|
||||
switch (attribute.getKey().toLowerCase(Locale.ENGLISH)) {
|
||||
case "address":
|
||||
builder.parseAddresses(attribute.getValue());
|
||||
break;
|
||||
case "dns":
|
||||
builder.parseDnsServers(attribute.getValue());
|
||||
break;
|
||||
case "excludedapplications":
|
||||
builder.parseExcludedApplications(attribute.getValue());
|
||||
break;
|
||||
case "includedapplications":
|
||||
builder.parseIncludedApplications(attribute.getValue());
|
||||
break;
|
||||
case "listenport":
|
||||
builder.parseListenPort(attribute.getValue());
|
||||
break;
|
||||
case "mtu":
|
||||
builder.parseMtu(attribute.getValue());
|
||||
break;
|
||||
case "privatekey":
|
||||
builder.parsePrivateKey(attribute.getValue());
|
||||
break;
|
||||
default:
|
||||
throw new BadConfigException(
|
||||
Section.INTERFACE, Location.TOP_LEVEL, Reason.UNKNOWN_ATTRIBUTE, attribute.getKey());
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (!(obj instanceof Interface))
|
||||
return false;
|
||||
final Interface other = (Interface) obj;
|
||||
return addresses.equals(other.addresses) && dnsServers.equals(other.dnsServers)
|
||||
&& excludedApplications.equals(other.excludedApplications)
|
||||
&& includedApplications.equals(other.includedApplications) && keyPair.equals(other.keyPair)
|
||||
&& listenPort.equals(other.listenPort) && mtu.equals(other.mtu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of IP addresses assigned to the interface.
|
||||
*
|
||||
* @return a set of {@link InetNetwork}s
|
||||
*/
|
||||
public Set<InetNetwork> getAddresses() {
|
||||
// The collection is already immutable.
|
||||
return addresses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of DNS servers associated with the interface.
|
||||
*
|
||||
* @return a set of {@link InetAddress}es
|
||||
*/
|
||||
public Set<InetAddress> getDnsServers() {
|
||||
// The collection is already immutable.
|
||||
return dnsServers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of applications excluded from using the interface.
|
||||
*
|
||||
* @return a set of package names
|
||||
*/
|
||||
public Set<String> getExcludedApplications() {
|
||||
// The collection is already immutable.
|
||||
return excludedApplications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of applications included exclusively for using the interface.
|
||||
*
|
||||
* @return a set of package names
|
||||
*/
|
||||
public Set<String> getIncludedApplications() {
|
||||
// The collection is already immutable.
|
||||
return includedApplications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public/private key pair used by the interface.
|
||||
*
|
||||
* @return a key pair
|
||||
*/
|
||||
public KeyPair getKeyPair() {
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the UDP port number that the WireGuard interface will listen on.
|
||||
*
|
||||
* @return a UDP port number, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<Integer> getListenPort() {
|
||||
return listenPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the MTU used for the WireGuard interface.
|
||||
*
|
||||
* @return the MTU, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<Integer> getMtu() {
|
||||
return mtu;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = 1;
|
||||
hash = 31 * hash + addresses.hashCode();
|
||||
hash = 31 * hash + dnsServers.hashCode();
|
||||
hash = 31 * hash + excludedApplications.hashCode();
|
||||
hash = 31 * hash + includedApplications.hashCode();
|
||||
hash = 31 * hash + keyPair.hashCode();
|
||||
hash = 31 * hash + listenPort.hashCode();
|
||||
hash = 31 * hash + mtu.hashCode();
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@code Interface} into a string suitable for debugging purposes. The {@code
|
||||
* Interface} is identified by its public key and (if set) the port used for its UDP socket.
|
||||
*
|
||||
* @return A concise single-line identifier for the {@code Interface}
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder("(Interface ");
|
||||
sb.append(keyPair.getPublicKey().toBase64());
|
||||
listenPort.ifPresent(lp -> sb.append(" @").append(lp));
|
||||
sb.append(')');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@code Interface} into a string suitable for inclusion in a {@code wg-quick}
|
||||
* configuration file.
|
||||
*
|
||||
* @return The {@code Interface} represented as a series of "Key = Value" lines
|
||||
*/
|
||||
public String toWgQuickString() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
if (!addresses.isEmpty())
|
||||
sb.append("Address = ").append(Attribute.join(addresses)).append('\n');
|
||||
if (!dnsServers.isEmpty()) {
|
||||
final List<String> dnsServerStrings =
|
||||
dnsServers.stream().map(InetAddress::getHostAddress).collect(Collectors.toList());
|
||||
sb.append("DNS = ").append(Attribute.join(dnsServerStrings)).append('\n');
|
||||
}
|
||||
if (!excludedApplications.isEmpty())
|
||||
sb.append("ExcludedApplications = ")
|
||||
.append(Attribute.join(excludedApplications))
|
||||
.append('\n');
|
||||
if (!includedApplications.isEmpty())
|
||||
sb.append("IncludedApplications = ")
|
||||
.append(Attribute.join(includedApplications))
|
||||
.append('\n');
|
||||
listenPort.ifPresent(lp -> sb.append("ListenPort = ").append(lp).append('\n'));
|
||||
mtu.ifPresent(m -> sb.append("MTU = ").append(m).append('\n'));
|
||||
sb.append("PrivateKey = ").append(keyPair.getPrivateKey().toBase64()).append('\n');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the {@code Interface} for use with the WireGuard cross-platform userspace API.
|
||||
* Note that not all attributes are included in this representation.
|
||||
*
|
||||
* @return the {@code Interface} represented as a series of "KEY=VALUE" lines
|
||||
*/
|
||||
public String toWgUserspaceString() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append("private_key=").append(keyPair.getPrivateKey().toHex()).append('\n');
|
||||
listenPort.ifPresent(lp -> sb.append("listen_port=").append(lp).append('\n'));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public static final class Builder {
|
||||
// Defaults to an empty set.
|
||||
private final Set<InetNetwork> addresses = new LinkedHashSet<>();
|
||||
// Defaults to an empty set.
|
||||
private final Set<InetAddress> dnsServers = new LinkedHashSet<>();
|
||||
// Defaults to an empty set.
|
||||
private final Set<String> excludedApplications = new LinkedHashSet<>();
|
||||
// Defaults to an empty set.
|
||||
private final Set<String> includedApplications = new LinkedHashSet<>();
|
||||
// No default; must be provided before building.
|
||||
@Nullable private KeyPair keyPair;
|
||||
// Defaults to not present.
|
||||
private Optional<Integer> listenPort = Optional.empty();
|
||||
// Defaults to not present.
|
||||
private Optional<Integer> mtu = Optional.empty();
|
||||
|
||||
public Builder addAddress(final InetNetwork address) {
|
||||
addresses.add(address);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addAddresses(final Collection<InetNetwork> addresses) {
|
||||
this.addresses.addAll(addresses);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addDnsServer(final InetAddress dnsServer) {
|
||||
dnsServers.add(dnsServer);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addDnsServers(final Collection<? extends InetAddress> dnsServers) {
|
||||
this.dnsServers.addAll(dnsServers);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Interface build() throws BadConfigException {
|
||||
if (keyPair == null)
|
||||
throw new BadConfigException(
|
||||
Section.INTERFACE, Location.PRIVATE_KEY, Reason.MISSING_ATTRIBUTE, null);
|
||||
if (!includedApplications.isEmpty() && !excludedApplications.isEmpty())
|
||||
throw new BadConfigException(
|
||||
Section.INTERFACE, Location.INCLUDED_APPLICATIONS, Reason.INVALID_KEY, null);
|
||||
return new Interface(this);
|
||||
}
|
||||
|
||||
public Builder excludeApplication(final String application) {
|
||||
excludedApplications.add(application);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder excludeApplications(final Collection<String> applications) {
|
||||
excludedApplications.addAll(applications);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder includeApplication(final String application) {
|
||||
includedApplications.add(application);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder includeApplications(final Collection<String> applications) {
|
||||
includedApplications.addAll(applications);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder parseAddresses(final CharSequence addresses) throws BadConfigException {
|
||||
try {
|
||||
for (final String address : Attribute.split(addresses))
|
||||
addAddress(InetNetwork.parse(address));
|
||||
return this;
|
||||
} catch (final ParseException e) {
|
||||
throw new BadConfigException(Section.INTERFACE, Location.ADDRESS, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parseDnsServers(final CharSequence dnsServers) throws BadConfigException {
|
||||
try {
|
||||
for (final String dnsServer : Attribute.split(dnsServers))
|
||||
addDnsServer(InetAddresses.parse(dnsServer));
|
||||
return this;
|
||||
} catch (final ParseException e) {
|
||||
throw new BadConfigException(Section.INTERFACE, Location.DNS, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parseExcludedApplications(final CharSequence apps) {
|
||||
return excludeApplications(List.of(Attribute.split(apps)));
|
||||
}
|
||||
|
||||
public Builder parseIncludedApplications(final CharSequence apps) {
|
||||
return includeApplications(List.of(Attribute.split(apps)));
|
||||
}
|
||||
|
||||
public Builder parseListenPort(final String listenPort) throws BadConfigException {
|
||||
try {
|
||||
return setListenPort(Integer.parseInt(listenPort));
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new BadConfigException(Section.INTERFACE, Location.LISTEN_PORT, listenPort, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parseMtu(final String mtu) throws BadConfigException {
|
||||
try {
|
||||
return setMtu(Integer.parseInt(mtu));
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new BadConfigException(Section.INTERFACE, Location.MTU, mtu, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parsePrivateKey(final String privateKey) throws BadConfigException {
|
||||
try {
|
||||
return setKeyPair(new KeyPair(Key.fromBase64(privateKey)));
|
||||
} catch (final KeyFormatException e) {
|
||||
throw new BadConfigException(Section.INTERFACE, Location.PRIVATE_KEY, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder setKeyPair(final KeyPair keyPair) {
|
||||
this.keyPair = keyPair;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setListenPort(final int listenPort) throws BadConfigException {
|
||||
if (listenPort < MIN_UDP_PORT || listenPort > MAX_UDP_PORT)
|
||||
throw new BadConfigException(Section.INTERFACE, Location.LISTEN_PORT, Reason.INVALID_VALUE,
|
||||
String.valueOf(listenPort));
|
||||
this.listenPort = listenPort == 0 ? Optional.empty() : Optional.of(listenPort);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setMtu(final int mtu) throws BadConfigException {
|
||||
if (mtu < 0)
|
||||
throw new BadConfigException(
|
||||
Section.INTERFACE, Location.LISTEN_PORT, Reason.INVALID_VALUE, String.valueOf(mtu));
|
||||
this.mtu = mtu == 0 ? Optional.empty() : Optional.of(mtu);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
client/android/src/com/wireguard/config/ParseException.java
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
public class ParseException extends Exception {
|
||||
private final Class<?> parsingClass;
|
||||
private final CharSequence text;
|
||||
|
||||
public ParseException(final Class<?> parsingClass, final CharSequence text,
|
||||
@Nullable final String message, @Nullable final Throwable cause) {
|
||||
super(message, cause);
|
||||
this.parsingClass = parsingClass;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public ParseException(
|
||||
final Class<?> parsingClass, final CharSequence text, @Nullable final String message) {
|
||||
this(parsingClass, text, message, null);
|
||||
}
|
||||
|
||||
public ParseException(
|
||||
final Class<?> parsingClass, final CharSequence text, @Nullable final Throwable cause) {
|
||||
this(parsingClass, text, null, cause);
|
||||
}
|
||||
|
||||
public ParseException(final Class<?> parsingClass, final CharSequence text) {
|
||||
this(parsingClass, text, null, null);
|
||||
}
|
||||
|
||||
public Class<?> getParsingClass() {
|
||||
return parsingClass;
|
||||
}
|
||||
|
||||
public CharSequence getText() {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
306
client/android/src/com/wireguard/config/Peer.java
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import com.wireguard.config.BadConfigException.Location;
|
||||
import com.wireguard.config.BadConfigException.Reason;
|
||||
import com.wireguard.config.BadConfigException.Section;
|
||||
import com.wireguard.crypto.Key;
|
||||
import com.wireguard.crypto.KeyFormatException;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Represents the configuration for a WireGuard peer (a [Peer] block). Peers must have a public key,
|
||||
* and may optionally have several other attributes.
|
||||
* <p>
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
|
||||
public final class Peer {
|
||||
private final Set<InetNetwork> allowedIps;
|
||||
private final Optional<InetEndpoint> endpoint;
|
||||
private final Optional<Integer> persistentKeepalive;
|
||||
private final Optional<Key> preSharedKey;
|
||||
private final Key publicKey;
|
||||
|
||||
private Peer(final Builder builder) {
|
||||
// Defensively copy to ensure immutability even if the Builder is reused.
|
||||
allowedIps = Collections.unmodifiableSet(new LinkedHashSet<>(builder.allowedIps));
|
||||
endpoint = builder.endpoint;
|
||||
persistentKeepalive = builder.persistentKeepalive;
|
||||
preSharedKey = builder.preSharedKey;
|
||||
publicKey = Objects.requireNonNull(builder.publicKey, "Peers must have a public key");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an series of "KEY = VALUE" lines into a {@code Peer}. Throws {@link ParseException} if
|
||||
* the input is not well-formed or contains unknown attributes.
|
||||
*
|
||||
* @param lines an iterable sequence of lines, containing at least a public key attribute
|
||||
* @return a {@code Peer} with all of its attributes set from {@code lines}
|
||||
*/
|
||||
public static Peer parse(final Iterable<? extends CharSequence> lines) throws BadConfigException {
|
||||
final Builder builder = new Builder();
|
||||
for (final CharSequence line : lines) {
|
||||
final Attribute attribute =
|
||||
Attribute.parse(line).orElseThrow(()
|
||||
-> new BadConfigException(Section.PEER,
|
||||
Location.TOP_LEVEL, Reason.SYNTAX_ERROR, line));
|
||||
switch (attribute.getKey().toLowerCase(Locale.ENGLISH)) {
|
||||
case "allowedips":
|
||||
builder.parseAllowedIPs(attribute.getValue());
|
||||
break;
|
||||
case "endpoint":
|
||||
builder.parseEndpoint(attribute.getValue());
|
||||
break;
|
||||
case "persistentkeepalive":
|
||||
builder.parsePersistentKeepalive(attribute.getValue());
|
||||
break;
|
||||
case "presharedkey":
|
||||
builder.parsePreSharedKey(attribute.getValue());
|
||||
break;
|
||||
case "publickey":
|
||||
builder.parsePublicKey(attribute.getValue());
|
||||
break;
|
||||
default:
|
||||
throw new BadConfigException(
|
||||
Section.PEER, Location.TOP_LEVEL, Reason.UNKNOWN_ATTRIBUTE, attribute.getKey());
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (!(obj instanceof Peer))
|
||||
return false;
|
||||
final Peer other = (Peer) obj;
|
||||
return allowedIps.equals(other.allowedIps) && endpoint.equals(other.endpoint)
|
||||
&& persistentKeepalive.equals(other.persistentKeepalive)
|
||||
&& preSharedKey.equals(other.preSharedKey) && publicKey.equals(other.publicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the peer's set of allowed IPs.
|
||||
*
|
||||
* @return the set of allowed IPs
|
||||
*/
|
||||
public Set<InetNetwork> getAllowedIps() {
|
||||
// The collection is already immutable.
|
||||
return allowedIps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the peer's endpoint.
|
||||
*
|
||||
* @return the endpoint, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<InetEndpoint> getEndpoint() {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the peer's persistent keepalive.
|
||||
*
|
||||
* @return the persistent keepalive, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<Integer> getPersistentKeepalive() {
|
||||
return persistentKeepalive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the peer's pre-shared key.
|
||||
*
|
||||
* @return the pre-shared key, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<Key> getPreSharedKey() {
|
||||
return preSharedKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the peer's public key.
|
||||
*
|
||||
* @return the public key
|
||||
*/
|
||||
public Key getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = 1;
|
||||
hash = 31 * hash + allowedIps.hashCode();
|
||||
hash = 31 * hash + endpoint.hashCode();
|
||||
hash = 31 * hash + persistentKeepalive.hashCode();
|
||||
hash = 31 * hash + preSharedKey.hashCode();
|
||||
hash = 31 * hash + publicKey.hashCode();
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@code Peer} into a string suitable for debugging purposes. The {@code Peer} is
|
||||
* identified by its public key and (if known) its endpoint.
|
||||
*
|
||||
* @return a concise single-line identifier for the {@code Peer}
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder("(Peer ");
|
||||
sb.append(publicKey.toBase64());
|
||||
endpoint.ifPresent(ep -> sb.append(" @").append(ep));
|
||||
sb.append(')');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@code Peer} into a string suitable for inclusion in a {@code wg-quick}
|
||||
* configuration file.
|
||||
*
|
||||
* @return the {@code Peer} represented as a series of "Key = Value" lines
|
||||
*/
|
||||
public String toWgQuickString() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
if (!allowedIps.isEmpty())
|
||||
sb.append("AllowedIPs = ").append(Attribute.join(allowedIps)).append('\n');
|
||||
endpoint.ifPresent(ep -> sb.append("Endpoint = ").append(ep).append('\n'));
|
||||
persistentKeepalive.ifPresent(
|
||||
pk -> sb.append("PersistentKeepalive = ").append(pk).append('\n'));
|
||||
preSharedKey.ifPresent(psk -> sb.append("PreSharedKey = ").append(psk.toBase64()).append('\n'));
|
||||
sb.append("PublicKey = ").append(publicKey.toBase64()).append('\n');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the {@code Peer} for use with the WireGuard cross-platform userspace API. Note
|
||||
* that not all attributes are included in this representation.
|
||||
*
|
||||
* @return the {@code Peer} represented as a series of "key=value" lines
|
||||
*/
|
||||
public String toWgUserspaceString() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
// The order here is important: public_key signifies the beginning of a new peer.
|
||||
sb.append("public_key=").append(publicKey.toHex()).append('\n');
|
||||
for (final InetNetwork allowedIp : allowedIps)
|
||||
sb.append("allowed_ip=").append(allowedIp).append('\n');
|
||||
endpoint.flatMap(InetEndpoint::getResolved)
|
||||
.ifPresent(ep -> sb.append("endpoint=").append(ep).append('\n'));
|
||||
persistentKeepalive.ifPresent(
|
||||
pk -> sb.append("persistent_keepalive_interval=").append(pk).append('\n'));
|
||||
preSharedKey.ifPresent(psk -> sb.append("preshared_key=").append(psk.toHex()).append('\n'));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public static final class Builder {
|
||||
// See wg(8)
|
||||
private static final int MAX_PERSISTENT_KEEPALIVE = 65535;
|
||||
|
||||
// Defaults to an empty set.
|
||||
private final Set<InetNetwork> allowedIps = new LinkedHashSet<>();
|
||||
// Defaults to not present.
|
||||
private Optional<InetEndpoint> endpoint = Optional.empty();
|
||||
// Defaults to not present.
|
||||
private Optional<Integer> persistentKeepalive = Optional.empty();
|
||||
// Defaults to not present.
|
||||
private Optional<Key> preSharedKey = Optional.empty();
|
||||
// No default; must be provided before building.
|
||||
@Nullable private Key publicKey;
|
||||
|
||||
public Builder addAllowedIp(final InetNetwork allowedIp) {
|
||||
allowedIps.add(allowedIp);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addAllowedIps(final Collection<InetNetwork> allowedIps) {
|
||||
this.allowedIps.addAll(allowedIps);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Peer build() throws BadConfigException {
|
||||
if (publicKey == null)
|
||||
throw new BadConfigException(
|
||||
Section.PEER, Location.PUBLIC_KEY, Reason.MISSING_ATTRIBUTE, null);
|
||||
return new Peer(this);
|
||||
}
|
||||
|
||||
public Builder parseAllowedIPs(final CharSequence allowedIps) throws BadConfigException {
|
||||
try {
|
||||
for (final String allowedIp : Attribute.split(allowedIps))
|
||||
addAllowedIp(InetNetwork.parse(allowedIp));
|
||||
return this;
|
||||
} catch (final ParseException e) {
|
||||
throw new BadConfigException(Section.PEER, Location.ALLOWED_IPS, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parseEndpoint(final String endpoint) throws BadConfigException {
|
||||
try {
|
||||
return setEndpoint(InetEndpoint.parse(endpoint));
|
||||
} catch (final ParseException e) {
|
||||
throw new BadConfigException(Section.PEER, Location.ENDPOINT, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parsePersistentKeepalive(final String persistentKeepalive)
|
||||
throws BadConfigException {
|
||||
try {
|
||||
return setPersistentKeepalive(Integer.parseInt(persistentKeepalive));
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new BadConfigException(
|
||||
Section.PEER, Location.PERSISTENT_KEEPALIVE, persistentKeepalive, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parsePreSharedKey(final String preSharedKey) throws BadConfigException {
|
||||
try {
|
||||
return setPreSharedKey(Key.fromBase64(preSharedKey));
|
||||
} catch (final KeyFormatException e) {
|
||||
throw new BadConfigException(Section.PEER, Location.PRE_SHARED_KEY, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parsePublicKey(final String publicKey) throws BadConfigException {
|
||||
try {
|
||||
return setPublicKey(Key.fromBase64(publicKey));
|
||||
} catch (final KeyFormatException e) {
|
||||
throw new BadConfigException(Section.PEER, Location.PUBLIC_KEY, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder setEndpoint(final InetEndpoint endpoint) {
|
||||
this.endpoint = Optional.of(endpoint);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setPersistentKeepalive(final int persistentKeepalive) throws BadConfigException {
|
||||
if (persistentKeepalive < 0 || persistentKeepalive > MAX_PERSISTENT_KEEPALIVE)
|
||||
throw new BadConfigException(Section.PEER, Location.PERSISTENT_KEEPALIVE,
|
||||
Reason.INVALID_VALUE, String.valueOf(persistentKeepalive));
|
||||
this.persistentKeepalive =
|
||||
persistentKeepalive == 0 ? Optional.empty() : Optional.of(persistentKeepalive);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setPreSharedKey(final Key preSharedKey) {
|
||||
this.preSharedKey = Optional.of(preSharedKey);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setPublicKey(final Key publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
497
client/android/src/com/wireguard/crypto/Curve25519.java
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
/*
|
||||
* Copyright © 2016 Southern Storm Software, Pty Ltd.
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.crypto;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Implementation of Curve25519 ECDH.
|
||||
* <p>
|
||||
* This implementation was imported to WireGuard from noise-java:
|
||||
* https://github.com/rweather/noise-java
|
||||
* <p>
|
||||
* This implementation is based on that from arduinolibs:
|
||||
* https://github.com/rweather/arduinolibs
|
||||
* <p>
|
||||
* Differences in this version are due to using 26-bit limbs for the
|
||||
* representation instead of the 8/16/32-bit limbs in the original.
|
||||
* <p>
|
||||
* References: http://cr.yp.to/ecdh.html, RFC 7748
|
||||
*/
|
||||
@SuppressWarnings({"MagicNumber", "NonConstantFieldWithUpperCaseName", "SuspiciousNameCombination"})
|
||||
|
||||
public final class Curve25519 {
|
||||
// Numbers modulo 2^255 - 19 are broken up into ten 26-bit words.
|
||||
private static final int NUM_LIMBS_255BIT = 10;
|
||||
private static final int NUM_LIMBS_510BIT = 20;
|
||||
|
||||
private final int[] A;
|
||||
private final int[] AA;
|
||||
private final int[] B;
|
||||
private final int[] BB;
|
||||
private final int[] C;
|
||||
private final int[] CB;
|
||||
private final int[] D;
|
||||
private final int[] DA;
|
||||
private final int[] E;
|
||||
private final long[] t1;
|
||||
private final int[] t2;
|
||||
private final int[] x_1;
|
||||
private final int[] x_2;
|
||||
private final int[] x_3;
|
||||
private final int[] z_2;
|
||||
private final int[] z_3;
|
||||
|
||||
/**
|
||||
* Constructs the temporary state holder for Curve25519 evaluation.
|
||||
*/
|
||||
private Curve25519() {
|
||||
// Allocate memory for all of the temporary variables we will need.
|
||||
x_1 = new int[NUM_LIMBS_255BIT];
|
||||
x_2 = new int[NUM_LIMBS_255BIT];
|
||||
x_3 = new int[NUM_LIMBS_255BIT];
|
||||
z_2 = new int[NUM_LIMBS_255BIT];
|
||||
z_3 = new int[NUM_LIMBS_255BIT];
|
||||
A = new int[NUM_LIMBS_255BIT];
|
||||
B = new int[NUM_LIMBS_255BIT];
|
||||
C = new int[NUM_LIMBS_255BIT];
|
||||
D = new int[NUM_LIMBS_255BIT];
|
||||
E = new int[NUM_LIMBS_255BIT];
|
||||
AA = new int[NUM_LIMBS_255BIT];
|
||||
BB = new int[NUM_LIMBS_255BIT];
|
||||
DA = new int[NUM_LIMBS_255BIT];
|
||||
CB = new int[NUM_LIMBS_255BIT];
|
||||
t1 = new long[NUM_LIMBS_510BIT];
|
||||
t2 = new int[NUM_LIMBS_510BIT];
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditional swap of two values.
|
||||
*
|
||||
* @param select Set to 1 to swap, 0 to leave as-is.
|
||||
* @param x The first value.
|
||||
* @param y The second value.
|
||||
*/
|
||||
private static void cswap(int select, final int[] x, final int[] y) {
|
||||
select = -select;
|
||||
for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
|
||||
final int dummy = select & (x[index] ^ y[index]);
|
||||
x[index] ^= dummy;
|
||||
y[index] ^= dummy;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the Curve25519 curve.
|
||||
*
|
||||
* @param result Buffer to place the result of the evaluation into.
|
||||
* @param offset Offset into the result buffer.
|
||||
* @param privateKey The private key to use in the evaluation.
|
||||
* @param publicKey The public key to use in the evaluation, or null
|
||||
* if the base point of the curve should be used.
|
||||
*/
|
||||
public static void eval(final byte[] result, final int offset, final byte[] privateKey,
|
||||
@Nullable final byte[] publicKey) {
|
||||
final Curve25519 state = new Curve25519();
|
||||
try {
|
||||
// Unpack the public key value. If null, use 9 as the base point.
|
||||
Arrays.fill(state.x_1, 0);
|
||||
if (publicKey != null) {
|
||||
// Convert the input value from little-endian into 26-bit limbs.
|
||||
for (int index = 0; index < 32; ++index) {
|
||||
final int bit = (index * 8) % 26;
|
||||
final int word = (index * 8) / 26;
|
||||
final int value = publicKey[index] & 0xFF;
|
||||
if (bit <= (26 - 8)) {
|
||||
state.x_1[word] |= value << bit;
|
||||
} else {
|
||||
state.x_1[word] |= value << bit;
|
||||
state.x_1[word] &= 0x03FFFFFF;
|
||||
state.x_1[word + 1] |= value >> (26 - bit);
|
||||
}
|
||||
}
|
||||
|
||||
// Just in case, we reduce the number modulo 2^255 - 19 to
|
||||
// make sure that it is in range of the field before we start.
|
||||
// This eliminates values between 2^255 - 19 and 2^256 - 1.
|
||||
state.reduceQuick(state.x_1);
|
||||
state.reduceQuick(state.x_1);
|
||||
} else {
|
||||
state.x_1[0] = 9;
|
||||
}
|
||||
|
||||
// Initialize the other temporary variables.
|
||||
Arrays.fill(state.x_2, 0); // x_2 = 1
|
||||
state.x_2[0] = 1;
|
||||
Arrays.fill(state.z_2, 0); // z_2 = 0
|
||||
System.arraycopy(state.x_1, 0, state.x_3, 0, state.x_1.length); // x_3 = x_1
|
||||
Arrays.fill(state.z_3, 0); // z_3 = 1
|
||||
state.z_3[0] = 1;
|
||||
|
||||
// Evaluate the curve for every bit of the private key.
|
||||
state.evalCurve(privateKey);
|
||||
|
||||
// Compute x_2 * (z_2 ^ (p - 2)) where p = 2^255 - 19.
|
||||
state.recip(state.z_3, state.z_2);
|
||||
state.mul(state.x_2, state.x_2, state.z_3);
|
||||
|
||||
// Convert x_2 into little-endian in the result buffer.
|
||||
for (int index = 0; index < 32; ++index) {
|
||||
final int bit = (index * 8) % 26;
|
||||
final int word = (index * 8) / 26;
|
||||
if (bit <= (26 - 8))
|
||||
result[offset + index] = (byte) (state.x_2[word] >> bit);
|
||||
else
|
||||
result[offset + index] =
|
||||
(byte) ((state.x_2[word] >> bit) | (state.x_2[word + 1] << (26 - bit)));
|
||||
}
|
||||
} finally {
|
||||
// Clean up all temporary state before we exit.
|
||||
state.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtracts two numbers modulo 2^255 - 19.
|
||||
*
|
||||
* @param result The result.
|
||||
* @param x The first number to subtract.
|
||||
* @param y The second number to subtract.
|
||||
*/
|
||||
private static void sub(final int[] result, final int[] x, final int[] y) {
|
||||
int index;
|
||||
int borrow;
|
||||
|
||||
// Subtract y from x to generate the intermediate result.
|
||||
borrow = 0;
|
||||
for (index = 0; index < NUM_LIMBS_255BIT; ++index) {
|
||||
borrow = x[index] - y[index] - ((borrow >> 26) & 0x01);
|
||||
result[index] = borrow & 0x03FFFFFF;
|
||||
}
|
||||
|
||||
// If we had a borrow, then the result has gone negative and we
|
||||
// have to add 2^255 - 19 to the result to make it positive again.
|
||||
// The top bits of "borrow" will be all 1's if there is a borrow
|
||||
// or it will be all 0's if there was no borrow. Easiest is to
|
||||
// conditionally subtract 19 and then mask off the high bits.
|
||||
borrow = result[0] - ((-((borrow >> 26) & 0x01)) & 19);
|
||||
result[0] = borrow & 0x03FFFFFF;
|
||||
for (index = 1; index < NUM_LIMBS_255BIT; ++index) {
|
||||
borrow = result[index] - ((borrow >> 26) & 0x01);
|
||||
result[index] = borrow & 0x03FFFFFF;
|
||||
}
|
||||
result[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds two numbers modulo 2^255 - 19.
|
||||
*
|
||||
* @param result The result.
|
||||
* @param x The first number to add.
|
||||
* @param y The second number to add.
|
||||
*/
|
||||
private void add(final int[] result, final int[] x, final int[] y) {
|
||||
int carry = x[0] + y[0];
|
||||
result[0] = carry & 0x03FFFFFF;
|
||||
for (int index = 1; index < NUM_LIMBS_255BIT; ++index) {
|
||||
carry = (carry >> 26) + x[index] + y[index];
|
||||
result[index] = carry & 0x03FFFFFF;
|
||||
}
|
||||
reduceQuick(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy all sensitive data in this object.
|
||||
*/
|
||||
private void destroy() {
|
||||
// Destroy all temporary variables.
|
||||
Arrays.fill(x_1, 0);
|
||||
Arrays.fill(x_2, 0);
|
||||
Arrays.fill(x_3, 0);
|
||||
Arrays.fill(z_2, 0);
|
||||
Arrays.fill(z_3, 0);
|
||||
Arrays.fill(A, 0);
|
||||
Arrays.fill(B, 0);
|
||||
Arrays.fill(C, 0);
|
||||
Arrays.fill(D, 0);
|
||||
Arrays.fill(E, 0);
|
||||
Arrays.fill(AA, 0);
|
||||
Arrays.fill(BB, 0);
|
||||
Arrays.fill(DA, 0);
|
||||
Arrays.fill(CB, 0);
|
||||
Arrays.fill(t1, 0L);
|
||||
Arrays.fill(t2, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the curve for every bit in a secret key.
|
||||
*
|
||||
* @param s The 32-byte secret key.
|
||||
*/
|
||||
private void evalCurve(final byte[] s) {
|
||||
int sposn = 31;
|
||||
int sbit = 6;
|
||||
int svalue = s[sposn] | 0x40;
|
||||
int swap = 0;
|
||||
|
||||
// Iterate over all 255 bits of "s" from the highest to the lowest.
|
||||
// We ignore the high bit of the 256-bit representation of "s".
|
||||
while (true) {
|
||||
// Conditional swaps on entry to this bit but only if we
|
||||
// didn't swap on the previous bit.
|
||||
final int select = (svalue >> sbit) & 0x01;
|
||||
swap ^= select;
|
||||
cswap(swap, x_2, x_3);
|
||||
cswap(swap, z_2, z_3);
|
||||
swap = select;
|
||||
|
||||
// Evaluate the curve.
|
||||
add(A, x_2, z_2); // A = x_2 + z_2
|
||||
square(AA, A); // AA = A^2
|
||||
sub(B, x_2, z_2); // B = x_2 - z_2
|
||||
square(BB, B); // BB = B^2
|
||||
sub(E, AA, BB); // E = AA - BB
|
||||
add(C, x_3, z_3); // C = x_3 + z_3
|
||||
sub(D, x_3, z_3); // D = x_3 - z_3
|
||||
mul(DA, D, A); // DA = D * A
|
||||
mul(CB, C, B); // CB = C * B
|
||||
add(x_3, DA, CB); // x_3 = (DA + CB)^2
|
||||
square(x_3, x_3);
|
||||
sub(z_3, DA, CB); // z_3 = x_1 * (DA - CB)^2
|
||||
square(z_3, z_3);
|
||||
mul(z_3, z_3, x_1);
|
||||
mul(x_2, AA, BB); // x_2 = AA * BB
|
||||
mulA24(z_2, E); // z_2 = E * (AA + a24 * E)
|
||||
add(z_2, z_2, AA);
|
||||
mul(z_2, z_2, E);
|
||||
|
||||
// Move onto the next lower bit of "s".
|
||||
if (sbit > 0) {
|
||||
--sbit;
|
||||
} else if (sposn == 0) {
|
||||
break;
|
||||
} else if (sposn == 1) {
|
||||
--sposn;
|
||||
svalue = s[sposn] & 0xF8;
|
||||
sbit = 7;
|
||||
} else {
|
||||
--sposn;
|
||||
svalue = s[sposn];
|
||||
sbit = 7;
|
||||
}
|
||||
}
|
||||
|
||||
// Final conditional swaps.
|
||||
cswap(swap, x_2, x_3);
|
||||
cswap(swap, z_2, z_3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies two numbers modulo 2^255 - 19.
|
||||
*
|
||||
* @param result The result.
|
||||
* @param x The first number to multiply.
|
||||
* @param y The second number to multiply.
|
||||
*/
|
||||
private void mul(final int[] result, final int[] x, final int[] y) {
|
||||
// Multiply the two numbers to create the intermediate result.
|
||||
long v = x[0];
|
||||
for (int i = 0; i < NUM_LIMBS_255BIT; ++i) {
|
||||
t1[i] = v * y[i];
|
||||
}
|
||||
for (int i = 1; i < NUM_LIMBS_255BIT; ++i) {
|
||||
v = x[i];
|
||||
for (int j = 0; j < (NUM_LIMBS_255BIT - 1); ++j) {
|
||||
t1[i + j] += v * y[j];
|
||||
}
|
||||
t1[i + NUM_LIMBS_255BIT - 1] = v * y[NUM_LIMBS_255BIT - 1];
|
||||
}
|
||||
|
||||
// Propagate carries and convert back into 26-bit words.
|
||||
v = t1[0];
|
||||
t2[0] = ((int) v) & 0x03FFFFFF;
|
||||
for (int i = 1; i < NUM_LIMBS_510BIT; ++i) {
|
||||
v = (v >> 26) + t1[i];
|
||||
t2[i] = ((int) v) & 0x03FFFFFF;
|
||||
}
|
||||
|
||||
// Reduce the result modulo 2^255 - 19.
|
||||
reduce(result, t2, NUM_LIMBS_255BIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies a number by the a24 constant, modulo 2^255 - 19.
|
||||
*
|
||||
* @param result The result.
|
||||
* @param x The number to multiply by a24.
|
||||
*/
|
||||
private void mulA24(final int[] result, final int[] x) {
|
||||
final long a24 = 121665;
|
||||
long carry = 0;
|
||||
for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
|
||||
carry += a24 * x[index];
|
||||
t2[index] = ((int) carry) & 0x03FFFFFF;
|
||||
carry >>= 26;
|
||||
}
|
||||
t2[NUM_LIMBS_255BIT] = ((int) carry) & 0x03FFFFFF;
|
||||
reduce(result, t2, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Raise x to the power of (2^250 - 1).
|
||||
*
|
||||
* @param result The result. Must not overlap with x.
|
||||
* @param x The argument.
|
||||
*/
|
||||
private void pow250(final int[] result, final int[] x) {
|
||||
// The big-endian hexadecimal expansion of (2^250 - 1) is:
|
||||
// 03FFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF
|
||||
//
|
||||
// The naive implementation needs to do 2 multiplications per 1 bit and
|
||||
// 1 multiplication per 0 bit. We can improve upon this by creating a
|
||||
// pattern 0000000001 ... 0000000001. If we square and multiply the
|
||||
// pattern by itself we can turn the pattern into the partial results
|
||||
// 0000000011 ... 0000000011, 0000000111 ... 0000000111, etc.
|
||||
// This averages out to about 1.1 multiplications per 1 bit instead of 2.
|
||||
|
||||
// Build a pattern of 250 bits in length of repeated copies of 0000000001.
|
||||
square(A, x);
|
||||
for (int j = 0; j < 9; ++j) square(A, A);
|
||||
mul(result, A, x);
|
||||
for (int i = 0; i < 23; ++i) {
|
||||
for (int j = 0; j < 10; ++j) square(A, A);
|
||||
mul(result, result, A);
|
||||
}
|
||||
|
||||
// Multiply bit-shifted versions of the 0000000001 pattern into
|
||||
// the result to "fill in" the gaps in the pattern.
|
||||
square(A, result);
|
||||
mul(result, result, A);
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
square(A, A);
|
||||
mul(result, result, A);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the reciprocal of a number modulo 2^255 - 19.
|
||||
*
|
||||
* @param result The result. Must not overlap with x.
|
||||
* @param x The argument.
|
||||
*/
|
||||
private void recip(final int[] result, final int[] x) {
|
||||
// The reciprocal is the same as x ^ (p - 2) where p = 2^255 - 19.
|
||||
// The big-endian hexadecimal expansion of (p - 2) is:
|
||||
// 7FFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFEB
|
||||
// Start with the 250 upper bits of the expansion of (p - 2).
|
||||
pow250(result, x);
|
||||
|
||||
// Deal with the 5 lowest bits of (p - 2), 01011, from highest to lowest.
|
||||
square(result, result);
|
||||
square(result, result);
|
||||
mul(result, result, x);
|
||||
square(result, result);
|
||||
square(result, result);
|
||||
mul(result, result, x);
|
||||
square(result, result);
|
||||
mul(result, result, x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce a number modulo 2^255 - 19.
|
||||
*
|
||||
* @param result The result.
|
||||
* @param x The value to be reduced. This array will be
|
||||
* modified during the reduction.
|
||||
* @param size The number of limbs in the high order half of x.
|
||||
*/
|
||||
private void reduce(final int[] result, final int[] x, final int size) {
|
||||
// Calculate (x mod 2^255) + ((x / 2^255) * 19) which will
|
||||
// either produce the answer we want or it will produce a
|
||||
// value of the form "answer + j * (2^255 - 19)". There are
|
||||
// 5 left-over bits in the top-most limb of the bottom half.
|
||||
int carry = 0;
|
||||
int limb = x[NUM_LIMBS_255BIT - 1] >> 21;
|
||||
x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
|
||||
for (int index = 0; index < size; ++index) {
|
||||
limb += x[NUM_LIMBS_255BIT + index] << 5;
|
||||
carry += (limb & 0x03FFFFFF) * 19 + x[index];
|
||||
x[index] = carry & 0x03FFFFFF;
|
||||
limb >>= 26;
|
||||
carry >>= 26;
|
||||
}
|
||||
if (size < NUM_LIMBS_255BIT) {
|
||||
// The high order half of the number is short; e.g. for mulA24().
|
||||
// Propagate the carry through the rest of the low order part.
|
||||
for (int index = size; index < NUM_LIMBS_255BIT; ++index) {
|
||||
carry += x[index];
|
||||
x[index] = carry & 0x03FFFFFF;
|
||||
carry >>= 26;
|
||||
}
|
||||
}
|
||||
|
||||
// The "j" value may still be too large due to the final carry-out.
|
||||
// We must repeat the reduction. If we already have the answer,
|
||||
// then this won't do any harm but we must still do the calculation
|
||||
// to preserve the overall timing. The "j" value will be between
|
||||
// 0 and 19, which means that the carry we care about is in the
|
||||
// top 5 bits of the highest limb of the bottom half.
|
||||
carry = (x[NUM_LIMBS_255BIT - 1] >> 21) * 19;
|
||||
x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
|
||||
for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
|
||||
carry += x[index];
|
||||
result[index] = carry & 0x03FFFFFF;
|
||||
carry >>= 26;
|
||||
}
|
||||
|
||||
// At this point "x" will either be the answer or it will be the
|
||||
// answer plus (2^255 - 19). Perform a trial subtraction to
|
||||
// complete the reduction process.
|
||||
reduceQuick(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a number modulo 2^255 - 19 where it is known that the
|
||||
* number can be reduced with only 1 trial subtraction.
|
||||
*
|
||||
* @param x The number to reduce, and the result.
|
||||
*/
|
||||
private void reduceQuick(final int[] x) {
|
||||
// Perform a trial subtraction of (2^255 - 19) from "x" which is
|
||||
// equivalent to adding 19 and subtracting 2^255. We add 19 here;
|
||||
// the subtraction of 2^255 occurs in the next step.
|
||||
int carry = 19;
|
||||
for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
|
||||
carry += x[index];
|
||||
t2[index] = carry & 0x03FFFFFF;
|
||||
carry >>= 26;
|
||||
}
|
||||
|
||||
// If there was a borrow, then the original "x" is the correct answer.
|
||||
// If there was no borrow, then "t2" is the correct answer. Select the
|
||||
// correct answer but do it in a way that instruction timing will not
|
||||
// reveal which value was selected. Borrow will occur if bit 21 of
|
||||
// "t2" is zero. Turn the bit into a selection mask.
|
||||
final int mask = -((t2[NUM_LIMBS_255BIT - 1] >> 21) & 0x01);
|
||||
final int nmask = ~mask;
|
||||
t2[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
|
||||
for (int index = 0; index < NUM_LIMBS_255BIT; ++index)
|
||||
x[index] = (x[index] & nmask) | (t2[index] & mask);
|
||||
}
|
||||
|
||||
/**
|
||||
* Squares a number modulo 2^255 - 19.
|
||||
*
|
||||
* @param result The result.
|
||||
* @param x The number to square.
|
||||
*/
|
||||
private void square(final int[] result, final int[] x) {
|
||||
mul(result, x, x);
|
||||
}
|
||||
}
|
||||
2435
client/android/src/com/wireguard/crypto/Ed25519.java
Normal file
283
client/android/src/com/wireguard/crypto/Key.java
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.crypto;
|
||||
|
||||
import com.wireguard.crypto.KeyFormatException.Type;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Represents a WireGuard public or private key. This class uses specialized constant-time base64
|
||||
* and hexadecimal codec implementations that resist side-channel attacks.
|
||||
* <p>
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
@SuppressWarnings("MagicNumber")
|
||||
|
||||
public final class Key {
|
||||
private final byte[] key;
|
||||
|
||||
/**
|
||||
* Constructs an object encapsulating the supplied key.
|
||||
*
|
||||
* @param key an array of bytes containing a binary key. Callers of this constructor are
|
||||
* responsible for ensuring that the array is of the correct length.
|
||||
*/
|
||||
private Key(final byte[] key) {
|
||||
// Defensively copy to ensure immutability.
|
||||
this.key = Arrays.copyOf(key, key.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a single 4-character base64 chunk to an integer in constant time.
|
||||
*
|
||||
* @param src an array of at least 4 characters in base64 format
|
||||
* @param srcOffset the offset of the beginning of the chunk in {@code src}
|
||||
* @return the decoded 3-byte integer, or some arbitrary integer value if the input was not
|
||||
* valid base64
|
||||
*/
|
||||
private static int decodeBase64(final char[] src, final int srcOffset) {
|
||||
int val = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
final char c = src[i + srcOffset];
|
||||
val |= (-1 + ((((('A' - 1) - c) & (c - ('Z' + 1))) >>> 8) & (c - 64))
|
||||
+ ((((('a' - 1) - c) & (c - ('z' + 1))) >>> 8) & (c - 70))
|
||||
+ ((((('0' - 1) - c) & (c - ('9' + 1))) >>> 8) & (c + 5))
|
||||
+ ((((('+' - 1) - c) & (c - ('+' + 1))) >>> 8) & 63)
|
||||
+ ((((('/' - 1) - c) & (c - ('/' + 1))) >>> 8) & 64))
|
||||
<< (18 - 6 * i);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a single 4-character base64 chunk from 3 consecutive bytes in constant time.
|
||||
*
|
||||
* @param src an array of at least 3 bytes
|
||||
* @param srcOffset the offset of the beginning of the chunk in {@code src}
|
||||
* @param dest an array of at least 4 characters
|
||||
* @param destOffset the offset of the beginning of the chunk in {@code dest}
|
||||
*/
|
||||
private static void encodeBase64(
|
||||
final byte[] src, final int srcOffset, final char[] dest, final int destOffset) {
|
||||
final byte[] input = {
|
||||
(byte) ((src[srcOffset] >>> 2) & 63),
|
||||
(byte) ((src[srcOffset] << 4 | ((src[1 + srcOffset] & 0xff) >>> 4)) & 63),
|
||||
(byte) ((src[1 + srcOffset] << 2 | ((src[2 + srcOffset] & 0xff) >>> 6)) & 63),
|
||||
(byte) ((src[2 + srcOffset]) & 63),
|
||||
};
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
dest[i + destOffset] =
|
||||
(char) (input[i] + 'A' + (((25 - input[i]) >>> 8) & 6) - (((51 - input[i]) >>> 8) & 75)
|
||||
- (((61 - input[i]) >>> 8) & 15) + (((62 - input[i]) >>> 8) & 3));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a WireGuard public or private key from its base64 string representation. This
|
||||
* function throws a {@link KeyFormatException} if the source string is not well-formed.
|
||||
*
|
||||
* @param str the base64 string representation of a WireGuard key
|
||||
* @return the decoded key encapsulated in an immutable container
|
||||
*/
|
||||
public static Key fromBase64(final String str) throws KeyFormatException {
|
||||
final char[] input = str.toCharArray();
|
||||
if (input.length != Format.BASE64.length || input[Format.BASE64.length - 1] != '=')
|
||||
throw new KeyFormatException(Format.BASE64, Type.LENGTH);
|
||||
final byte[] key = new byte[Format.BINARY.length];
|
||||
int i;
|
||||
int ret = 0;
|
||||
for (i = 0; i < key.length / 3; ++i) {
|
||||
final int val = decodeBase64(input, i * 4);
|
||||
ret |= val >>> 31;
|
||||
key[i * 3] = (byte) ((val >>> 16) & 0xff);
|
||||
key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
|
||||
key[i * 3 + 2] = (byte) (val & 0xff);
|
||||
}
|
||||
final char[] endSegment = {
|
||||
input[i * 4],
|
||||
input[i * 4 + 1],
|
||||
input[i * 4 + 2],
|
||||
'A',
|
||||
};
|
||||
final int val = decodeBase64(endSegment, 0);
|
||||
ret |= (val >>> 31) | (val & 0xff);
|
||||
key[i * 3] = (byte) ((val >>> 16) & 0xff);
|
||||
key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
|
||||
|
||||
if (ret != 0)
|
||||
throw new KeyFormatException(Format.BASE64, Type.CONTENTS);
|
||||
return new Key(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a WireGuard public or private key in an immutable container. This function throws a
|
||||
* {@link KeyFormatException} if the source data is not the correct length.
|
||||
*
|
||||
* @param bytes an array of bytes containing a WireGuard key in binary format
|
||||
* @return the key encapsulated in an immutable container
|
||||
*/
|
||||
public static Key fromBytes(final byte[] bytes) throws KeyFormatException {
|
||||
if (bytes.length != Format.BINARY.length)
|
||||
throw new KeyFormatException(Format.BINARY, Type.LENGTH);
|
||||
return new Key(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a WireGuard public or private key from its hexadecimal string representation. This
|
||||
* function throws a {@link KeyFormatException} if the source string is not well-formed.
|
||||
*
|
||||
* @param str the hexadecimal string representation of a WireGuard key
|
||||
* @return the decoded key encapsulated in an immutable container
|
||||
*/
|
||||
public static Key fromHex(final String str) throws KeyFormatException {
|
||||
final char[] input = str.toCharArray();
|
||||
if (input.length != Format.HEX.length)
|
||||
throw new KeyFormatException(Format.HEX, Type.LENGTH);
|
||||
final byte[] key = new byte[Format.BINARY.length];
|
||||
int ret = 0;
|
||||
for (int i = 0; i < key.length; ++i) {
|
||||
int c;
|
||||
int cNum;
|
||||
int cNum0;
|
||||
int cAlpha;
|
||||
int cAlpha0;
|
||||
int cVal;
|
||||
final int cAcc;
|
||||
|
||||
c = input[i * 2];
|
||||
cNum = c ^ 48;
|
||||
cNum0 = ((cNum - 10) >>> 8) & 0xff;
|
||||
cAlpha = (c & ~32) - 55;
|
||||
cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
|
||||
ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
|
||||
cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
|
||||
cAcc = cVal * 16;
|
||||
|
||||
c = input[i * 2 + 1];
|
||||
cNum = c ^ 48;
|
||||
cNum0 = ((cNum - 10) >>> 8) & 0xff;
|
||||
cAlpha = (c & ~32) - 55;
|
||||
cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
|
||||
ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
|
||||
cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
|
||||
key[i] = (byte) (cAcc | cVal);
|
||||
}
|
||||
if (ret != 0)
|
||||
throw new KeyFormatException(Format.HEX, Type.CONTENTS);
|
||||
return new Key(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a private key using the system's {@link SecureRandom} number generator.
|
||||
*
|
||||
* @return a well-formed random private key
|
||||
*/
|
||||
static Key generatePrivateKey() {
|
||||
final SecureRandom secureRandom = new SecureRandom();
|
||||
final byte[] privateKey = new byte[Format.BINARY.getLength()];
|
||||
secureRandom.nextBytes(privateKey);
|
||||
privateKey[0] &= 248;
|
||||
privateKey[31] &= 127;
|
||||
privateKey[31] |= 64;
|
||||
return new Key(privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a public key from an existing private key.
|
||||
*
|
||||
* @param privateKey a private key
|
||||
* @return a well-formed public key that corresponds to the supplied private key
|
||||
*/
|
||||
static Key generatePublicKey(final Key privateKey) {
|
||||
final byte[] publicKey = new byte[Format.BINARY.getLength()];
|
||||
Curve25519.eval(publicKey, 0, privateKey.getBytes(), null);
|
||||
return new Key(publicKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (obj == this)
|
||||
return true;
|
||||
if (obj == null || obj.getClass() != getClass())
|
||||
return false;
|
||||
final Key other = (Key) obj;
|
||||
return MessageDigest.isEqual(key, other.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key as an array of bytes.
|
||||
*
|
||||
* @return an array of bytes containing the raw binary key
|
||||
*/
|
||||
public byte[] getBytes() {
|
||||
// Defensively copy to ensure immutability.
|
||||
return Arrays.copyOf(key, key.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int ret = 0;
|
||||
for (int i = 0; i < key.length / 4; ++i)
|
||||
ret ^= (key[i * 4 + 0] >> 0) + (key[i * 4 + 1] >> 8) + (key[i * 4 + 2] >> 16)
|
||||
+ (key[i * 4 + 3] >> 24);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the key to base64.
|
||||
*
|
||||
* @return a string containing the encoded key
|
||||
*/
|
||||
public String toBase64() {
|
||||
final char[] output = new char[Format.BASE64.length];
|
||||
int i;
|
||||
for (i = 0; i < key.length / 3; ++i) encodeBase64(key, i * 3, output, i * 4);
|
||||
final byte[] endSegment = {
|
||||
key[i * 3],
|
||||
key[i * 3 + 1],
|
||||
0,
|
||||
};
|
||||
encodeBase64(endSegment, 0, output, i * 4);
|
||||
output[Format.BASE64.length - 1] = '=';
|
||||
return new String(output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the key to hexadecimal ASCII characters.
|
||||
*
|
||||
* @return a string containing the encoded key
|
||||
*/
|
||||
public String toHex() {
|
||||
final char[] output = new char[Format.HEX.length];
|
||||
for (int i = 0; i < key.length; ++i) {
|
||||
output[i * 2] = (char) (87 + (key[i] >> 4 & 0xf) + ((((key[i] >> 4 & 0xf) - 10) >> 8) & ~38));
|
||||
output[i * 2 + 1] = (char) (87 + (key[i] & 0xf) + ((((key[i] & 0xf) - 10) >> 8) & ~38));
|
||||
}
|
||||
return new String(output);
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported formats for encoding a WireGuard key.
|
||||
*/
|
||||
public enum Format {
|
||||
BASE64(44),
|
||||
BINARY(32),
|
||||
HEX(64);
|
||||
|
||||
private final int length;
|
||||
|
||||
Format(final int length) {
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.crypto;
|
||||
|
||||
/**
|
||||
* An exception thrown when attempting to parse an invalid key (too short, too long, or byte
|
||||
* data inappropriate for the format). The format being parsed can be accessed with the
|
||||
* {@link #getFormat} method.
|
||||
*/
|
||||
|
||||
public final class KeyFormatException extends Exception {
|
||||
private final Key.Format format;
|
||||
private final Type type;
|
||||
|
||||
KeyFormatException(final Key.Format format, final Type type) {
|
||||
this.format = format;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Key.Format getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public enum Type { CONTENTS, LENGTH }
|
||||
}
|
||||
52
client/android/src/com/wireguard/crypto/KeyPair.java
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.crypto;
|
||||
|
||||
/**
|
||||
* Represents a Curve25519 key pair as used by WireGuard.
|
||||
* <p>
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
|
||||
public class KeyPair {
|
||||
private final Key privateKey;
|
||||
private final Key publicKey;
|
||||
|
||||
/**
|
||||
* Creates a key pair using a newly-generated private key.
|
||||
*/
|
||||
public KeyPair() {
|
||||
this(Key.generatePrivateKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a key pair using an existing private key.
|
||||
*
|
||||
* @param privateKey a private key, used to derive the public key
|
||||
*/
|
||||
public KeyPair(final Key privateKey) {
|
||||
this.privateKey = privateKey;
|
||||
publicKey = Key.generatePublicKey(privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the private key from the key pair.
|
||||
*
|
||||
* @return the private key
|
||||
*/
|
||||
public Key getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public key from the key pair.
|
||||
*
|
||||
* @return the public key
|
||||
*/
|
||||
public Key getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/vpnicon_background"/>
|
||||
<foreground android:drawable="@mipmap/vpnicon_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/vpnicon_background"/>
|
||||
<foreground android:drawable="@mipmap/vpnicon_round"/>
|
||||
</adaptive-icon>
|
||||
BIN
client/android/src/debug/res/mipmap-hdpi/vpnicon.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
client/android/src/debug/res/mipmap-hdpi/vpnicon_foreground.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
client/android/src/debug/res/mipmap-hdpi/vpnicon_round.png
Normal file
|
After Width: | Height: | Size: 4 KiB |
BIN
client/android/src/debug/res/mipmap-mdpi/vpnicon.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
client/android/src/debug/res/mipmap-mdpi/vpnicon_foreground.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
client/android/src/debug/res/mipmap-mdpi/vpnicon_round.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
client/android/src/debug/res/mipmap-xhdpi/vpnicon.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
client/android/src/debug/res/mipmap-xhdpi/vpnicon_foreground.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
client/android/src/debug/res/mipmap-xhdpi/vpnicon_round.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
client/android/src/debug/res/mipmap-xxhdpi/vpnicon.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
BIN
client/android/src/debug/res/mipmap-xxhdpi/vpnicon_round.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
client/android/src/debug/res/mipmap-xxxhdpi/vpnicon.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
BIN
client/android/src/debug/res/mipmap-xxxhdpi/vpnicon_round.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="vpnicon_background">#000000</color>
|
||||
</resources>
|
||||
115
client/android/src/org/amnezia/vpn/NotificationUtil.kt
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import androidx.core.app.NotificationCompat
|
||||
import org.json.JSONObject
|
||||
|
||||
object NotificationUtil {
|
||||
var sCurrentContext: Context? = null
|
||||
private var sNotificationBuilder: NotificationCompat.Builder? = null
|
||||
|
||||
const val NOTIFICATION_CHANNEL_ID = "com.amnezia.vpnNotification"
|
||||
const val CONNECTED_NOTIFICATION_ID = 1337
|
||||
const val tag = "NotificationUtil"
|
||||
|
||||
/**
|
||||
* Updates the current shown notification from a
|
||||
* Parcel - Gets called from AndroidController.cpp
|
||||
*/
|
||||
fun update(data: Parcel) {
|
||||
// [data] is here a json containing the noification content
|
||||
val buffer = data.createByteArray()
|
||||
val json = buffer?.let { String(it) }
|
||||
val content = JSONObject(json)
|
||||
|
||||
update(content.getString("title"), content.getString("message"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current shown notification
|
||||
*/
|
||||
fun update(heading: String, message: String) {
|
||||
if (sCurrentContext == null) return
|
||||
val notificationManager: NotificationManager =
|
||||
sCurrentContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
sNotificationBuilder?.let {
|
||||
it.setContentTitle(heading)
|
||||
.setContentText(message)
|
||||
notificationManager.notify(CONNECTED_NOTIFICATION_ID, it.build())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the default translated "connected" notification, in case the vpn gets started
|
||||
* without the app.
|
||||
*/
|
||||
fun saveFallBackMessage(data: Parcel, context: Context) {
|
||||
// [data] is here a json containing the notification content
|
||||
val buffer = data.createByteArray()
|
||||
val json = buffer?.let { String(it) }
|
||||
val content = JSONObject(json)
|
||||
|
||||
val prefs = Prefs.get(context)
|
||||
prefs.edit()
|
||||
.putString("fallbackNotificationHeader", content.getString("title"))
|
||||
.putString("fallbackNotificationMessage", content.getString("message"))
|
||||
.apply()
|
||||
Log.v(tag, "Saved new fallback message -> ${content.getString("title")}")
|
||||
}
|
||||
|
||||
/*
|
||||
* Creates a new Notification using the current set of Strings
|
||||
* Shows the notification in the given {context}
|
||||
*/
|
||||
fun show(service: VPNService) {
|
||||
sNotificationBuilder = NotificationCompat.Builder(service, NOTIFICATION_CHANNEL_ID)
|
||||
sCurrentContext = service
|
||||
val notificationManager: NotificationManager =
|
||||
sCurrentContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
// From Oreo on we need to have a "notification channel" to post to.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val name = "vpn"
|
||||
val descriptionText = " "
|
||||
val importance = NotificationManager.IMPORTANCE_LOW
|
||||
val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance).apply {
|
||||
description = descriptionText
|
||||
}
|
||||
// Register the channel with the system
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
// In case we do not have gotten a message to show from the Frontend
|
||||
// try to populate the notification with a translated Fallback message
|
||||
val prefs = Prefs.get(service)
|
||||
val message =
|
||||
"" + prefs.getString("fallbackNotificationMessage", "Running in the Background")
|
||||
val header = "" + prefs.getString("fallbackNotificationHeader", "Mozilla VPN")
|
||||
|
||||
// Create the Intent that Should be Fired if the User Clicks the notification
|
||||
val mainActivityName = "org.amnezia.vpn.qt.VPNActivity"
|
||||
val activity = Class.forName(mainActivityName)
|
||||
val intent = Intent(service, activity)
|
||||
val pendingIntent = PendingIntent.getActivity(service, 0, intent, 0)
|
||||
// Build our notification
|
||||
sNotificationBuilder?.let {
|
||||
it.setSmallIcon(org.amnezia.vpn.R.drawable.ic_amnezia_round)
|
||||
.setContentTitle(header)
|
||||
.setContentText(message)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setContentIntent(pendingIntent)
|
||||
|
||||
service.startForeground(CONNECTED_NOTIFICATION_ID, it.build())
|
||||
}
|
||||
}
|
||||
}
|
||||
35
client/android/src/org/amnezia/vpn/Prefs.kt
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
|
||||
object Prefs {
|
||||
// Opens and returns an instance of EncryptedSharedPreferences
|
||||
fun get(context: Context): SharedPreferences {
|
||||
try {
|
||||
val mainKey = MasterKey.Builder(context.applicationContext)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build()
|
||||
|
||||
val sharedPrefsFile = "com.amnezia.vpn.secure.prefs"
|
||||
val sharedPreferences: SharedPreferences = EncryptedSharedPreferences.create(
|
||||
context.applicationContext,
|
||||
sharedPrefsFile,
|
||||
mainKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
return sharedPreferences
|
||||
} catch (e: Exception) {
|
||||
Log.e("Android-Prefs", "Getting Encryption Storage failed, plaintext fallback")
|
||||
return context.getSharedPreferences("com.amnezia.vpn.prefrences", Context.MODE_PRIVATE)
|
||||
}
|
||||
}
|
||||
}
|
||||
72
client/android/src/org/amnezia/vpn/VPNLogger.kt
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.content.Context
|
||||
import java.io.File
|
||||
import java.time.LocalDateTime
|
||||
import android.util.Log as nativeLog
|
||||
|
||||
/*
|
||||
* Drop in replacement for android.util.Log
|
||||
* Also stores a copy of all logs in tmp/mozilla_deamon_logs.txt
|
||||
*/
|
||||
class Log {
|
||||
val LOG_MAX_FILE_SIZE = 204800
|
||||
private var file: File
|
||||
private constructor(context: Context) {
|
||||
val tempDIR = context.cacheDir
|
||||
file = File(tempDIR, "mozilla_deamon_logs.txt")
|
||||
if (file.length() > LOG_MAX_FILE_SIZE) {
|
||||
file.writeText("")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
var instance: Log? = null
|
||||
fun init(ctx: Context) {
|
||||
if (instance == null) {
|
||||
instance = Log(ctx)
|
||||
}
|
||||
}
|
||||
fun i(tag: String, message: String) {
|
||||
instance?.write("[info] - ($tag) - $message")
|
||||
if (!BuildConfig.DEBUG) { return; }
|
||||
nativeLog.i(tag, message)
|
||||
}
|
||||
fun v(tag: String, message: String) {
|
||||
instance?.write("($tag) - $message")
|
||||
if (!BuildConfig.DEBUG) { return; }
|
||||
nativeLog.v(tag, message)
|
||||
}
|
||||
fun e(tag: String, message: String) {
|
||||
instance?.write("[error] - ($tag) - $message")
|
||||
if (!BuildConfig.DEBUG) { return; }
|
||||
nativeLog.e(tag, message)
|
||||
}
|
||||
// Only Prints && Loggs when in debug, noop in release.
|
||||
fun sensitive(tag: String, message: String?) {
|
||||
if (!BuildConfig.DEBUG) { return; }
|
||||
if (message == null) { return; }
|
||||
e(tag, message)
|
||||
}
|
||||
|
||||
fun getContent(): String? {
|
||||
return try {
|
||||
instance?.file?.readText()
|
||||
} catch (e: Exception) {
|
||||
"=== Failed to read Daemon Logs === \n ${e.localizedMessage} "
|
||||
}
|
||||
}
|
||||
|
||||
fun clearFile() {
|
||||
instance?.file?.writeText("")
|
||||
}
|
||||
}
|
||||
private fun write(message: String) {
|
||||
LocalDateTime.now()
|
||||
file.appendText("[${LocalDateTime.now()}] $message \n")
|
||||
}
|
||||
}
|
||||
309
client/android/src/org/amnezia/vpn/VPNService.kt
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.system.OsConstants
|
||||
import com.wireguard.android.util.SharedLibraryLoader
|
||||
import com.wireguard.config.*
|
||||
import com.wireguard.crypto.Key
|
||||
import org.json.JSONObject
|
||||
|
||||
class VPNService : android.net.VpnService() {
|
||||
private val tag = "VPNService"
|
||||
private var mBinder: VPNServiceBinder = VPNServiceBinder(this)
|
||||
private var mConfig: JSONObject? = null
|
||||
private var mConnectionTime: Long = 0
|
||||
private var mAlreadyInitialised = false
|
||||
|
||||
private var currentTunnelHandle = -1
|
||||
|
||||
fun init() {
|
||||
if (mAlreadyInitialised) {
|
||||
return
|
||||
}
|
||||
Log.init(this)
|
||||
SharedLibraryLoader.loadSharedLibrary(this, "wg-go")
|
||||
Log.i(tag, "loaded lib")
|
||||
Log.e(tag, "Wireguard Version ${wgVersion()}")
|
||||
mAlreadyInitialised = true
|
||||
}
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
if (!isUp) {
|
||||
// If the Qt Client got closed while we were not connected
|
||||
// we do not need to stay as a foreground service.
|
||||
stopForeground(true)
|
||||
}
|
||||
return super.onUnbind(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* EntryPoint for the Service, gets Called when AndroidController.cpp
|
||||
* calles bindService. Returns the [VPNServiceBinder] so QT can send Requests to it.
|
||||
*/
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
Log.v(tag, "Got Bind request")
|
||||
init()
|
||||
return mBinder
|
||||
}
|
||||
|
||||
/**
|
||||
* Might be the entryPoint if the Service gets Started via an
|
||||
* Service Intent: Might be from Always-On-Vpn from Settings
|
||||
* or from Booting the device and having "connect on boot" enabled.
|
||||
*/
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
init()
|
||||
intent?.let {
|
||||
if (intent.getBooleanExtra("startOnly", false)) {
|
||||
Log.i(tag, "Start only!")
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
}
|
||||
// This start is from always-on
|
||||
if (this.mConfig == null) {
|
||||
// We don't have tunnel to turn on - Try to create one with last config the service got
|
||||
val prefs = Prefs.get(this)
|
||||
val lastConfString = prefs.getString("lastConf", "")
|
||||
if (lastConfString.isNullOrEmpty()) {
|
||||
// We have nothing to connect to -> Exit
|
||||
Log.e(
|
||||
tag,
|
||||
"VPN service was triggered without defining a Server or having a tunnel"
|
||||
)
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
this.mConfig = JSONObject(lastConfString)
|
||||
}
|
||||
turnOn(this.mConfig!!)
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
// Invoked when the application is revoked.
|
||||
// At this moment, the VPN interface is already deactivated by the system.
|
||||
override fun onRevoke() {
|
||||
this.turnOff()
|
||||
super.onRevoke()
|
||||
}
|
||||
|
||||
var connectionTime: Long = 0
|
||||
get() {
|
||||
return mConnectionTime
|
||||
}
|
||||
|
||||
var isUp: Boolean
|
||||
get() {
|
||||
return currentTunnelHandle >= 0
|
||||
}
|
||||
private set(value) {
|
||||
if (value) {
|
||||
mBinder.dispatchEvent(VPNServiceBinder.EVENTS.connected, "")
|
||||
mConnectionTime = System.currentTimeMillis()
|
||||
return
|
||||
}
|
||||
mBinder.dispatchEvent(VPNServiceBinder.EVENTS.disconnected, "")
|
||||
mConnectionTime = 0
|
||||
}
|
||||
val status: JSONObject
|
||||
get() {
|
||||
val deviceIpv4: String = ""
|
||||
return JSONObject().apply {
|
||||
putOpt("rx_bytes", getConfigValue("rx_bytes"))
|
||||
putOpt("tx_bytes", getConfigValue("tx_bytes"))
|
||||
putOpt("endpoint", mConfig?.getJSONObject("server")?.getString("ipv4Gateway"))
|
||||
putOpt("deviceIpv4", mConfig?.getJSONObject("device")?.getString("ipv4Address"))
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Checks if the VPN Permission is given.
|
||||
* If the permission is given, returns true
|
||||
* Requests permission and returns false if not.
|
||||
*/
|
||||
fun checkPermissions(): Boolean {
|
||||
// See https://developer.android.com/guide/topics/connectivity/vpn#connect_a_service
|
||||
// Call Prepare, if we get an Intent back, we dont have the VPN Permission
|
||||
// from the user. So we need to pass this to our main Activity and exit here.
|
||||
val intent = prepare(this)
|
||||
if (intent == null) {
|
||||
Log.e(tag, "VPN Permission Already Present")
|
||||
return true
|
||||
}
|
||||
Log.e(tag, "Requesting VPN Permission")
|
||||
return false
|
||||
}
|
||||
|
||||
fun turnOn(json: JSONObject) {
|
||||
Log.sensitive(tag, json.toString())
|
||||
val wireguard_conf = buildWireugardConfig(json)
|
||||
|
||||
if (!checkPermissions()) {
|
||||
Log.e(tag, "turn on was called without no permissions present!")
|
||||
isUp = false
|
||||
return
|
||||
}
|
||||
Log.i(tag, "Permission okay")
|
||||
if (currentTunnelHandle != -1) {
|
||||
Log.e(tag, "Tunnel already up")
|
||||
// Turn the tunnel down because this might be a switch
|
||||
wgTurnOff(currentTunnelHandle)
|
||||
}
|
||||
val wgConfig: String = wireguard_conf!!.toWgUserspaceString()
|
||||
val builder = Builder()
|
||||
setupBuilder(wireguard_conf, builder)
|
||||
builder.setSession("mvpn0")
|
||||
builder.establish().use { tun ->
|
||||
if (tun == null)return
|
||||
Log.i(tag, "Go backend " + wgVersion())
|
||||
currentTunnelHandle = wgTurnOn("mvpn0", tun.detachFd(), wgConfig)
|
||||
}
|
||||
if (currentTunnelHandle < 0) {
|
||||
Log.e(tag, "Activation Error Code -> $currentTunnelHandle")
|
||||
isUp = false
|
||||
return
|
||||
}
|
||||
protect(wgGetSocketV4(currentTunnelHandle))
|
||||
protect(wgGetSocketV6(currentTunnelHandle))
|
||||
mConfig = json
|
||||
isUp = true
|
||||
|
||||
// Store the config in case the service gets
|
||||
// asked boot vpn from the OS
|
||||
val prefs = Prefs.get(this)
|
||||
prefs.edit()
|
||||
.putString("lastConf", json.toString())
|
||||
.apply()
|
||||
|
||||
NotificationUtil.show(this) // Go foreground
|
||||
}
|
||||
|
||||
fun turnOff() {
|
||||
Log.v(tag, "Try to disable tunnel")
|
||||
wgTurnOff(currentTunnelHandle)
|
||||
currentTunnelHandle = -1
|
||||
stopForeground(false)
|
||||
isUp = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures an Android VPN Service Tunnel
|
||||
* with a given Wireguard Config
|
||||
*/
|
||||
private fun setupBuilder(config: Config, builder: Builder) {
|
||||
// Setup Split tunnel
|
||||
for (excludedApplication in config.`interface`.excludedApplications)
|
||||
builder.addDisallowedApplication(excludedApplication)
|
||||
|
||||
// Device IP
|
||||
for (addr in config.`interface`.addresses) builder.addAddress(addr.address, addr.mask)
|
||||
// DNS
|
||||
for (addr in config.`interface`.dnsServers) builder.addDnsServer(addr.hostAddress)
|
||||
// Add All routes the VPN may route tos
|
||||
for (peer in config.peers) {
|
||||
for (addr in peer.allowedIps) {
|
||||
builder.addRoute(addr.address, addr.mask)
|
||||
}
|
||||
}
|
||||
builder.allowFamily(OsConstants.AF_INET)
|
||||
builder.allowFamily(OsConstants.AF_INET6)
|
||||
builder.setMtu(config.`interface`.mtu.orElse(1280))
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) builder.setMetered(false)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) setUnderlyingNetworks(null)
|
||||
|
||||
builder.setBlocking(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets config value for {key} from the Current
|
||||
* running Wireguard tunnel
|
||||
*/
|
||||
private fun getConfigValue(key: String): String? {
|
||||
if (!isUp) {
|
||||
return null
|
||||
}
|
||||
val config = wgGetConfig(currentTunnelHandle) ?: return null
|
||||
val lines = config.split("\n")
|
||||
for (line in lines) {
|
||||
val parts = line.split("=")
|
||||
val k = parts.first()
|
||||
val value = parts.last()
|
||||
if (key == k) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Wireguard [Config] from a [json] string -
|
||||
* The [json] will be created in AndroidController.cpp
|
||||
*/
|
||||
private fun buildWireugardConfig(obj: JSONObject): Config {
|
||||
val confBuilder = Config.Builder()
|
||||
val jServer = obj.getJSONObject("server")
|
||||
val peerBuilder = Peer.Builder()
|
||||
val ep =
|
||||
InetEndpoint.parse(jServer.getString("ipv4AddrIn") + ":" + jServer.getString("port"))
|
||||
peerBuilder.setEndpoint(ep)
|
||||
peerBuilder.setPublicKey(Key.fromBase64(jServer.getString("publicKey")))
|
||||
|
||||
val jAllowedIPList = obj.getJSONArray("allowedIPs")
|
||||
if (jAllowedIPList.length() == 0) {
|
||||
val internet = InetNetwork.parse("0.0.0.0/0") // aka The whole internet.
|
||||
peerBuilder.addAllowedIp(internet)
|
||||
} else {
|
||||
(0 until jAllowedIPList.length()).toList().forEach {
|
||||
val network = InetNetwork.parse(jAllowedIPList.getString(it))
|
||||
peerBuilder.addAllowedIp(network)
|
||||
}
|
||||
}
|
||||
|
||||
confBuilder.addPeer(peerBuilder.build())
|
||||
|
||||
val privateKey = obj.getJSONObject("keys").getString("privateKey")
|
||||
val jDevice = obj.getJSONObject("device")
|
||||
|
||||
val ifaceBuilder = Interface.Builder()
|
||||
ifaceBuilder.parsePrivateKey(privateKey)
|
||||
ifaceBuilder.addAddress(InetNetwork.parse(jDevice.getString("ipv4Address")))
|
||||
ifaceBuilder.addAddress(InetNetwork.parse(jDevice.getString("ipv6Address")))
|
||||
ifaceBuilder.addDnsServer(InetNetwork.parse(obj.getString("dns")).address)
|
||||
val jExcludedApplication = obj.getJSONArray("excludedApps")
|
||||
(0 until jExcludedApplication.length()).toList().forEach {
|
||||
val appName = jExcludedApplication.get(it).toString()
|
||||
ifaceBuilder.excludeApplication(appName)
|
||||
}
|
||||
confBuilder.setInterface(ifaceBuilder.build())
|
||||
return confBuilder.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun startService(c: Context) {
|
||||
c.applicationContext.startService(
|
||||
Intent(c.applicationContext, VPNService::class.java).apply {
|
||||
putExtra("startOnly", true)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private external fun wgGetConfig(handle: Int): String?
|
||||
@JvmStatic
|
||||
private external fun wgGetSocketV4(handle: Int): Int
|
||||
@JvmStatic
|
||||
private external fun wgGetSocketV6(handle: Int): Int
|
||||
@JvmStatic
|
||||
private external fun wgTurnOff(handle: Int)
|
||||
@JvmStatic
|
||||
private external fun wgTurnOn(ifName: String, tunFd: Int, settings: String): Int
|
||||
@JvmStatic
|
||||
private external fun wgVersion(): String?
|
||||
}
|
||||
}
|
||||
170
client/android/src/org/amnezia/vpn/VPNServiceBinder.kt
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.amnezia.vpn
|
||||
import android.os.Binder
|
||||
import android.os.DeadObjectException
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import com.wireguard.config.*
|
||||
import org.json.JSONObject
|
||||
import java.lang.Exception
|
||||
|
||||
class VPNServiceBinder(service: VPNService) : Binder() {
|
||||
|
||||
private val mService = service
|
||||
private val tag = "VPNServiceBinder"
|
||||
private var mListener: IBinder? = null
|
||||
private var mResumeConfig: JSONObject? = null
|
||||
|
||||
/**
|
||||
* The codes this Binder does accept in [onTransact]
|
||||
*/
|
||||
object ACTIONS {
|
||||
const val activate = 1
|
||||
const val deactivate = 2
|
||||
const val registerEventListener = 3
|
||||
const val requestStatistic = 4
|
||||
const val requestGetLog = 5
|
||||
const val requestCleanupLog = 6
|
||||
const val resumeActivate = 7
|
||||
const val setNotificationText = 8
|
||||
const val setFallBackNotification = 9
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets called when the VPNServiceBinder gets a request from a Client.
|
||||
* The [code] determines what action is requested. - see [ACTIONS]
|
||||
* [data] may contain a utf-8 encoded json string with optional args or is null.
|
||||
* [reply] is a pointer to a buffer in the clients memory, to reply results.
|
||||
* we use this to send result data.
|
||||
*
|
||||
* returns true if the [code] was accepted
|
||||
*/
|
||||
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
|
||||
Log.i(tag, "GOT TRANSACTION $code")
|
||||
|
||||
when (code) {
|
||||
ACTIONS.activate -> {
|
||||
try {
|
||||
Log.i(tag, "Activiation Requested, parsing Config")
|
||||
// [data] is here a json containing the wireguard conf
|
||||
val buffer = data.createByteArray()
|
||||
val json = buffer?.let { String(it) }
|
||||
val config = JSONObject(json)
|
||||
Log.v(tag, "Stored new Tunnel config in Service")
|
||||
|
||||
if (!mService.checkPermissions()) {
|
||||
mResumeConfig = config
|
||||
// The Permission prompt was already
|
||||
// send, in case it's accepted we will
|
||||
// receive ACTIONS.resumeActivate
|
||||
return true
|
||||
}
|
||||
this.mService.turnOn(config)
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "An Error occurred while enabling the VPN: ${e.localizedMessage}")
|
||||
dispatchEvent(EVENTS.activationError, e.localizedMessage)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
ACTIONS.resumeActivate -> {
|
||||
// [data] is empty
|
||||
// Activate the current tunnel
|
||||
try {
|
||||
mResumeConfig?.let { this.mService.turnOn(it) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "An Error occurred while enabling the VPN: ${e.localizedMessage}")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
ACTIONS.deactivate -> {
|
||||
// [data] here is empty
|
||||
this.mService.turnOff()
|
||||
return true
|
||||
}
|
||||
|
||||
ACTIONS.registerEventListener -> {
|
||||
// [data] contains the Binder that we need to dispatch the Events
|
||||
val binder = data.readStrongBinder()
|
||||
mListener = binder
|
||||
val obj = JSONObject()
|
||||
obj.put("connected", mService.isUp)
|
||||
obj.put("time", mService.connectionTime)
|
||||
dispatchEvent(EVENTS.init, obj.toString())
|
||||
return true
|
||||
}
|
||||
|
||||
ACTIONS.requestStatistic -> {
|
||||
dispatchEvent(EVENTS.statisticUpdate, mService.status.toString())
|
||||
return true
|
||||
}
|
||||
|
||||
ACTIONS.requestGetLog -> {
|
||||
// Grabs all the Logs and dispatch new Log Event
|
||||
dispatchEvent(EVENTS.backendLogs, Log.getContent())
|
||||
return true
|
||||
}
|
||||
ACTIONS.requestCleanupLog -> {
|
||||
Log.clearFile()
|
||||
return true
|
||||
}
|
||||
ACTIONS.setNotificationText -> {
|
||||
NotificationUtil.update(data)
|
||||
return true
|
||||
}
|
||||
ACTIONS.setFallBackNotification -> {
|
||||
NotificationUtil.saveFallBackMessage(data, mService)
|
||||
return true
|
||||
}
|
||||
IBinder.LAST_CALL_TRANSACTION -> {
|
||||
Log.e(tag, "The OS Requested to shut down the VPN")
|
||||
this.mService.turnOff()
|
||||
return true
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.e(tag, "Received invalid bind request \t Code -> $code")
|
||||
// If we're hitting this there is probably something wrong in the client.
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an Event to all registered Binders
|
||||
* [code] the Event that happened - see [EVENTS]
|
||||
* To register an Eventhandler use [onTransact] with
|
||||
* [ACTIONS.registerEventListener]
|
||||
*/
|
||||
fun dispatchEvent(code: Int, payload: String?) {
|
||||
try {
|
||||
mListener?.let {
|
||||
if (it.isBinderAlive) {
|
||||
val data = Parcel.obtain()
|
||||
data.writeByteArray(payload?.toByteArray(charset("UTF-8")))
|
||||
it.transact(code, data, Parcel.obtain(), 0)
|
||||
}
|
||||
}
|
||||
} catch (e: DeadObjectException) {
|
||||
// If the QT Process is killed (not just inactive)
|
||||
// we cant access isBinderAlive, so nothing to do here.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The codes we Are Using in case of [dispatchEvent]
|
||||
*/
|
||||
object EVENTS {
|
||||
const val init = 0
|
||||
const val connected = 1
|
||||
const val disconnected = 2
|
||||
const val statisticUpdate = 3
|
||||
const val backendLogs = 4
|
||||
const val activationError = 5
|
||||
}
|
||||
}
|
||||
189
client/android/src/org/amnezia/vpn/qt/PackageManagerHelper.java
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.amnezia.vpn.qt;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.Manifest.permission;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
// Gets used by /platforms/android/androidAppListProvider.cpp
|
||||
public class PackageManagerHelper {
|
||||
final static String TAG = "PackageManagerHelper";
|
||||
final static int MIN_CHROME_VERSION = 65;
|
||||
|
||||
final static List<String> CHROME_BROWSERS = Arrays.asList(
|
||||
new String[] {"com.google.android.webview", "com.android.webview", "com.google.chrome"});
|
||||
|
||||
private static String getAllAppNames(Context ctx) {
|
||||
JSONObject output = new JSONObject();
|
||||
PackageManager pm = ctx.getPackageManager();
|
||||
List<String> browsers = getBrowserIDs(pm);
|
||||
List<PackageInfo> packs = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS);
|
||||
for (int i = 0; i < packs.size(); i++) {
|
||||
PackageInfo p = packs.get(i);
|
||||
// Do not add ourselves and System Apps to the list, unless it might be a browser
|
||||
if ((!isSystemPackage(p,pm) || browsers.contains(p.packageName))
|
||||
&& !isSelf(p)) {
|
||||
String appid = p.packageName;
|
||||
String appName = p.applicationInfo.loadLabel(pm).toString();
|
||||
try {
|
||||
output.put(appid, appName);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
private static Drawable getAppIcon(Context ctx, String id) {
|
||||
try {
|
||||
return ctx.getPackageManager().getApplicationIcon(id);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return new ColorDrawable(Color.TRANSPARENT);
|
||||
}
|
||||
|
||||
private static boolean isSystemPackage(PackageInfo pkgInfo, PackageManager pm) {
|
||||
if( (pkgInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0){
|
||||
// no system app
|
||||
return false;
|
||||
}
|
||||
// For Systems Packages there are Cases where we want to add it anyway:
|
||||
// Has the use Internet permission (otherwise makes no sense)
|
||||
// Had at least 1 update (this means it's probably on any AppStore)
|
||||
// Has a a launch activity (has a ui and is not just a system service)
|
||||
|
||||
if(!usesInternet(pkgInfo)){
|
||||
return true;
|
||||
}
|
||||
if(!hadUpdate(pkgInfo)){
|
||||
return true;
|
||||
}
|
||||
if(pm.getLaunchIntentForPackage(pkgInfo.packageName) == null){
|
||||
// If there is no way to launch this from a homescreen, def a sys package
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private static boolean isSelf(PackageInfo pkgInfo) {
|
||||
return pkgInfo.packageName.equals("org.amnezia.vpn")
|
||||
|| pkgInfo.packageName.equals("org.amnezia.vpn.debug");
|
||||
}
|
||||
private static boolean usesInternet(PackageInfo pkgInfo){
|
||||
if(pkgInfo.requestedPermissions == null){
|
||||
return false;
|
||||
}
|
||||
for(int i=0; i < pkgInfo.requestedPermissions.length; i++) {
|
||||
String permission = pkgInfo.requestedPermissions[i];
|
||||
if(Manifest.permission.INTERNET.equals(permission)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private static boolean hadUpdate(PackageInfo pkgInfo){
|
||||
return pkgInfo.lastUpdateTime > pkgInfo.firstInstallTime;
|
||||
}
|
||||
|
||||
// Returns List of all Packages that can classify themselves as browsers
|
||||
private static List<String> getBrowserIDs(PackageManager pm) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.mozilla.org/"));
|
||||
intent.addCategory(Intent.CATEGORY_BROWSABLE);
|
||||
// We've tried using PackageManager.MATCH_DEFAULT_ONLY flag and found that browsers that
|
||||
// are not set as the default browser won't be matched even if they had CATEGORY_DEFAULT set
|
||||
// in the intent filter
|
||||
|
||||
List<ResolveInfo> resolveInfos = pm.queryIntentActivities(intent, PackageManager.MATCH_ALL);
|
||||
List<String> browsers = new ArrayList<String>();
|
||||
for (int i = 0; i < resolveInfos.size(); i++) {
|
||||
ResolveInfo info = resolveInfos.get(i);
|
||||
String browserID = info.activityInfo.packageName;
|
||||
browsers.add(browserID);
|
||||
}
|
||||
return browsers;
|
||||
}
|
||||
|
||||
// Gets called in AndroidAuthenticationListener;
|
||||
public static boolean isWebViewSupported(Context ctx) {
|
||||
Log.v(TAG, "Checking if installed Webview is compatible with FxA");
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
// The default Webview is able do to FXA
|
||||
return true;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
PackageInfo pi = WebView.getCurrentWebViewPackage();
|
||||
if (CHROME_BROWSERS.contains(pi.packageName)) {
|
||||
return isSupportedChromeBrowser(pi);
|
||||
}
|
||||
return isNotAncientBrowser(pi);
|
||||
}
|
||||
|
||||
// Before O the webview is hardcoded, but we dont know which package it is.
|
||||
// Check if com.google.android.webview is installed
|
||||
PackageManager pm = ctx.getPackageManager();
|
||||
try {
|
||||
PackageInfo pi = pm.getPackageInfo("com.google.android.webview", 0);
|
||||
return isSupportedChromeBrowser(pi);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
}
|
||||
// Otherwise check com.android.webview
|
||||
try {
|
||||
PackageInfo pi = pm.getPackageInfo("com.android.webview", 0);
|
||||
return isSupportedChromeBrowser(pi);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
}
|
||||
Log.e(TAG, "Android System WebView is not found");
|
||||
// Giving up :(
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isSupportedChromeBrowser(PackageInfo pi) {
|
||||
Log.d(TAG, "Checking Chrome Based Browser: " + pi.packageName);
|
||||
Log.d(TAG, "version name: " + pi.versionName);
|
||||
Log.d(TAG, "version code: " + pi.versionCode);
|
||||
try {
|
||||
String versionCode = pi.versionName.split(Pattern.quote(" "))[0];
|
||||
String majorVersion = versionCode.split(Pattern.quote("."))[0];
|
||||
int version = Integer.parseInt(majorVersion);
|
||||
return version >= MIN_CHROME_VERSION;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to check Chrome Version Code " + pi.versionName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isNotAncientBrowser(PackageInfo pi) {
|
||||
// Not a google chrome - So the version name is worthless
|
||||
// Lets just make sure the WebView
|
||||
// used is not ancient ==> Was updated in at least the last 365 days
|
||||
Log.d(TAG, "Checking Chrome Based Browser: " + pi.packageName);
|
||||
Log.d(TAG, "version name: " + pi.versionName);
|
||||
Log.d(TAG, "version code: " + pi.versionCode);
|
||||
double oneYearInMillis = 31536000000L;
|
||||
return pi.lastUpdateTime > (System.currentTimeMillis() - oneYearInMillis);
|
||||
}
|
||||
}
|
||||
33
client/android/src/org/amnezia/vpn/qt/VPNActivity.java
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.amnezia.vpn.qt;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
|
||||
public class VPNActivity extends org.qtproject.qt5.android.bindings.QtActivity {
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
try {
|
||||
if (!handleBackButton()) {
|
||||
// Move the activity into paused state if back button was pressed
|
||||
moveTaskToBack(true);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if MVPN has handled the back button
|
||||
native boolean handleBackButton();
|
||||
}
|
||||
18
client/android/src/org/amnezia/vpn/qt/VPNApplication.java
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package org.amnezia.vpn.qt;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.amnezia.vpn.BuildConfig;
|
||||
|
||||
public class VPNApplication extends org.qtproject.qt5.android.bindings.QtApplication {
|
||||
|
||||
private static VPNApplication instance;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
VPNApplication.instance = this;
|
||||
}
|
||||
|
||||
}
|
||||
37
client/android/src/org/amnezia/vpn/qt/VPNPermissionHelper.kt
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.amnezia.vpn.qt
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
||||
class VPNPermissionHelper : android.net.VpnService() {
|
||||
/**
|
||||
* This small service does nothing else then checking if the vpn permission
|
||||
* is present and prompting if not.
|
||||
*/
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val intent = prepare(this.applicationContext)
|
||||
if (intent != null) {
|
||||
startActivityForResult(intent)
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun startService(c: Context) {
|
||||
val appC = c.applicationContext
|
||||
appC.startService(Intent(appC, VPNPermissionHelper::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the Global QTAndroidActivity and calls startActivityForResult with the given intent
|
||||
* Is used to request the VPN-Permission, if not given.
|
||||
* Actually Implemented in src/platforms/android/AndroidController.cpp
|
||||
*/
|
||||
external fun startActivityForResult(i: Intent)
|
||||
}
|
||||