diff --git a/app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java b/app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java index a637eba..d562c19 100644 --- a/app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java +++ b/app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java @@ -9,8 +9,8 @@ import android.content.Context; import android.util.Log; import com.wireguard.android.R; +import com.wireguard.config.BadConfigException; import com.wireguard.config.Config; -import com.wireguard.config.ParseException; import java.io.File; import java.io.FileInputStream; @@ -69,7 +69,7 @@ public final class FileConfigStore implements ConfigStore { } @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))) { return Config.parse(stream); } diff --git a/app/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java b/app/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java index 9c8a300..2cdf79a 100644 --- a/app/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java +++ b/app/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java @@ -18,8 +18,8 @@ import android.view.inputmethod.InputMethodManager; import com.wireguard.android.Application; import com.wireguard.android.R; import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding; +import com.wireguard.config.BadConfigException; import com.wireguard.config.Config; -import com.wireguard.config.ParseException; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -67,8 +67,8 @@ public class ConfigNamingDialogFragment extends DialogFragment { try { 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); } } diff --git a/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java b/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java index bbcbab9..4a6103b 100644 --- a/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java +++ b/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java @@ -40,8 +40,8 @@ import com.wireguard.android.model.Tunnel; import com.wireguard.android.util.ExceptionLoggers; import com.wireguard.android.widget.MultiselectableRelativeLayout; import com.wireguard.android.widget.fab.FloatingActionsMenuRecyclerViewScrollListener; +import com.wireguard.config.BadConfigException; import com.wireguard.config.Config; -import com.wireguard.config.ParseException; import java.io.BufferedReader; import java.io.ByteArrayInputStream; @@ -89,8 +89,8 @@ public class TunnelListFragment extends BaseFragment { final FragmentManager fragmentManager = getFragmentManager(); if (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)); } } diff --git a/app/src/main/java/com/wireguard/android/util/ExceptionLoggers.java b/app/src/main/java/com/wireguard/android/util/ExceptionLoggers.java index f9cadf5..3cb5ca9 100644 --- a/app/src/main/java/com/wireguard/android/util/ExceptionLoggers.java +++ b/app/src/main/java/com/wireguard/android/util/ExceptionLoggers.java @@ -11,8 +11,9 @@ import android.util.Log; import com.wireguard.android.Application; import com.wireguard.android.R; +import com.wireguard.config.BadConfigException; import com.wireguard.config.ParseException; -import com.wireguard.crypto.Key; +import com.wireguard.crypto.KeyFormatException; import java9.util.concurrent.CompletionException; import java9.util.function.BiConsumer; @@ -37,6 +38,8 @@ public enum ExceptionLoggers implements BiConsumer { public static Throwable unwrap(final Throwable throwable) { if (throwable instanceof CompletionException && throwable.getCause() != null) return throwable.getCause(); + if (throwable instanceof ParseException && throwable.getCause() != null) + return throwable.getCause(); return throwable; } @@ -44,13 +47,14 @@ public enum ExceptionLoggers implements BiConsumer { final Throwable innerThrowable = unwrap(throwable); final Resources resources = Application.get().getResources(); 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()) { case BASE64: message = resources.getString(R.string.key_length_base64_exception_message); diff --git a/app/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.java b/app/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.java index abe8cbc..6252329 100644 --- a/app/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.java +++ b/app/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.java @@ -10,8 +10,8 @@ import android.databinding.ObservableList; import android.os.Parcel; import android.os.Parcelable; +import com.wireguard.config.BadConfigException; import com.wireguard.config.Config; -import com.wireguard.config.ParseException; import com.wireguard.config.Peer; import java.util.ArrayList; @@ -63,7 +63,7 @@ public class ConfigProxy implements Parcelable { return peers; } - public Config resolve() throws ParseException { + public Config resolve() throws BadConfigException { final Collection resolvedPeers = new ArrayList<>(); for (final PeerProxy proxy : peers) resolvedPeers.add(proxy.resolve()); diff --git a/app/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.java b/app/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.java index 63d8204..3b303f4 100644 --- a/app/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.java +++ b/app/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.java @@ -14,9 +14,10 @@ import android.os.Parcelable; import com.wireguard.android.BR; import com.wireguard.config.Attribute; +import com.wireguard.config.BadConfigException; import com.wireguard.config.Interface; -import com.wireguard.config.ParseException; import com.wireguard.crypto.Key; +import com.wireguard.crypto.KeyFormatException; import com.wireguard.crypto.KeyPair; import java.net.InetAddress; @@ -116,7 +117,7 @@ public class InterfaceProxy extends BaseObservable implements Parcelable { return publicKey; } - public Interface resolve() throws ParseException { + public Interface resolve() throws BadConfigException { final Interface.Builder builder = new Interface.Builder(); if (!addresses.isEmpty()) builder.parseAddresses(addresses); @@ -157,7 +158,7 @@ public class InterfaceProxy extends BaseObservable implements Parcelable { this.privateKey = privateKey; try { publicKey = new KeyPair(Key.fromBase64(privateKey)).getPublicKey().toBase64(); - } catch (final Key.KeyFormatException ignored) { + } catch (final KeyFormatException ignored) { publicKey = ""; } notifyPropertyChanged(BR.privateKey); diff --git a/app/src/main/java/com/wireguard/android/viewmodel/PeerProxy.java b/app/src/main/java/com/wireguard/android/viewmodel/PeerProxy.java index 822a427..c9bae98 100644 --- a/app/src/main/java/com/wireguard/android/viewmodel/PeerProxy.java +++ b/app/src/main/java/com/wireguard/android/viewmodel/PeerProxy.java @@ -15,8 +15,8 @@ import android.support.annotation.Nullable; import com.wireguard.android.BR; import com.wireguard.config.Attribute; +import com.wireguard.config.BadConfigException; import com.wireguard.config.InetEndpoint; -import com.wireguard.config.ParseException; import com.wireguard.config.Peer; import com.wireguard.crypto.Key; @@ -164,7 +164,7 @@ public class PeerProxy extends BaseObservable implements Parcelable { return allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS; } - public Peer resolve() throws ParseException { + public Peer resolve() throws BadConfigException { final Peer.Builder builder = new Peer.Builder(); if (!allowedIps.isEmpty()) builder.parseAllowedIPs(allowedIps); diff --git a/app/src/main/java/com/wireguard/config/BadConfigException.java b/app/src/main/java/com/wireguard/config/BadConfigException.java new file mode 100644 index 0000000..7f794a3 --- /dev/null +++ b/app/src/main/java/com/wireguard/config/BadConfigException.java @@ -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; + } + } +} diff --git a/app/src/main/java/com/wireguard/config/Config.java b/app/src/main/java/com/wireguard/config/Config.java index d2c1395..8b393be 100644 --- a/app/src/main/java/com/wireguard/config/Config.java +++ b/app/src/main/java/com/wireguard/config/Config.java @@ -7,6 +7,10 @@ package com.wireguard.config; 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.IOException; 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 - * {@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 */ - 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))); } /** * 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 */ - 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 Collection interfaceLines = new ArrayList<>(); final Collection peerLines = new ArrayList<>(); @@ -80,20 +88,23 @@ public final class Config { inInterfaceSection = false; inPeerSection = true; } 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) { interfaceLines.add(line); } else if (inPeerSection) { peerLines.add(line); } 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) builder.parsePeer(peerLines); 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. builder.parseInterface(interfaceLines); return builder.build(); @@ -192,11 +203,13 @@ public final class Config { return new Config(this); } - public Builder parseInterface(final Iterable lines) throws ParseException { + public Builder parseInterface(final Iterable lines) + throws BadConfigException { return setInterface(Interface.parse(lines)); } - public Builder parsePeer(final Iterable lines) throws ParseException { + public Builder parsePeer(final Iterable lines) + throws BadConfigException { return addPeer(Peer.parse(lines)); } diff --git a/app/src/main/java/com/wireguard/config/InetAddresses.java b/app/src/main/java/com/wireguard/config/InetAddresses.java index 989598d..487b136 100644 --- a/app/src/main/java/com/wireguard/config/InetAddresses.java +++ b/app/src/main/java/com/wireguard/config/InetAddresses.java @@ -37,17 +37,18 @@ public final class InetAddresses { * @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) { + public static InetAddress parse(final String address) throws ParseException { if (address.isEmpty()) - throw new IllegalArgumentException("Empty address"); + throw new ParseException(InetAddress.class, address, "Empty address"); try { return (InetAddress) PARSER_METHOD.invoke(null, address); } catch (final IllegalAccessException | InvocationTargetException e) { final Throwable cause = e.getCause(); // Re-throw parsing exceptions with the original type, as callers might try to catch // them. On the other hand, callers cannot be expected to handle reflection failures. - throw cause instanceof IllegalArgumentException ? - (IllegalArgumentException) cause : new RuntimeException(e); + if (cause instanceof IllegalArgumentException) + throw new ParseException(InetAddress.class, address, cause); + throw 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 355b325..f64bc25 100644 --- a/app/src/main/java/com/wireguard/config/InetEndpoint.java +++ b/app/src/main/java/com/wireguard/config/InetEndpoint.java @@ -42,9 +42,9 @@ public final class InetEndpoint { 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()) - throw new IllegalArgumentException("Forbidden characters in endpoint"); + throw new ParseException(InetEndpoint.class, endpoint, "Forbidden characters"); final URI uri; try { uri = new URI("wg://" + endpoint); @@ -52,12 +52,12 @@ public final class InetEndpoint { throw new IllegalArgumentException(e); } 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 { 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) { + } catch (final ParseException ignored) { // Failed to parse the host as a numeric address, so it must be a DNS hostname/FQDN. return new InetEndpoint(uri.getHost(), false, uri.getPort()); } diff --git a/app/src/main/java/com/wireguard/config/InetNetwork.java b/app/src/main/java/com/wireguard/config/InetNetwork.java index 9e5e8c6..444af58 100644 --- a/app/src/main/java/com/wireguard/config/InetNetwork.java +++ b/app/src/main/java/com/wireguard/config/InetNetwork.java @@ -22,19 +22,28 @@ public final class InetNetwork { 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 String maskString; final int rawMask; final String rawAddress; 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); } else { + maskString = ""; rawMask = -1; rawAddress = network; } final InetAddress address = InetAddresses.parse(rawAddress); final int maxMask = (address instanceof Inet4Address) ? 32 : 128; + if (rawMask > maxMask) + throw new ParseException(InetNetwork.class, maskString, "Invalid network mask"); final int mask = rawMask >= 0 && rawMask <= maxMask ? rawMask : maxMask; return new InetNetwork(address, mask); } diff --git a/app/src/main/java/com/wireguard/config/Interface.java b/app/src/main/java/com/wireguard/config/Interface.java index da30fbc..2fd34a3 100644 --- a/app/src/main/java/com/wireguard/config/Interface.java +++ b/app/src/main/java/com/wireguard/config/Interface.java @@ -7,7 +7,11 @@ package com.wireguard.config; 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.KeyFormatException; import com.wireguard.crypto.KeyPair; import java.net.InetAddress; @@ -22,7 +26,6 @@ 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; /** @@ -60,11 +63,13 @@ public final class Interface { * @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 lines) throws ParseException { + public static Interface parse(final Iterable lines) + throws BadConfigException { final Builder builder = new Builder(); 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)) { case "address": builder.parseAddresses(attribute.getValue()); @@ -85,7 +90,8 @@ public final class Interface { builder.parsePrivateKey(attribute.getValue()); break; 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(); @@ -260,9 +266,10 @@ public final class Interface { return this; } - public Interface build() { + public Interface build() throws BadConfigException { 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); } @@ -276,57 +283,51 @@ public final class Interface { return this; } - public Builder parseAddresses(final CharSequence addresses) throws ParseException { + public Builder parseAddresses(final CharSequence addresses) throws BadConfigException { try { - 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); + 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 { - 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); + 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 { 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 { 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 { 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; } - public Builder setListenPort(final int listenPort) { + public Builder setListenPort(final int listenPort) throws BadConfigException { 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); return this; } - public Builder setMtu(final int mtu) { + public Builder setMtu(final int mtu) throws BadConfigException { 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); return this; } diff --git a/app/src/main/java/com/wireguard/config/ParseException.java b/app/src/main/java/com/wireguard/config/ParseException.java index 1fccb53..c8482af 100644 --- a/app/src/main/java/com/wireguard/config/ParseException.java +++ b/app/src/main/java/com/wireguard/config/ParseException.java @@ -5,34 +5,37 @@ 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 { - private final String context; + private final Class parsingClass; 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; } - 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() { diff --git a/app/src/main/java/com/wireguard/config/Peer.java b/app/src/main/java/com/wireguard/config/Peer.java index 1250fcb..cdea030 100644 --- a/app/src/main/java/com/wireguard/config/Peer.java +++ b/app/src/main/java/com/wireguard/config/Peer.java @@ -7,19 +7,20 @@ package com.wireguard.config; 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.KeyFormatException; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; -import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Set; 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, @@ -50,11 +51,13 @@ public final class Peer { * @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 lines) throws ParseException { + public static Peer parse(final Iterable lines) + throws BadConfigException { final Builder builder = new Builder(); 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)) { case "allowedips": builder.parseAllowedIPs(attribute.getValue()); @@ -72,7 +75,8 @@ public final class Peer { builder.parsePublicKey(attribute.getValue()); break; default: - throw new ParseException("[Peer]", line, "Unknown attribute"); + throw new BadConfigException(Section.PEER, Location.TOP_LEVEL, + Reason.UNKNOWN_ATTRIBUTE, attribute.getKey()); } } return builder.build(); @@ -223,52 +227,54 @@ public final class Peer { return this; } - public Peer build() { + public Peer build() throws BadConfigException { 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); } - public Builder parseAllowedIPs(final CharSequence allowedIps) throws ParseException { + public Builder parseAllowedIPs(final CharSequence allowedIps) throws BadConfigException { 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); + 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 { 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 { 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 { 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 { 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; } - public Builder setPersistentKeepalive(final int persistentKeepalive) { + public Builder setPersistentKeepalive(final int persistentKeepalive) + throws BadConfigException { 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 ? Optional.empty() : Optional.of(persistentKeepalive); return this; diff --git a/app/src/main/java/com/wireguard/crypto/Key.java b/app/src/main/java/com/wireguard/crypto/Key.java index 8514679..ded9941 100644 --- a/app/src/main/java/com/wireguard/crypto/Key.java +++ b/app/src/main/java/com/wireguard/crypto/Key.java @@ -5,6 +5,9 @@ package com.wireguard.crypto; +import com.wireguard.crypto.KeyFormatException.Type; + +import java.security.SecureRandom; import java.util.Arrays; /** @@ -83,10 +86,10 @@ public final class Key { * @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) { + public static Key fromBase64(final String str) throws KeyFormatException { final char[] input = str.toCharArray(); if (input.length != Format.BASE64.length || input[Format.BASE64.length - 1] != '=') - throw new KeyFormatException(Format.BASE64); + throw new KeyFormatException(Format.BASE64, Type.LENGTH); final byte[] key = new byte[Format.BINARY.length]; int i; int ret = 0; @@ -109,7 +112,7 @@ public final class Key { key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff); if (ret != 0) - throw new KeyFormatException(Format.BASE64); + throw new KeyFormatException(Format.BASE64, Type.CONTENTS); 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 * @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) - throw new KeyFormatException(Format.BINARY); + throw new KeyFormatException(Format.BINARY, Type.LENGTH); return new Key(bytes); } @@ -133,10 +136,10 @@ public final class Key { * @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) { + public static Key fromHex(final String str) throws KeyFormatException { final char[] input = str.toCharArray(); if (input.length != Format.HEX.length) - throw new KeyFormatException(Format.HEX); + throw new KeyFormatException(Format.HEX, Type.LENGTH); final byte[] key = new byte[Format.BINARY.length]; int ret = 0; for (int i = 0; i < key.length; ++i) { @@ -167,10 +170,37 @@ public final class Key { key[i] = (byte) (cAcc | cVal); } if (ret != 0) - throw new KeyFormatException(Format.HEX); + throw new KeyFormatException(Format.HEX, Type.CONTENTS); return new Key(key); } + /** + * Generates a private key using the system's {@link SecureRandom} number generator. + * + * @return a well-formed random private key + */ + static Key generatePrivateKey() { + final SecureRandom secureRandom = new SecureRandom(); + final byte[] privateKey = new byte[Format.BINARY.getLength()]; + secureRandom.nextBytes(privateKey); + privateKey[0] &= 248; + privateKey[31] &= 127; + privateKey[31] |= 64; + return new Key(privateKey); + } + + /** + * Generates a public key from an existing private key. + * + * @param privateKey a private key + * @return a well-formed public key that corresponds to the supplied private key + */ + static Key generatePublicKey(final Key privateKey) { + final byte[] publicKey = new byte[Format.BINARY.getLength()]; + Curve25519.eval(publicKey, 0, privateKey.getBytes(), null); + return new Key(publicKey); + } + /** * 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; - } - } } diff --git a/app/src/main/java/com/wireguard/crypto/KeyFormatException.java b/app/src/main/java/com/wireguard/crypto/KeyFormatException.java new file mode 100644 index 0000000..b44297d --- /dev/null +++ b/app/src/main/java/com/wireguard/crypto/KeyFormatException.java @@ -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 + } +} diff --git a/app/src/main/java/com/wireguard/crypto/KeyPair.java b/app/src/main/java/com/wireguard/crypto/KeyPair.java index 2b2bf56..2e771ed 100644 --- a/app/src/main/java/com/wireguard/crypto/KeyPair.java +++ b/app/src/main/java/com/wireguard/crypto/KeyPair.java @@ -5,8 +5,6 @@ package com.wireguard.crypto; -import java.security.SecureRandom; - /** * Represents a Curve25519 key pair as used by WireGuard. *

@@ -20,7 +18,7 @@ public class KeyPair { * Creates a key pair using a newly-generated private key. */ public KeyPair() { - this(generatePrivateKey()); + this(Key.generatePrivateKey()); } /** @@ -30,35 +28,7 @@ public class KeyPair { */ 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); + publicKey = Key.generatePublicKey(privateKey); } /**