Remodel the Model

- The configuration and crypto model is now entirely independent
of Android classes other than Nullable and TextUtils.
- Model classes are immutable and use builders that enforce the
appropriate optional/required attributes.
- The Android config proxies (for Parcelable and databinding) are
moved to the Android side of the codebase, and are designed to be
safe for two-way databinding. This allows proper observability in
TunnelDetailFragment.
- Various robustness fixes and documentation updates to helper classes.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
Samuel Holland 2018-09-05 20:17:14 -05:00 committed by Jason A. Donenfeld
parent a264f7ab36
commit d1e85633fb
47 changed files with 2082 additions and 1340 deletions

View File

@ -1,6 +1,6 @@
<component name="CopyrightManager"> <component name="CopyrightManager">
<copyright> <copyright>
<option name="notice" value="Copyright © &amp;#36;today.year Firstname Lastname &lt;email@example.org&gt;&#10;SPDX-License-Identifier: Apache-2.0 <option name="notice" value="Copyright © &amp;#36;today.year WireGuard LLC. All Rights Reserved.&#10;SPDX-License-Identifier: Apache-2.0" />
<option name="myName" value="Default" /> <option name="myName" value="Default" />
</copyright> </copyright>
</component> </component>

View File

@ -1,3 +1,3 @@
<component name="CopyrightManager"> <component name="CopyrightManager">
<settings default="GPL-2.0-or-later" /> <settings default="Default" />
</component> </component>

View File

@ -385,7 +385,6 @@
<option name="ignorePrivateMethods" value="true" /> <option name="ignorePrivateMethods" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="ReturnOfDateField" enabled="false" level="WARNING" enabled_by_default="false" /> <inspection_tool class="ReturnOfDateField" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ReturnOfInnerClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ScalarTypeRequired" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="ScalarTypeRequired" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="SerializableClassInSecureContext" enabled="false" level="WARNING" enabled_by_default="false" /> <inspection_tool class="SerializableClassInSecureContext" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SetReplaceableByEnumSet" enabled="true" level="WARNING" enabled_by_default="true" /> <inspection_tool class="SetReplaceableByEnumSet" enabled="true" level="WARNING" enabled_by_default="true" />

View File

