diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml
index 6e6577a26..20c5f550b 100644
--- a/OpenKeychain/src/main/AndroidManifest.xml
+++ b/OpenKeychain/src/main/AndroidManifest.xml
@@ -719,6 +719,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java
index fa3ac244d..9e1e676f0 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java
@@ -126,6 +126,7 @@ public final class Constants {
public static final String CACHED_CONSOLIDATE = "cachedConsolidate";
public static final String SEARCH_KEYSERVER = "search_keyserver_pref";
public static final String SEARCH_KEYBASE = "search_keybase_pref";
+ public static final String SEARCH_WEB_KEY_DIRECTORY = "search_wkd_pref";
public static final String USE_NUMKEYPAD_FOR_SECURITY_TOKEN_PIN = "useNumKeypadForYubikeyPin";
public static final String ENCRYPT_FILENAMES = "encryptFilenames";
public static final String FILE_USE_COMPRESSION = "useFileCompression";
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/CloudSearch.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/CloudSearch.java
index de6807e4a..51c840d89 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/CloudSearch.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/CloudSearch.java
@@ -42,16 +42,16 @@ public class CloudSearch {
// it's a Vector for sync, multiple threads might report problems
final Vector problems = new Vector<>();
- if (cloudPrefs.searchKeyserver) {
- servers.add(HkpKeyserverClient.fromHkpKeyserverAddress(cloudPrefs.keyserver));
+ if (cloudPrefs.isKeyserverEnabled()) {
+ servers.add(HkpKeyserverClient.fromHkpKeyserverAddress(cloudPrefs.getKeyserver()));
}
- if (cloudPrefs.searchKeybase) {
+ if (cloudPrefs.isKeybaseEnabled()) {
servers.add(KeybaseKeyserverClient.getInstance());
}
- if (cloudPrefs.searchFacebook) {
+ if (cloudPrefs.isFacebookEnabled()) {
servers.add(FacebookKeyserverClient.getInstance());
}
- if (cloudPrefs.searchWebKeyDirectory) {
+ if (cloudPrefs.isWebKeyDirectoryEnabled()) {
servers.add(WebKeyDirectoryClient.getInstance());
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/WebKeyDirectoryClient.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/WebKeyDirectoryClient.java
index 06570ea21..66058aedf 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/WebKeyDirectoryClient.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/WebKeyDirectoryClient.java
@@ -19,30 +19,23 @@ package org.sufficientlysecure.keychain.keyimport;
import android.support.annotation.Nullable;
-
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
import org.sufficientlysecure.keychain.network.OkHttpClientFactory;
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.util.ParcelableProxy;
-import org.sufficientlysecure.keychain.util.ZBase32;
+import org.sufficientlysecure.keychain.util.WebKeyDirectoryUtil;
+import timber.log.Timber;
import java.io.IOException;
import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URL;
import java.net.UnknownHostException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.Response;
-import timber.log.Timber;
/**
@@ -59,12 +52,10 @@ public class WebKeyDirectoryClient implements KeyserverClient {
private WebKeyDirectoryClient() {
}
- private static final Pattern EMAIL_PATTERN = Pattern.compile("^\\s*(.+)@(.+)\\s*$");
-
@Override
public List search(String name, ParcelableProxy proxy)
throws QueryFailedException {
- URL webKeyDirectoryURL = toWebKeyDirectoryURL(name);
+ URL webKeyDirectoryURL = WebKeyDirectoryUtil.toWebKeyDirectoryURL(name);
if (webKeyDirectoryURL == null) {
Timber.d("Name not supported by Web Key Directory Client: " + name);
@@ -103,7 +94,7 @@ public class WebKeyDirectoryClient implements KeyserverClient {
Request request = new Request.Builder().url(url).build();
- OkHttpClient client = OkHttpClientFactory.getClientPinnedIfAvailable(url, proxy);
+ OkHttpClient client = OkHttpClientFactory.getClientPinnedIfAvailableWithRedirects(url, proxy);
Response response = client.newCall(request).execute();
if (response.isSuccessful()) {
@@ -130,31 +121,4 @@ public class WebKeyDirectoryClient implements KeyserverClient {
public void add(String armoredKey, ParcelableProxy proxy) {
throw new UnsupportedOperationException("Uploading keys to Web Key Directory is not supported");
}
-
- @Nullable
- private static URL toWebKeyDirectoryURL(String name) {
- Matcher matcher = EMAIL_PATTERN.matcher(name);
-
- if (!matcher.matches()) {
- return null;
- }
-
- String localPart = matcher.group(1);
- String encodedPart = ZBase32.encode(toSHA1(localPart.toLowerCase().getBytes()));
- String domain = matcher.group(2);
-
- try {
- return new URL("https://" + domain + "/.well-known/openpgpkey/hu/" + encodedPart);
- } catch (MalformedURLException e) {
- return null;
- }
- }
-
- private static byte[] toSHA1(byte[] input) {
- try {
- return MessageDigest.getInstance("SHA-1").digest(input);
- } catch (NoSuchAlgorithmException e) {
- throw new AssertionError("SHA-1 should always be available");
- }
- }
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/OkHttpClientFactory.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/OkHttpClientFactory.java
index 1d2bdc6f5..507a9509c 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/OkHttpClientFactory.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/OkHttpClientFactory.java
@@ -47,10 +47,18 @@ public class OkHttpClientFactory {
}
public static OkHttpClient getClientPinnedIfAvailable(URL url, Proxy proxy) {
+ // don't follow any redirects for keyservers, as discussed in the security audit
+ return getClientPinnedIfAvailable(url, proxy, false);
+ }
+
+ public static OkHttpClient getClientPinnedIfAvailableWithRedirects(URL url, Proxy proxy) {
+ return getClientPinnedIfAvailable(url, proxy, true);
+ }
+
+ private static OkHttpClient getClientPinnedIfAvailable(URL url, Proxy proxy, boolean followRedirects) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
- // don't follow any redirects for keyservers, as discussed in the security audit
- builder.followRedirects(false)
+ builder.followRedirects(followRedirects)
.followSslRedirects(false);
if (proxy != null) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java
index 27d5ed87d..b8399a96b 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java
@@ -57,6 +57,8 @@ public class ImportKeysActivity extends BaseActivity implements ImportKeysListen
public static final String ACTION_IMPORT_KEY_FROM_KEYSERVER = Constants.IMPORT_KEY_FROM_KEYSERVER;
public static final String ACTION_IMPORT_KEY_FROM_FACEBOOK
= Constants.INTENT_PREFIX + "IMPORT_KEY_FROM_FACEBOOK";
+ public static final String ACTION_IMPORT_KEY_FROM_WEB_KEY_DIRECTORY
+ = Constants.INTENT_PREFIX + "ACTION_IMPORT_KEY_FROM_WEB_KEY_DIRECTORY";
public static final String ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_RESULT =
Constants.INTENT_PREFIX + "IMPORT_KEY_FROM_KEY_SERVER_AND_RETURN_RESULT";
public static final String ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN = Constants.INTENT_PREFIX
@@ -122,6 +124,8 @@ public class ImportKeysActivity extends BaseActivity implements ImportKeysListen
if (Intent.ACTION_VIEW.equals(action)) {
if (FacebookKeyserverClient.isFacebookHost(dataUri)) {
action = ACTION_IMPORT_KEY_FROM_FACEBOOK;
+ } else if ("https".equalsIgnoreCase(scheme) || dataUri.getPath().startsWith("/.well-known/openpgpkey/hu/")) {
+ action = ACTION_IMPORT_KEY_FROM_WEB_KEY_DIRECTORY;
} else if ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme)) {
action = ACTION_SEARCH_KEYSERVER_FROM_URL;
} else if (Constants.FINGERPRINT_SCHEME.equalsIgnoreCase(scheme)) {
@@ -208,17 +212,23 @@ public class ImportKeysActivity extends BaseActivity implements ImportKeysListen
String fbUsername = FacebookKeyserverClient.getUsernameFromUri(dataUri);
Preferences.CloudSearchPrefs cloudSearchPrefs =
- new Preferences.CloudSearchPrefs(false, true, true, false, null);
+ Preferences.CloudSearchPrefs.createSocialOnly();
// search immediately
startListFragment(null, null, fbUsername, cloudSearchPrefs);
break;
}
+ case ACTION_IMPORT_KEY_FROM_WEB_KEY_DIRECTORY: {
+ Preferences.CloudSearchPrefs cloudSearchPrefs =
+ Preferences.CloudSearchPrefs.createWebKeyDirectoryOnly();
+ // search immediately
+ startListFragment(null, null, dataUri.toString(), cloudSearchPrefs);
+ break;
+ }
case ACTION_SEARCH_KEYSERVER_FROM_URL: {
// get keyserver from URL
HkpKeyserverAddress keyserver = HkpKeyserverAddress.createFromUri(
dataUri.getScheme() + "://" + dataUri.getAuthority());
- Preferences.CloudSearchPrefs cloudSearchPrefs = new Preferences.CloudSearchPrefs(
- true, false, false, false, keyserver);
+ Preferences.CloudSearchPrefs cloudSearchPrefs = Preferences.CloudSearchPrefs.createKeyserverOnly(keyserver);
Timber.d("Using keyserver: " + keyserver);
// process URL to get operation and query
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java
index 233eddf5f..a44c101fb 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java
@@ -18,28 +18,27 @@
package org.sufficientlysecure.keychain.util;
-import java.net.Proxy;
-import java.util.ArrayList;
-import java.util.ListIterator;
-
import android.accounts.Account;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
-import android.os.Parcel;
import android.os.Parcelable;
import android.preference.PreferenceManager;
-
+import android.support.annotation.Nullable;
+import com.google.auto.value.AutoValue;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Constants.Pref;
import org.sufficientlysecure.keychain.KeychainApplication;
import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress;
import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService;
-
import timber.log.Timber;
+import java.net.Proxy;
+import java.util.ArrayList;
+import java.util.ListIterator;
+
/**
* Singleton Implementation of a Preference Helper
@@ -345,10 +344,10 @@ public class Preferences {
// cloud prefs
public CloudSearchPrefs getCloudSearchPrefs() {
- return new CloudSearchPrefs(mSharedPreferences.getBoolean(Pref.SEARCH_KEYSERVER, true),
+ return CloudSearchPrefs.create(mSharedPreferences.getBoolean(Pref.SEARCH_KEYSERVER, true),
mSharedPreferences.getBoolean(Pref.SEARCH_KEYBASE, true),
false,
- true,
+ mSharedPreferences.getBoolean(Pref.SEARCH_WEB_KEY_DIRECTORY, true),
getPreferredKeyserver());
}
@@ -362,62 +361,44 @@ public class Preferences {
editor.commit();
}
- public static class CloudSearchPrefs implements Parcelable {
- public final boolean searchKeyserver;
- public final boolean searchKeybase;
- public final boolean searchFacebook;
- public final boolean searchWebKeyDirectory;
- public final HkpKeyserverAddress keyserver;
+ @AutoValue
+ public static abstract class CloudSearchPrefs implements Parcelable {
+ public abstract boolean isKeyserverEnabled();
+ public abstract boolean isKeybaseEnabled();
+ public abstract boolean isFacebookEnabled();
+ public abstract boolean isWebKeyDirectoryEnabled();
+
+ @Nullable
+ public abstract HkpKeyserverAddress getKeyserver();
/**
- * @param searchKeyserver should passed keyserver be searched
- * @param searchKeybase should keybase.io be searched
- * @param keyserver the keyserver url authority to search on
+ * @param searchKeyserver should passed keyserver be searched
+ * @param searchKeybase should keybase.io be searched
+ * @param searchFacebook should Facebook be searched
+ * @param searchWebKeyDirectory should WKD be searched
+ * @param keyserver the keyserver url authority to search on
*/
- public CloudSearchPrefs(boolean searchKeyserver, boolean searchKeybase,
- boolean searchFacebook, boolean searchWebKeyDirectory,
- HkpKeyserverAddress keyserver) {
- this.searchKeyserver = searchKeyserver;
- this.searchKeybase = searchKeybase;
- this.searchFacebook = searchFacebook;
- this.searchWebKeyDirectory = searchWebKeyDirectory;
- this.keyserver = keyserver;
+ public static CloudSearchPrefs create(boolean searchKeyserver, boolean searchKeybase,
+ boolean searchFacebook, boolean searchWebKeyDirectory,
+ @Nullable HkpKeyserverAddress keyserver) {
+ return new AutoValue_Preferences_CloudSearchPrefs(searchKeyserver,
+ searchKeybase,
+ searchFacebook,
+ searchWebKeyDirectory,
+ keyserver);
}
- protected CloudSearchPrefs(Parcel in) {
- searchKeyserver = in.readByte() != 0x00;
- searchKeybase = in.readByte() != 0x00;
- searchFacebook = in.readByte() != 0x00;
- searchWebKeyDirectory = in.readByte() != 0x00;
- keyserver = in.readParcelable(HkpKeyserverAddress.class.getClassLoader());
+ public static CloudSearchPrefs createWebKeyDirectoryOnly() {
+ return create(false, false, false, true, null);
}
- @Override
- public int describeContents() {
- return 0;
+ public static CloudSearchPrefs createKeyserverOnly(HkpKeyserverAddress keyserver) {
+ return create(true, false, false, false, keyserver);
}
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeByte((byte) (searchKeyserver ? 0x01 : 0x00));
- dest.writeByte((byte) (searchKeybase ? 0x01 : 0x00));
- dest.writeByte((byte) (searchFacebook ? 0x01 : 0x00));
- dest.writeByte((byte) (searchWebKeyDirectory ? 0x01 : 0x00));
- dest.writeParcelable(keyserver, flags);
+ public static CloudSearchPrefs createSocialOnly() {
+ return create(false, true, true, false, null);
}
-
- public static final Parcelable.Creator CREATOR
- = new Parcelable.Creator() {
- @Override
- public CloudSearchPrefs createFromParcel(Parcel in) {
- return new CloudSearchPrefs(in);
- }
-
- @Override
- public CloudSearchPrefs[] newArray(int size) {
- return new CloudSearchPrefs[size];
- }
- };
}
// sync preferences
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/WebKeyDirectoryUtil.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/WebKeyDirectoryUtil.java
new file mode 100644
index 000000000..0531986ad
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/WebKeyDirectoryUtil.java
@@ -0,0 +1,64 @@
+package org.sufficientlysecure.keychain.util;
+
+import android.support.annotation.Nullable;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class WebKeyDirectoryUtil {
+
+ private static final Pattern EMAIL_PATTERN = Pattern.compile("^\\s*([^\\s]+)@([^\\s]+)\\s*$");
+
+ private WebKeyDirectoryUtil() {
+ }
+
+ /**
+ * Tries to construct a Web Key Directory from a given name.
+ * Returns {@code null} if unsuccessful.
+ *
+ * @see Key Discovery
+ */
+ @Nullable
+ public static URL toWebKeyDirectoryURL(String name) {
+ if (name == null) {
+ return null;
+ }
+
+ if (name.startsWith("https://") && name.contains("/.well-known/openpgpkey/hu/")) {
+ try {
+ return new URL(name);
+ } catch (MalformedURLException e) {
+ return null;
+ }
+ }
+
+ Matcher matcher = EMAIL_PATTERN.matcher(name);
+
+ if (!matcher.matches()) {
+ return null;
+ }
+
+ String localPart = matcher.group(1);
+ String encodedPart = ZBase32.encode(toSHA1(localPart.toLowerCase().getBytes()));
+ String domain = matcher.group(2);
+
+ try {
+ return new URL("https://" + domain + "/.well-known/openpgpkey/hu/" + encodedPart);
+ } catch (MalformedURLException e) {
+ return null;
+ }
+ }
+
+ private static byte[] toSHA1(byte[] input) {
+ try {
+ return MessageDigest.getInstance("SHA-1").digest(input);
+ } catch (NoSuchAlgorithmException e) {
+ throw new AssertionError("SHA-1 should always be available");
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml
index 0ed0fb1c2..1d14bfd09 100644
--- a/OpenKeychain/src/main/res/values/strings.xml
+++ b/OpenKeychain/src/main/res/values/strings.xml
@@ -211,6 +211,8 @@
"Search keys on keybase.io"
"Facebook"
"Search keys on Facebook by username"
+ "Web Key Directory"
+ "Search keys using Web Key Directory"
"Automatic key updates"
"Every three days, keys are updated from the preferred keyserver"
diff --git a/OpenKeychain/src/main/res/xml/cloud_search_preferences.xml b/OpenKeychain/src/main/res/xml/cloud_search_preferences.xml
index e1ab50d63..6c5540098 100644
--- a/OpenKeychain/src/main/res/xml/cloud_search_preferences.xml
+++ b/OpenKeychain/src/main/res/xml/cloud_search_preferences.xml
@@ -14,4 +14,9 @@
android:key="search_keybase_pref"
android:summary="@string/pref_keybase_summary"
android:title="@string/pref_keybase" />
+
\ No newline at end of file
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/WebKeyDirectoryUtilTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/WebKeyDirectoryUtilTest.java
new file mode 100644
index 000000000..f468774ff
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/WebKeyDirectoryUtilTest.java
@@ -0,0 +1,39 @@
+package org.sufficientlysecure.keychain.util;
+
+import org.junit.Test;
+
+import java.net.URL;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+
+public class WebKeyDirectoryUtilTest {
+
+ @Test
+ public void testWkd() {
+ URL url = WebKeyDirectoryUtil.toWebKeyDirectoryURL("test-wkd@openkeychain.org");
+ assertNotNull(url);
+ assertEquals("openkeychain.org", url.getHost());
+ assertEquals("https", url.getProtocol());
+ assertEquals("/.well-known/openpgpkey/hu/4hg7tescnttreaouu4z1izeuuyibwww1", url.getPath());
+ }
+
+ @Test
+ public void testWkdWithSpaces() {
+ URL url = WebKeyDirectoryUtil.toWebKeyDirectoryURL(" test-wkd@openkeychain.org ");
+ assertNotNull(url);
+ assertEquals("openkeychain.org", url.getHost());
+ assertEquals("https", url.getProtocol());
+ assertEquals("/.well-known/openpgpkey/hu/4hg7tescnttreaouu4z1izeuuyibwww1", url.getPath());
+ }
+
+ @Test
+ public void testWkdDirectUrl() {
+ URL url = WebKeyDirectoryUtil.toWebKeyDirectoryURL("https://openkeychain.org/.well-known/openpgpkey/hu/4hg7tescnttreaouu4z1izeuuyibwww1");
+ assertNotNull(url);
+ assertEquals("openkeychain.org", url.getHost());
+ assertEquals("https", url.getProtocol());
+ assertEquals("/.well-known/openpgpkey/hu/4hg7tescnttreaouu4z1izeuuyibwww1", url.getPath());
+ }
+
+}