Browse Source

Provide semantically meaningful exceptions for translation

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
master
Samuel Holland 5 years ago
parent
commit
dcb0e9b3e8
18 changed files with 364 additions and 186 deletions
  1. +2
    -2
      app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java
  2. +3
    -3
      app/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java
  3. +3
    -3
      app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java
  4. +12
    -8
      app/src/main/java/com/wireguard/android/util/ExceptionLoggers.java
  5. +2
    -2
      app/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.java
  6. +4
    -3
      app/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.java
  7. +2
    -2
      app/src/main/java/com/wireguard/android/viewmodel/PeerProxy.java
  8. +118
    -0
      app/src/main/java/com/wireguard/config/BadConfigException.java
  9. +24
    -11
      app/src/main/java/com/wireguard/config/Config.java
  10. +5
    -4
      app/src/main/java/com/wireguard/config/InetAddresses.java
  11. +4
    -4
      app/src/main/java/com/wireguard/config/InetEndpoint.java
  12. +11
    -2
      app/src/main/java/com/wireguard/config/InetNetwork.java
  13. +43
    -40
      app/src/main/java/com/wireguard/config/Interface.java
  14. +19
    -16
      app/src/main/java/com/wireguard/config/ParseException.java
  15. +38
    -30
      app/src/main/java/com/wireguard/config/Peer.java
  16. +38
    -24
      app/src/main/java/com/wireguard/crypto/Key.java
  17. +34
    -0
      app/src/main/java/com/wireguard/crypto/KeyFormatException.java
  18. +2
    -32
      app/src/main/java/com/wireguard/crypto/KeyPair.java

+ 2
- 2
app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java View File

@@ -9,8 +9,8 @@ import android.content.Context;
import android.util.Log; import android.util.Log;


import com.wireguard.android.R; import com.wireguard.android.R;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Config; import com.wireguard.config.Config;
import com.wireguard.config.ParseException;


import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@@ -69,7 +69,7 @@ public final class FileConfigStore implements ConfigStore {
} }


@Override @Override
public Config load(final String name) throws IOException, ParseException {
public Config load(final String name) throws BadConfigException, IOException {
try (final FileInputStream stream = new FileInputStream(fileFor(name))) { try (final FileInputStream stream = new FileInputStream(fileFor(name))) {
return Config.parse(stream); return Config.parse(stream);
} }


+ 3
- 3
app/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java View File

@@ -18,8 +18,8 @@ import android.view.inputmethod.InputMethodManager;
import com.wireguard.android.Application; import com.wireguard.android.Application;
import com.wireguard.android.R; import com.wireguard.android.R;
import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding; import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Config; import com.wireguard.config.Config;
import com.wireguard.config.ParseException;


import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
@@ -67,8 +67,8 @@ public class ConfigNamingDialogFragment extends DialogFragment {


try { try {
config = Config.parse(new ByteArrayInputStream(getArguments().getString(KEY_CONFIG_TEXT).getBytes(StandardCharsets.UTF_8))); config = Config.parse(new ByteArrayInputStream(getArguments().getString(KEY_CONFIG_TEXT).getBytes(StandardCharsets.UTF_8)));
} catch (final IOException | ParseException exception) {
throw new RuntimeException(getResources().getString(R.string.invalid_config_error, getClass().getSimpleName()), exception);
} catch (final BadConfigException | IOException e) {
throw new RuntimeException(getResources().getString(R.string.invalid_config_error, getClass().getSimpleName()), e);
} }
} }




+ 3
- 3
app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java View File

@@ -40,8 +40,8 @@ import com.wireguard.android.model.Tunnel;
import com.wireguard.android.util.ExceptionLoggers; import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.widget.MultiselectableRelativeLayout; import com.wireguard.android.widget.MultiselectableRelativeLayout;
import com.wireguard.android.widget.fab.FloatingActionsMenuRecyclerViewScrollListener; import com.wireguard.android.widget.fab.FloatingActionsMenuRecyclerViewScrollListener;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Config; import com.wireguard.config.Config;
import com.wireguard.config.ParseException;


import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@@ -89,8 +89,8 @@ public class TunnelListFragment extends BaseFragment {
final FragmentManager fragmentManager = getFragmentManager(); final FragmentManager fragmentManager = getFragmentManager();
if (fragmentManager != null) if (fragmentManager != null)
ConfigNamingDialogFragment.newInstance(configText).show(fragmentManager, null); ConfigNamingDialogFragment.newInstance(configText).show(fragmentManager, null);
} catch (final IllegalArgumentException | IOException | ParseException exception) {
onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception));
} catch (final BadConfigException | IOException e) {
onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(e));
} }
} }