@ -65,12 +65,13 @@ android {
} }
ext { ext {
databindingVersion = '3.1.3'
supportLibsVersion = '27.1.1'
streamsupportVersion = '1.6.0'
jsr305Version = '3.0.2'
zxingEmbeddedVersion = '3.6.0'
acraVersion = '5.2.0-rc2' acraVersion = '5.2.0-rc2'
databindingVersion = '3.1.3'
jsr305Version = '3.0.2'
streamsupportVersion = '1.6.0'
supportLibsVersion = '28.0.0'
threetenabpVersion = '1.1.1'
zxingEmbeddedVersion = '3.6.0'
} }
dependencies { dependencies {
@ -81,6 +82,7 @@ dependencies {
implementation "com.android.support:preference-v14:$supportLibsVersion" implementation "com.android.support:preference-v14:$supportLibsVersion"
implementation "com.android.support:support-annotations:$supportLibsVersion" implementation "com.android.support:support-annotations:$supportLibsVersion"
implementation "com.google.code.findbugs:jsr305:$jsr305Version" implementation "com.google.code.findbugs:jsr305:$jsr305Version"
implementation "com.jakewharton.threetenabp:threetenabp:$threetenabpVersion"
implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion" implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion"
implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion" implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion" implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"

View File

@ -22,13 +22,10 @@ import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.util.SharedLibraryLoader; import com.wireguard.android.util.SharedLibraryLoader;
import com.wireguard.config.Config; import com.wireguard.config.Config;
import com.wireguard.config.InetNetwork; import com.wireguard.config.InetNetwork;
import com.wireguard.config.Interface;
import com.wireguard.config.Peer; import com.wireguard.config.Peer;
import com.wireguard.crypto.KeyEncoding;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.Collections; import java.util.Collections;
import java.util.Formatter;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -146,29 +143,7 @@ public final class GoBackend implements Backend {
} }
// Build config // Build config
final Interface iface = config.getInterface(); final String goConfig = config.toWgUserspaceString();
final String goConfig;
try (final Formatter fmt = new Formatter(new StringBuilder())) {
fmt.format("replace_peers=true\n");
if (iface.getPrivateKey() != null)
fmt.format("private_key=%s\n", KeyEncoding.keyToHex(KeyEncoding.keyFromBase64(iface.getPrivateKey())));
if (iface.getListenPort() != 0)
fmt.format("listen_port=%d\n", config.getInterface().getListenPort());
for (final Peer peer : config.getPeers()) {
if (peer.getPublicKey() != null)
fmt.format("public_key=%s\n", KeyEncoding.keyToHex(KeyEncoding.keyFromBase64(peer.getPublicKey())));
if (peer.getPreSharedKey() != null)
fmt.format("preshared_key=%s\n", KeyEncoding.keyToHex(KeyEncoding.keyFromBase64(peer.getPreSharedKey())));
if (peer.getEndpoint() != null)
fmt.format("endpoint=%s\n", peer.getResolvedEndpointString());
if (peer.getPersistentKeepalive() != 0)
fmt.format("persistent_keepalive_interval=%d\n", peer.getPersistentKeepalive());
for (final InetNetwork addr : peer.getAllowedIPs()) {
fmt.format("allowed_ip=%s\n", addr.toString());
}
}
goConfig = fmt.toString();
}
// Create the vpn tunnel with android API // Create the vpn tunnel with android API
final VpnService.Builder builder = service.getBuilder(); final VpnService.Builder builder = service.getBuilder();
@ -184,18 +159,15 @@ public final class GoBackend implements Backend {
for (final InetNetwork addr : config.getInterface().getAddresses()) for (final InetNetwork addr : config.getInterface().getAddresses())
builder.addAddress(addr.getAddress(), addr.getMask()); builder.addAddress(addr.getAddress(), addr.getMask());
for (final InetAddress addr : config.getInterface().getDnses()) for (final InetAddress addr : config.getInterface().getDnsServers())
builder.addDnsServer(addr.getHostAddress()); builder.addDnsServer(addr.getHostAddress());
for (final Peer peer : config.getPeers()) { for (final Peer peer : config.getPeers()) {
for (final InetNetwork addr : peer.getAllowedIPs()) for (final InetNetwork addr : peer.getAllowedIps())
builder.addRoute(addr.getAddress(), addr.getMask()); builder.addRoute(addr.getAddress(), addr.getMask());
} }
int mtu = config.getInterface().getMtu(); builder.setMtu(config.getInterface().getMtu().orElse(1280));
if (mtu == 0)
mtu = 1280;
builder.setMtu(mtu);
builder.setBlocking(true); builder.setBlocking(true);
try (final ParcelFileDescriptor tun = builder.establish()) { try (final ParcelFileDescriptor tun = builder.establish()) {

View File

@ -114,7 +114,7 @@ public final class WgQuickBackend implements Backend {
final File tempFile = new File(localTemporaryDir, tunnel.getName() + ".conf"); final File tempFile = new File(localTemporaryDir, tunnel.getName() + ".conf");
try (final FileOutputStream stream = new FileOutputStream(tempFile, false)) { try (final FileOutputStream stream = new FileOutputStream(tempFile, false)) {
stream.write(config.toString().getBytes(StandardCharsets.UTF_8)); stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
} }
String command = String.format("wg-quick %s '%s'", String command = String.format("wg-quick %s '%s'",
state.toString().toLowerCase(), tempFile.getAbsolutePath()); state.toString().toLowerCase(), tempFile.getAbsolutePath());

View File

@ -9,6 +9,7 @@ import android.content.Context;
import android.util.Log; import android.util.Log;
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;
@ -41,7 +42,7 @@ public final class FileConfigStore implements ConfigStore {
if (!file.createNewFile()) if (!file.createNewFile())
throw new IOException("Configuration file " + file.getName() + " already exists"); throw new IOException("Configuration file " + file.getName() + " already exists");
try (final FileOutputStream stream = new FileOutputStream(file, false)) { try (final FileOutputStream stream = new FileOutputStream(file, false)) {
stream.write(config.toString().getBytes(StandardCharsets.UTF_8)); stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
} }
return config; return config;
} }
@ -67,9 +68,9 @@ public final class FileConfigStore implements ConfigStore {
} }
@Override @Override
public Config load(final String name) throws IOException { public Config load(final String name) throws IOException, ParseException {
try (final FileInputStream stream = new FileInputStream(fileFor(name))) { try (final FileInputStream stream = new FileInputStream(fileFor(name))) {
return Config.from(stream); return Config.parse(stream);
} }
} }
@ -94,7 +95,7 @@ public final class FileConfigStore implements ConfigStore {
if (!file.isFile()) if (!file.isFile())
throw new FileNotFoundException("Configuration file " + file.getName() + " not found"); throw new FileNotFoundException("Configuration file " + file.getName() + " not found");
try (final FileOutputStream stream = new FileOutputStream(file, false)) { try (final FileOutputStream stream = new FileOutputStream(file, false)) {
stream.write(config.toString().getBytes(StandardCharsets.UTF_8)); stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
} }
return config; return config;
} }

View File

@ -6,21 +6,30 @@
package com.wireguard.android.databinding; package com.wireguard.android.databinding;
import android.databinding.BindingAdapter; import android.databinding.BindingAdapter;
import android.databinding.DataBindingUtil;
import android.databinding.ObservableList; import android.databinding.ObservableList;
import android.databinding.ViewDataBinding;
import android.databinding.adapters.ListenerUtil; import android.databinding.adapters.ListenerUtil;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.text.InputFilter; import android.text.InputFilter;
import android.view.LayoutInflater;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import com.wireguard.android.BR;
import com.wireguard.android.R; import com.wireguard.android.R;
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler; import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler;
import com.wireguard.android.util.ObservableKeyedList; import com.wireguard.android.util.ObservableKeyedList;
import com.wireguard.android.widget.ToggleSwitch; import com.wireguard.android.widget.ToggleSwitch;
import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener; import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener;
import com.wireguard.config.Attribute;
import com.wireguard.config.InetNetwork;
import com.wireguard.util.Keyed; import com.wireguard.util.Keyed;
import java9.util.Optional;
/** /**
* Static methods for use by generated code in the Android data binding library. * Static methods for use by generated code in the Android data binding library.
*/ */
@ -42,9 +51,10 @@ public final class BindingAdapters {
} }
@BindingAdapter({"items", "layout"}) @BindingAdapter({"items", "layout"})
public static <E> void setItems(final LinearLayout view, public static <E>
final ObservableList<E> oldList, final int oldLayoutId, void setItems(final LinearLayout view,
final ObservableList<E> newList, final int newLayoutId) { @Nullable final ObservableList<E> oldList, final int oldLayoutId,
@Nullable final ObservableList<E> newList, final int newLayoutId) {
if (oldList == newList && oldLayoutId == newLayoutId) if (oldList == newList && oldLayoutId == newLayoutId)
return; return;
ItemChangeListener<E> listener = ListenerUtil.getListener(view, R.id.item_change_listener); ItemChangeListener<E> listener = ListenerUtil.getListener(view, R.id.item_change_listener);
@ -66,11 +76,34 @@ public final class BindingAdapters {
listener.setList(newList); listener.setList(newList);
} }
@BindingAdapter({"items", "layout"})
public static <E>
void setItems(final LinearLayout view,
@Nullable final Iterable<E> oldList, final int oldLayoutId,
@Nullable final Iterable<E> newList, final int newLayoutId) {
if (oldList == newList && oldLayoutId == newLayoutId)
return;
view.removeAllViews();
if (newList == null)
return;
final LayoutInflater layoutInflater = LayoutInflater.from(view.getContext());
for (final E item : newList) {
final ViewDataBinding binding =
DataBindingUtil.inflate(layoutInflater, newLayoutId, view, false);
binding.setVariable(BR.collection, newList);
binding.setVariable(BR.item, item);
binding.executePendingBindings();
view.addView(binding.getRoot());
}
}
@BindingAdapter(requireAll = false, value = {"items", "layout", "configurationHandler"}) @BindingAdapter(requireAll = false, value = {"items", "layout", "configurationHandler"})
public static <K, E extends Keyed<? extends K>> public static <K, E extends Keyed<? extends K>>
void setItems(final RecyclerView view, void setItems(final RecyclerView view,
final ObservableKeyedList<K, E> oldList, final int oldLayoutId, final RowConfigurationHandler oldRowConfigurationHandler, @Nullable final ObservableKeyedList<K, E> oldList, final int oldLayoutId,
final ObservableKeyedList<K, E> newList, final int newLayoutId, final RowConfigurationHandler newRowConfigurationHandler) { final RowConfigurationHandler oldRowConfigurationHandler,
@Nullable final ObservableKeyedList<K, E> newList, final int newLayoutId,
final RowConfigurationHandler newRowConfigurationHandler) {
if (view.getLayoutManager() == null) if (view.getLayoutManager() == null)
view.setLayoutManager(new LinearLayoutManager(view.getContext(), RecyclerView.VERTICAL, false)); view.setLayoutManager(new LinearLayoutManager(view.getContext(), RecyclerView.VERTICAL, false));
@ -103,4 +136,13 @@ public final class BindingAdapters {
view.setOnBeforeCheckedChangeListener(listener); view.setOnBeforeCheckedChangeListener(listener);
} }
@BindingAdapter("android:text")
public static void setText(final TextView view, final Optional<?> text) {
view.setText(text.map(Object::toString).orElse(""));
}
@BindingAdapter("android:text")
public static void setText(final TextView view, @Nullable final Iterable<InetNetwork> networks) {
view.setText(networks != null ? Attribute.join(networks) : "");
}
} }

View File

@ -73,7 +73,7 @@ public class ObservableKeyedRecyclerViewAdapter<K, E extends Keyed<? extends K>>
holder.binding.executePendingBindings(); holder.binding.executePendingBindings();
if (rowConfigurationHandler != null) { if (rowConfigurationHandler != null) {
E item = getItem(position); final E item = getItem(position);
if (item != null) { if (item != null) {
rowConfigurationHandler.onConfigureRow(holder.binding, item, position); rowConfigurationHandler.onConfigureRow(holder.binding, item, position);
} }

View File

@ -27,19 +27,21 @@ import com.wireguard.android.util.ObservableKeyedArrayList;
import com.wireguard.android.util.ObservableKeyedList; import com.wireguard.android.util.ObservableKeyedList;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java9.util.Comparators;
public class AppListDialogFragment extends DialogFragment { public class AppListDialogFragment extends DialogFragment {
private static final String KEY_EXCLUDED_APPS = "excludedApps"; private static final String KEY_EXCLUDED_APPS = "excludedApps";
private final ObservableKeyedList<String, ApplicationData> appData = new ObservableKeyedArrayList<>(); private final ObservableKeyedList<String, ApplicationData> appData = new ObservableKeyedArrayList<>();
private List<String> currentlyExcludedApps; @Nullable private List<String> currentlyExcludedApps;
public static <T extends Fragment & AppExclusionListener> AppListDialogFragment newInstance(final String[] excludedApps, final T target) { public static <T extends Fragment & AppExclusionListener>
AppListDialogFragment newInstance(final ArrayList<String> excludedApps, final T target) {
final Bundle extras = new Bundle(); final Bundle extras = new Bundle();
extras.putStringArray(KEY_EXCLUDED_APPS, excludedApps); extras.putStringArrayList(KEY_EXCLUDED_APPS, excludedApps);
final AppListDialogFragment fragment = new AppListDialogFragment(); final AppListDialogFragment fragment = new AppListDialogFragment();
fragment.setTargetFragment(target, 0); fragment.setTargetFragment(target, 0);
fragment.setArguments(extras); fragment.setArguments(extras);
@ -64,7 +66,7 @@ public class AppListDialogFragment extends DialogFragment {
appData.add(new ApplicationData(resolveInfo.loadIcon(pm), resolveInfo.loadLabel(pm).toString(), packageName, currentlyExcludedApps.contains(packageName))); appData.add(new ApplicationData(resolveInfo.loadIcon(pm), resolveInfo.loadLabel(pm).toString(), packageName, currentlyExcludedApps.contains(packageName)));
} }
Collections.sort(appData, (lhs, rhs) -> lhs.getName().toLowerCase().compareTo(rhs.getName().toLowerCase())); Collections.sort(appData, Comparators.comparing(ApplicationData::getName, String.CASE_INSENSITIVE_ORDER));
return appData; return appData;
}).whenComplete(((data, throwable) -> { }).whenComplete(((data, throwable) -> {
if (data != null) { if (data != null) {
@ -82,12 +84,11 @@ public class AppListDialogFragment extends DialogFragment {
@Override @Override
public void onCreate(@Nullable final Bundle savedInstanceState) { public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
currentlyExcludedApps = getArguments().getStringArrayList(KEY_EXCLUDED_APPS);
currentlyExcludedApps = Arrays.asList(getArguments().getStringArray(KEY_EXCLUDED_APPS));
} }
@Override @Override
public Dialog onCreateDialog(final Bundle savedInstanceState) { public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity());
alertDialogBuilder.setTitle(R.string.excluded_applications); alertDialogBuilder.setTitle(R.string.excluded_applications);

View File

@ -19,8 +19,11 @@ 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.Config; import com.wireguard.config.Config;
import com.wireguard.config.ParseException;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects; import java.util.Objects;
public class ConfigNamingDialogFragment extends DialogFragment { public class ConfigNamingDialogFragment extends DialogFragment {
@ -63,8 +66,8 @@ public class ConfigNamingDialogFragment extends DialogFragment {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
try { try {
config = Config.from(getArguments().getString(KEY_CONFIG_TEXT)); config = Config.parse(new ByteArrayInputStream(getArguments().getString(KEY_CONFIG_TEXT).getBytes(StandardCharsets.UTF_8)));
} catch (final IOException exception) { } catch (final IOException | ParseException exception) {
throw new RuntimeException("Invalid config passed to " + getClass().getSimpleName(), exception); throw new RuntimeException("Invalid config passed to " + getClass().getSimpleName(), exception);
} }
} }

View File

@ -16,7 +16,6 @@ import android.view.ViewGroup;
import com.wireguard.android.R; import com.wireguard.android.R;
import com.wireguard.android.databinding.TunnelDetailFragmentBinding; import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
import com.wireguard.android.model.Tunnel; import com.wireguard.android.model.Tunnel;
import com.wireguard.config.Config;
/** /**
* Fragment that shows details about a specific tunnel. * Fragment that shows details about a specific tunnel.
@ -25,12 +24,6 @@ import com.wireguard.config.Config;
public class TunnelDetailFragment extends BaseFragment { public class TunnelDetailFragment extends BaseFragment {
@Nullable private TunnelDetailFragmentBinding binding; @Nullable private TunnelDetailFragmentBinding binding;
private void onConfigLoaded(final String name, final Config config) {
if (binding != null) {
binding.setConfig(new Config.Observable(config, name));
}
}
@Override @Override
public void onCreate(@Nullable final Bundle savedInstanceState) { public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -65,7 +58,7 @@ public class TunnelDetailFragment extends BaseFragment {
if (newTunnel == null) if (newTunnel == null)
binding.setConfig(null); binding.setConfig(null);
else else
newTunnel.getConfigAsync().thenAccept(a -> onConfigLoaded(newTunnel.getName(), a)); newTunnel.getConfigAsync().thenAccept(binding::setConfig);
} }
@Override @Override

View File

@ -7,7 +7,6 @@ package com.wireguard.android.fragment;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.databinding.Observable;
import android.databinding.ObservableList; import android.databinding.ObservableList;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@ -24,19 +23,16 @@ import android.view.inputmethod.InputMethodManager;
import android.widget.Toast; import android.widget.Toast;
import com.wireguard.android.Application; import com.wireguard.android.Application;
import com.wireguard.android.BR;
import com.wireguard.android.R; import com.wireguard.android.R;
import com.wireguard.android.databinding.TunnelEditorFragmentBinding; import com.wireguard.android.databinding.TunnelEditorFragmentBinding;
import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener; import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener;
import com.wireguard.android.model.Tunnel; import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.TunnelManager; import com.wireguard.android.model.TunnelManager;
import com.wireguard.android.util.ExceptionLoggers; import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.config.Attribute; import com.wireguard.android.viewmodel.ConfigProxy;
import com.wireguard.config.Config; import com.wireguard.config.Config;
import com.wireguard.config.Peer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -48,64 +44,13 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi
private static final String KEY_LOCAL_CONFIG = "local_config"; private static final String KEY_LOCAL_CONFIG = "local_config";
private static final String KEY_ORIGINAL_NAME = "original_name"; private static final String KEY_ORIGINAL_NAME = "original_name";
private static final String TAG = "WireGuard/" + TunnelEditorFragment.class.getSimpleName(); private static final String TAG = "WireGuard/" + TunnelEditorFragment.class.getSimpleName();
private final Collection<Object> breakObjectOrientedLayeringHandlerReceivers = new ArrayList<>();
@Nullable private TunnelEditorFragmentBinding binding; @Nullable private TunnelEditorFragmentBinding binding;
private final Observable.OnPropertyChangedCallback breakObjectOrientedLayeringHandler = new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(final Observable sender, final int propertyId) {
if (binding == null)
return;
final Config.Observable config = binding.getConfig();
if (config == null)
return;
if (propertyId == BR.config) {
config.addOnPropertyChangedCallback(breakObjectOrientedLayeringHandler);
breakObjectOrientedLayeringHandlerReceivers.add(config);
config.getInterfaceSection().addOnPropertyChangedCallback(breakObjectOrientedLayeringHandler);
breakObjectOrientedLayeringHandlerReceivers.add(config.getInterfaceSection());
config.getPeers().addOnListChangedCallback(breakObjectListOrientedLayeringHandler);
breakObjectOrientedLayeringHandlerReceivers.add(config.getPeers());
} else if (propertyId == BR.dnses || propertyId == BR.peers)
;
else
return;
final int numSiblings = config.getPeers().size() - 1;
for (final Peer.Observable peer : config.getPeers()) {
peer.setInterfaceDNSRoutes(config.getInterfaceSection().getDnses());
peer.setNumSiblings(numSiblings);
}
}
};
private final ObservableList.OnListChangedCallback<? extends ObservableList<Peer.Observable>> breakObjectListOrientedLayeringHandler = new ObservableList.OnListChangedCallback<ObservableList<Peer.Observable>>() {
@Override
public void onChanged(final ObservableList<Peer.Observable> sender) {
}
@Override
public void onItemRangeChanged(final ObservableList<Peer.Observable> sender, final int positionStart, final int itemCount) {
}
@Override
public void onItemRangeInserted(final ObservableList<Peer.Observable> sender, final int positionStart, final int itemCount) {
if (binding != null)
breakObjectOrientedLayeringHandler.onPropertyChanged(binding.getConfig(), BR.peers);
}
@Override
public void onItemRangeMoved(final ObservableList<Peer.Observable> sender, final int fromPosition, final int toPosition, final int itemCount) {
}
@Override
public void onItemRangeRemoved(final ObservableList<Peer.Observable> sender, final int positionStart, final int itemCount) {
if (binding != null)
breakObjectOrientedLayeringHandler.onPropertyChanged(binding.getConfig(), BR.peers);
}
};
@Nullable private Tunnel tunnel; @Nullable private Tunnel tunnel;
private void onConfigLoaded(final String name, final Config config) { private void onConfigLoaded(final Config config) {
if (binding != null) { if (binding != null) {
binding.setConfig(new Config.Observable(config, name)); binding.setConfig(new ConfigProxy(config));
} }
} }
@ -143,29 +88,23 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi
@Nullable final Bundle savedInstanceState) { @Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState); super.onCreateView(inflater, container, savedInstanceState);
binding = TunnelEditorFragmentBinding.inflate(inflater, container, false); binding = TunnelEditorFragmentBinding.inflate(inflater, container, false);
binding.addOnPropertyChangedCallback(breakObjectOrientedLayeringHandler);
breakObjectOrientedLayeringHandlerReceivers.add(binding);
binding.executePendingBindings(); binding.executePendingBindings();
return binding.getRoot(); return binding.getRoot();
} }
@SuppressWarnings("unchecked")
@Override @Override
public void onDestroyView() { public void onDestroyView() {
binding = null; binding = null;
for (final Object o : breakObjectOrientedLayeringHandlerReceivers) {
if (o instanceof Observable)
((Observable) o).removeOnPropertyChangedCallback(breakObjectOrientedLayeringHandler);
else if (o instanceof ObservableList)
((ObservableList) o).removeOnListChangedCallback(breakObjectListOrientedLayeringHandler);
}
super.onDestroyView(); super.onDestroyView();
} }
@Override @Override
public void onExcludedAppsSelected(final List<String> excludedApps) { public void onExcludedAppsSelected(final List<String> excludedApps) {
Objects.requireNonNull(binding, "Tried to set excluded apps while no view was loaded"); Objects.requireNonNull(binding, "Tried to set excluded apps while no view was loaded");
binding.getConfig().getInterfaceSection().setExcludedApplications(Attribute.iterableToString(excludedApps)); final ObservableList<String> excludedApplications =
binding.getConfig().getInterface().getExcludedApplications();
excludedApplications.clear();
excludedApplications.addAll(excludedApps);
} }
private void onFinished() { private void onFinished() {
@ -195,25 +134,27 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi
public boolean onOptionsItemSelected(final MenuItem item) { public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.menu_action_save: case R.id.menu_action_save:
final Config newConfig = new Config(); if (binding == null)
return false;
final Config newConfig;
try { try {
binding.getConfig().commitData(newConfig); newConfig = binding.getConfig().resolve();
} catch (final Exception e) { } catch (final Exception e) {
final String error = ExceptionLoggers.unwrapMessage(e); final String error = ExceptionLoggers.unwrapMessage(e);
final String tunnelName = tunnel == null ? binding.getConfig().getName() : tunnel.getName(); final String tunnelName = tunnel == null ? binding.getName() : tunnel.getName();
final String message = getString(R.string.config_save_error, tunnelName, error); final String message = getString(R.string.config_save_error, tunnelName, error);
Log.e(TAG, message, e); Log.e(TAG, message, e);
Snackbar.make(binding.mainContainer, error, Snackbar.LENGTH_LONG).show(); Snackbar.make(binding.mainContainer, error, Snackbar.LENGTH_LONG).show();
return false; return false;
} }
if (tunnel == null) { if (tunnel == null) {
Log.d(TAG, "Attempting to create new tunnel " + binding.getConfig().getName()); Log.d(TAG, "Attempting to create new tunnel " + binding.getName());
final TunnelManager manager = Application.getTunnelManager(); final TunnelManager manager = Application.getTunnelManager();
manager.create(binding.getConfig().getName(), newConfig) manager.create(binding.getName(), newConfig)
.whenComplete(this::onTunnelCreated); .whenComplete(this::onTunnelCreated);
} else if (!tunnel.getName().equals(binding.getConfig().getName())) { } else if (!tunnel.getName().equals(binding.getName())) {
Log.d(TAG, "Attempting to rename tunnel to " + binding.getConfig().getName()); Log.d(TAG, "Attempting to rename tunnel to " + binding.getName());
tunnel.setName(binding.getConfig().getName()) tunnel.setName(binding.getName())
.whenComplete((a, b) -> onTunnelRenamed(tunnel, newConfig, b)); .whenComplete((a, b) -> onTunnelRenamed(tunnel, newConfig, b));
} else { } else {
Log.d(TAG, "Attempting to save config of " + tunnel.getName()); Log.d(TAG, "Attempting to save config of " + tunnel.getName());
@ -229,7 +170,7 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi
public void onRequestSetExcludedApplications(@SuppressWarnings("unused") final View view) { public void onRequestSetExcludedApplications(@SuppressWarnings("unused") final View view) {
final FragmentManager fragmentManager = getFragmentManager(); final FragmentManager fragmentManager = getFragmentManager();
if (fragmentManager != null && binding != null) { if (fragmentManager != null && binding != null) {
final String[] excludedApps = Attribute.stringToList(binding.getConfig().getInterfaceSection().getExcludedApplications()); final ArrayList<String> excludedApps = new ArrayList<>(binding.getConfig().getInterface().getExcludedApplications());
final AppListDialogFragment fragment = AppListDialogFragment.newInstance(excludedApps, this); final AppListDialogFragment fragment = AppListDialogFragment.newInstance(excludedApps, this);
fragment.show(fragmentManager, null); fragment.show(fragmentManager, null);
} }
@ -237,19 +178,25 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi
@Override @Override
public void onSaveInstanceState(final Bundle outState) { public void onSaveInstanceState(final Bundle outState) {
outState.putParcelable(KEY_LOCAL_CONFIG, binding.getConfig()); if (binding != null)
outState.putParcelable(KEY_LOCAL_CONFIG, binding.getConfig());
outState.putString(KEY_ORIGINAL_NAME, tunnel == null ? null : tunnel.getName()); outState.putString(KEY_ORIGINAL_NAME, tunnel == null ? null : tunnel.getName());
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
} }
@Override @Override
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) { public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel,
@Nullable final Tunnel newTunnel) {
tunnel = newTunnel; tunnel = newTunnel;
if (binding == null) if (binding == null)
return; return;
binding.setConfig(new Config.Observable(null, null)); binding.setConfig(new ConfigProxy());
if (tunnel != null) if (tunnel != null) {
tunnel.getConfigAsync().thenAccept(a -> onConfigLoaded(tunnel.getName(), a)); binding.setName(tunnel.getName());
tunnel.getConfigAsync().thenAccept(this::onConfigLoaded);
} else {
binding.setName("");
}
} }
private void onTunnelCreated(final Tunnel newTunnel, @Nullable final Throwable throwable) { private void onTunnelCreated(final Tunnel newTunnel, @Nullable final Throwable throwable) {
@ -301,7 +248,7 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi
onSelectedTunnelChanged(null, getSelectedTunnel()); onSelectedTunnelChanged(null, getSelectedTunnel());
} else { } else {
tunnel = getSelectedTunnel(); tunnel = getSelectedTunnel();
final Config.Observable config = savedInstanceState.getParcelable(KEY_LOCAL_CONFIG); final ConfigProxy config = savedInstanceState.getParcelable(KEY_LOCAL_CONFIG);
final String originalName = savedInstanceState.getString(KEY_ORIGINAL_NAME); final String originalName = savedInstanceState.getString(KEY_ORIGINAL_NAME);
if (tunnel != null && !tunnel.getName().equals(originalName)) if (tunnel != null && !tunnel.getName().equals(originalName))
onSelectedTunnelChanged(null, tunnel); onSelectedTunnelChanged(null, tunnel);

View File

@ -41,9 +41,10 @@ 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.Config; import com.wireguard.config.Config;
import com.wireguard.config.ParseException;
import java.io.BufferedReader; import java.io.ByteArrayInputStream;
import java.io.InputStreamReader; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@ -79,13 +80,13 @@ public class TunnelListFragment extends BaseFragment {
private void importTunnel(@NonNull final String configText) { private void importTunnel(@NonNull final String configText) {
try { try {
// Ensure the config text is parseable before proceeding // Ensure the config text is parseable before proceeding
Config.from(configText); Config.parse(new ByteArrayInputStream(configText.getBytes(StandardCharsets.UTF_8)));
// Config text is valid, now create the tunnel // Config text is valid, now create the tunnel
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 Exception exception) { } catch (final IllegalArgumentException | IOException | ParseException exception) {
onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception)); onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception));
} }
} }
@ -122,7 +123,6 @@ public class TunnelListFragment extends BaseFragment {
if (isZip) { if (isZip) {
try (ZipInputStream zip = new ZipInputStream(contentResolver.openInputStream(uri))) { try (ZipInputStream zip = new ZipInputStream(contentResolver.openInputStream(uri))) {
BufferedReader reader = new BufferedReader(new InputStreamReader(zip, StandardCharsets.UTF_8));
ZipEntry entry; ZipEntry entry;
while ((entry = zip.getNextEntry()) != null) { while ((entry = zip.getNextEntry()) != null) {
if (entry.isDirectory()) if (entry.isDirectory())
@ -140,7 +140,7 @@ public class TunnelListFragment extends BaseFragment {
continue; continue;
Config config = null; Config config = null;
try { try {
config = Config.from(reader); config = Config.parse(zip);
} catch (Exception e) { } catch (Exception e) {
throwables.add(e); throwables.add(e);
} }
@ -150,7 +150,7 @@ public class TunnelListFragment extends BaseFragment {
} }
} else { } else {
futureTunnels.add(Application.getTunnelManager().create(name, futureTunnels.add(Application.getTunnelManager().create(name,
Config.from(contentResolver.openInputStream(uri))).toCompletableFuture()); Config.parse(contentResolver.openInputStream(uri))).toCompletableFuture());
} }
if (futureTunnels.isEmpty()) { if (futureTunnels.isEmpty()) {

View File

@ -13,7 +13,6 @@ import com.wireguard.android.BR;
import com.wireguard.util.Keyed; import com.wireguard.util.Keyed;
public class ApplicationData extends BaseObservable implements Keyed<String> { public class ApplicationData extends BaseObservable implements Keyed<String> {
private final Drawable icon; private final Drawable icon;
private final String name; private final String name;
private final String packageName; private final String packageName;

View File

@ -49,7 +49,8 @@ public class Tunnel extends BaseObservable implements Keyed<String> {
return manager.delete(this); return manager.delete(this);
} }
@Bindable @Nullable @Bindable
@Nullable
public Config getConfig() { public Config getConfig() {
if (config == null) if (config == null)
manager.getTunnelConfig(this).whenComplete(ExceptionLoggers.E); manager.getTunnelConfig(this).whenComplete(ExceptionLoggers.E);
@ -81,7 +82,8 @@ public class Tunnel extends BaseObservable implements Keyed<String> {
return TunnelManager.getTunnelState(this); return TunnelManager.getTunnelState(this);
} }
@Bindable @Nullable @Bindable
@Nullable
public Statistics getStatistics() { public Statistics getStatistics() {
// FIXME: Check age of statistics. // FIXME: Check age of statistics.
if (statistics == null) if (statistics == null)

View File

@ -44,6 +44,7 @@ public final class TunnelManager extends BaseObservable {
private static final String KEY_LAST_USED_TUNNEL = "last_used_tunnel"; private static final String KEY_LAST_USED_TUNNEL = "last_used_tunnel";
private static final String KEY_RESTORE_ON_BOOT = "restore_on_boot"; private static final String KEY_RESTORE_ON_BOOT = "restore_on_boot";
private static final String KEY_RUNNING_TUNNELS = "enabled_configs"; private static final String KEY_RUNNING_TUNNELS = "enabled_configs";
private final CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> completableTunnels = new CompletableFuture<>(); private final CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> completableTunnels = new CompletableFuture<>();
private final ConfigStore configStore; private final ConfigStore configStore;
private final Context context = Application.get(); private final Context context = Application.get();
@ -111,7 +112,8 @@ public final class TunnelManager extends BaseObservable {
}); });
} }
@Bindable @Nullable @Bindable
@Nullable
public Tunnel getLastUsedTunnel() { public Tunnel getLastUsedTunnel() {
return lastUsedTunnel; return lastUsedTunnel;
} }

View File

@ -34,7 +34,8 @@ public class VersionPreference extends Preference {
}); });
} }
@Override @Nullable @Nullable
@Override
public CharSequence getSummary() { public CharSequence getSummary() {
return versionSummary; return versionSummary;
} }

View File

@ -5,9 +5,15 @@
package com.wireguard.android.util; package com.wireguard.android.util;
import android.content.res.Resources;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.config.ParseException;
import com.wireguard.crypto.Key;
import java9.util.concurrent.CompletionException; import java9.util.concurrent.CompletionException;
import java9.util.function.BiConsumer; import java9.util.function.BiConsumer;
@ -34,12 +40,35 @@ public enum ExceptionLoggers implements BiConsumer<Object, Throwable> {
return throwable; return throwable;
} }
public static String unwrapMessage(Throwable throwable) { public static String unwrapMessage(final Throwable throwable) {
throwable = unwrap(throwable); final Throwable innerThrowable = unwrap(throwable);
final String message = throwable.getMessage(); final Resources resources = Application.get().getResources();
if (message != null) String message;
return message; if (innerThrowable instanceof ParseException) {
return throwable.getClass().getSimpleName(); 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;
switch (keyFormatException.getFormat()) {
case BASE64:
message = resources.getString(R.string.key_length_base64_exception_message);
break;
case BINARY:
message = resources.getString(R.string.key_length_exception_message);
break;
case HEX:
message = resources.getString(R.string.key_length_hex_exception_message);
break;
default:
// Will never happen, as getFormat is not nullable.
message = null;
}
} else {
message = throwable.getMessage();
}
return message != null ? message : innerThrowable.getClass().getSimpleName();
} }
@Override @Override

View File

@ -11,7 +11,6 @@ import android.view.ContextThemeWrapper;
import com.wireguard.android.activity.SettingsActivity; import com.wireguard.android.activity.SettingsActivity;
public final class FragmentUtils { public final class FragmentUtils {
private FragmentUtils() { private FragmentUtils() {
// Prevent instantiation // Prevent instantiation
} }

View File

@ -64,13 +64,15 @@ public class ObservableKeyedArrayList<K, E extends Keyed<? extends K>>
return indexOfKey(key) >= 0; return indexOfKey(key) >= 0;
} }
@Override @Nullable @Nullable
@Override
public E get(final K key) { public E get(final K key) {
final int index = indexOfKey(key); final int index = indexOfKey(key);
return index >= 0 ? get(index) : null; return index >= 0 ? get(index) : null;
} }
@Override @Nullable @Nullable
@Override
public E getLast(final K key) { public E getLast(final K key) {
final int index = lastIndexOfKey(key); final int index = lastIndexOfKey(key);
return index >= 0 ? get(index) : null; return index >= 0 ? get(index) : null;

View File

@ -28,8 +28,7 @@ import java.util.Spliterator;
public class ObservableSortedKeyedArrayList<K, E extends Keyed<? extends K>> public class ObservableSortedKeyedArrayList<K, E extends Keyed<? extends K>>
extends ObservableKeyedArrayList<K, E> implements ObservableSortedKeyedList<K, E> { extends ObservableKeyedArrayList<K, E> implements ObservableSortedKeyedList<K, E> {
@Nullable @Nullable private final Comparator<? super K> comparator;
private final Comparator<? super K> comparator;
private final transient KeyList<K, E> keyList = new KeyList<>(this); private final transient KeyList<K, E> keyList = new KeyList<>(this);
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")

View File

@ -0,0 +1,93 @@
/*
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.viewmodel;
import android.databinding.ObservableArrayList;
import android.databinding.ObservableList;
import android.os.Parcel;
import android.os.Parcelable;
import com.wireguard.config.Config;
import com.wireguard.config.ParseException;
import com.wireguard.config.Peer;
import java.util.ArrayList;
import java.util.Collection;
public class ConfigProxy implements Parcelable {
public static final Parcelable.Creator<ConfigProxy> CREATOR = new ConfigProxyCreator();
private final InterfaceProxy interfaze;
private final ObservableList<PeerProxy> peers = new ObservableArrayList<>();
private ConfigProxy(final Parcel in) {
interfaze = in.readParcelable(InterfaceProxy.class.getClassLoader());
in.readTypedList(peers, PeerProxy.CREATOR);
for (final PeerProxy proxy : peers)
proxy.bind(this);
}
public ConfigProxy(final Config other) {
interfaze = new InterfaceProxy(other.getInterface());
for (final Peer peer : other.getPeers()) {
final PeerProxy proxy = new PeerProxy(peer);
peers.add(proxy);
proxy.bind(this);
}
}
public ConfigProxy() {
interfaze = new InterfaceProxy();
}
public PeerProxy addPeer() {
final PeerProxy proxy = new PeerProxy();
peers.add(proxy);
proxy.bind(this);
return proxy;
}
@Override
public int describeContents() {
return 0;
}
public InterfaceProxy getInterface() {
return interfaze;
}
public ObservableList<PeerProxy> getPeers() {
return peers;
}
public Config resolve() throws ParseException {
final Collection<Peer> resolvedPeers = new ArrayList<>();
for (final PeerProxy proxy : peers)
resolvedPeers.add(proxy.resolve());
return new Config.Builder()
.setInterface(interfaze.resolve())
.addPeers(resolvedPeers)
.build();
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeParcelable(interfaze, flags);
dest.writeTypedList(peers);
}
private static class ConfigProxyCreator implements Parcelable.Creator<ConfigProxy> {
@Override
public ConfigProxy createFromParcel(final Parcel in) {
return new ConfigProxy(in);
}
@Override
public ConfigProxy[] newArray(final int size) {
return new ConfigProxy[size];
}
}
}

View File

@ -0,0 +1,189 @@
/*
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.viewmodel;
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 com.wireguard.android.BR;
import com.wireguard.config.Attribute;
import com.wireguard.config.Interface;
import com.wireguard.config.ParseException;
import com.wireguard.crypto.Key;
import com.wireguard.crypto.KeyPair;
import java.net.InetAddress;
import java.util.List;
import java9.util.stream.Collectors;
import java9.util.stream.StreamSupport;
public class InterfaceProxy extends BaseObservable implements Parcelable {
public static final Parcelable.Creator<InterfaceProxy> CREATOR = new InterfaceProxyCreator();
private final ObservableList<String> excludedApplications = new ObservableArrayList<>();
private String addresses;
private String dnsServers;
private String listenPort;
private String mtu;
private String privateKey;
private String publicKey;
private InterfaceProxy(final Parcel in) {
addresses = in.readString();
dnsServers = in.readString();
in.readStringList(excludedApplications);
listenPort = in.readString();
mtu = in.readString();
privateKey = in.readString();
publicKey = in.readString();
}
public InterfaceProxy(final Interface other) {
addresses = Attribute.join(other.getAddresses());
final List<String> dnsServerStrings = StreamSupport.stream(other.getDnsServers())
.map(InetAddress::getHostAddress)
.collect(Collectors.toUnmodifiableList());
dnsServers = Attribute.join(dnsServerStrings);
excludedApplications.addAll(other.getExcludedApplications());
listenPort = other.getListenPort().map(String::valueOf).orElse("");
mtu = other.getMtu().map(String::valueOf).orElse("");
final KeyPair keyPair = other.getKeyPair();
privateKey = keyPair.getPrivateKey().toBase64();
publicKey = keyPair.getPublicKey().toBase64();
}
public InterfaceProxy() {
addresses = "";
dnsServers = "";
listenPort = "";
mtu = "";
privateKey = "";
publicKey = "";
}
@Override
public int describeContents() {
return 0;
}
public void generateKeyPair() {
final KeyPair keyPair = new KeyPair();
privateKey = keyPair.getPrivateKey().toBase64();
publicKey = keyPair.getPublicKey().toBase64();
notifyPropertyChanged(BR.privateKey);
notifyPropertyChanged(BR.publicKey);
}
@Bindable
public String getAddresses() {
return addresses;
}
@Bindable
public String getDnsServers() {
return dnsServers;
}
public ObservableList<String> getExcludedApplications() {
return excludedApplications;
}
@Bindable
public String getListenPort() {
return listenPort;
}
@Bindable
public String getMtu() {
return mtu;
}
@Bindable
public String getPrivateKey() {
return privateKey;
}
@Bindable
public String getPublicKey() {
return publicKey;
}
public Interface resolve() throws ParseException {
final Interface.Builder builder = new Interface.Builder();
if (!addresses.isEmpty())
builder.parseAddresses(addresses);
if (!dnsServers.isEmpty())
builder.parseDnsServers(dnsServers);
if (!excludedApplications.isEmpty())
builder.excludeApplications(excludedApplications);
if (!listenPort.isEmpty())
builder.parseListenPort(listenPort);
if (!mtu.isEmpty())
builder.parseMtu(mtu);
if (!privateKey.isEmpty())
builder.parsePrivateKey(privateKey);
return builder.build();
}
public void setAddresses(final String addresses) {
this.addresses = addresses;
notifyPropertyChanged(BR.addresses);
}
public void setDnsServers(final String dnsServers) {
this.dnsServers = dnsServers;
notifyPropertyChanged(BR.dnsServers);
}
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;
try {
publicKey = new KeyPair(Key.fromBase64(privateKey)).getPublicKey().toBase64();
} catch (final Key.KeyFormatException ignored) {
publicKey = "";
}
notifyPropertyChanged(BR.privateKey);
notifyPropertyChanged(BR.publicKey);
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeString(addresses);
dest.writeString(dnsServers);
dest.writeStringList(excludedApplications);
dest.writeString(listenPort);
dest.writeString(mtu);
dest.writeString(privateKey);
dest.writeString(publicKey);
}
private static class InterfaceProxyCreator implements Parcelable.Creator<InterfaceProxy> {
@Override
public InterfaceProxy createFromParcel(final Parcel in) {
return new InterfaceProxy(in);
}
@Override
public InterfaceProxy[] newArray(final int size) {
return new InterfaceProxy[size];
}
}
}

View File

@ -0,0 +1,379 @@
/*
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.viewmodel;
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.databinding.Observable;
import android.databinding.ObservableList;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.wireguard.android.BR;
import com.wireguard.config.Attribute;
import com.wireguard.config.InetEndpoint;
import com.wireguard.config.ParseException;
import com.wireguard.config.Peer;
import com.wireguard.crypto.Key;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java9.util.Lists;
import java9.util.Sets;
import java9.util.stream.Collectors;
import java9.util.stream.Stream;
public class PeerProxy extends BaseObservable implements Parcelable {
public static final Parcelable.Creator<PeerProxy> CREATOR = new PeerProxyCreator();
private static final Set<String> IPV4_PUBLIC_NETWORKS = new LinkedHashSet<>(Lists.of(
"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 Set<String> IPV4_WILDCARD = Sets.of("0.0.0.0/0");
private final List<String> dnsRoutes = new ArrayList<>();
private String allowedIps;
private AllowedIpsState allowedIpsState = AllowedIpsState.INVALID;
private String endpoint;
@Nullable private InterfaceDnsListener interfaceDnsListener;
@Nullable private ConfigProxy owner;
@Nullable private PeerListListener peerListListener;
private String persistentKeepalive;
private String preSharedKey;
private String publicKey;
private int totalPeers;
private PeerProxy(final Parcel in) {
allowedIps = in.readString();
endpoint = in.readString();
persistentKeepalive = in.readString();
preSharedKey = in.readString();
publicKey = in.readString();
}
public PeerProxy(final Peer other) {
allowedIps = Attribute.join(other.getAllowedIps());
endpoint = other.getEndpoint().map(InetEndpoint::toString).orElse("");
persistentKeepalive = other.getPersistentKeepalive().map(String::valueOf).orElse("");
preSharedKey = other.getPreSharedKey().map(Key::toBase64).orElse("");
publicKey = other.getPublicKey().toBase64();
}
public PeerProxy() {
allowedIps = "";
endpoint = "";
persistentKeepalive = "";
preSharedKey = "";
publicKey = "";
}
public void bind(final ConfigProxy owner) {
final InterfaceProxy interfaze = owner.getInterface();
final ObservableList<PeerProxy> peers = owner.getPeers();
if (interfaceDnsListener == null)
interfaceDnsListener = new InterfaceDnsListener(this);
interfaze.addOnPropertyChangedCallback(interfaceDnsListener);
setInterfaceDns(interfaze.getDnsServers());
if (peerListListener == null)
peerListListener = new PeerListListener(this);
peers.addOnListChangedCallback(peerListListener);
setTotalPeers(peers.size());
this.owner = owner;
}
private void calculateAllowedIpsState() {
final AllowedIpsState newState;
if (totalPeers == 1) {
// String comparison works because we only care if allowedIps is a superset of one of
// the above sets of (valid) *networks*. We are not checking for a superset based on
// the individual addresses in each set.
final Collection<String> networkStrings = getAllowedIpsSet();
// If allowedIps contains both the wildcard and the public networks, then private
// networks aren't excluded!
if (networkStrings.containsAll(IPV4_WILDCARD))
newState = AllowedIpsState.CONTAINS_IPV4_WILDCARD;
else if (networkStrings.containsAll(IPV4_PUBLIC_NETWORKS))
newState = AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS;
else
newState = AllowedIpsState.OTHER;
} else {
newState = AllowedIpsState.INVALID;
}
if (newState != allowedIpsState) {
allowedIpsState = newState;
notifyPropertyChanged(BR.ableToExcludePrivateIps);
notifyPropertyChanged(BR.excludingPrivateIps);
}
}
@Override
public int describeContents() {
return 0;
}
@Bindable
public String getAllowedIps() {
return allowedIps;
}
private Set<String> getAllowedIpsSet() {
return new LinkedHashSet<>(Lists.of(Attribute.split(allowedIps)));
}
@Bindable
public String getEndpoint() {
return endpoint;
}
@Bindable
public String getPersistentKeepalive() {
return persistentKeepalive;
}
@Bindable
public String getPreSharedKey() {
return preSharedKey;
}
@Bindable
public String getPublicKey() {
return publicKey;
}
@Bindable
public boolean isAbleToExcludePrivateIps() {
return allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS
|| allowedIpsState == AllowedIpsState.CONTAINS_IPV4_WILDCARD;
}
@Bindable
public boolean isExcludingPrivateIps() {
return allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS;
}
public Peer resolve() throws ParseException {
final Peer.Builder builder = new Peer.Builder();
if (!allowedIps.isEmpty())
builder.parseAllowedIPs(allowedIps);
if (!endpoint.isEmpty())
builder.parseEndpoint(endpoint);
if (!persistentKeepalive.isEmpty())
builder.parsePersistentKeepalive(persistentKeepalive);
if (!preSharedKey.isEmpty())
builder.parsePreSharedKey(preSharedKey);
if (!publicKey.isEmpty())
builder.parsePublicKey(publicKey);
return builder.build();
}
public void setAllowedIps(final String allowedIps) {
this.allowedIps = allowedIps;
notifyPropertyChanged(BR.allowedIps);
calculateAllowedIpsState();
}
public void setEndpoint(final String endpoint) {
this.endpoint = endpoint;
notifyPropertyChanged(BR.endpoint);
}
public void setExcludingPrivateIps(final boolean excludingPrivateIps) {
if (!isAbleToExcludePrivateIps() || isExcludingPrivateIps() == excludingPrivateIps)
return;
final Set<String> oldNetworks = excludingPrivateIps ? IPV4_WILDCARD : IPV4_PUBLIC_NETWORKS;
final Set<String> newNetworks = excludingPrivateIps ? IPV4_PUBLIC_NETWORKS : IPV4_WILDCARD;
final Collection<String> input = getAllowedIpsSet();
final int outputSize = input.size() - oldNetworks.size() + newNetworks.size();
final Collection<String> output = new LinkedHashSet<>(outputSize);
boolean replaced = false;
// Replace the first instance of the wildcard with the public network list, or vice versa.
for (final String network : input) {
if (oldNetworks.contains(network)) {
if (!replaced) {
for (final String replacement : newNetworks)
if (!output.contains(replacement))
output.add(replacement);
replaced = true;
}
} else if (!output.contains(network)) {
output.add(network);
}
}
// DNS servers only need to handled specially when we're excluding private IPs.
if (excludingPrivateIps)
output.addAll(dnsRoutes);
else
output.removeAll(dnsRoutes);
allowedIps = Attribute.join(output);
allowedIpsState = excludingPrivateIps ?
AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS : AllowedIpsState.CONTAINS_IPV4_WILDCARD;
notifyPropertyChanged(BR.allowedIps);
notifyPropertyChanged(BR.excludingPrivateIps);
}
private void setInterfaceDns(final CharSequence dnsServers) {
final List<String> newDnsRoutes = Stream.of(Attribute.split(dnsServers))
.map(server -> server + "/32")
.collect(Collectors.toUnmodifiableList());
if (allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS) {
final Collection<String> input = getAllowedIpsSet();
final Collection<String> output = new LinkedHashSet<>(input.size() + 1);
// Yes, this is quadratic in the number of DNS servers, but most users have 1 or 2.
for (final String network : input)
if (!dnsRoutes.contains(network) || newDnsRoutes.contains(network))
output.add(network);
// Since output is a Set, this does the Right Thing (it does not duplicate networks).
output.addAll(newDnsRoutes);
// None of the public networks are /32s, so this cannot change the AllowedIPs state.
allowedIps = Attribute.join(output);
notifyPropertyChanged(BR.allowedIps);
}
dnsRoutes.clear();
dnsRoutes.addAll(newDnsRoutes);
}
public void setPersistentKeepalive(final String persistentKeepalive) {
this.persistentKeepalive = persistentKeepalive;
notifyPropertyChanged(BR.persistentKeepalive);
}
public void setPreSharedKey(final String preSharedKey) {
this.preSharedKey = preSharedKey;
notifyPropertyChanged(BR.preSharedKey);
}
public void setPublicKey(final String publicKey) {
this.publicKey = publicKey;
notifyPropertyChanged(BR.publicKey);
}
private void setTotalPeers(final int totalPeers) {
if (this.totalPeers == totalPeers)
return;
this.totalPeers = totalPeers;
calculateAllowedIpsState();
}
public void unbind() {
if (owner == null)
return;
final InterfaceProxy interfaze = owner.getInterface();
final ObservableList<PeerProxy> peers = owner.getPeers();
if (interfaceDnsListener != null)
interfaze.removeOnPropertyChangedCallback(interfaceDnsListener);
if (peerListListener != null)
peers.removeOnListChangedCallback(peerListListener);
peers.remove(this);
setInterfaceDns("");
setTotalPeers(0);
owner = null;
}
@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);
}
private enum AllowedIpsState {
CONTAINS_IPV4_PUBLIC_NETWORKS,
CONTAINS_IPV4_WILDCARD,
INVALID,
OTHER
}
private static final class InterfaceDnsListener extends Observable.OnPropertyChangedCallback {
private final WeakReference<PeerProxy> weakPeerProxy;
private InterfaceDnsListener(final PeerProxy peerProxy) {
weakPeerProxy = new WeakReference<>(peerProxy);
}
@Override
public void onPropertyChanged(final Observable sender, final int propertyId) {
@Nullable final PeerProxy peerProxy = weakPeerProxy.get();
if (peerProxy == null) {
sender.removeOnPropertyChangedCallback(this);
return;
}
// This shouldn't be possible, but try to avoid a ClassCastException anyway.
if (!(sender instanceof InterfaceProxy))
return;
if (!(propertyId == BR._all || propertyId == BR.dnsServers))
return;
peerProxy.setInterfaceDns(((InterfaceProxy) sender).getDnsServers());
}
}
private static final class PeerListListener
extends ObservableList.OnListChangedCallback<ObservableList<PeerProxy>> {
private final WeakReference<PeerProxy> weakPeerProxy;
private PeerListListener(final PeerProxy peerProxy) {
weakPeerProxy = new WeakReference<>(peerProxy);
}
@Override
public void onChanged(final ObservableList<PeerProxy> sender) {
@Nullable final PeerProxy peerProxy = weakPeerProxy.get();
if (peerProxy == null) {
sender.removeOnListChangedCallback(this);
return;
}
peerProxy.setTotalPeers(sender.size());
}
@Override
public void onItemRangeChanged(final ObservableList<PeerProxy> sender,
final int positionStart, final int itemCount) {
// Do nothing.
}
@Override
public void onItemRangeInserted(final ObservableList<PeerProxy> sender,
final int positionStart, final int itemCount) {
onChanged(sender);
}
@Override
public void onItemRangeMoved(final ObservableList<PeerProxy> sender,
final int fromPosition, final int toPosition,
final int itemCount) {
// Do nothing.
}
@Override
public void onItemRangeRemoved(final ObservableList<PeerProxy> sender,
final int positionStart, final int itemCount) {
onChanged(sender);
}
}
private static class PeerProxyCreator implements Parcelable.Creator<PeerProxy> {
@Override
public PeerProxy createFromParcel(final Parcel in) {
return new PeerProxy(in);
}
@Override
public PeerProxy[] newArray(final int size) {
return new PeerProxy[size];
}
}
}

View File

@ -10,7 +10,7 @@ import android.text.InputFilter;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import com.wireguard.crypto.KeyEncoding; import com.wireguard.crypto.Key;
/** /**
* InputFilter for entering WireGuard private/public keys encoded with base64. * InputFilter for entering WireGuard private/public keys encoded with base64.
@ -25,7 +25,8 @@ public class KeyInputFilter implements InputFilter {
return new KeyInputFilter(); return new KeyInputFilter();
} }
@Override @Nullable @Nullable
@Override
public CharSequence filter(final CharSequence source, public CharSequence filter(final CharSequence source,
final int sStart, final int sEnd, final int sStart, final int sEnd,
final Spanned dest, final Spanned dest,
@ -38,9 +39,9 @@ public class KeyInputFilter implements InputFilter {
final int dIndex = dStart + (sIndex - sStart); final int dIndex = dStart + (sIndex - sStart);
// Restrict characters to the base64 character set. // Restrict characters to the base64 character set.
// Ensure adding this character does not push the length over the limit. // Ensure adding this character does not push the length over the limit.
if (((dIndex + 1 < KeyEncoding.KEY_LENGTH_BASE64 && isAllowed(c)) || if (((dIndex + 1 < Key.Format.BASE64.getLength() && isAllowed(c)) ||
(dIndex + 1 == KeyEncoding.KEY_LENGTH_BASE64 && c == '=')) && (dIndex + 1 == Key.Format.BASE64.getLength() && c == '=')) &&
dLength + (sIndex - sStart) < KeyEncoding.KEY_LENGTH_BASE64) { dLength + (sIndex - sStart) < Key.Format.BASE64.getLength()) {
++rIndex; ++rIndex;
} else { } else {
if (replacement == null) if (replacement == null)

View File

@ -25,7 +25,8 @@ public class NameInputFilter implements InputFilter {
return new NameInputFilter(); return new NameInputFilter();
} }
@Override @Nullable @Nullable
@Override
public CharSequence filter(final CharSequence source, public CharSequence filter(final CharSequence source,
final int sStart, final int sEnd, final int sStart, final int sEnd,
final Spanned dest, final Spanned dest,

View File

@ -539,7 +539,7 @@ public class FloatingActionsMenu extends ViewGroup {
return new SavedState[size]; return new SavedState[size];
} }
}; };
public boolean mExpanded; private boolean mExpanded;
public SavedState(final Parcelable parcel) { public SavedState(final Parcelable parcel) {
super(parcel); super(parcel);

View File

@ -1,94 +1,49 @@
/* /*
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved. * Copyright © 2018 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
package com.wireguard.config; package com.wireguard.config;
import android.annotation.SuppressLint;
import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** import java9.util.Optional;
* The set of valid attributes for an interface or peer in a WireGuard configuration file.
*/
public enum Attribute { public final class Attribute {
ADDRESS("Address"), private static final Pattern LINE_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*([^\\s#][^#]*)");
ALLOWED_IPS("AllowedIPs"), private static final Pattern LIST_SEPARATOR = Pattern.compile("\\s*,\\s*");
DNS("DNS"),
EXCLUDED_APPLICATIONS("ExcludedApplications"),
ENDPOINT("Endpoint"),
LISTEN_PORT("ListenPort"),
MTU("MTU"),
PERSISTENT_KEEPALIVE("PersistentKeepalive"),
PRESHARED_KEY("PresharedKey"),
PRIVATE_KEY("PrivateKey"),
PUBLIC_KEY("PublicKey");
private static final String[] EMPTY_LIST = new String[0]; private final String key;
private static final Map<String, Attribute> KEY_MAP; private final String value;
private static final Pattern LIST_SEPARATOR_PATTERN = Pattern.compile("\\s*,\\s*");
private static final Pattern SEPARATOR_PATTERN = Pattern.compile("\\s|=");
static { private Attribute(final String key, final String value) {
KEY_MAP = new HashMap<>(Attribute.values().length); this.key = key;
for (final Attribute key : Attribute.values()) { this.value = value;
KEY_MAP.put(key.token.toLowerCase(), key);
}
} }
private final Pattern pattern; public static String join(final Iterable<?> values) {
private final String token; return TextUtils.join(", ", values);
Attribute(final String token) {
pattern = Pattern.compile(token + "\\s*=\\s*(\\S.*)");
this.token = token;
} }
public static <T> String iterableToString(final Iterable<T> iterable) { public static Optional<Attribute> parse(final CharSequence line) {
return TextUtils.join(", ", iterable); final Matcher matcher = LINE_PATTERN.matcher(line);
if (!matcher.matches())
return Optional.empty();
return Optional.of(new Attribute(matcher.group(1), matcher.group(2)));
} }
@Nullable public static String[] split(final CharSequence value) {
public static Attribute match(final CharSequence line) { return LIST_SEPARATOR.split(value);
return KEY_MAP.get(SEPARATOR_PATTERN.split(line)[0].toLowerCase());
} }
public static String[] stringToList(@Nullable final String string) { public String getKey() {
if (TextUtils.isEmpty(string)) return key;
return EMPTY_LIST;
return LIST_SEPARATOR_PATTERN.split(string.trim());
} }
@SuppressLint("DefaultLocale") public String getValue() {
public String composeWith(@Nullable final Object value) { return value;
return String.format("%s = %s%n", token, value);
}
@SuppressLint("DefaultLocale")
public String composeWith(final int value) {
return String.format("%s = %d%n", token, value);
}
public <T> String composeWith(final Iterable<T> 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;
} }
} }

View File

@ -5,170 +5,193 @@
package com.wireguard.config; 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 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.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List; 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).
* <p>
* Instances of this class are immutable.
*/ */
public final class Config {
private final Interface interfaze;
private final List<Peer> peers;
public class Config { private Config(final Builder builder) {
private final Interface interfaceSection = new Interface(); interfaze = Objects.requireNonNull(builder.interfaze, "An [Interface] section is required");
private List<Peer> peers = new ArrayList<>(); // Defensively copy to ensure immutability even if the Builder is reused.
peers = Collections.unmodifiableList(new ArrayList<>(builder.peers));
public static Config from(final String string) throws IOException {
return from(new BufferedReader(new StringReader(string)));
} }
public static Config from(final InputStream stream) throws IOException { /**
return from(new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))); * 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.
*
public static Config from(final BufferedReader reader) throws IOException { * @param stream a stream of UTF-8 text that is interpreted as a WireGuard configuration file
final Config config = new Config(); * @return a {@code Config} instance representing the supplied configuration
final Context context = Application.get(); */
Peer currentPeer = null; public static Config parse(final InputStream stream) throws IOException, ParseException {
String line; final Builder builder = new Builder();
boolean inInterfaceSection = false; try (final BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
while ((line = reader.readLine()) != null) { final Collection<String> interfaceLines = new ArrayList<>();
final int commentIndex = line.indexOf('#'); final Collection<String> peerLines = new ArrayList<>();
if (commentIndex != -1) boolean inInterfaceSection = false;
line = line.substring(0, commentIndex); boolean inPeerSection = false;
line = line.trim(); @Nullable String line;
if (line.isEmpty()) while ((line = reader.readLine()) != null) {
continue; final int commentIndex = line.indexOf('#');
if ("[Interface]".toLowerCase().equals(line.toLowerCase())) { if (commentIndex != -1)
currentPeer = null; line = line.substring(0, commentIndex);
inInterfaceSection = true; line = line.trim();
} else if ("[Peer]".toLowerCase().equals(line.toLowerCase())) { if (line.isEmpty())
currentPeer = new Peer(); continue;
config.peers.add(currentPeer); if (line.startsWith("[")) {
inInterfaceSection = false; // Consume all [Peer] lines read so far.
} else if (inInterfaceSection) { if (inPeerSection) {
config.interfaceSection.parse(line); builder.parsePeer(peerLines);
} else if (currentPeer != null) { peerLines.clear();
currentPeer.parse(line); }
} else { if ("[Interface]".equalsIgnoreCase(line)) {
throw new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_config_line, 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) { return builder.build();
throw new IllegalArgumentException(context.getString(R.string.tunnel_error_no_config_information));
}
return config;
} }
@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() { 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<Peer> getPeers() { public List<Peer> getPeers() {
return peers; 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 @Override
public String toString() { 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) for (final Peer peer : peers)
sb.append('\n').append(peer); sb.append("\n[Peer]\n").append(peer.toWgQuickString());
return sb.toString(); return sb.toString();
} }
public static class Observable extends BaseObservable implements Parcelable { /**
public static final Creator<Observable> CREATOR = new Creator<Observable>() { * Serializes the {@code Config} for use with the WireGuard cross-platform userspace API.
@Override *
public Observable createFromParcel(final Parcel in) { * @return the {@code Config} represented as a series of "key=value" lines
return new Observable(in); */
} 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 @SuppressWarnings("UnusedReturnValue")
public Observable[] newArray(final int size) { public static final class Builder {
return new Observable[size]; // Defaults to an empty set.
} private final Set<Peer> peers = new LinkedHashSet<>();
}; // No default; must be provided before building.
private final Interface.Observable observableInterface; @Nullable private Interface interfaze;
private final ObservableList<Peer.Observable> observablePeers;
@Nullable private String name;
public Observable(@Nullable final Config parent, @Nullable final String name) { public Builder addPeer(final Peer peer) {
this.name = name; peers.add(peer);
return this;
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));
}
} }
private Observable(final Parcel in) { public Builder addPeers(final Collection<Peer> peers) {
name = in.readString(); this.peers.addAll(peers);
observableInterface = in.readParcelable(Interface.Observable.class.getClassLoader()); return this;
observablePeers = new ObservableArrayList<>();
in.readTypedList(observablePeers, Peer.Observable.CREATOR);
} }
public void commitData(final Config parent) { public Config build() {
observableInterface.commitData(parent.interfaceSection); return new Config(this);
final List<Peer> 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();
} }
@Override public Builder parseInterface(final Iterable<? extends CharSequence> lines) throws ParseException {
public int describeContents() { return setInterface(Interface.parse(lines));
return 0;
} }
@Bindable public Builder parsePeer(final Iterable<? extends CharSequence> lines) throws ParseException {
public Interface.Observable getInterfaceSection() { return addPeer(Peer.parse(lines));
return observableInterface;
} }
@Bindable public Builder setInterface(final Interface interfaze) {
public String getName() { this.interfaze = interfaze;
return name == null ? "" : name; return this;
}
@Bindable
public ObservableList<Peer.Observable> 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);
} }
} }
} }

View File

@ -5,21 +5,22 @@
package com.wireguard.config; 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.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress; import java.net.InetAddress;
/**
* Utility methods for creating instances of {@link InetAddress}.
*/
public final class InetAddresses { public final class InetAddresses {
private static final Method PARSER_METHOD; private static final Method PARSER_METHOD;
static { static {
try { try {
// This method is only present on Android. // This method is only present on Android.
// noinspection JavaReflectionMemberAccess
PARSER_METHOD = InetAddress.class.getMethod("parseNumericAddress", String.class); PARSER_METHOD = InetAddress.class.getMethod("parseNumericAddress", String.class);
} catch (final NoSuchMethodException e) { } catch (final NoSuchMethodException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
@ -30,13 +31,23 @@ public final class InetAddresses {
// Prevent instantiation. // Prevent instantiation.
} }
public static InetAddress parse(@Nullable final String address) { /**
if (address == null || address.isEmpty()) * Parses a numeric IPv4 or IPv6 address without performing any DNS lookups.
throw new IllegalArgumentException(Application.get().getString(R.string.tunnel_error_empty_inetaddress)); *
* @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 { 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) {
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);
} }
} }
} }

View File

@ -5,36 +5,68 @@
package com.wireguard.config; package com.wireguard.config;
import android.annotation.SuppressLint; import android.support.annotation.Nullable;
import com.wireguard.android.Application; import org.threeten.bp.Duration;
import com.wireguard.android.R; import org.threeten.bp.Instant;
import java.net.Inet4Address; import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.UnknownHostException; 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}.
* <p>
* Instances of this class are externally immutable.
*/
public final class InetEndpoint {
private static final Pattern BARE_IPV6 = Pattern.compile("^[^\\[]*:");
private static final Pattern FORBIDDEN_CHARACTERS = Pattern.compile("[/?#]");
public class InetEndpoint {
private final String host; private final String host;
private final boolean isResolved;
private final Object lock = new Object();
private final int port; private final int port;
@Nullable private InetAddress resolvedHost; private Instant lastResolution = Instant.EPOCH;
@Nullable private InetEndpoint resolved;
public InetEndpoint(@Nullable final String endpoint) { private InetEndpoint(final String host, final boolean isResolved, final int port) {
if (endpoint.indexOf('/') != -1 || endpoint.indexOf('?') != -1 || endpoint.indexOf('#') != -1) this.host = host;
throw new IllegalArgumentException(Application.get().getString(R.string.tunnel_error_forbidden_endpoint_chars)); 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; final URI uri;
try { try {
uri = new URI("wg://" + endpoint); uri = new URI("wg://" + endpoint);
} catch (final URISyntaxException e) { } catch (final URISyntaxException e) {
throw new IllegalArgumentException(e); throw new IllegalArgumentException(e);
} }
host = uri.getHost(); try {
port = uri.getPort(); 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() { public String getHost() {
@ -45,28 +77,47 @@ public class InetEndpoint {
return port; return port;
} }
@SuppressLint("DefaultLocale") /**
public String getResolvedEndpoint() throws UnknownHostException { * Generate an {@code InetEndpoint} instance with the same port and the host resolved using DNS
if (resolvedHost == null) { * to a numeric address. If the host is already numeric, the existing instance may be returned.
final InetAddress[] candidates = InetAddress.getAllByName(host); * Because this function may perform network I/O, it must not be called from the main thread.
if (candidates.length == 0) *
throw new UnknownHostException(host); * @return the resolved endpoint, or {@link Optional#empty()}
for (final InetAddress addr : candidates) { */
if (addr instanceof Inet4Address) { public Optional<InetEndpoint> getResolved() {
resolvedHost = addr; if (isResolved)
break; 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) return Optional.ofNullable(resolved);
resolvedHost = candidates[0];
} }
return String.format(resolvedHost instanceof Inet6Address ?
"[%s]:%d" : "%s:%d", resolvedHost.getHostAddress(), port);
} }
@SuppressLint("DefaultLocale") @Override
public String getEndpoint() { public int hashCode() {
return String.format(host.contains(":") && !host.contains("[") ? return host.hashCode() ^ port;
"[%s]:%d" : "%s:%d", host, port); }
@Override
public String toString() {
final boolean isBareIpv6 = isResolved && BARE_IPV6.matcher(host).matches();
return (isBareIpv6 ? '[' + host + ']' : host) + ':' + port;
} }
} }

View File

@ -7,26 +7,36 @@ package com.wireguard.config;
import java.net.Inet4Address; import java.net.Inet4Address;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.Objects;
public class InetNetwork { /**
* An Internet network, denoted by its address and netmask
* <p>
* Instances of this class are immutable.
*/
public final class InetNetwork {
private final InetAddress address; private final InetAddress address;
private final int mask; private final int mask;
public InetNetwork(final String input) { private InetNetwork(final InetAddress address, final int mask) {
final int slash = input.lastIndexOf('/'); this.address = address;
this.mask = mask;
}
public static InetNetwork parse(final String network) {
final int slash = network.lastIndexOf('/');
final int rawMask; final int rawMask;
final String rawAddress; final String rawAddress;
if (slash >= 0) { if (slash >= 0) {
rawMask = Integer.parseInt(input.substring(slash + 1), 10); rawMask = Integer.parseInt(network.substring(slash + 1), 10);
rawAddress = input.substring(0, slash); rawAddress = network.substring(0, slash);
} else { } else {
rawMask = -1; rawMask = -1;
rawAddress = input; rawAddress = network;
} }
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;
mask = rawMask >= 0 && rawMask <= maxMask ? rawMask : maxMask; final int mask = rawMask >= 0 && rawMask <= maxMask ? rawMask : maxMask;
return new InetNetwork(address, mask);
} }
@Override @Override
@ -34,7 +44,7 @@ public class InetNetwork {
if (!(obj instanceof InetNetwork)) if (!(obj instanceof InetNetwork))
return false; return false;
final InetNetwork other = (InetNetwork) obj; 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() { public InetAddress getAddress() {

View File

@ -5,395 +5,345 @@
package com.wireguard.config; 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 android.support.annotation.Nullable;
import com.wireguard.android.Application; import com.wireguard.crypto.Key;
import com.wireguard.android.BR; import com.wireguard.crypto.KeyPair;
import com.wireguard.android.R;
import com.wireguard.crypto.Keypair;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.ArrayList; import java.util.Collection;
import java.util.Arrays; import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List; 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.
* <p>
* Instances of this class are immutable.
*/ */
public final class Interface {
private static final int MAX_UDP_PORT = 65535;
private static final int MIN_UDP_PORT = 0;
public class Interface { private final Set<InetNetwork> addresses;
private final List<InetNetwork> addressList; private final Set<InetAddress> dnsServers;
private final Context context = Application.get(); private final Set<String> excludedApplications;
private final List<InetAddress> dnsList; private final KeyPair keyPair;
private final List<String> excludedApplications; private final Optional<Integer> listenPort;
@Nullable private Keypair keypair; private final Optional<Integer> mtu;
private int listenPort;
private int mtu;
public Interface() { private Interface(final Builder builder) {
addressList = new ArrayList<>(); // Defensively copy to ensure immutability even if the Builder is reused.
dnsList = new ArrayList<>(); addresses = Collections.unmodifiableSet(new LinkedHashSet<>(builder.addresses));
excludedApplications = new ArrayList<>(); 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) { * Parses an series of "KEY = VALUE" lines into an {@code Interface}. Throws
for (final String addr : addresses) { * {@link ParseException} if the input is not well-formed or contains unknown attributes.
if (addr.isEmpty()) *
throw new IllegalArgumentException(context.getString(R.string.tunnel_error_empty_interface_address)); * @param lines An iterable sequence of lines, containing at least a private key attribute
addressList.add(new InetNetwork(addr)); * @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");
} }
} }
} return builder.build();
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<String> getDnsStrings() {
final List<String> 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);
} }
@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<InetNetwork> getAddresses() {
// The collection is already immutable.
return addresses;
}
/**
* Returns the set of DNS servers associated with the interface.
*
* @return a set of {@link InetAddress}es
*/
public Set<InetAddress> getDnsServers() {
// The collection is already immutable.
return dnsServers;
}
/**
* Returns the set of applications excluded from using the interface.
*
* @return a set of package names
*/
public Set<String> getExcludedApplications() {
// The collection is already immutable.
return excludedApplications;
}
/**
* Returns the public/private key pair used by the interface.
*
* @return a key pair
*/
public KeyPair getKeyPair() {
return keyPair;
}
/**
* Returns the UDP port number that the WireGuard interface will listen on.
*
* @return a UDP port number, or {@code Optional.empty()} if none is configured
*/
public Optional<Integer> getListenPort() {
return listenPort;
}
/**
* Returns the MTU used for the WireGuard interface.
*
* @return the MTU, or {@code Optional.empty()} if none is configured
*/
public Optional<Integer> getMtu() {
return mtu;
}
@Override
public int hashCode() {
int hash = 1;
hash = 31 * hash + addresses.hashCode();
hash = 31 * hash + dnsServers.hashCode();
hash = 31 * hash + excludedApplications.hashCode();
hash = 31 * hash + 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 @Override
public String toString() { public String toString() {
final StringBuilder sb = new StringBuilder().append("[Interface]\n"); final StringBuilder sb = new StringBuilder("(Interface ");
if (!addressList.isEmpty()) sb.append(keyPair.getPublicKey().toBase64());
sb.append(Attribute.ADDRESS.composeWith(addressList)); listenPort.ifPresent(lp -> sb.append(" @").append(lp));
if (!dnsList.isEmpty()) sb.append(')');
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()));
return sb.toString(); return sb.toString();
} }
public static class Observable extends BaseObservable implements Parcelable { /**
public static final Creator<Observable> CREATOR = new Creator<Observable>() { * Converts the {@code Interface} into a string suitable for inclusion in a {@code wg-quick}
@Override * configuration file.
public Observable createFromParcel(final Parcel in) { *
return new Observable(in); * @return The {@code Interface} represented as a series of "Key = Value" lines
} */
public String toWgQuickString() {
final StringBuilder sb = new StringBuilder();
if (!addresses.isEmpty())
sb.append("Address = ").append(Attribute.join(addresses)).append('\n');
if (!dnsServers.isEmpty()) {
final List<String> dnsServerStrings = 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) { * Serializes the {@code Interface} for use with the WireGuard cross-platform userspace API.
return new Observable[size]; * Note that not all attributes are included in this representation.
} *
}; * @return the {@code Interface} represented as a series of "KEY=VALUE" lines
@Nullable private String addresses; */
@Nullable private String dnses; public String toWgUserspaceString() {
@Nullable private String excludedApplications; final StringBuilder sb = new StringBuilder();
@Nullable private String listenPort; sb.append("private_key=").append(keyPair.getPrivateKey().toHex()).append('\n');
@Nullable private String mtu; listenPort.ifPresent(lp -> sb.append("listen_port=").append(lp).append('\n'));
@Nullable private String privateKey; return sb.toString();
@Nullable private String publicKey; }
public Observable(@Nullable final Interface parent) { @SuppressWarnings("UnusedReturnValue")
if (parent != null) public static final class Builder {
loadData(parent); // Defaults to an empty set.
private final Set<InetNetwork> addresses = new LinkedHashSet<>();
// Defaults to an empty set.
private final Set<InetAddress> dnsServers = new LinkedHashSet<>();
// Defaults to an empty set.
private final Set<String> excludedApplications = new LinkedHashSet<>();
// No default; must be provided before building.
@Nullable private KeyPair keyPair;
// Defaults to not present.
private Optional<Integer> listenPort = Optional.empty();
// Defaults to not present.
private Optional<Integer> mtu = Optional.empty();
public Builder addAddress(final InetNetwork address) {
addresses.add(address);
return this;
} }
private Observable(final Parcel in) { public Builder addAddresses(final Collection<InetNetwork> addresses) {
addresses = in.readString(); this.addresses.addAll(addresses);
dnses = in.readString(); return this;
publicKey = in.readString();
privateKey = in.readString();
listenPort = in.readString();
mtu = in.readString();
excludedApplications = in.readString();
} }
public void commitData(final Interface parent) { public Builder addDnsServer(final InetAddress dnsServer) {
parent.setAddressString(addresses); dnsServers.add(dnsServer);
parent.setDnsString(dnses); return this;
parent.setExcludedApplicationsString(excludedApplications);
parent.setPrivateKey(privateKey);
parent.setListenPortString(listenPort);
parent.setMtuString(mtu);
loadData(parent);
notifyChange();
} }
@Override public Builder addDnsServers(final Collection<? extends InetAddress> dnsServers) {
public int describeContents() { this.dnsServers.addAll(dnsServers);
return 0; return this;
} }
public void generateKeypair() { public Interface build() {
final Keypair keypair = new Keypair(); return new Interface(this);
privateKey = keypair.getPrivateKey();
publicKey = keypair.getPublicKey();
notifyPropertyChanged(BR.privateKey);
notifyPropertyChanged(BR.publicKey);
} }
@Nullable public Builder excludeApplication(final String application) {
@Bindable excludedApplications.add(application);
public String getAddresses() { return this;
return addresses;
} }
@Nullable public Builder excludeApplications(final Collection<String> applications) {
@Bindable excludedApplications.addAll(applications);
public String getDnses() { return this;
return dnses;
} }
@Nullable public Builder parseAddresses(final CharSequence addresses) throws ParseException {
@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;
try { try {
publicKey = new Keypair(privateKey).getPublicKey(); final List<InetNetwork> parsed = Stream.of(Attribute.split(addresses))
} catch (final IllegalArgumentException ignored) { .map(InetNetwork::parse)
publicKey = ""; .collect(Collectors.toUnmodifiableList());
return addAddresses(parsed);
} catch (final IllegalArgumentException e) {
throw new ParseException("Address", addresses, e);
} }
notifyPropertyChanged(BR.privateKey);
notifyPropertyChanged(BR.publicKey);
} }
@Override public Builder parseDnsServers(final CharSequence dnsServers) throws ParseException {
public void writeToParcel(final Parcel dest, final int flags) { try {
dest.writeString(addresses); final List<InetAddress> parsed = Stream.of(Attribute.split(dnsServers))
dest.writeString(dnses); .map(InetAddresses::parse)
dest.writeString(publicKey); .collect(Collectors.toUnmodifiableList());
dest.writeString(privateKey); return addDnsServers(parsed);
dest.writeString(listenPort); } catch (final IllegalArgumentException e) {
dest.writeString(mtu); throw new ParseException("DNS", dnsServers, e);
dest.writeString(excludedApplications); }
}
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;
} }
} }
} }

View File

@ -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;
}
}

View File

@ -5,363 +5,291 @@
package com.wireguard.config; 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 android.support.annotation.Nullable;
import com.android.databinding.library.baseAdapters.BR; import com.wireguard.crypto.Key;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.crypto.KeyEncoding;
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.Collection;
import java.util.HashSet; import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List; 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.
* <p>
* Instances of this class are immutable.
*/ */
public final class Peer {
private final Set<InetNetwork> allowedIps;
private final Optional<InetEndpoint> endpoint;
private final Optional<Integer> persistentKeepalive;
private final Optional<Key> preSharedKey;
private final Key publicKey;
public class Peer { private Peer(final Builder builder) {
private final List<InetNetwork> allowedIPsList; // Defensively copy to ensure immutability even if the Builder is reused.
private final Context context = Application.get(); allowedIps = Collections.unmodifiableSet(new LinkedHashSet<>(builder.allowedIps));
@Nullable private InetEndpoint endpoint; endpoint = builder.endpoint;
private int persistentKeepalive; persistentKeepalive = builder.persistentKeepalive;
@Nullable private String preSharedKey; preSharedKey = builder.preSharedKey;
@Nullable private String publicKey; publicKey = Objects.requireNonNull(builder.publicKey, "Peers must have a public key");
public Peer() {
allowedIPsList = new ArrayList<>();
} }
private void addAllowedIPs(@Nullable final String[] allowedIPs) { /**
if (allowedIPs != null && allowedIPs.length > 0) { * Parses an series of "KEY = VALUE" lines into a {@code Peer}. Throws {@link ParseException} if
for (final String allowedIP : allowedIPs) { * the input is not well-formed or contains unknown attributes.
allowedIPsList.add(new InetNetwork(allowedIP)); *
* @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");
} }
} }
} return builder.build();
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;
} }
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof Peer))
return false;
final Peer other = (Peer) obj;
return allowedIps.equals(other.allowedIps)
&& endpoint.equals(other.endpoint)
&& persistentKeepalive.equals(other.persistentKeepalive)
&& preSharedKey.equals(other.preSharedKey)
&& publicKey.equals(other.publicKey);
}
/**
* Returns the peer's set of allowed IPs.
*
* @return the set of allowed IPs
*/
public Set<InetNetwork> getAllowedIps() {
// The collection is already immutable.
return allowedIps;
}
/**
* Returns the peer's endpoint.
*
* @return the endpoint, or {@code Optional.empty()} if none is configured
*/
public Optional<InetEndpoint> getEndpoint() {
return endpoint;
}
/**
* Returns the peer's persistent keepalive.
*
* @return the persistent keepalive, or {@code Optional.empty()} if none is configured
*/
public Optional<Integer> getPersistentKeepalive() {
return persistentKeepalive;
}
/**
* Returns the peer's pre-shared key.
*
* @return the pre-shared key, or {@code Optional.empty()} if none is configured
*/
public Optional<Key> getPreSharedKey() {
return preSharedKey;
}
/**
* Returns the peer's public key.
*
* @return the public key
*/
public Key getPublicKey() {
return publicKey;
}
@Override
public int hashCode() {
int hash = 1;
hash = 31 * hash + allowedIps.hashCode();
hash = 31 * hash + endpoint.hashCode();
hash = 31 * hash + persistentKeepalive.hashCode();
hash = 31 * hash + preSharedKey.hashCode();
hash = 31 * hash + publicKey.hashCode();
return hash;
}
/**
* Converts the {@code Peer} into a string suitable for debugging purposes. The {@code Peer} is
* identified by its public key and (if known) its endpoint.
*
* @return a concise single-line identifier for the {@code Peer}
*/
@Override @Override
public String toString() { public String toString() {
final StringBuilder sb = new StringBuilder().append("[Peer]\n"); final StringBuilder sb = new StringBuilder("(Peer ");
if (!allowedIPsList.isEmpty()) sb.append(publicKey.toBase64());
sb.append(Attribute.ALLOWED_IPS.composeWith(allowedIPsList)); endpoint.ifPresent(ep -> sb.append(" @").append(ep));
if (endpoint != null) sb.append(')');
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));
return sb.toString(); return sb.toString();
} }
public static class Observable extends BaseObservable implements Parcelable { /**
public static final Creator<Observable> CREATOR = new Creator<Observable>() { * Converts the {@code Peer} into a string suitable for inclusion in a {@code wg-quick}
@Override * configuration file.
public Observable createFromParcel(final Parcel in) { *
return new Observable(in); * @return the {@code Peer} represented as a series of "Key = Value" lines
*/
public String toWgQuickString() {
final StringBuilder sb = new StringBuilder();
if (!allowedIps.isEmpty())
sb.append("AllowedIPs = ").append(Attribute.join(allowedIps)).append('\n');
endpoint.ifPresent(ep -> sb.append("Endpoint = ").append(ep).append('\n'));
persistentKeepalive.ifPresent(pk -> sb.append("PersistentKeepalive = ").append(pk).append('\n'));
preSharedKey.ifPresent(psk -> sb.append("PreSharedKey = ").append(psk.toBase64()).append('\n'));
sb.append("PublicKey = ").append(publicKey.toBase64()).append('\n');
return sb.toString();
}
/**
* Serializes the {@code Peer} for use with the WireGuard cross-platform userspace API. Note
* that not all attributes are included in this representation.
*
* @return the {@code Peer} represented as a series of "key=value" lines
*/
public String toWgUserspaceString() {
final StringBuilder sb = new StringBuilder();
// The order here is important: public_key signifies the beginning of a new peer.
sb.append("public_key=").append(publicKey.toHex()).append('\n');
for (final InetNetwork allowedIp : allowedIps)
sb.append("allowed_ip=").append(allowedIp).append('\n');
endpoint.flatMap(InetEndpoint::getResolved).ifPresent(ep -> sb.append("endpoint=").append(ep).append('\n'));
persistentKeepalive.ifPresent(pk -> sb.append("persistent_keepalive_interval=").append(pk).append('\n'));
preSharedKey.ifPresent(psk -> sb.append("preshared_key=").append(psk.toHex()).append('\n'));
return sb.toString();
}
@SuppressWarnings("UnusedReturnValue")
public static final class Builder {
// See wg(8)
private static final int MAX_PERSISTENT_KEEPALIVE = 65535;
// Defaults to an empty set.
private final Set<InetNetwork> allowedIps = new LinkedHashSet<>();
// Defaults to not present.
private Optional<InetEndpoint> endpoint = Optional.empty();
// Defaults to not present.
private Optional<Integer> persistentKeepalive = Optional.empty();
// Defaults to not present.
private Optional<Key> preSharedKey = Optional.empty();
// No default; must be provided before building.
@Nullable private Key publicKey;
public Builder addAllowedIp(final InetNetwork allowedIp) {
allowedIps.add(allowedIp);
return this;
}
public Builder addAllowedIps(final Collection<InetNetwork> allowedIps) {
this.allowedIps.addAll(allowedIps);
return this;
}
public Peer build() {
return new Peer(this);
}
public Builder parseAllowedIPs(final CharSequence allowedIps) throws ParseException {
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);
} }
}
@Override public Builder parseEndpoint(final String endpoint) throws ParseException {
public Observable[] newArray(final int size) { try {
return new Observable[size]; return setEndpoint(InetEndpoint.parse(endpoint));
} catch (final IllegalArgumentException e) {
throw new ParseException("Endpoint", endpoint, e);
} }
};
private static final List<String> 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<String> 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) { public Builder parsePersistentKeepalive(final String persistentKeepalive) throws ParseException {
allowedIPs = in.readString(); try {
endpoint = in.readString(); return setPersistentKeepalive(Integer.parseInt(persistentKeepalive));
persistentKeepalive = in.readString(); } catch (final IllegalArgumentException e) {
preSharedKey = in.readString(); throw new ParseException("PersistentKeepalive", persistentKeepalive, e);
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<String> 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<String> 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");
} }
ips.addAll(interfaceDNSRoutes);
if (modifyAllowedIPs)
setAllowedIPs(Attribute.iterableToString(ips));
} }
public void setNumSiblings(final int num) { public Builder parsePreSharedKey(final String preSharedKey) throws ParseException {
numSiblings = num; try {
notifyPropertyChanged(BR.canToggleExcludePrivateIPs); return setPreSharedKey(Key.fromBase64(preSharedKey));
notifyPropertyChanged(BR.isExcludePrivateIPsOn); } catch (final Key.KeyFormatException e) {
throw new ParseException("PresharedKey", preSharedKey, e);
}
} }
public void setPersistentKeepalive(final String persistentKeepalive) { public Builder parsePublicKey(final String publicKey) throws ParseException {
this.persistentKeepalive = persistentKeepalive; try {
notifyPropertyChanged(BR.persistentKeepalive); return setPublicKey(Key.fromBase64(publicKey));
} catch (final Key.KeyFormatException e) {
throw new ParseException("PublicKey", publicKey, e);
}
} }
public void setPreSharedKey(final String preSharedKey) { public Builder setEndpoint(final InetEndpoint endpoint) {
this.preSharedKey = preSharedKey; this.endpoint = Optional.of(endpoint);
notifyPropertyChanged(BR.preSharedKey); 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; this.publicKey = publicKey;
notifyPropertyChanged(BR.publicKey); return this;
}
public void toggleExcludePrivateIPs() {
final Collection<String> 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);
} }
} }
} }

View File

@ -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.
* <p>
* Instances of this class are immutable.
*/
@SuppressWarnings("MagicNumber")
public final class Key {
private final byte[] key;
/**
* Constructs an object encapsulating the supplied key.
*
* @param key an array of bytes containing a binary key. Callers of this constructor are
* responsible for ensuring that the array is of the correct length.
*/
private Key(final byte[] key) {
// Defensively copy to ensure immutability.
this.key = Arrays.copyOf(key, key.length);
}
/**
* Decodes a single 4-character base64 chunk to an integer in constant time.
*
* @param src an array of at least 4 characters in base64 format
* @param srcOffset the offset of the beginning of the chunk in {@code src}
* @return the decoded 3-byte integer, or some arbitrary integer value if the input was not
* valid base64
*/
private static int decodeBase64(final char[] src, final int srcOffset) {
int val = 0;
for (int i = 0; i < 4; ++i) {
final char c = src[i + srcOffset];
val |= (-1
+ ((((('A' - 1) - c) & (c - ('Z' + 1))) >>> 8) & (c - 64))
+ ((((('a' - 1) - c) & (c - ('z' + 1))) >>> 8) & (c - 70))
+ ((((('0' - 1) - c) & (c - ('9' + 1))) >>> 8) & (c + 5))
+ ((((('+' - 1) - c) & (c - ('+' + 1))) >>> 8) & 63)
+ ((((('/' - 1) - c) & (c - ('/' + 1))) >>> 8) & 64)
) << (18 - 6 * i);
}
return val;
}
/**
* Encodes a single 4-character base64 chunk from 3 consecutive bytes in constant time.
*
* @param src an array of at least 3 bytes
* @param srcOffset the offset of the beginning of the chunk in {@code src}
* @param dest an array of at least 4 characters
* @param destOffset the offset of the beginning of the chunk in {@code dest}
*/
private static void encodeBase64(final byte[] src, final int srcOffset,
final char[] dest, final int destOffset) {
final byte[] input = {
(byte) ((src[srcOffset] >>> 2) & 63),
(byte) ((src[srcOffset] << 4 | ((src[1 + srcOffset] & 0xff) >>> 4)) & 63),
(byte) ((src[1 + srcOffset] << 2 | ((src[2 + srcOffset] & 0xff) >>> 6)) & 63),
(byte) ((src[2 + srcOffset]) & 63),
};
for (int i = 0; i < 4; ++i) {
dest[i + destOffset] = (char) (input[i] + 'A'
+ (((25 - input[i]) >>> 8) & 6)
- (((51 - input[i]) >>> 8) & 75)
- (((61 - input[i]) >>> 8) & 15)
+ (((62 - input[i]) >>> 8) & 3));
}
}
/**
* Decodes a WireGuard public or private key from its base64 string representation. This
* function throws a {@link KeyFormatException} if the source string is not well-formed.
*
* @param str the base64 string representation of a WireGuard key
* @return the decoded key encapsulated in an immutable container
*/
public static Key fromBase64(final String str) {
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;
}
}
}

View File

@ -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);
}
}

View File

@ -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.
* <p>
* Instances of this class are immutable.
*/
public class KeyPair {
private final Key privateKey;
private final Key publicKey;
/**
* Creates a key pair using a newly-generated private key.
*/
public KeyPair() {
this(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;
}
}

View File

@ -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);
}
}

View File

@ -19,7 +19,7 @@
<variable <variable
name="config" name="config"
type="com.wireguard.config.Config.Observable" /> type="com.wireguard.config.Config" />
</data> </data>
<ScrollView <ScrollView
@ -102,7 +102,7 @@
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:onClick="@{ClipboardUtils::copyTextView}" android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interfaceSection.publicKey}" /> android:text="@{config.interface.keyPair.publicKey.toBase64}" />
<TextView <TextView
android:id="@+id/addresses_label" android:id="@+id/addresses_label"
@ -120,7 +120,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/addresses_label" android:layout_below="@+id/addresses_label"
android:contentDescription="@string/addresses" android:contentDescription="@string/addresses"
android:text="@{config.interfaceSection.addresses}" /> android:text="@{config.interface.addresses}" />
</RelativeLayout> </RelativeLayout>
</android.support.v7.widget.CardView> </android.support.v7.widget.CardView>

View File

@ -8,7 +8,7 @@
<variable <variable
name="item" name="item"
type="com.wireguard.config.Peer.Observable" /> type="com.wireguard.config.Peer" />
</data> </data>
<android.support.v7.widget.CardView <android.support.v7.widget.CardView
@ -54,7 +54,7 @@
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:onClick="@{ClipboardUtils::copyTextView}" android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{item.publicKey}" /> android:text="@{item.publicKey.toBase64}" />
<TextView <TextView
android:id="@+id/allowed_ips_label" android:id="@+id/allowed_ips_label"
@ -71,7 +71,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/allowed_ips_label" android:layout_below="@+id/allowed_ips_label"
android:text="@{item.allowedIPs}" /> android:text="@{item.allowedIps}" />
<TextView <TextView
android:id="@+id/endpoint_label" android:id="@+id/endpoint_label"

View File

@ -11,15 +11,17 @@
<import type="com.wireguard.android.widget.NameInputFilter" /> <import type="com.wireguard.android.widget.NameInputFilter" />
<import type="com.wireguard.config.Peer" />
<variable <variable
name="fragment" name="fragment"
type="com.wireguard.android.fragment.TunnelEditorFragment" /> type="com.wireguard.android.fragment.TunnelEditorFragment" />
<variable <variable
name="config" name="config"
type="com.wireguard.config.Config.Observable" /> type="com.wireguard.android.viewmodel.ConfigProxy" />
<variable
name="name"
type="String" />
</data> </data>
<android.support.design.widget.CoordinatorLayout <android.support.design.widget.CoordinatorLayout
@ -76,7 +78,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/interface_name_label" android:layout_below="@+id/interface_name_label"
android:inputType="textNoSuggestions|textVisiblePassword" android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={config.name}" android:text="@={name}"
app:filter="@{NameInputFilter.newInstance()}" /> app:filter="@{NameInputFilter.newInstance()}" />
<TextView <TextView
@ -96,7 +98,7 @@
android:layout_toStartOf="@+id/generate_private_key_button" android:layout_toStartOf="@+id/generate_private_key_button"
android:contentDescription="@string/public_key_description" android:contentDescription="@string/public_key_description"
android:inputType="textNoSuggestions|textVisiblePassword" android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={config.interfaceSection.privateKey}" android:text="@={config.interface.privateKey}"
app:filter="@{KeyInputFilter.newInstance()}" /> app:filter="@{KeyInputFilter.newInstance()}" />
<Button <Button
@ -107,7 +109,7 @@
android:layout_alignBottom="@id/private_key_text" android:layout_alignBottom="@id/private_key_text"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_below="@+id/private_key_label" android:layout_below="@+id/private_key_label"
android:onClick="@{() -> config.interfaceSection.generateKeypair()}" android:onClick="@{() -> config.interface.generateKeyPair()}"
android:text="@string/generate" /> android:text="@string/generate" />
<TextView <TextView
@ -130,7 +132,7 @@
android:hint="@string/hint_generated" android:hint="@string/hint_generated"
android:maxLines="1" android:maxLines="1"
android:onClick="@{ClipboardUtils::copyTextView}" android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interfaceSection.publicKey}" /> android:text="@{config.interface.publicKey}" />
<TextView <TextView
android:id="@+id/addresses_label" android:id="@+id/addresses_label"
@ -150,7 +152,7 @@
android:layout_below="@+id/addresses_label" android:layout_below="@+id/addresses_label"
android:layout_toStartOf="@+id/listen_port_text" android:layout_toStartOf="@+id/listen_port_text"
android:inputType="textNoSuggestions|textVisiblePassword" android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={config.interfaceSection.addresses}" /> android:text="@={config.interface.addresses}" />
<TextView <TextView
android:id="@+id/listen_port_label" android:id="@+id/listen_port_label"
@ -171,7 +173,7 @@
android:layout_alignStart="@+id/generate_private_key_button" android:layout_alignStart="@+id/generate_private_key_button"
android:hint="@string/hint_random" android:hint="@string/hint_random"
android:inputType="number" android:inputType="number"
android:text="@={config.interfaceSection.listenPort}" android:text="@={config.interface.listenPort}"
android:textAlignment="center" /> android:textAlignment="center" />
<TextView <TextView
@ -192,7 +194,7 @@
android:layout_below="@+id/dns_servers_label" android:layout_below="@+id/dns_servers_label"
android:layout_toStartOf="@+id/mtu_text" android:layout_toStartOf="@+id/mtu_text"
android:inputType="textNoSuggestions|textVisiblePassword" android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={config.interfaceSection.dnses}" /> android:text="@={config.interface.dnsServers}" />
<TextView <TextView
android:id="@+id/mtu_label" android:id="@+id/mtu_label"
@ -213,7 +215,7 @@
android:layout_alignStart="@+id/generate_private_key_button" android:layout_alignStart="@+id/generate_private_key_button"
android:hint="@string/hint_automatic" android:hint="@string/hint_automatic"
android:inputType="number" android:inputType="number"
android:text="@={config.interfaceSection.mtu}" android:text="@={config.interface.mtu}"
android:textAlignment="center" /> android:textAlignment="center" />
<Button <Button
@ -224,7 +226,7 @@
android:layout_below="@+id/dns_servers_text" android:layout_below="@+id/dns_servers_text"
android:layout_marginLeft="-8dp" android:layout_marginLeft="-8dp"
android:onClick="@{fragment::onRequestSetExcludedApplications}" android:onClick="@{fragment::onRequestSetExcludedApplications}"
android:text="@{@plurals/set_excluded_applications(config.interfaceSection.excludedApplicationsCount, config.interfaceSection.excludedApplicationsCount)}" /> android:text="@{@plurals/set_excluded_applications(config.interface.excludedApplications.size, config.interface.excludedApplications.size)}" />
</RelativeLayout> </RelativeLayout>
</android.support.v7.widget.CardView> </android.support.v7.widget.CardView>
@ -244,7 +246,7 @@
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
android:onClick="@{() -> config.peers.add(Peer.Observable.newInstance())}" android:onClick="@{() -> config.addPeer()}"
android:text="@string/add_peer" /> android:text="@string/add_peer" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View File

