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()); + } + +}