+ 12
- 8
app/src/main/java/com/wireguard/android/util/ExceptionLoggers.java View File

@@ -11,8 +11,9 @@ import android.util.Log;


import com.wireguard.android.Application; import com.wireguard.android.Application;
import com.wireguard.android.R; import com.wireguard.android.R;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.ParseException; import com.wireguard.config.ParseException;
import com.wireguard.crypto.Key;
import com.wireguard.crypto.KeyFormatException;


import java9.util.concurrent.CompletionException; import java9.util.concurrent.CompletionException;
import java9.util.function.BiConsumer; import java9.util.function.BiConsumer;
@@ -37,6 +38,8 @@ public enum ExceptionLoggers implements BiConsumer<Object, Throwable> {
public static Throwable unwrap(final Throwable throwable) { public static Throwable unwrap(final Throwable throwable) {
if (throwable instanceof CompletionException && throwable.getCause() != null) if (throwable instanceof CompletionException && throwable.getCause() != null)
return throwable.getCause(); return throwable.getCause();
if (throwable instanceof ParseException && throwable.getCause() != null)
return throwable.getCause();
return throwable; return throwable;
} }


@@ -44,13 +47,14 @@ public enum ExceptionLoggers implements BiConsumer<Object, Throwable> {
final Throwable innerThrowable = unwrap(throwable); final Throwable innerThrowable = unwrap(throwable);
final Resources resources = Application.get().getResources(); final Resources resources = Application.get().getResources();
String message; String message;
if (innerThrowable instanceof ParseException) {
final ParseException parseException = (ParseException) innerThrowable;
message = resources.getString(R.string.parse_error, parseException.getText(), parseException.getContext());
if (parseException.getMessage() != null)
message += ": " + parseException.getMessage();
} else if (innerThrowable instanceof Key.KeyFormatException) {
final Key.KeyFormatException keyFormatException = (Key.KeyFormatException) innerThrowable;
if (innerThrowable instanceof BadConfigException) {
final BadConfigException configException = (BadConfigException) innerThrowable;
message = resources.getString(R.string.parse_error, configException.getText(), configException.getLocation());
final Throwable cause = unwrap(configException);
if (cause.getMessage() != null)
message += ": " + cause.getMessage();
} else if (innerThrowable instanceof KeyFormatException) {
final KeyFormatException keyFormatException = (KeyFormatException) innerThrowable;
switch (keyFormatException.getFormat()) { switch (keyFormatException.getFormat()) {
case BASE64: case BASE64:
message = resources.getString(R.string.key_length_base64_exception_message); message = resources.getString(R.string.key_length_base64_exception_message);


+ 2
- 2
app/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.java View File

@@ -10,8 +10,8 @@ import android.databinding.ObservableList;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;


import com.wireguard.config.BadConfigException;
import com.wireguard.config.Config; import com.wireguard.config.Config;
import com.wireguard.config.ParseException;
import com.wireguard.config.Peer; import com.wireguard.config.Peer;


import java.util.ArrayList; import java.util.ArrayList;
@@ -63,7 +63,7 @@ public class ConfigProxy implements Parcelable {
return peers; return peers;
} }


public Config resolve() throws ParseException {
public Config resolve() throws BadConfigException {
final Collection<Peer> resolvedPeers = new ArrayList<>(); final Collection<Peer> resolvedPeers = new ArrayList<>();
for (final PeerProxy proxy : peers) for (final PeerProxy proxy : peers)
resolvedPeers.add(proxy.resolve()); resolvedPeers.add(proxy.resolve());


+ 4
- 3
app/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.java View File

@@ -14,9 +14,10 @@ import android.os.Parcelable;


import com.wireguard.android.BR; import com.wireguard.android.BR;
import com.wireguard.config.Attribute; import com.wireguard.config.Attribute;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Interface; import com.wireguard.config.Interface;
import com.wireguard.config.ParseException;
import com.wireguard.crypto.Key; import com.wireguard.crypto.Key;
import com.wireguard.crypto.KeyFormatException;
import com.wireguard.crypto.KeyPair; import com.wireguard.crypto.KeyPair;


import java.net.InetAddress; import java.net.InetAddress;
@@ -116,7 +117,7 @@ public class InterfaceProxy extends BaseObservable implements Parcelable {
return publicKey; return publicKey;
} }


public Interface resolve() throws ParseException {
public Interface resolve() throws BadConfigException {
final Interface.Builder builder = new Interface.Builder(); final Interface.Builder builder = new Interface.Builder();
if (!addresses.isEmpty()) if (!addresses.isEmpty())
builder.parseAddresses(addresses); builder.parseAddresses(addresses);
@@ -157,7 +158,7 @@ public class InterfaceProxy extends BaseObservable implements Parcelable {
this.privateKey = privateKey; this.privateKey = privateKey;
try { try {
publicKey = new KeyPair(Key.fromBase64(privateKey)).getPublicKey().toBase64(); publicKey = new KeyPair(Key.fromBase64(privateKey)).getPublicKey().toBase64();
} catch (final Key.KeyFormatException ignored) {
} catch (final KeyFormatException ignored) {
publicKey = ""; publicKey = "";
} }
notifyPropertyChanged(BR.privateKey); notifyPropertyChanged(BR.privateKey);


+ 2
- 2
app/src/main/java/com/wireguard/android/viewmodel/PeerProxy.java View File

@@ -15,8 +15,8 @@ import android.support.annotation.Nullable;


import com.wireguard.android.BR; import com.wireguard.android.BR;
import com.wireguard.config.Attribute; import com.wireguard.config.Attribute;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.InetEndpoint; import com.wireguard.config.InetEndpoint;
import com.wireguard.config.ParseException;
import com.wireguard.config.Peer; import com.wireguard.config.Peer;
import com.wireguard.crypto.Key; import com.wireguard.crypto.Key;


@@ -164,7 +164,7 @@ public class PeerProxy extends BaseObservable implements Parcelable {
return allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS; return allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS;
} }


public Peer resolve() throws ParseException {
public Peer resolve() throws BadConfigException {
final Peer.Builder builder = new Peer.Builder(); final Peer.Builder builder = new Peer.Builder();
if (!allowedIps.isEmpty()) if (!allowedIps.isEmpty())
builder.parseAllowedIPs(allowedIps); builder.parseAllowedIPs(allowedIps);


+ 118
- 0
app/src/main/java/com/wireguard/config/BadConfigException.java View File

@@ -0,0 +1,118 @@
/*
* Copyright © 2018 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package com.wireguard.config;

import android.support.annotation.Nullable;

import com.wireguard.crypto.KeyFormatException;

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"),
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,
MISSING_VALUE,
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;
}
}
}

+ 24
- 11
app/src/main/java/com/wireguard/config/Config.java View File

@@ -7,6 +7,10 @@ package com.wireguard.config;


import android.support.annotation.Nullable; import android.support.annotation.Nullable;


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.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@@ -37,23 +41,27 @@ public final class Config {


/** /**
* Parses an series of "Interface" and "Peer" sections into a {@code Config}. Throws * 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.
* {@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 file
* @param stream a stream of UTF-8 text that is interpreted as a WireGuard configuration
* @return a {@code Config} instance representing the supplied configuration * @return a {@code Config} instance representing the supplied configuration
*/ */
public static Config parse(final InputStream stream) throws IOException, ParseException {
public static Config parse(final InputStream stream)
throws IOException, BadConfigException {
return parse(new BufferedReader(new InputStreamReader(stream))); return parse(new BufferedReader(new InputStreamReader(stream)));
} }


/** /**
* Parses an series of "Interface" and "Peer" sections into a {@code Config}. Throws * 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.
* {@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 file
* @param reader a BufferedReader of UTF-8 text that is interpreted as a WireGuard configuration
* @return a {@code Config} instance representing the supplied configuration * @return a {@code Config} instance representing the supplied configuration
*/ */
public static Config parse(final BufferedReader reader) throws IOException, ParseException {
public static Config parse(final BufferedReader reader)
throws IOException, BadConfigException {
final Builder builder = new Builder(); final Builder builder = new Builder();
final Collection<String> interfaceLines = new ArrayList<>(); final Collection<String> interfaceLines = new ArrayList<>();
final Collection<String> peerLines = new ArrayList<>(); final Collection<String> peerLines = new ArrayList<>();
@@ -80,20 +88,23 @@ public final class Config {
inInterfaceSection = false; inInterfaceSection = false;
inPeerSection = true; inPeerSection = true;
} else { } else {
throw new ParseException("top level", line, "Unknown section name");
throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL,
Reason.UNKNOWN_SECTION, line);
} }
} else if (inInterfaceSection) { } else if (inInterfaceSection) {
interfaceLines.add(line); interfaceLines.add(line);
} else if (inPeerSection) { } else if (inPeerSection) {
peerLines.add(line); peerLines.add(line);
} else { } else {
throw new ParseException("top level", line, "Expected [Interface] or [Peer]");
throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL,
Reason.UNKNOWN_SECTION, line);
} }
} }
if (inPeerSection) if (inPeerSection)
builder.parsePeer(peerLines); builder.parsePeer(peerLines);
else if (!inInterfaceSection) else if (!inInterfaceSection)
throw new ParseException("top level", "", "Empty configuration");
throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL,
Reason.MISSING_SECTION, null);
// Combine all [Interface] sections in the file. // Combine all [Interface] sections in the file.
builder.parseInterface(interfaceLines); builder.parseInterface(interfaceLines);
return builder.build(); return builder.build();
@@ -192,11 +203,13 @@ public final class Config {
return new Config(this); return new Config(this);
} }


public Builder parseInterface(final Iterable<? extends CharSequence> lines) throws ParseException {
public Builder parseInterface(final Iterable<? extends CharSequence> lines)
throws BadConfigException {
return setInterface(Interface.parse(lines)); return setInterface(Interface.parse(lines));
} }


public Builder parsePeer(final Iterable<? extends CharSequence> lines) throws ParseException {
public Builder parsePeer(final Iterable<? extends CharSequence> lines)
throws BadConfigException {
return addPeer(Peer.parse(lines)); return addPeer(Peer.parse(lines));
} }




+ 5
- 4
app/src/main/java/com/wireguard/config/InetAddresses.java View File

@@ -37,17 +37,18 @@ public final class InetAddresses {
* @param address a string representing the IP address * @param address a string representing the IP address
* @return an instance of {@link Inet4Address} or {@link Inet6Address}, as appropriate * @return an instance of {@link Inet4Address} or {@link Inet6Address}, as appropriate
*/ */
public static InetAddress parse(final String address) {
public static InetAddress parse(final String address) throws ParseException {
if (address.isEmpty()) if (address.isEmpty())
throw new IllegalArgumentException("Empty address");
throw new ParseException(InetAddress.class, address, "Empty address");
try { try {
return (InetAddress) PARSER_METHOD.invoke(null, address); return (InetAddress) PARSER_METHOD.invoke(null, address);
} catch (final IllegalAccessException | InvocationTargetException e) { } catch (final IllegalAccessException | InvocationTargetException e) {
final Throwable cause = e.getCause(); final Throwable cause = e.getCause();
// Re-throw parsing exceptions with the original type, as callers might try to catch // 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. // them. On the other hand, callers cannot be expected to handle reflection failures.
throw cause instanceof IllegalArgumentException ?
(IllegalArgumentException) cause : new RuntimeException(e);
if (cause instanceof IllegalArgumentException)
throw new ParseException(InetAddress.class, address, cause);
throw new RuntimeException(e);
} }
} }
} }

+ 4
- 4
app/src/main/java/com/wireguard/config/InetEndpoint.java View File

@@ -42,9 +42,9 @@ public final class InetEndpoint {
this.port = port; this.port = port;
} }


public static InetEndpoint parse(final String endpoint) {
public static InetEndpoint parse(final String endpoint) throws ParseException {
if (FORBIDDEN_CHARACTERS.matcher(endpoint).find()) if (FORBIDDEN_CHARACTERS.matcher(endpoint).find())
throw new IllegalArgumentException("Forbidden characters in endpoint");
throw new ParseException(InetEndpoint.class, endpoint, "Forbidden characters");
final URI uri; final URI uri;
try { try {
uri = new URI("wg://" + endpoint); uri = new URI("wg://" + endpoint);
@@ -52,12 +52,12 @@ public final class InetEndpoint {
throw new IllegalArgumentException(e); throw new IllegalArgumentException(e);
} }
if (uri.getPort() < 0) if (uri.getPort() < 0)
throw new IllegalArgumentException("An endpoint must specify a port (e.g. 51820)");
throw new ParseException(InetEndpoint.class, endpoint, "Missing/invalid port number");
try { try {
InetAddresses.parse(uri.getHost()); InetAddresses.parse(uri.getHost());
// Parsing ths host as a numeric address worked, so we don't need to do DNS lookups. // 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()); return new InetEndpoint(uri.getHost(), true, uri.getPort());
} catch (final IllegalArgumentException ignored) {
} catch (final ParseException ignored) {
// Failed to parse the host as a numeric address, so it must be a DNS hostname/FQDN. // 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()); return new InetEndpoint(uri.getHost(), false, uri.getPort());
} }


+ 11
- 2
app/src/main/java/com/wireguard/config/InetNetwork.java View File

@@ -22,19 +22,28 @@ public final class InetNetwork {
this.mask = mask; this.mask = mask;
} }


public static InetNetwork parse(final String network) {
public static InetNetwork parse(final String network) throws ParseException {
final int slash = network.lastIndexOf('/'); final int slash = network.lastIndexOf('/');
final String maskString;
final int rawMask; final int rawMask;
final String rawAddress; final String rawAddress;
if (slash >= 0) { if (slash >= 0) {
rawMask = Integer.parseInt(network.substring(slash + 1), 10);
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); rawAddress = network.substring(0, slash);
} else { } else {
maskString = "";
rawMask = -1; rawMask = -1;
rawAddress = network; rawAddress = network;
} }
final InetAddress address = InetAddresses.parse(rawAddress); final InetAddress address = InetAddresses.parse(rawAddress);
final int maxMask = (address instanceof Inet4Address) ? 32 : 128; 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 ? rawMask : maxMask; final int mask = rawMask >= 0 && rawMask <= maxMask ? rawMask : maxMask;
return new InetNetwork(address, mask); return new InetNetwork(address, mask);
} }


+ 43
- 40
app/src/main/java/com/wireguard/config/Interface.java View File

@@ -7,7 +7,11 @@ package com.wireguard.config;


import android.support.annotation.Nullable; import android.support.annotation.Nullable;


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.Key;
import com.wireguard.crypto.KeyFormatException;
import com.wireguard.crypto.KeyPair; import com.wireguard.crypto.KeyPair;


import java.net.InetAddress; import java.net.InetAddress;
@@ -22,7 +26,6 @@ import java.util.Set;
import java9.util.Lists; import java9.util.Lists;
import java9.util.Optional; import java9.util.Optional;
import java9.util.stream.Collectors; import java9.util.stream.Collectors;
import java9.util.stream.Stream;
import java9.util.stream.StreamSupport; import java9.util.stream.StreamSupport;


/** /**
@@ -60,11 +63,13 @@ public final class Interface {
* @param lines An iterable sequence of lines, containing at least a private key attribute * @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 * @return An {@code Interface} with all of the attributes from {@code lines} set
*/ */
public static Interface parse(final Iterable<? extends CharSequence> lines) throws ParseException {
public static Interface parse(final Iterable<? extends CharSequence> lines)
throws BadConfigException {
final Builder builder = new Builder(); final Builder builder = new Builder();
for (final CharSequence line : lines) { for (final CharSequence line : lines) {
final Attribute attribute = Attribute.parse(line)
.orElseThrow(() -> new ParseException("[Interface]", line, "Syntax error"));
final Attribute attribute = Attribute.parse(line).orElseThrow(() ->
new BadConfigException(Section.INTERFACE, Location.TOP_LEVEL,
Reason.SYNTAX_ERROR, line));
switch (attribute.getKey().toLowerCase(Locale.ENGLISH)) { switch (attribute.getKey().toLowerCase(Locale.ENGLISH)) {
case "address": case "address":
builder.parseAddresses(attribute.getValue()); builder.parseAddresses(attribute.getValue());
@@ -85,7 +90,8 @@ public final class Interface {
builder.parsePrivateKey(attribute.getValue()); builder.parsePrivateKey(attribute.getValue());
break; break;
default: default:
throw new ParseException("[Interface]", attribute.getKey(), "Unknown attribute");
throw new BadConfigException(Section.INTERFACE, Location.TOP_LEVEL,
Reason.UNKNOWN_ATTRIBUTE, attribute.getKey());
} }
} }
return builder.build(); return builder.build();
@@ -260,9 +266,10 @@ public final class Interface {
return this; return this;
} }


public Interface build() {
public Interface build() throws BadConfigException {
if (keyPair == null) if (keyPair == null)
throw new IllegalArgumentException("Interfaces must have a private key");
throw new BadConfigException(Section.INTERFACE, Location.PRIVATE_KEY,
Reason.MISSING_ATTRIBUTE, null);
return new Interface(this); return new Interface(this);
} }


@@ -276,57 +283,51 @@ public final class Interface {
return this; return this;
} }


public Builder parseAddresses(final CharSequence addresses) throws ParseException {
public Builder parseAddresses(final CharSequence addresses) throws BadConfigException {
try { try {
final List<InetNetwork> 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);
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 ParseException {
public Builder parseDnsServers(final CharSequence dnsServers) throws BadConfigException {
try { try {
final List<InetAddress> 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);
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) throws ParseException {
try {
return excludeApplications(Lists.of(Attribute.split(apps)));
} catch (final IllegalArgumentException e) {
throw new ParseException("ExcludedApplications", apps, e);
}
public Builder parseExcludedApplications(final CharSequence apps) {
return excludeApplications(Lists.of(Attribute.split(apps)));
} }


public Builder parseListenPort(final String listenPort) throws ParseException {
public Builder parseListenPort(final String listenPort) throws BadConfigException {
try { try {
return setListenPort(Integer.parseInt(listenPort)); return setListenPort(Integer.parseInt(listenPort));
} catch (final IllegalArgumentException e) {
throw new ParseException("ListenPort", listenPort, e);
} catch (final NumberFormatException e) {
throw new BadConfigException(Section.INTERFACE, Location.LISTEN_PORT, listenPort, e);
} }
} }


public Builder parseMtu(final String mtu) throws ParseException {
public Builder parseMtu(final String mtu) throws BadConfigException {
try { try {
return setMtu(Integer.parseInt(mtu)); return setMtu(Integer.parseInt(mtu));
} catch (final IllegalArgumentException e) {
throw new ParseException("MTU", mtu, e);
} catch (final NumberFormatException e) {
throw new BadConfigException(Section.INTERFACE, Location.MTU, mtu, e);
} }
} }


public Builder parsePrivateKey(final String privateKey) throws ParseException {
public Builder parsePrivateKey(final String privateKey) throws BadConfigException {
try { try {
return setKeyPair(new KeyPair(Key.fromBase64(privateKey))); return setKeyPair(new KeyPair(Key.fromBase64(privateKey)));
} catch (final Key.KeyFormatException e) {
throw new ParseException("PrivateKey", "(omitted)", e);
} catch (final KeyFormatException e) {
throw new BadConfigException(Section.INTERFACE, Location.PRIVATE_KEY, e);
} }
} }


@@ -335,16 +336,18 @@ public final class Interface {
return this; return this;
} }


public Builder setListenPort(final int listenPort) {
public Builder setListenPort(final int listenPort) throws BadConfigException {
if (listenPort < MIN_UDP_PORT || listenPort > MAX_UDP_PORT) if (listenPort < MIN_UDP_PORT || listenPort > MAX_UDP_PORT)
throw new IllegalArgumentException("ListenPort must be a valid UDP port number");
throw new BadConfigException(Section.INTERFACE, Location.LISTEN_PORT,
Reason.INVALID_VALUE, String.valueOf(listenPort));
this.listenPort = listenPort == 0 ? Optional.empty() : Optional.of(listenPort); this.listenPort = listenPort == 0 ? Optional.empty() : Optional.of(listenPort);
return this; return this;
} }


public Builder setMtu(final int mtu) {
public Builder setMtu(final int mtu) throws BadConfigException {
if (mtu < 0) if (mtu < 0)
throw new IllegalArgumentException("MTU must not be negative");
throw new BadConfigException(Section.INTERFACE, Location.LISTEN_PORT,
Reason.INVALID_VALUE, String.valueOf(mtu));
this.mtu = mtu == 0 ? Optional.empty() : Optional.of(mtu); this.mtu = mtu == 0 ? Optional.empty() : Optional.of(mtu);
return this; return this;
} }


+ 19
- 16
app/src/main/java/com/wireguard/config/ParseException.java View File

@@ -5,34 +5,37 @@


package com.wireguard.config; package com.wireguard.config;


import android.support.annotation.Nullable;

/** /**
* 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 { public class ParseException extends Exception {
private final String context;
private final Class<?> parsingClass;
private final CharSequence text; private final CharSequence text;


public ParseException(final String context, final CharSequence text, final String message) {
super(message);
this.context = context;
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; 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 Class<?> parsingClass, final CharSequence text,
@Nullable final String message) {
this(parsingClass, text, message, null);
} }


public ParseException(final String context, final CharSequence text) {
this.context = context;
this.text = text;
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 String getContext() {
return context;
public Class<?> getParsingClass() {
return parsingClass;
} }


public CharSequence getText() { public CharSequence getText() {


+ 38
- 30
app/src/main/java/com/wireguard/config/Peer.java View File

@@ -7,19 +7,20 @@ package com.wireguard.config;


import android.support.annotation.Nullable; import android.support.annotation.Nullable;


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.Key;
import com.wireguard.crypto.KeyFormatException;


import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;


import java9.util.Optional; import java9.util.Optional;
import java9.util.stream.Collectors;
import java9.util.stream.Stream;


/** /**
* Represents the configuration for a WireGuard peer (a [Peer] block). Peers must have a public key, * Represents the configuration for a WireGuard peer (a [Peer] block). Peers must have a public key,
@@ -50,11 +51,13 @@ public final class Peer {
* @param lines an iterable sequence of lines, containing at least a public key attribute * @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} * @return a {@code Peer} with all of its attributes set from {@code lines}
*/ */
public static Peer parse(final Iterable<? extends CharSequence> lines) throws ParseException {
public static Peer parse(final Iterable<? extends CharSequence> lines)
throws BadConfigException {
final Builder builder = new Builder(); final Builder builder = new Builder();
for (final CharSequence line : lines) { for (final CharSequence line : lines) {
final Attribute attribute = Attribute.parse(line)
.orElseThrow(() -> new ParseException("[Peer]", line, "Syntax error"));
final Attribute attribute = Attribute.parse(line).orElseThrow(() ->
new BadConfigException(Section.PEER, Location.TOP_LEVEL,
Reason.SYNTAX_ERROR, line));
switch (attribute.getKey().toLowerCase(Locale.ENGLISH)) { switch (attribute.getKey().toLowerCase(Locale.ENGLISH)) {
case "allowedips": case "allowedips":
builder.parseAllowedIPs(attribute.getValue()); builder.parseAllowedIPs(attribute.getValue());
@@ -72,7 +75,8 @@ public final class Peer {
builder.parsePublicKey(attribute.getValue()); builder.parsePublicKey(attribute.getValue());
break; break;
default: default:
throw new ParseException("[Peer]", line, "Unknown attribute");
throw new BadConfigException(Section.PEER, Location.TOP_LEVEL,
Reason.UNKNOWN_ATTRIBUTE, attribute.getKey());
} }
} }
return builder.build(); return builder.build();
@@ -223,52 +227,54 @@ public final class Peer {
return this; return this;
} }


public Peer build() {
public Peer build() throws BadConfigException {
if (publicKey == null) if (publicKey == null)
throw new IllegalArgumentException("Peers must have a public key");
throw new BadConfigException(Section.PEER, Location.PUBLIC_KEY,
Reason.MISSING_ATTRIBUTE, null);
return new Peer(this); return new Peer(this);
} }


public Builder parseAllowedIPs(final CharSequence allowedIps) throws ParseException {
public Builder parseAllowedIPs(final CharSequence allowedIps) throws BadConfigException {
try { try {
final List<InetNetwork> 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);
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 ParseException {
public Builder parseEndpoint(final String endpoint) throws BadConfigException {
try { try {
return setEndpoint(InetEndpoint.parse(endpoint)); return setEndpoint(InetEndpoint.parse(endpoint));
} catch (final IllegalArgumentException e) {
throw new ParseException("Endpoint", endpoint, e);
} catch (final ParseException e) {
throw new BadConfigException(Section.PEER, Location.ENDPOINT, e);
} }
} }


public Builder parsePersistentKeepalive(final String persistentKeepalive) throws ParseException {
public Builder parsePersistentKeepalive(final String persistentKeepalive)
throws BadConfigException {
try { try {
return setPersistentKeepalive(Integer.parseInt(persistentKeepalive)); return setPersistentKeepalive(Integer.parseInt(persistentKeepalive));
} catch (final IllegalArgumentException e) {
throw new ParseException("PersistentKeepalive", persistentKeepalive, e);
} catch (final NumberFormatException e) {
throw new BadConfigException(Section.PEER, Location.PERSISTENT_KEEPALIVE,
persistentKeepalive, e);
} }
} }


public Builder parsePreSharedKey(final String preSharedKey) throws ParseException {
public Builder parsePreSharedKey(final String preSharedKey) throws BadConfigException {
try { try {
return setPreSharedKey(Key.fromBase64(preSharedKey)); return setPreSharedKey(Key.fromBase64(preSharedKey));
} catch (final Key.KeyFormatException e) {
throw new ParseException("PresharedKey", preSharedKey, e);
} catch (final KeyFormatException e) {
throw new BadConfigException(Section.PEER, Location.PRE_SHARED_KEY, e);
} }
} }


public Builder parsePublicKey(final String publicKey) throws ParseException {
public Builder parsePublicKey(final String publicKey) throws BadConfigException {
try { try {
return setPublicKey(Key.fromBase64(publicKey)); return setPublicKey(Key.fromBase64(publicKey));
} catch (final Key.KeyFormatException e) {
throw new ParseException("PublicKey", publicKey, e);
} catch (final KeyFormatException e) {
throw new BadConfigException(Section.PEER, Location.PUBLIC_KEY, e);
} }
} }


@@ -277,9 +283,11 @@ public final class Peer {
return this; return this;
} }


public Builder setPersistentKeepalive(final int persistentKeepalive) {
public Builder setPersistentKeepalive(final int persistentKeepalive)
throws BadConfigException {
if (persistentKeepalive < 0 || persistentKeepalive > MAX_PERSISTENT_KEEPALIVE) if (persistentKeepalive < 0 || persistentKeepalive > MAX_PERSISTENT_KEEPALIVE)
throw new IllegalArgumentException("Invalid value for PersistentKeepalive");
throw new BadConfigException(Section.PEER, Location.PERSISTENT_KEEPALIVE,
Reason.INVALID_VALUE, String.valueOf(persistentKeepalive));
this.persistentKeepalive = persistentKeepalive == 0 ? this.persistentKeepalive = persistentKeepalive == 0 ?
Optional.empty() : Optional.of(persistentKeepalive); Optional.empty() : Optional.of(persistentKeepalive);
return this; return this;


+ 38
- 24
app/src/main/java/com/wireguard/crypto/Key.java View File

@@ -5,6 +5,9 @@


package com.wireguard.crypto; package com.wireguard.crypto;


import com.wireguard.crypto.KeyFormatException.Type;

import java.security.SecureRandom;
import java.util.Arrays; import java.util.Arrays;


/** /**
@@ -83,10 +86,10 @@ public final class Key {
* @param str the base64 string representation of a WireGuard key * @param str the base64 string representation of a WireGuard key
* @return the decoded key encapsulated in an immutable container * @return the decoded key encapsulated in an immutable container
*/ */
public static Key fromBase64(final String str) {
public static Key fromBase64(final String str) throws KeyFormatException {
final char[] input = str.toCharArray(); final char[] input = str.toCharArray();
if (input.length != Format.BASE64.length || input[Format.BASE64.length - 1] != '=') if (input.length != Format.BASE64.length || input[Format.BASE64.length - 1] != '=')
throw new KeyFormatException(Format.BASE64);
throw new KeyFormatException(Format.BASE64, Type.LENGTH);
final byte[] key = new byte[Format.BINARY.length]; final byte[] key = new byte[Format.BINARY.length];
int i; int i;
int ret = 0; int ret = 0;
@@ -109,7 +112,7 @@ public final class Key {
key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff); key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);


if (ret != 0) if (ret != 0)
throw new KeyFormatException(Format.BASE64);
throw new KeyFormatException(Format.BASE64, Type.CONTENTS);
return new Key(key); return new Key(key);
} }


@@ -120,9 +123,9 @@ public final class Key {
* @param bytes an array of bytes containing a WireGuard key in binary format * @param bytes an array of bytes containing a WireGuard key in binary format
* @return the key encapsulated in an immutable container * @return the key encapsulated in an immutable container
*/ */
public static Key fromBytes(final byte[] bytes) {
public static Key fromBytes(final byte[] bytes) throws KeyFormatException {
if (bytes.length != Format.BINARY.length) if (bytes.length != Format.BINARY.length)
throw new KeyFormatException(Format.BINARY);
throw new KeyFormatException(Format.BINARY, Type.LENGTH);
return new Key(bytes); return new Key(bytes);
} }


@@ -133,10 +136,10 @@ public final class Key {
* @param str the hexadecimal string representation of a WireGuard key * @param str the hexadecimal string representation of a WireGuard key
* @return the decoded key encapsulated in an immutable container * @return the decoded key encapsulated in an immutable container
*/ */
public static Key fromHex(final String str) {
public static Key fromHex(final String str) throws KeyFormatException {
final char[] input = str.toCharArray(); final char[] input = str.toCharArray();
if (input.length != Format.HEX.length) if (input.length != Format.HEX.length)
throw new KeyFormatException(Format.HEX);
throw new KeyFormatException(Format.HEX, Type.LENGTH);
final byte[] key = new byte[Format.BINARY.length]; final byte[] key = new byte[Format.BINARY.length];
int ret = 0; int ret = 0;
for (int i = 0; i < key.length; ++i) { for (int i = 0; i < key.length; ++i) {
@@ -167,10 +170,37 @@ public final class Key {
key[i] = (byte) (cAcc | cVal); key[i] = (byte) (cAcc | cVal);
} }
if (ret != 0) if (ret != 0)
throw new KeyFormatException(Format.HEX);
throw new KeyFormatException(Format.HEX, Type.CONTENTS);
return new Key(key); 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);
}

/** /**
* Returns the key as an array of bytes. * Returns the key as an array of bytes.
* *
@@ -236,20 +266,4 @@ public final class Key {
} }
} }


/**
* 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;
}
}
} }

+ 34
- 0
app/src/main/java/com/wireguard/crypto/KeyFormatException.java View File

@@ -0,0 +1,34 @@
/*
* Copyright © 2018 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
}
}

+ 2
- 32
app/src/main/java/com/wireguard/crypto/KeyPair.java View File

@@ -5,8 +5,6 @@


package com.wireguard.crypto; package com.wireguard.crypto;


import java.security.SecureRandom;

/** /**
* Represents a Curve25519 key pair as used by WireGuard. * Represents a Curve25519 key pair as used by WireGuard.
* <p> * <p>
@@ -20,7 +18,7 @@ public class KeyPair {
* Creates a key pair using a newly-generated private key. * Creates a key pair using a newly-generated private key.
*/ */
public KeyPair() { public KeyPair() {
this(generatePrivateKey());
this(Key.generatePrivateKey());
} }


/** /**
@@ -30,35 +28,7 @@ public class KeyPair {
*/ */
public KeyPair(final Key privateKey) { public KeyPair(final Key privateKey) {
this.privateKey = 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);
publicKey = Key.generatePublicKey(privateKey);
} }


/** /**


Loading…
Cancel
Save