value) {
- return String.format("%s = %s%n", token, iterableToString(value));
- }
-
- @Nullable
- public String parse(final CharSequence line) {
- final Matcher matcher = pattern.matcher(line);
- return matcher.matches() ? matcher.group(1) : null;
- }
-
- @Nullable
- public String[] parseList(final CharSequence line) {
- final Matcher matcher = pattern.matcher(line);
- return matcher.matches() ? stringToList(matcher.group(1)) : null;
+ public String getValue() {
+ return value;
}
}
diff --git a/app/src/main/java/com/wireguard/config/Config.java b/app/src/main/java/com/wireguard/config/Config.java
index 61e31838..7645583d 100644
--- a/app/src/main/java/com/wireguard/config/Config.java
+++ b/app/src/main/java/com/wireguard/config/Config.java
@@ -5,170 +5,193 @@
package com.wireguard.config;
-import android.content.Context;
-import android.databinding.BaseObservable;
-import android.databinding.Bindable;
-import android.databinding.ObservableArrayList;
-import android.databinding.ObservableList;
-import android.os.Parcel;
-import android.os.Parcelable;
import android.support.annotation.Nullable;
-import com.android.databinding.library.baseAdapters.BR;
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
-import java.io.StringReader;
-import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Objects;
+import java.util.Set;
/**
- * Represents a wg-quick configuration file, its name, and its connection state.
+ * 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).
+ *
+ * Instances of this class are immutable.
*/
+public final class Config {
+ private final Interface interfaze;
+ private final List peers;
-public class Config {
- private final Interface interfaceSection = new Interface();
- private List peers = new ArrayList<>();
-
- public static Config from(final String string) throws IOException {
- return from(new BufferedReader(new StringReader(string)));
+ 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));
}
- public static Config from(final InputStream stream) throws IOException {
- return from(new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)));
- }
-
- public static Config from(final BufferedReader reader) throws IOException {
- final Config config = new Config();
- final Context context = Application.get();
- Peer currentPeer = null;
- String line;
- boolean inInterfaceSection = false;
- 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 ("[Interface]".toLowerCase().equals(line.toLowerCase())) {
- currentPeer = null;
- inInterfaceSection = true;
- } else if ("[Peer]".toLowerCase().equals(line.toLowerCase())) {
- currentPeer = new Peer();
- config.peers.add(currentPeer);
- inInterfaceSection = false;
- } else if (inInterfaceSection) {
- config.interfaceSection.parse(line);
- } else if (currentPeer != null) {
- currentPeer.parse(line);
- } else {
- throw new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_config_line, line));
+ /**
+ * Parses an series of "Interface" and "Peer" sections into a {@code Config}. Throws
+ * {@link ParseException} if the input is not well-formed or contains unparseable sections.
+ *
+ * @param stream a stream of UTF-8 text that is interpreted as a WireGuard configuration file
+ * @return a {@code Config} instance representing the supplied configuration
+ */
+ public static Config parse(final InputStream stream) throws IOException, ParseException {
+ final Builder builder = new Builder();
+ try (final BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
+ final Collection interfaceLines = new ArrayList<>();
+ final Collection peerLines = new ArrayList<>();
+ boolean inInterfaceSection = false;
+ boolean inPeerSection = 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;
+ } else if ("[Peer]".equalsIgnoreCase(line)) {
+ inInterfaceSection = false;
+ inPeerSection = true;
+ } else {
+ throw new ParseException("top level", line, "Unknown section name");
+ }
+ } else if (inInterfaceSection) {
+ interfaceLines.add(line);
+ } else if (inPeerSection) {
+ peerLines.add(line);
+ } else {
+ throw new ParseException("top level", line, "Expected [Interface] or [Peer]");
+ }
}
+ if (inPeerSection)
+ builder.parsePeer(peerLines);
+ else if (!inInterfaceSection)
+ throw new ParseException("top level", "", "Empty configuration");
+ // Combine all [Interface] sections in the file.
+ builder.parseInterface(interfaceLines);
}
- if (!inInterfaceSection && currentPeer == null) {
- throw new IllegalArgumentException(context.getString(R.string.tunnel_error_no_config_information));
- }
- return config;
+ 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 interfaceSection;
+ return interfaze;
}
+ /**
+ * Returns a list of the configuration's peer sections.
+ *
+ * @return a list of {@link Peer}s
+ */
public List 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() {
- final StringBuilder sb = new StringBuilder().append(interfaceSection);
+ 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').append(peer);
+ sb.append("\n[Peer]\n").append(peer.toWgQuickString());
return sb.toString();
}
- public static class Observable extends BaseObservable implements Parcelable {
- public static final Creator CREATOR = new Creator() {
- @Override
- public Observable createFromParcel(final Parcel in) {
- return new Observable(in);
- }
+ /**
+ * 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();
+ }
- @Override
- public Observable[] newArray(final int size) {
- return new Observable[size];
- }
- };
- private final Interface.Observable observableInterface;
- private final ObservableList observablePeers;
- @Nullable private String name;
+ @SuppressWarnings("UnusedReturnValue")
+ public static final class Builder {
+ // Defaults to an empty set.
+ private final Set peers = new LinkedHashSet<>();
+ // No default; must be provided before building.
+ @Nullable private Interface interfaze;
- public Observable(@Nullable final Config parent, @Nullable final String name) {
- this.name = name;
-
- observableInterface = new Interface.Observable(parent == null ? null : parent.interfaceSection);
- observablePeers = new ObservableArrayList<>();
- if (parent != null) {
- for (final Peer peer : parent.getPeers())
- observablePeers.add(new Peer.Observable(peer));
- }
+ public Builder addPeer(final Peer peer) {
+ peers.add(peer);
+ return this;
}
- private Observable(final Parcel in) {
- name = in.readString();
- observableInterface = in.readParcelable(Interface.Observable.class.getClassLoader());
- observablePeers = new ObservableArrayList<>();
- in.readTypedList(observablePeers, Peer.Observable.CREATOR);
+ public Builder addPeers(final Collection peers) {
+ this.peers.addAll(peers);
+ return this;
}
- public void commitData(final Config parent) {
- observableInterface.commitData(parent.interfaceSection);
- final List newPeers = new ArrayList<>(observablePeers.size());
- for (final Peer.Observable observablePeer : observablePeers) {
- final Peer peer = new Peer();
- observablePeer.commitData(peer);
- newPeers.add(peer);
- }
- parent.peers = newPeers;
- notifyChange();
+ public Config build() {
+ return new Config(this);
}
- @Override
- public int describeContents() {
- return 0;
+ public Builder parseInterface(final Iterable extends CharSequence> lines) throws ParseException {
+ return setInterface(Interface.parse(lines));
}
- @Bindable
- public Interface.Observable getInterfaceSection() {
- return observableInterface;
+ public Builder parsePeer(final Iterable extends CharSequence> lines) throws ParseException {
+ return addPeer(Peer.parse(lines));
}
- @Bindable
- public String getName() {
- return name == null ? "" : name;
- }
-
- @Bindable
- public ObservableList getPeers() {
- return observablePeers;
- }
-
- public void setName(final String name) {
- this.name = name;
- notifyPropertyChanged(BR.name);
- }
-
- @Override
- public void writeToParcel(final Parcel dest, final int flags) {
- dest.writeString(name);
- dest.writeParcelable(observableInterface, flags);
- dest.writeTypedList(observablePeers);
+ public Builder setInterface(final Interface interfaze) {
+ this.interfaze = interfaze;
+ return this;
}
}
}
diff --git a/app/src/main/java/com/wireguard/config/InetAddresses.java b/app/src/main/java/com/wireguard/config/InetAddresses.java
index c50c5a0e..989598da 100644
--- a/app/src/main/java/com/wireguard/config/InetAddresses.java
+++ b/app/src/main/java/com/wireguard/config/InetAddresses.java
@@ -5,21 +5,22 @@
package com.wireguard.config;
-import android.support.annotation.Nullable;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
import java.net.InetAddress;
+/**
+ * Utility methods for creating instances of {@link InetAddress}.
+ */
public final class InetAddresses {
private static final Method PARSER_METHOD;
static {
try {
// This method is only present on Android.
+ // noinspection JavaReflectionMemberAccess
PARSER_METHOD = InetAddress.class.getMethod("parseNumericAddress", String.class);
} catch (final NoSuchMethodException e) {
throw new RuntimeException(e);
@@ -30,13 +31,23 @@ public final class InetAddresses {
// Prevent instantiation.
}
- public static InetAddress parse(@Nullable final String address) {
- if (address == null || address.isEmpty())
- throw new IllegalArgumentException(Application.get().getString(R.string.tunnel_error_empty_inetaddress));
+ /**
+ * 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) {
+ if (address.isEmpty())
+ throw new IllegalArgumentException("Empty address");
try {
return (InetAddress) PARSER_METHOD.invoke(null, address);
} catch (final IllegalAccessException | InvocationTargetException e) {
- throw new RuntimeException(e.getCause() == null ? e : e.getCause());
+ 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.
+ throw cause instanceof IllegalArgumentException ?
+ (IllegalArgumentException) cause : new RuntimeException(e);
}
}
}
diff --git a/app/src/main/java/com/wireguard/config/InetEndpoint.java b/app/src/main/java/com/wireguard/config/InetEndpoint.java
index 3efe4203..06d0ca80 100644
--- a/app/src/main/java/com/wireguard/config/InetEndpoint.java
+++ b/app/src/main/java/com/wireguard/config/InetEndpoint.java
@@ -5,36 +5,68 @@
package com.wireguard.config;
-import android.annotation.SuppressLint;
+import android.support.annotation.Nullable;
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
+import org.threeten.bp.Duration;
+import org.threeten.bp.Instant;
import java.net.Inet4Address;
-import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
+import java.util.regex.Pattern;
-import javax.annotation.Nullable;
+import java9.util.Optional;
+
+
+/**
+ * An external endpoint (host and port) used to connect to a WireGuard {@link Peer}.
+ *
+ * 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("[/?#]");
-public class InetEndpoint {
private final String host;
+ private final boolean isResolved;
+ private final Object lock = new Object();
private final int port;
- @Nullable private InetAddress resolvedHost;
+ private Instant lastResolution = Instant.EPOCH;
+ @Nullable private InetEndpoint resolved;
- public InetEndpoint(@Nullable final String endpoint) {
- if (endpoint.indexOf('/') != -1 || endpoint.indexOf('?') != -1 || endpoint.indexOf('#') != -1)
- throw new IllegalArgumentException(Application.get().getString(R.string.tunnel_error_forbidden_endpoint_chars));
+ 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) {
+ if (FORBIDDEN_CHARACTERS.matcher(endpoint).find())
+ throw new IllegalArgumentException("Forbidden characters in Endpoint");
final URI uri;
try {
uri = new URI("wg://" + endpoint);
} catch (final URISyntaxException e) {
throw new IllegalArgumentException(e);
}
- host = uri.getHost();
- port = uri.getPort();
+ 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 IllegalArgumentException 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() {
@@ -45,28 +77,47 @@ public class InetEndpoint {
return port;
}
- @SuppressLint("DefaultLocale")
- public String getResolvedEndpoint() throws UnknownHostException {
- if (resolvedHost == null) {
- final InetAddress[] candidates = InetAddress.getAllByName(host);
- if (candidates.length == 0)
- throw new UnknownHostException(host);
- for (final InetAddress addr : candidates) {
- if (addr instanceof Inet4Address) {
- resolvedHost = addr;
- break;
+ /**
+ * 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 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;
}
}
- if (resolvedHost == null)
- resolvedHost = candidates[0];
+ return Optional.ofNullable(resolved);
}
- return String.format(resolvedHost instanceof Inet6Address ?
- "[%s]:%d" : "%s:%d", resolvedHost.getHostAddress(), port);
}
- @SuppressLint("DefaultLocale")
- public String getEndpoint() {
- return String.format(host.contains(":") && !host.contains("[") ?
- "[%s]:%d" : "%s:%d", host, port);
+ @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;
}
}
diff --git a/app/src/main/java/com/wireguard/config/InetNetwork.java b/app/src/main/java/com/wireguard/config/InetNetwork.java
index 836a1335..9e5e8c64 100644
--- a/app/src/main/java/com/wireguard/config/InetNetwork.java
+++ b/app/src/main/java/com/wireguard/config/InetNetwork.java
@@ -7,26 +7,36 @@ package com.wireguard.config;
import java.net.Inet4Address;
import java.net.InetAddress;
-import java.util.Objects;
-public class InetNetwork {
+/**
+ * An Internet network, denoted by its address and netmask
+ *
+ * Instances of this class are immutable.
+ */
+public final class InetNetwork {
private final InetAddress address;
private final int mask;
- public InetNetwork(final String input) {
- final int slash = input.lastIndexOf('/');
+ private InetNetwork(final InetAddress address, final int mask) {
+ this.address = address;
+ this.mask = mask;
+ }
+
+ public static InetNetwork parse(final String network) {
+ final int slash = network.lastIndexOf('/');
final int rawMask;
final String rawAddress;
if (slash >= 0) {
- rawMask = Integer.parseInt(input.substring(slash + 1), 10);
- rawAddress = input.substring(0, slash);
+ rawMask = Integer.parseInt(network.substring(slash + 1), 10);
+ rawAddress = network.substring(0, slash);
} else {
rawMask = -1;
- rawAddress = input;
+ rawAddress = network;
}
- address = InetAddresses.parse(rawAddress);
+ final InetAddress address = InetAddresses.parse(rawAddress);
final int maxMask = (address instanceof Inet4Address) ? 32 : 128;
- mask = rawMask >= 0 && rawMask <= maxMask ? rawMask : maxMask;
+ final int mask = rawMask >= 0 && rawMask <= maxMask ? rawMask : maxMask;
+ return new InetNetwork(address, mask);
}
@Override
@@ -34,7 +44,7 @@ public class InetNetwork {
if (!(obj instanceof InetNetwork))
return false;
final InetNetwork other = (InetNetwork) obj;
- return Objects.equals(address, other.address) && mask == other.mask;
+ return address.equals(other.address) && mask == other.mask;
}
public InetAddress getAddress() {
diff --git a/app/src/main/java/com/wireguard/config/Interface.java b/app/src/main/java/com/wireguard/config/Interface.java
index aa1d986b..dc1a291d 100644
--- a/app/src/main/java/com/wireguard/config/Interface.java
+++ b/app/src/main/java/com/wireguard/config/Interface.java
@@ -5,395 +5,345 @@
package com.wireguard.config;
-import android.content.Context;
-import android.databinding.BaseObservable;
-import android.databinding.Bindable;
-import android.os.Parcel;
-import android.os.Parcelable;
import android.support.annotation.Nullable;
-import com.wireguard.android.Application;
-import com.wireguard.android.BR;
-import com.wireguard.android.R;
-import com.wireguard.crypto.Keypair;
+import com.wireguard.crypto.Key;
+import com.wireguard.crypto.KeyPair;
import java.net.InetAddress;
-import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import java9.util.Lists;
+import java9.util.Optional;
+import java9.util.stream.Collectors;
+import java9.util.stream.Stream;
+import java9.util.stream.StreamSupport;
/**
- * Represents the configuration for a WireGuard interface (an [Interface] block).
+ * 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.
+ *
+ * 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;
-public class Interface {
- private final List addressList;
- private final Context context = Application.get();
- private final List dnsList;
- private final List excludedApplications;
- @Nullable private Keypair keypair;
- private int listenPort;
- private int mtu;
+ private final Set addresses;
+ private final Set dnsServers;
+ private final Set excludedApplications;
+ private final KeyPair keyPair;
+ private final Optional listenPort;
+ private final Optional mtu;
- public Interface() {
- addressList = new ArrayList<>();
- dnsList = new ArrayList<>();
- excludedApplications = new ArrayList<>();
+ 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));
+ keyPair = Objects.requireNonNull(builder.keyPair, "Interfaces must have a private key");
+ listenPort = builder.listenPort;
+ mtu = builder.mtu;
}
- private void addAddresses(@Nullable final String[] addresses) {
- if (addresses != null && addresses.length > 0) {
- for (final String addr : addresses) {
- if (addr.isEmpty())
- throw new IllegalArgumentException(context.getString(R.string.tunnel_error_empty_interface_address));
- addressList.add(new InetNetwork(addr));
+ /**
+ * 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 ParseException {
+ final Builder builder = new Builder();
+ for (final CharSequence line : lines) {
+ final Attribute attribute = Attribute.parse(line)
+ .orElseThrow(() -> new ParseException("[Interface]", line, "Syntax error"));
+ switch (attribute.getKey().toLowerCase()) {
+ case "address":
+ builder.parseAddresses(attribute.getValue());
+ break;
+ case "dns":
+ builder.parseDnsServers(attribute.getValue());
+ break;
+ case "excludedapplications":
+ builder.parseExcludedApplications(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 ParseException("[Interface]", attribute.getKey(), "Unknown attribute");
}
}
- }
-
- private void addDnses(@Nullable final String[] dnses) {
- if (dnses != null && dnses.length > 0) {
- for (final String dns : dnses) {
- dnsList.add(InetAddresses.parse(dns));
- }
- }
- }
-
- private void addExcludedApplications(@Nullable final String[] applications) {
- if (applications != null && applications.length > 0) {
- excludedApplications.addAll(Arrays.asList(applications));
- }
- }
-
- @Nullable
- private String getAddressString() {
- if (addressList.isEmpty())
- return null;
- return Attribute.iterableToString(addressList);
- }
-
- public InetNetwork[] getAddresses() {
- return addressList.toArray(new InetNetwork[addressList.size()]);
- }
-
- @Nullable
- private String getDnsString() {
- if (dnsList.isEmpty())
- return null;
- return Attribute.iterableToString(getDnsStrings());
- }
-
- private List getDnsStrings() {
- final List strings = new ArrayList<>();
- for (final InetAddress addr : dnsList)
- strings.add(addr.getHostAddress());
- return strings;
- }
-
- public InetAddress[] getDnses() {
- return dnsList.toArray(new InetAddress[dnsList.size()]);
- }
-
- public String[] getExcludedApplications() {
- return excludedApplications.toArray(new String[excludedApplications.size()]);
- }
-
- @Nullable
- private String getExcludedApplicationsString() {
- if (excludedApplications.isEmpty())
- return null;
- return Attribute.iterableToString(excludedApplications);
- }
-
- public int getListenPort() {
- return listenPort;
- }
-
- @Nullable
- private String getListenPortString() {
- if (listenPort == 0)
- return null;
- return Integer.valueOf(listenPort).toString();
- }
-
- public int getMtu() {
- return mtu;
- }
-
- @Nullable
- private String getMtuString() {
- if (mtu == 0)
- return null;
- return Integer.toString(mtu);
- }
-
- @Nullable
- public String getPrivateKey() {
- if (keypair == null)
- return null;
- return keypair.getPrivateKey();
- }
-
- @Nullable
- public String getPublicKey() {
- if (keypair == null)
- return null;
- return keypair.getPublicKey();
- }
-
- public void parse(final String line) {
- final Attribute key = Attribute.match(line);
- if (key == null)
- throw new IllegalArgumentException(String.format(context.getString(R.string.tunnel_error_interface_parse_failed), line));
- switch (key) {
- case ADDRESS:
- addAddresses(key.parseList(line));
- break;
- case DNS:
- addDnses(key.parseList(line));
- break;
- case EXCLUDED_APPLICATIONS:
- addExcludedApplications(key.parseList(line));
- break;
- case LISTEN_PORT:
- setListenPortString(key.parse(line));
- break;
- case MTU:
- setMtuString(key.parse(line));
- break;
- case PRIVATE_KEY:
- setPrivateKey(key.parse(line));
- break;
- default:
- throw new IllegalArgumentException(line);
- }
- }
-
- private void setAddressString(@Nullable final String addressString) {
- addressList.clear();
- addAddresses(Attribute.stringToList(addressString));
- }
-
- private void setDnsString(@Nullable final String dnsString) {
- dnsList.clear();
- addDnses(Attribute.stringToList(dnsString));
- }
-
- private void setExcludedApplicationsString(@Nullable final String applicationsString) {
- excludedApplications.clear();
- addExcludedApplications(Attribute.stringToList(applicationsString));
- }
-
- private void setListenPort(final int listenPort) {
- this.listenPort = listenPort;
- }
-
- private void setListenPortString(@Nullable final String port) {
- if (port != null && !port.isEmpty())
- setListenPort(Integer.parseInt(port, 10));
- else
- setListenPort(0);
- }
-
- private void setMtu(final int mtu) {
- this.mtu = mtu;
- }
-
- private void setMtuString(@Nullable final String mtu) {
- if (mtu != null && !mtu.isEmpty())
- setMtu(Integer.parseInt(mtu, 10));
- else
- setMtu(0);
- }
-
- private void setPrivateKey(@Nullable String privateKey) {
- if (privateKey != null && privateKey.isEmpty())
- privateKey = null;
- keypair = privateKey == null ? null : new Keypair(privateKey);
+ 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)
+ && 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 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 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 getExcludedApplications() {
+ // The collection is already immutable.
+ return excludedApplications;
+ }
+
+ /**
+ * 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 getListenPort() {
+ return listenPort;
+ }
+
+ /**
+ * Returns the MTU used for the WireGuard interface.
+ *
+ * @return the MTU, or {@code Optional.empty()} if none is configured
+ */
+ public Optional 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 + 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().append("[Interface]\n");
- if (!addressList.isEmpty())
- sb.append(Attribute.ADDRESS.composeWith(addressList));
- if (!dnsList.isEmpty())
- sb.append(Attribute.DNS.composeWith(getDnsStrings()));
- if (!excludedApplications.isEmpty())
- sb.append(Attribute.EXCLUDED_APPLICATIONS.composeWith(excludedApplications));
- if (listenPort != 0)
- sb.append(Attribute.LISTEN_PORT.composeWith(listenPort));
- if (mtu != 0)
- sb.append(Attribute.MTU.composeWith(mtu));
- if (keypair != null)
- sb.append(Attribute.PRIVATE_KEY.composeWith(keypair.getPrivateKey()));
+ final StringBuilder sb = new StringBuilder("(Interface ");
+ sb.append(keyPair.getPublicKey().toBase64());
+ listenPort.ifPresent(lp -> sb.append(" @").append(lp));
+ sb.append(')');
return sb.toString();
}
- public static class Observable extends BaseObservable implements Parcelable {
- public static final Creator CREATOR = new Creator() {
- @Override
- public Observable createFromParcel(final Parcel in) {
- return new Observable(in);
- }
+ /**
+ * 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 dnsServerStrings = StreamSupport.stream(dnsServers)
+ .map(InetAddress::getHostAddress)
+ .collect(Collectors.toUnmodifiableList());
+ sb.append("DNS = ").append(Attribute.join(dnsServerStrings)).append('\n');
+ }
+ if (!excludedApplications.isEmpty())
+ sb.append("ExcludedApplications = ").append(Attribute.join(excludedApplications)).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();
+ }
- @Override
- public Observable[] newArray(final int size) {
- return new Observable[size];
- }
- };
- @Nullable private String addresses;
- @Nullable private String dnses;
- @Nullable private String excludedApplications;
- @Nullable private String listenPort;
- @Nullable private String mtu;
- @Nullable private String privateKey;
- @Nullable private String publicKey;
+ /**
+ * 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();
+ }
- public Observable(@Nullable final Interface parent) {
- if (parent != null)
- loadData(parent);
+ @SuppressWarnings("UnusedReturnValue")
+ public static final class Builder {
+ // Defaults to an empty set.
+ private final Set addresses = new LinkedHashSet<>();
+ // Defaults to an empty set.
+ private final Set dnsServers = new LinkedHashSet<>();
+ // Defaults to an empty set.
+ private final Set excludedApplications = new LinkedHashSet<>();
+ // No default; must be provided before building.
+ @Nullable private KeyPair keyPair;
+ // Defaults to not present.
+ private Optional listenPort = Optional.empty();
+ // Defaults to not present.
+ private Optional mtu = Optional.empty();
+
+ public Builder addAddress(final InetNetwork address) {
+ addresses.add(address);
+ return this;
}
- private Observable(final Parcel in) {
- addresses = in.readString();
- dnses = in.readString();
- publicKey = in.readString();
- privateKey = in.readString();
- listenPort = in.readString();
- mtu = in.readString();
- excludedApplications = in.readString();
+ public Builder addAddresses(final Collection addresses) {
+ this.addresses.addAll(addresses);
+ return this;
}
- public void commitData(final Interface parent) {
- parent.setAddressString(addresses);
- parent.setDnsString(dnses);
- parent.setExcludedApplicationsString(excludedApplications);
- parent.setPrivateKey(privateKey);
- parent.setListenPortString(listenPort);
- parent.setMtuString(mtu);
- loadData(parent);
- notifyChange();
+ public Builder addDnsServer(final InetAddress dnsServer) {
+ dnsServers.add(dnsServer);
+ return this;
}
- @Override
- public int describeContents() {
- return 0;
+ public Builder addDnsServers(final Collection extends InetAddress> dnsServers) {
+ this.dnsServers.addAll(dnsServers);
+ return this;
}
- public void generateKeypair() {
- final Keypair keypair = new Keypair();
- privateKey = keypair.getPrivateKey();
- publicKey = keypair.getPublicKey();
- notifyPropertyChanged(BR.privateKey);
- notifyPropertyChanged(BR.publicKey);
+ public Interface build() {
+ return new Interface(this);
}
- @Nullable
- @Bindable
- public String getAddresses() {
- return addresses;
+ public Builder excludeApplication(final String application) {
+ excludedApplications.add(application);
+ return this;
}
- @Nullable
- @Bindable
- public String getDnses() {
- return dnses;
+ public Builder excludeApplications(final Collection applications) {
+ excludedApplications.addAll(applications);
+ return this;
}
- @Nullable
- @Bindable
- public String getExcludedApplications() {
- return excludedApplications;
- }
-
- @Bindable
- public int getExcludedApplicationsCount() {
- return Attribute.stringToList(excludedApplications).length;
- }
-
- @Nullable
- @Bindable
- public String getListenPort() {
- return listenPort;
- }
-
- @Nullable
- @Bindable
- public String getMtu() {
- return mtu;
- }
-
- @Nullable
- @Bindable
- public String getPrivateKey() {
- return privateKey;
- }
-
- @Nullable
- @Bindable
- public String getPublicKey() {
- return publicKey;
- }
-
- private void loadData(final Interface parent) {
- addresses = parent.getAddressString();
- dnses = parent.getDnsString();
- excludedApplications = parent.getExcludedApplicationsString();
- publicKey = parent.getPublicKey();
- privateKey = parent.getPrivateKey();
- listenPort = parent.getListenPortString();
- mtu = parent.getMtuString();
- }
-
- public void setAddresses(final String addresses) {
- this.addresses = addresses;
- notifyPropertyChanged(BR.addresses);
- }
-
- public void setDnses(final String dnses) {
- this.dnses = dnses;
- notifyPropertyChanged(BR.dnses);
- }
-
- public void setExcludedApplications(final String excludedApplications) {
- this.excludedApplications = excludedApplications;
- notifyPropertyChanged(BR.excludedApplications);
- notifyPropertyChanged(BR.excludedApplicationsCount);
- }
-
- public void setListenPort(final String listenPort) {
- this.listenPort = listenPort;
- notifyPropertyChanged(BR.listenPort);
- }
-
- public void setMtu(final String mtu) {
- this.mtu = mtu;
- notifyPropertyChanged(BR.mtu);
- }
-
- public void setPrivateKey(final String privateKey) {
- this.privateKey = privateKey;
-
+ public Builder parseAddresses(final CharSequence addresses) throws ParseException {
try {
- publicKey = new Keypair(privateKey).getPublicKey();
- } catch (final IllegalArgumentException ignored) {
- publicKey = "";
+ final List parsed = Stream.of(Attribute.split(addresses))
+ .map(InetNetwork::parse)
+ .collect(Collectors.toUnmodifiableList());
+ return addAddresses(parsed);
+ } catch (final IllegalArgumentException e) {
+ throw new ParseException("Address", addresses, e);
}
-
- notifyPropertyChanged(BR.privateKey);
- notifyPropertyChanged(BR.publicKey);
}
- @Override
- public void writeToParcel(final Parcel dest, final int flags) {
- dest.writeString(addresses);
- dest.writeString(dnses);
- dest.writeString(publicKey);
- dest.writeString(privateKey);
- dest.writeString(listenPort);
- dest.writeString(mtu);
- dest.writeString(excludedApplications);
+ public Builder parseDnsServers(final CharSequence dnsServers) throws ParseException {
+ try {
+ final List parsed = Stream.of(Attribute.split(dnsServers))
+ .map(InetAddresses::parse)
+ .collect(Collectors.toUnmodifiableList());
+ return addDnsServers(parsed);
+ } catch (final IllegalArgumentException e) {
+ throw new ParseException("DNS", dnsServers, e);
+ }
+ }
+
+ public Builder parseExcludedApplications(final CharSequence apps) throws ParseException {
+ try {
+ return excludeApplications(Lists.of(Attribute.split(apps)));
+ } catch (final IllegalArgumentException e) {
+ throw new ParseException("ExcludedApplications", apps, e);
+ }
+ }
+
+ public Builder parseListenPort(final String listenPort) throws ParseException {
+ try {
+ return setListenPort(Integer.parseInt(listenPort));
+ } catch (final IllegalArgumentException e) {
+ throw new ParseException("ListenPort", listenPort, e);
+ }
+ }
+
+ public Builder parseMtu(final String mtu) throws ParseException {
+ try {
+ return setMtu(Integer.parseInt(mtu));
+ } catch (final IllegalArgumentException e) {
+ throw new ParseException("MTU", mtu, e);
+ }
+ }
+
+ public Builder parsePrivateKey(final String privateKey) throws ParseException {
+ try {
+ return setKeyPair(new KeyPair(Key.fromBase64(privateKey)));
+ } catch (final Key.KeyFormatException e) {
+ throw new ParseException("PrivateKey", "(omitted)", e);
+ }
+ }
+
+ public Builder setKeyPair(final KeyPair keyPair) {
+ this.keyPair = keyPair;
+ return this;
+ }
+
+ public Builder setListenPort(final int listenPort) {
+ if (listenPort < MIN_UDP_PORT || listenPort > MAX_UDP_PORT)
+ throw new IllegalArgumentException("ListenPort must be a valid UDP port number");
+ this.listenPort = listenPort == 0 ? Optional.empty() : Optional.of(listenPort);
+ return this;
+ }
+
+ public Builder setMtu(final int mtu) {
+ if (mtu < 0)
+ throw new IllegalArgumentException("MTU must not be negative");
+ this.mtu = mtu == 0 ? Optional.empty() : Optional.of(mtu);
+ return this;
}
}
}
diff --git a/app/src/main/java/com/wireguard/config/ParseException.java b/app/src/main/java/com/wireguard/config/ParseException.java
new file mode 100644
index 00000000..1fccb534
--- /dev/null
+++ b/app/src/main/java/com/wireguard/config/ParseException.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright © 2018 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+/**
+ * An exception representing a failure to parse an element of a WireGuard configuration. The context
+ * for this failure can be retrieved with {@link #getContext}, and the text that failed to parse can
+ * be retrieved with {@link #getText}.
+ */
+public class ParseException extends Exception {
+ private final String context;
+ private final CharSequence text;
+
+ public ParseException(final String context, final CharSequence text, final String message) {
+ super(message);
+ this.context = context;
+ this.text = text;
+ }
+
+ public ParseException(final String context, final CharSequence text, final Throwable cause) {
+ super(cause.getMessage(), cause);
+ this.context = context;
+ this.text = text;
+ }
+
+ public ParseException(final String context, final CharSequence text) {
+ this.context = context;
+ this.text = text;
+ }
+
+ public String getContext() {
+ return context;
+ }
+
+ public CharSequence getText() {
+ return text;
+ }
+}
diff --git a/app/src/main/java/com/wireguard/config/Peer.java b/app/src/main/java/com/wireguard/config/Peer.java
index 5cf0283c..50135fb0 100644
--- a/app/src/main/java/com/wireguard/config/Peer.java
+++ b/app/src/main/java/com/wireguard/config/Peer.java
@@ -5,363 +5,291 @@
package com.wireguard.config;
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.databinding.BaseObservable;
-import android.databinding.Bindable;
-import android.os.Parcel;
-import android.os.Parcelable;
import android.support.annotation.Nullable;
-import com.android.databinding.library.baseAdapters.BR;
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-import com.wireguard.crypto.KeyEncoding;
+import com.wireguard.crypto.Key;
-import java.net.Inet6Address;
-import java.net.InetSocketAddress;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.UnknownHostException;
-import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
-import java.util.HashSet;
+import java.util.Collections;
+import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Objects;
+import java.util.Set;
-import java9.lang.Iterables;
+import java9.util.Optional;
+import java9.util.stream.Collectors;
+import java9.util.stream.Stream;
/**
- * Represents the configuration for a WireGuard peer (a [Peer] block).
+ * Represents the configuration for a WireGuard peer (a [Peer] block). Peers must have a public key,
+ * and may optionally have several other attributes.
+ *
+ * Instances of this class are immutable.
*/
+public final class Peer {
+ private final Set allowedIps;
+ private final Optional endpoint;
+ private final Optional persistentKeepalive;
+ private final Optional preSharedKey;
+ private final Key publicKey;
-public class Peer {
- private final List allowedIPsList;
- private final Context context = Application.get();
- @Nullable private InetEndpoint endpoint;
- private int persistentKeepalive;
- @Nullable private String preSharedKey;
- @Nullable private String publicKey;
-
- public Peer() {
- allowedIPsList = new ArrayList<>();
+ 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");
}
- private void addAllowedIPs(@Nullable final String[] allowedIPs) {
- if (allowedIPs != null && allowedIPs.length > 0) {
- for (final String allowedIP : allowedIPs) {
- allowedIPsList.add(new InetNetwork(allowedIP));
+ /**
+ * 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 ParseException {
+ final Builder builder = new Builder();
+ for (final CharSequence line : lines) {
+ final Attribute attribute = Attribute.parse(line)
+ .orElseThrow(() -> new ParseException("[Peer]", line, "Syntax error"));
+ switch (attribute.getKey().toLowerCase()) {
+ 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 ParseException("[Peer]", line, "Unknown attribute");
}
}
- }
-
- public InetNetwork[] getAllowedIPs() {
- return allowedIPsList.toArray(new InetNetwork[allowedIPsList.size()]);
- }
-
- @Nullable
- private String getAllowedIPsString() {
- if (allowedIPsList.isEmpty())
- return null;
- return Attribute.iterableToString(allowedIPsList);
- }
-
- @Nullable
- public InetEndpoint getEndpoint() {
- return endpoint;
- }
-
- @Nullable
- private String getEndpointString() {
- if (endpoint == null)
- return null;
- return endpoint.getEndpoint();
- }
-
- public int getPersistentKeepalive() {
- return persistentKeepalive;
- }
-
- @Nullable
- private String getPersistentKeepaliveString() {
- if (persistentKeepalive == 0)
- return null;
- return Integer.valueOf(persistentKeepalive).toString();
- }
-
- @Nullable
- public String getPreSharedKey() {
- return preSharedKey;
- }
-
- @Nullable
- public String getPublicKey() {
- return publicKey;
- }
-
- public String getResolvedEndpointString() throws UnknownHostException {
- if (endpoint == null)
- throw new UnknownHostException("{empty}");
- return endpoint.getResolvedEndpoint();
- }
-
- public void parse(final String line) {
- final Attribute key = Attribute.match(line);
- if (key == null)
- throw new IllegalArgumentException(context.getString(R.string.tunnel_error_interface_parse_failed, line));
- switch (key) {
- case ALLOWED_IPS:
- addAllowedIPs(key.parseList(line));
- break;
- case ENDPOINT:
- setEndpointString(key.parse(line));
- break;
- case PERSISTENT_KEEPALIVE:
- setPersistentKeepaliveString(key.parse(line));
- break;
- case PRESHARED_KEY:
- setPreSharedKey(key.parse(line));
- break;
- case PUBLIC_KEY:
- setPublicKey(key.parse(line));
- break;
- default:
- throw new IllegalArgumentException(line);
- }
- }
-
- private void setAllowedIPsString(@Nullable final String allowedIPsString) {
- allowedIPsList.clear();
- addAllowedIPs(Attribute.stringToList(allowedIPsString));
- }
-
- private void setEndpoint(@Nullable final InetEndpoint endpoint) {
- this.endpoint = endpoint;
- }
-
- private void setEndpointString(@Nullable final String endpoint) {
- if (endpoint != null && !endpoint.isEmpty())
- setEndpoint(new InetEndpoint(endpoint));
- else
- setEndpoint(null);
- }
-
- private void setPersistentKeepalive(final int persistentKeepalive) {
- this.persistentKeepalive = persistentKeepalive;
- }
-
- private void setPersistentKeepaliveString(@Nullable final String persistentKeepalive) {
- if (persistentKeepalive != null && !persistentKeepalive.isEmpty())
- setPersistentKeepalive(Integer.parseInt(persistentKeepalive, 10));
- else
- setPersistentKeepalive(0);
- }
-
- private void setPreSharedKey(@Nullable String preSharedKey) {
- if (preSharedKey != null && preSharedKey.isEmpty())
- preSharedKey = null;
- if (preSharedKey != null)
- KeyEncoding.keyFromBase64(preSharedKey);
- this.preSharedKey = preSharedKey;
- }
-
- private void setPublicKey(@Nullable String publicKey) {
- if (publicKey != null && publicKey.isEmpty())
- publicKey = null;
- if (publicKey != null)
- KeyEncoding.keyFromBase64(publicKey);
- this.publicKey = publicKey;
+ 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 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 getEndpoint() {
+ return endpoint;
+ }
+
+ /**
+ * Returns the peer's persistent keepalive.
+ *
+ * @return the persistent keepalive, or {@code Optional.empty()} if none is configured
+ */
+ public Optional 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 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().append("[Peer]\n");
- if (!allowedIPsList.isEmpty())
- sb.append(Attribute.ALLOWED_IPS.composeWith(allowedIPsList));
- if (endpoint != null)
- sb.append(Attribute.ENDPOINT.composeWith(getEndpointString()));
- if (persistentKeepalive != 0)
- sb.append(Attribute.PERSISTENT_KEEPALIVE.composeWith(persistentKeepalive));
- if (preSharedKey != null)
- sb.append(Attribute.PRESHARED_KEY.composeWith(preSharedKey));
- if (publicKey != null)
- sb.append(Attribute.PUBLIC_KEY.composeWith(publicKey));
+ final StringBuilder sb = new StringBuilder("(Peer ");
+ sb.append(publicKey.toBase64());
+ endpoint.ifPresent(ep -> sb.append(" @").append(ep));
+ sb.append(')');
return sb.toString();
}
- public static class Observable extends BaseObservable implements Parcelable {
- public static final Creator CREATOR = new Creator() {
- @Override
- public Observable createFromParcel(final Parcel in) {
- return new Observable(in);
+ /**
+ * 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 allowedIps = new LinkedHashSet<>();
+ // Defaults to not present.
+ private Optional endpoint = Optional.empty();
+ // Defaults to not present.
+ private Optional persistentKeepalive = Optional.empty();
+ // Defaults to not present.
+ private Optional 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 allowedIps) {
+ this.allowedIps.addAll(allowedIps);
+ return this;
+ }
+
+ public Peer build() {
+ return new Peer(this);
+ }
+
+ public Builder parseAllowedIPs(final CharSequence allowedIps) throws ParseException {
+ try {
+ final List parsed = Stream.of(Attribute.split(allowedIps))
+ .map(InetNetwork::parse)
+ .collect(Collectors.toUnmodifiableList());
+ return addAllowedIps(parsed);
+ } catch (final IllegalArgumentException e) {
+ throw new ParseException("AllowedIPs", allowedIps, e);
}
+ }
- @Override
- public Observable[] newArray(final int size) {
- return new Observable[size];
+ public Builder parseEndpoint(final String endpoint) throws ParseException {
+ try {
+ return setEndpoint(InetEndpoint.parse(endpoint));
+ } catch (final IllegalArgumentException e) {
+ throw new ParseException("Endpoint", endpoint, e);
}
- };
- private static final List DEFAULT_ROUTE_MOD_RFC1918_V4 = Arrays.asList("0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12", "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7", "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16", "192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10", "193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4");
- private static final String DEFAULT_ROUTE_V4 = "0.0.0.0/0";
- private final List interfaceDNSRoutes = new ArrayList<>();
- @Nullable private String allowedIPs;
- @Nullable private String endpoint;
- private int numSiblings;
- @Nullable private String persistentKeepalive;
- @Nullable private String preSharedKey;
- @Nullable private String publicKey;
-
- public Observable(final Peer parent) {
- loadData(parent);
}
- private Observable(final Parcel in) {
- allowedIPs = in.readString();
- endpoint = in.readString();
- persistentKeepalive = in.readString();
- preSharedKey = in.readString();
- publicKey = in.readString();
- numSiblings = in.readInt();
- in.readStringList(interfaceDNSRoutes);
- }
-
- public static Observable newInstance() {
- return new Observable(new Peer());
- }
-
- public void commitData(final Peer parent) {
- parent.setAllowedIPsString(allowedIPs);
- parent.setEndpointString(endpoint);
- parent.setPersistentKeepaliveString(persistentKeepalive);
- parent.setPreSharedKey(preSharedKey);
- parent.setPublicKey(publicKey);
- if (parent.getPublicKey() == null)
- throw new IllegalArgumentException(Application.get().getString(R.string.tunnel_error_empty_peer_public_key));
- loadData(parent);
- notifyChange();
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Bindable @Nullable
- public String getAllowedIPs() {
- return allowedIPs;
- }
-
- @Bindable
- public boolean getCanToggleExcludePrivateIPs() {
- final Collection ips = Arrays.asList(Attribute.stringToList(allowedIPs));
- return numSiblings == 0 && (ips.contains(DEFAULT_ROUTE_V4) || ips.containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4));
- }
-
- @Bindable @Nullable
- public String getEndpoint() {
- return endpoint;
- }
-
- @Bindable
- public boolean getIsExcludePrivateIPsOn() {
- return numSiblings == 0 && Arrays.asList(Attribute.stringToList(allowedIPs)).containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
- }
-
- @Bindable @Nullable
- public String getPersistentKeepalive() {
- return persistentKeepalive;
- }
-
- @Bindable @Nullable
- public String getPreSharedKey() {
- return preSharedKey;
- }
-
- @Bindable @Nullable
- public String getPublicKey() {
- return publicKey;
- }
-
- private void loadData(final Peer parent) {
- allowedIPs = parent.getAllowedIPsString();
- endpoint = parent.getEndpointString();
- persistentKeepalive = parent.getPersistentKeepaliveString();
- preSharedKey = parent.getPreSharedKey();
- publicKey = parent.getPublicKey();
- }
-
- public void setAllowedIPs(final String allowedIPs) {
- this.allowedIPs = allowedIPs;
- notifyPropertyChanged(BR.allowedIPs);
- notifyPropertyChanged(BR.canToggleExcludePrivateIPs);
- notifyPropertyChanged(BR.isExcludePrivateIPsOn);
- }
-
- public void setEndpoint(final String endpoint) {
- this.endpoint = endpoint;
- notifyPropertyChanged(BR.endpoint);
- }
-
- public void setInterfaceDNSRoutes(@Nullable final String dnsServers) {
- final Collection ips = new HashSet<>(Arrays.asList(Attribute.stringToList(allowedIPs)));
- final boolean modifyAllowedIPs = ips.containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
-
- ips.removeAll(interfaceDNSRoutes);
- interfaceDNSRoutes.clear();
- for (final String dnsServer : Attribute.stringToList(dnsServers)) {
- if (!dnsServer.contains(":"))
- interfaceDNSRoutes.add(dnsServer + "/32");
+ public Builder parsePersistentKeepalive(final String persistentKeepalive) throws ParseException {
+ try {
+ return setPersistentKeepalive(Integer.parseInt(persistentKeepalive));
+ } catch (final IllegalArgumentException e) {
+ throw new ParseException("PersistentKeepalive", persistentKeepalive, e);
}
- ips.addAll(interfaceDNSRoutes);
- if (modifyAllowedIPs)
- setAllowedIPs(Attribute.iterableToString(ips));
}
- public void setNumSiblings(final int num) {
- numSiblings = num;
- notifyPropertyChanged(BR.canToggleExcludePrivateIPs);
- notifyPropertyChanged(BR.isExcludePrivateIPsOn);
+ public Builder parsePreSharedKey(final String preSharedKey) throws ParseException {
+ try {
+ return setPreSharedKey(Key.fromBase64(preSharedKey));
+ } catch (final Key.KeyFormatException e) {
+ throw new ParseException("PresharedKey", preSharedKey, e);
+ }
}
- public void setPersistentKeepalive(final String persistentKeepalive) {
- this.persistentKeepalive = persistentKeepalive;
- notifyPropertyChanged(BR.persistentKeepalive);
+ public Builder parsePublicKey(final String publicKey) throws ParseException {
+ try {
+ return setPublicKey(Key.fromBase64(publicKey));
+ } catch (final Key.KeyFormatException e) {
+ throw new ParseException("PublicKey", publicKey, e);
+ }
}
- public void setPreSharedKey(final String preSharedKey) {
- this.preSharedKey = preSharedKey;
- notifyPropertyChanged(BR.preSharedKey);
+ public Builder setEndpoint(final InetEndpoint endpoint) {
+ this.endpoint = Optional.of(endpoint);
+ return this;
}
- public void setPublicKey(final String publicKey) {
+ public Builder setPersistentKeepalive(final int persistentKeepalive) {
+ if (persistentKeepalive < 0 || persistentKeepalive > MAX_PERSISTENT_KEEPALIVE)
+ throw new IllegalArgumentException("Invalid value for 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;
- notifyPropertyChanged(BR.publicKey);
- }
-
- public void toggleExcludePrivateIPs() {
- final Collection ips = new HashSet<>(Arrays.asList(Attribute.stringToList(allowedIPs)));
- final boolean hasDefaultRoute = ips.contains(DEFAULT_ROUTE_V4);
- final boolean hasDefaultRouteModRFC1918 = ips.containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
- if ((!hasDefaultRoute && !hasDefaultRouteModRFC1918) || numSiblings > 0)
- return;
- Iterables.removeIf(ips, ip -> !ip.contains(":"));
- if (hasDefaultRoute) {
- ips.addAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
- ips.addAll(interfaceDNSRoutes);
- } else if (hasDefaultRouteModRFC1918)
- ips.add(DEFAULT_ROUTE_V4);
- setAllowedIPs(Attribute.iterableToString(ips));
- }
-
- @Override
- public void writeToParcel(final Parcel dest, final int flags) {
- dest.writeString(allowedIPs);
- dest.writeString(endpoint);
- dest.writeString(persistentKeepalive);
- dest.writeString(preSharedKey);
- dest.writeString(publicKey);
- dest.writeInt(numSiblings);
- dest.writeStringList(interfaceDNSRoutes);
+ return this;
}
}
}
diff --git a/app/src/main/java/com/wireguard/crypto/Key.java b/app/src/main/java/com/wireguard/crypto/Key.java
new file mode 100644
index 00000000..85146794
--- /dev/null
+++ b/app/src/main/java/com/wireguard/crypto/Key.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.crypto;
+
+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.
+ *
+ * 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) {
+ final char[] input = str.toCharArray();
+ if (input.length != Format.BASE64.length || input[Format.BASE64.length - 1] != '=')
+ throw new KeyFormatException(Format.BASE64);
+ 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);
+ 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) {
+ if (bytes.length != Format.BINARY.length)
+ throw new KeyFormatException(Format.BINARY);
+ 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) {
+ final char[] input = str.toCharArray();
+ if (input.length != Format.HEX.length)
+ throw new KeyFormatException(Format.HEX);
+ 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);
+ return new Key(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);
+ }
+
+ /**
+ * 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;
+ }
+ }
+
+ /**
+ * 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 static final class KeyFormatException extends RuntimeException {
+ private final Format format;
+
+ private KeyFormatException(final Format format) {
+ this.format = format;
+ }
+
+ public Format getFormat() {
+ return format;
+ }
+ }
+}
diff --git a/app/src/main/java/com/wireguard/crypto/KeyEncoding.java b/app/src/main/java/com/wireguard/crypto/KeyEncoding.java
deleted file mode 100644
index d29c2d44..00000000
--- a/app/src/main/java/com/wireguard/crypto/KeyEncoding.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.crypto;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-
-/**
- * This is a specialized constant-time base64 and hex implementation that resists side-channel attacks.
- */
-
-@SuppressWarnings("MagicNumber")
-public final class KeyEncoding {
- public static final int KEY_LENGTH = 32;
- public static final int KEY_LENGTH_BASE64 = 44;
- public static final int KEY_LENGTH_HEX = 64;
- private static final String KEY_LENGTH_BASE64_EXCEPTION_MESSAGE =
- Application.get().getString(R.string.key_length_base64_exception_message);
- private static final String KEY_LENGTH_EXCEPTION_MESSAGE =
- Application.get().getString(R.string.key_length_exception_message);
- private static final String KEY_LENGTH_HEX_EXCEPTION_MESSAGE =
- Application.get().getString(R.string.key_length_hex_exception_message);
-
- private KeyEncoding() {
- // Prevent instantiation.
- }
-
- 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;
- }
-
- 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));
- }
- }
-
- public static byte[] keyFromBase64(final String str) {
- final char[] input = str.toCharArray();
- final byte[] key = new byte[KEY_LENGTH];
- if (input.length != KEY_LENGTH_BASE64 || input[KEY_LENGTH_BASE64 - 1] != '=')
- throw new IllegalArgumentException(KEY_LENGTH_BASE64_EXCEPTION_MESSAGE);
- 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 IllegalArgumentException(KEY_LENGTH_BASE64_EXCEPTION_MESSAGE);
- return key;
- }
-
- public static byte[] keyFromHex(final String str) {
- final char[] input = str.toCharArray();
- final byte[] key = new byte[KEY_LENGTH];
- if (input.length != KEY_LENGTH_HEX)
- throw new IllegalArgumentException(KEY_LENGTH_HEX_EXCEPTION_MESSAGE);
- int ret = 0;
-
- for (int i = 0; i < KEY_LENGTH_HEX; i += 2) {
- int c;
- int cNum;
- int cNum0;
- int cAlpha;
- int cAlpha0;
- int cVal;
- final int cAcc;
-
- c = input[i];
- 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 + 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 / 2] = (byte) (cAcc | cVal);
- }
- if (ret != 0)
- throw new IllegalArgumentException(KEY_LENGTH_HEX_EXCEPTION_MESSAGE);
- return key;
- }
-
- public static String keyToBase64(final byte[] key) {
- final char[] output = new char[KEY_LENGTH_BASE64];
- if (key.length != KEY_LENGTH)
- throw new IllegalArgumentException(KEY_LENGTH_EXCEPTION_MESSAGE);
- 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[KEY_LENGTH_BASE64 - 1] = '=';
- return new String(output);
- }
-
- public static String keyToHex(final byte[] key) {
- final char[] output = new char[KEY_LENGTH_HEX];
- if (key.length != KEY_LENGTH)
- throw new IllegalArgumentException(KEY_LENGTH_EXCEPTION_MESSAGE);
- 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);
- }
-}
diff --git a/app/src/main/java/com/wireguard/crypto/KeyPair.java b/app/src/main/java/com/wireguard/crypto/KeyPair.java
new file mode 100644
index 00000000..2b2bf564
--- /dev/null
+++ b/app/src/main/java/com/wireguard/crypto/KeyPair.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.crypto;
+
+import java.security.SecureRandom;
+
+/**
+ * Represents a Curve25519 key pair as used by WireGuard.
+ *
+ * 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(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 = generatePublicKey(privateKey);
+ }
+
+ /**
+ * Generates a private key using the system's {@link SecureRandom} number generator.
+ *
+ * @return a well-formed random private key
+ */
+ @SuppressWarnings("MagicNumber")
+ private static Key generatePrivateKey() {
+ final SecureRandom secureRandom = new SecureRandom();
+ final byte[] privateKey = new byte[Key.Format.BINARY.getLength()];
+ secureRandom.nextBytes(privateKey);
+ privateKey[0] &= 248;
+ privateKey[31] &= 127;
+ privateKey[31] |= 64;
+ return Key.fromBytes(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
+ */
+ private static Key generatePublicKey(final Key privateKey) {
+ final byte[] publicKey = new byte[Key.Format.BINARY.getLength()];
+ Curve25519.eval(publicKey, 0, privateKey.getBytes(), null);
+ return Key.fromBytes(publicKey);
+ }
+
+ /**
+ * 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;
+ }
+}
diff --git a/app/src/main/java/com/wireguard/crypto/Keypair.java b/app/src/main/java/com/wireguard/crypto/Keypair.java
deleted file mode 100644
index 0ee27542..00000000
--- a/app/src/main/java/com/wireguard/crypto/Keypair.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.crypto;
-
-import java.security.SecureRandom;
-
-/**
- * Represents a Curve25519 keypair as used by WireGuard.
- */
-
-public class Keypair {
- private final byte[] privateKey;
- private final byte[] publicKey;
-
- public Keypair() {
- this(generatePrivateKey());
- }
-
- private Keypair(final byte[] privateKey) {
- this.privateKey = privateKey;
- publicKey = generatePublicKey(privateKey);
- }
-
- public Keypair(final String privateKey) {
- this(KeyEncoding.keyFromBase64(privateKey));
- }
-
- @SuppressWarnings("MagicNumber")
- private static byte[] generatePrivateKey() {
- final SecureRandom secureRandom = new SecureRandom();
- final byte[] privateKey = new byte[KeyEncoding.KEY_LENGTH];
- secureRandom.nextBytes(privateKey);
- privateKey[0] &= 248;
- privateKey[31] &= 127;
- privateKey[31] |= 64;
- return privateKey;
- }
-
- private static byte[] generatePublicKey(final byte[] privateKey) {
- final byte[] publicKey = new byte[KeyEncoding.KEY_LENGTH];
- Curve25519.eval(publicKey, 0, privateKey, null);
- return publicKey;
- }
-
- public String getPrivateKey() {
- return KeyEncoding.keyToBase64(privateKey);
- }
-
- public String getPublicKey() {
- return KeyEncoding.keyToBase64(publicKey);
- }
-}
diff --git a/app/src/main/res/layout/tunnel_detail_fragment.xml b/app/src/main/res/layout/tunnel_detail_fragment.xml
index 11cfafae..9b06ddd4 100644
--- a/app/src/main/res/layout/tunnel_detail_fragment.xml
+++ b/app/src/main/res/layout/tunnel_detail_fragment.xml
@@ -19,7 +19,7 @@
+ type="com.wireguard.config.Config" />
+ android:text="@{config.interface.keyPair.publicKey.toBase64}" />
+ android:text="@{config.interface.addresses}" />
diff --git a/app/src/main/res/layout/tunnel_detail_peer.xml b/app/src/main/res/layout/tunnel_detail_peer.xml
index 91be0011..767e0383 100644
--- a/app/src/main/res/layout/tunnel_detail_peer.xml
+++ b/app/src/main/res/layout/tunnel_detail_peer.xml
@@ -8,7 +8,7 @@
+ type="com.wireguard.config.Peer" />
+ android:text="@{item.publicKey.toBase64}" />
+ android:text="@{item.allowedIps}" />
-
-
+ type="com.wireguard.android.viewmodel.ConfigProxy" />
+
+
+ android:text="@{config.interface.publicKey}" />
+ android:text="@={config.interface.addresses}" />
+ android:text="@={config.interface.dnsServers}" />
+ android:text="@{@plurals/set_excluded_applications(config.interface.excludedApplications.size, config.interface.excludedApplications.size)}" />
@@ -244,7 +246,7 @@
android:layout_marginBottom="4dp"
android:layout_marginEnd="4dp"
android:layout_marginStart="4dp"
- android:onClick="@{() -> config.peers.add(Peer.Observable.newInstance())}"
+ android:onClick="@{() -> config.addPeer()}"
android:text="@string/add_peer" />
diff --git a/app/src/main/res/layout/tunnel_editor_peer.xml b/app/src/main/res/layout/tunnel_editor_peer.xml
index e270b71a..1224ddd3 100644
--- a/app/src/main/res/layout/tunnel_editor_peer.xml
+++ b/app/src/main/res/layout/tunnel_editor_peer.xml
@@ -10,11 +10,11 @@
+ type="android.databinding.ObservableList<com.wireguard.android.viewmodel.PeerProxy>" />
+ type="com.wireguard.android.viewmodel.PeerProxy" />
+ android:visibility="@{item.ableToExcludePrivateIps ? View.VISIBLE : View.GONE}" />
+ android:text="@={item.allowedIps}" />
-
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 87ea18f5..6202ee78 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -63,6 +63,7 @@
Log file will be saved to downloads folder
MTU
Name
+ Cannot parse “%s” at %s
Peer
control WireGuard tunnels
Allows an app to control WireGuard tunnels. Apps with this permission may enable and disable WireGuard tunnels at will, potentially misdirecting Internet traffic.
@@ -99,13 +100,9 @@
Successfully renamed tunnel to “%s”
Invalid name
Tunnel %s already exists
- Empty address
Address is empty
- Unable to parse line: “%s”
Forbidden characters in endpoint
Peer public key may not be empty
- Invalid configuration line: %s
- Could not find any config information
WireGuard base64 keys must be 44 characters encoding 32 bytes
WireGuard keys must be 32 bytes
WireGuard hex keys must be 64 characters encoding 32 bytes
diff --git a/build.gradle b/build.gradle
index 68038e2a..421858de 100644
--- a/build.gradle
+++ b/build.gradle
@@ -7,7 +7,7 @@ allprojects {
buildscript {
dependencies {
- classpath 'com.android.tools.build:gradle:3.2.0'
+ classpath 'com.android.tools.build:gradle:3.2.1'
}
repositories {
google()