Merge pull request #2329 from wiktor-k/wkd-url

Extend WKD support to include WKD URLs
This commit is contained in:
Vincent Breitmoser 2018-05-23 15:30:46 +02:00 committed by GitHub
commit 1e05b7999e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 218 additions and 108 deletions

View file

@ -719,6 +719,42 @@
<data android:pathPattern="/..*/publickey/download" />
</intent-filter>
<!-- VIEW from Web Key Directory urls opened in a browser/Chrome -->
<intent-filter android:label="@string/intent_import_key">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="https" />
<!-- if we don't specify a host, pathPattern will be ignored-->
<data android:host="*" />
<data android:pathPattern="/.well-known/openpgpkey/hu/.*" />
<!-- Chrome/Built-in browser will not be triggered if there is mimeType -->
</intent-filter>
<!-- VIEW from Web Key Directory urls opened in Firefox for Android -->
<intent-filter android:label="@string/intent_import_key">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="https" />
<!-- if we don't specify a host, pathPattern will be ignored-->
<data android:host="*" />
<data android:pathPattern="/.well-known/openpgpkey/hu/.*" />
<!-- Firefox for Android requires mimeType -->
<data android:mimeType="application/octet-stream" />
<data android:mimeType="application/pgp-keys" />
</intent-filter>
<!-- IMPORT_KEY with files TODO: does this work? -->
<intent-filter android:label="@string/intent_import_key">
<action android:name="org.sufficientlysecure.keychain.action.IMPORT_KEY" />

View file

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

View file

@ -42,16 +42,16 @@ public class CloudSearch {
// it's a Vector for sync, multiple threads might report problems
final Vector<KeyserverClient.CloudSearchFailureException> 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());
}

View file

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

View file

@ -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) {

View file

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

View file

@ -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<CloudSearchPrefs> CREATOR
= new Parcelable.Creator<CloudSearchPrefs>() {
@Override
public CloudSearchPrefs createFromParcel(Parcel in) {
return new CloudSearchPrefs(in);
}
@Override
public CloudSearchPrefs[] newArray(int size) {
return new CloudSearchPrefs[size];
}
};
}
// sync preferences

View file

@ -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 <a href="https://tools.ietf.org/html/draft-koch-openpgp-webkey-service-05#section-3.1">Key Discovery</a>
*/
@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");
}
}
}

View file

@ -211,6 +211,8 @@
<string name="pref_keybase_summary">"Search keys on keybase.io"</string>
<string name="pref_facebook">"Facebook"</string>
<string name="pref_facebook_summary">"Search keys on Facebook by username"</string>
<string name="pref_wkd">"Web Key Directory"</string>
<string name="pref_wkd_summary">"Search keys using Web Key Directory"</string>
<string name="label_sync_settings_keyserver_title">"Automatic key updates"</string>
<string name="label_sync_settings_keyserver_summary_on">"Every three days, keys are updated from the preferred keyserver"</string>

View file

@ -14,4 +14,9 @@
android:key="search_keybase_pref"
android:summary="@string/pref_keybase_summary"
android:title="@string/pref_keybase" />
<SwitchPreference
android:defaultValue="true"
android:key="search_wkd_pref"
android:summary="@string/pref_wkd_summary"
android:title="@string/pref_wkd" />
</PreferenceScreen>

View file

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