@ -10,11 +10,11 @@
<variable <variable
name="collection" name="collection"
type="android.databinding.ObservableList&lt;com.wireguard.config.Peer.Observable&gt;" /> type="android.databinding.ObservableList&lt;com.wireguard.android.viewmodel.PeerProxy&gt;" />
<variable <variable
name="item" name="item"
type="com.wireguard.config.Peer.Observable" /> type="com.wireguard.android.viewmodel.PeerProxy" />
</data> </data>
<android.support.v7.widget.CardView <android.support.v7.widget.CardView
@ -52,7 +52,7 @@
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:background="@null" android:background="@null"
android:contentDescription="@string/delete" android:contentDescription="@string/delete"
android:onClick="@{() -> collection.remove(item)}" android:onClick="@{() -> item.unbind()}"
android:src="@drawable/ic_action_delete" /> android:src="@drawable/ic_action_delete" />
<TextView <TextView
@ -104,10 +104,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/allowed_ips_label" android:layout_alignBaseline="@+id/allowed_ips_label"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:checked="@{item.isExcludePrivateIPsOn}" android:checked="@={item.excludingPrivateIps}"
android:onClick="@{() -> item.toggleExcludePrivateIPs()}"
android:text="@string/exclude_private_ips" android:text="@string/exclude_private_ips"
android:visibility="@{item.canToggleExcludePrivateIPs ? View.VISIBLE : View.GONE}" /> android:visibility="@{item.ableToExcludePrivateIps ? View.VISIBLE : View.GONE}" />
<EditText <EditText
android:id="@+id/allowed_ips_text" android:id="@+id/allowed_ips_text"
@ -115,7 +114,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/allowed_ips_label" android:layout_below="@+id/allowed_ips_label"
android:inputType="textNoSuggestions|textVisiblePassword" android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={item.allowedIPs}" /> android:text="@={item.allowedIps}" />
<TextView <TextView
android:id="@+id/endpoint_label" android:id="@+id/endpoint_label"

