From fa3b32eddc41d05cd49521c8f47951b678111da4 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Tue, 8 May 2018 16:08:34 +0200 Subject: [PATCH] Add ability to search for keys using WKD protocol If a search pattern that looks like an email address is found an additional query using Web Key Directory will be performed. Implements basic flow described in "Key Discovery" [0] I-D. Querying SRV records is not supported. Fixes partially #2270. [0]: https://tools.ietf.org/html/draft-koch-openpgp-webkey-service-05#section-3.1 --- .../keychain/keyimport/CloudSearch.java | 3 + .../keyimport/WebKeyDirectoryClient.java | 160 ++++++++++++++++++ .../processing/ImportKeysListCloudLoader.java | 6 +- .../keychain/ui/ImportKeysActivity.java | 4 +- .../keychain/util/Preferences.java | 8 +- .../keychain/util/ZBase32.java | 45 +++++ .../keychain/util/ZBase32Test.java | 14 ++ 7 files changed, 235 insertions(+), 5 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/WebKeyDirectoryClient.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ZBase32.java create mode 100644 OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/ZBase32Test.java 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})); + } + +}