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 b48ade564..de6807e4a 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/CloudSearch.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/CloudSearch.java
@@ -51,6 +51,9 @@ public class CloudSearch {
if (cloudPrefs.searchFacebook) {
servers.add(FacebookKeyserverClient.getInstance());
}
+ if (cloudPrefs.searchWebKeyDirectory) {
+ servers.add(WebKeyDirectoryClient.getInstance());
+ }
int numberOfServers = servers.size();
final ImportKeysList results = new ImportKeysList(numberOfServers);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/WebKeyDirectoryClient.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/WebKeyDirectoryClient.java
new file mode 100644
index 000000000..06570ea21
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/WebKeyDirectoryClient.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2017 Schürmann & Breitmoser GbR
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.sufficientlysecure.keychain.keyimport;
+
+
+import android.support.annotation.Nullable;
+
+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 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;
+
+
+/**
+ * Searches for keys using Web Key Directory protocol.
+ *
+ * @see Key Discovery
+ */
+public class WebKeyDirectoryClient implements KeyserverClient {
+
+ public static WebKeyDirectoryClient getInstance() {
+ return new WebKeyDirectoryClient();
+ }
+
+ 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);
+
+ if (webKeyDirectoryURL == null) {
+ Timber.d("Name not supported by Web Key Directory Client: " + name);
+ return Collections.emptyList();
+ }
+
+ Timber.d("Web Key Directory import: " + name + " using Proxy: " + proxy.getProxy());
+
+ byte[] data = query(webKeyDirectoryURL, proxy.getProxy());
+
+ if (data == null) {
+ Timber.d("No Web Key Directory endpoint for: " + name);
+ return Collections.emptyList();
+ }
+
+ // if we're here that means key retrieval succeeded,
+ // would have thrown an exception otherwise
+ try {
+ UncachedKeyRing ring = UncachedKeyRing.decodeFromData(data);
+ return Collections.singletonList(new ImportKeysListEntry(null, ring));
+ } catch (PgpGeneralException | IOException e) {
+ Timber.e(e, "Failed parsing key from Web Key Directory during search");
+ throw new QueryFailedException("No valid key found on Web Key Directory");
+ }
+ }
+
+ @Override
+ public String get(String name, ParcelableProxy proxy) {
+ throw new UnsupportedOperationException("Returning armored key from Web Key Directory not supported");
+ }
+
+ @Nullable
+ private byte[] query(URL url, Proxy proxy) throws QueryFailedException {
+ try {
+ Timber.d("fetching from Web Key Directory with: %s proxy: %s", url, proxy);
+
+ Request request = new Request.Builder().url(url).build();
+
+ OkHttpClient client = OkHttpClientFactory.getClientPinnedIfAvailable(url, proxy);
+ Response response = client.newCall(request).execute();
+
+ if (response.isSuccessful()) {
+ return response.body().bytes();
+ } else if (response.code() == HttpURLConnection.HTTP_NOT_FOUND) {
+ return null;
+ } else {
+ throw new QueryFailedException("Error while fetching key from Web Key Directory. " +
+ "Response:" + response);
+ }
+
+ } catch (UnknownHostException e) {
+ Timber.e(e, "Unknown host at Web Key Directory key download");
+ return null;
+ } catch (IOException e) {
+ Timber.e(e, "IOException at Web Key Directory key download");
+ throw new QueryFailedException("Cannot connect to Web Key Directory. "
+ + "Check your Internet connection!"
+ + (proxy == Proxy.NO_PROXY ? "" : " Using proxy " + proxy));
+ }
+ }
+
+ @Override
+ 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/keyimport/processing/ImportKeysListCloudLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysListCloudLoader.java
index 25a08ddb8..a6ac852c2 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysListCloudLoader.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysListCloudLoader.java
@@ -83,8 +83,10 @@ public class ImportKeysListCloudLoader
// Now we have all the data needed to build the parcelable key ring for this key
for (ImportKeysListEntry e : mEntryList) {
- e.setParcelableKeyRing(ParcelableKeyRing.createFromReference(e.getFingerprint(), e.getKeyIdHex(),
- e.getKeybaseName(), e.getFbUsername()));
+ if (e.getParcelableKeyRing() == null) {
+ e.setParcelableKeyRing(ParcelableKeyRing.createFromReference(e.getFingerprint(), e.getKeyIdHex(),
+ e.getKeybaseName(), e.getFbUsername()));
+ }
}
return mEntryListWrapper;
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 c96b36c64..33963d7b3 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java
@@ -203,7 +203,7 @@ public class ImportKeysActivity extends BaseActivity implements ImportKeysListen
String fbUsername = FacebookKeyserverClient.getUsernameFromUri(dataUri);
Preferences.CloudSearchPrefs cloudSearchPrefs =
- new Preferences.CloudSearchPrefs(false, true, true, null);
+ new Preferences.CloudSearchPrefs(false, true, true, false, null);
// search immediately
startListFragment(null, null, fbUsername, cloudSearchPrefs);
break;
@@ -213,7 +213,7 @@ public class ImportKeysActivity extends BaseActivity implements ImportKeysListen
HkpKeyserverAddress keyserver = HkpKeyserverAddress.createFromUri(
dataUri.getScheme() + "://" + dataUri.getAuthority());
Preferences.CloudSearchPrefs cloudSearchPrefs = new Preferences.CloudSearchPrefs(
- true, false, false, keyserver);
+ true, false, false, false, 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 4eb649948..233eddf5f 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java
@@ -348,6 +348,7 @@ public class Preferences {
return new CloudSearchPrefs(mSharedPreferences.getBoolean(Pref.SEARCH_KEYSERVER, true),
mSharedPreferences.getBoolean(Pref.SEARCH_KEYBASE, true),
false,
+ true,
getPreferredKeyserver());
}
@@ -365,6 +366,7 @@ public class Preferences {
public final boolean searchKeyserver;
public final boolean searchKeybase;
public final boolean searchFacebook;
+ public final boolean searchWebKeyDirectory;
public final HkpKeyserverAddress keyserver;
/**
@@ -373,10 +375,12 @@ public class Preferences {
* @param keyserver the keyserver url authority to search on
*/
public CloudSearchPrefs(boolean searchKeyserver, boolean searchKeybase,
- boolean searchFacebook, HkpKeyserverAddress keyserver) {
+ boolean searchFacebook, boolean searchWebKeyDirectory,
+ HkpKeyserverAddress keyserver) {
this.searchKeyserver = searchKeyserver;
this.searchKeybase = searchKeybase;
this.searchFacebook = searchFacebook;
+ this.searchWebKeyDirectory = searchWebKeyDirectory;
this.keyserver = keyserver;
}
@@ -384,6 +388,7 @@ public class Preferences {
searchKeyserver = in.readByte() != 0x00;
searchKeybase = in.readByte() != 0x00;
searchFacebook = in.readByte() != 0x00;
+ searchWebKeyDirectory = in.readByte() != 0x00;
keyserver = in.readParcelable(HkpKeyserverAddress.class.getClassLoader());
}
@@ -397,6 +402,7 @@ public class Preferences {
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);
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ZBase32.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ZBase32.java
new file mode 100644
index 000000000..29ca50dd2
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ZBase32.java
@@ -0,0 +1,45 @@
+package org.sufficientlysecure.keychain.util;
+
+/**
+ * Utilities for handling ZBase-32 encoding.
+ *
+ * @see Z-Base32 encoding as used in RFC 6189
+ */
+public final class ZBase32 {
+
+ private static final char[] ALPHABET = "ybndrfg8ejkmcpqxot1uwisza345h769".toCharArray();
+ private static final int SHIFT = Integer.numberOfTrailingZeros(ALPHABET.length);
+ private static final int MASK = ALPHABET.length - 1;
+
+ public static String encode(byte[] data) {
+ if (data.length == 0) {
+ return "";
+ }
+
+ StringBuilder result = new StringBuilder();
+
+ int buffer = data[0];
+ int index = 1;
+ int bitsLeft = 8;
+ while (bitsLeft > 0 || index < data.length) {
+ if (bitsLeft < SHIFT) {
+ if (index < data.length) {
+ buffer <<= 8;
+ buffer |= (data[index++] & 0xff);
+ bitsLeft += 8;
+ } else {
+ int pad = SHIFT - bitsLeft;
+ buffer <<= pad;
+ bitsLeft += pad;
+ }
+ }
+ bitsLeft -= SHIFT;
+ result.append(ALPHABET[MASK & (buffer >> bitsLeft)]);
+ }
+ return result.toString();
+ }
+
+ private ZBase32() {
+ }
+
+}
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/ZBase32Test.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/ZBase32Test.java
new file mode 100644
index 000000000..824e098a9
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/ZBase32Test.java
@@ -0,0 +1,14 @@
+package org.sufficientlysecure.keychain.util;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class ZBase32Test {
+
+ @Test
+ public void encode() {
+ assertEquals("yyyoryar", ZBase32.encode(new byte[]{0, 1, 2, 3, 4}));
+ }
+
+}