View File

@ -84,7 +84,7 @@
app:srcCompat="@drawable/ic_action_open_white" /> app:srcCompat="@drawable/ic_action_open_white" />
<com.wireguard.android.widget.fab.LabeledFloatingActionButton <com.wireguard.android.widget.fab.LabeledFloatingActionButton
android:id="@+id/scan_qr_code" android:id="@+id/create_from_qrcode"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:onClick="@{fragment::onRequestScanQRCode}" android:onClick="@{fragment::onRequestScanQRCode}"
@ -101,6 +101,5 @@
app:fab_title="@string/create_empty" app:fab_title="@string/create_empty"
app:srcCompat="@drawable/ic_action_edit_white" /> app:srcCompat="@drawable/ic_action_edit_white" />
</com.wireguard.android.widget.fab.FloatingActionsMenu> </com.wireguard.android.widget.fab.FloatingActionsMenu>
</android.support.design.widget.CoordinatorLayout> </android.support.design.widget.CoordinatorLayout>
</layout> </layout>

View File

@ -63,6 +63,7 @@
<string name="log_export_summary">Log file will be saved to downloads folder</string> <string name="log_export_summary">Log file will be saved to downloads folder</string>
<string name="mtu">MTU</string> <string name="mtu">MTU</string>
<string name="name">Name</string> <string name="name">Name</string>
<string name="parse_error">Cannot parse “%s” at %s</string>
<string name="peer">Peer</string> <string name="peer">Peer</string>
<string name="permission_label">control WireGuard tunnels</string> <string name="permission_label">control WireGuard tunnels</string>
<string name="permission_description">Allows an app to control WireGuard tunnels. Apps with this permission may enable and disable WireGuard tunnels at will, potentially misdirecting Internet traffic.</string> <string name="permission_description">Allows an app to control WireGuard tunnels. Apps with this permission may enable and disable WireGuard tunnels at will, potentially misdirecting Internet traffic.</string>
@ -99,13 +100,9 @@
<string name="tunnel_rename_success">Successfully renamed tunnel to “%s”</string> <string name="tunnel_rename_success">Successfully renamed tunnel to “%s”</string>
<string name="tunnel_error_invalid_name">Invalid name</string> <string name="tunnel_error_invalid_name">Invalid name</string>
<string name="tunnel_error_already_exists">Tunnel %s already exists</string> <string name="tunnel_error_already_exists">Tunnel %s already exists</string>
<string name="tunnel_error_empty_inetaddress">Empty address</string>
<string name="tunnel_error_empty_interface_address">Address is empty</string> <string name="tunnel_error_empty_interface_address">Address is empty</string>
<string name="tunnel_error_interface_parse_failed">Unable to parse line: “%s”</string>
<string name="tunnel_error_forbidden_endpoint_chars">Forbidden characters in endpoint</string> <string name="tunnel_error_forbidden_endpoint_chars">Forbidden characters in endpoint</string>
<string name="tunnel_error_empty_peer_public_key">Peer public key may not be empty</string> <string name="tunnel_error_empty_peer_public_key">Peer public key may not be empty</string>
<string name="tunnel_error_invalid_config_line">Invalid configuration line: %s</string>
<string name="tunnel_error_no_config_information">Could not find any config information</string>
<string name="key_length_base64_exception_message">WireGuard base64 keys must be 44 characters encoding 32 bytes</string> <string name="key_length_base64_exception_message">WireGuard base64 keys must be 44 characters encoding 32 bytes</string>
<string name="key_length_exception_message">WireGuard keys must be 32 bytes</string> <string name="key_length_exception_message">WireGuard keys must be 32 bytes</string>
<string name="key_length_hex_exception_message">WireGuard hex keys must be 64 characters encoding 32 bytes</string> <string name="key_length_hex_exception_message">WireGuard hex keys must be 64 characters encoding 32 bytes</string>

View File

@ -7,7 +7,7 @@ allprojects {
buildscript { buildscript {
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.2.0' classpath 'com.android.tools.build:gradle:3.2.1'
} }
repositories { repositories {
google() google()