diff --git a/.gitmodules b/.gitmodules index 572293f94..20a0f60e0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,3 +25,6 @@ [submodule "extern/openkeychain-api-lib"] path = extern/openkeychain-api-lib url = https://github.com/open-keychain/openkeychain-api-lib.git +[submodule "extern/dnsjava"] + path = extern/dnsjava + url = https://github.com/open-keychain/dnsjava.git diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle index c8ef7b418..20e5ca594 100644 --- a/OpenKeychain/build.gradle +++ b/OpenKeychain/build.gradle @@ -26,6 +26,7 @@ dependencies { compile project(':extern:spongycastle:pkix') compile project(':extern:spongycastle:prov') compile project(':extern:AppMsg:library') + compile project(':extern:dnsjava') // Dependencies for the `instrumentTest` task, make sure to list all your global dependencies here as well androidTestCompile 'junit:junit:4.10' diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index f4007c098..31c809334 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -53,6 +53,12 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java index 0fd109484..c769da421 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java @@ -46,6 +46,8 @@ public final class Constants { public static final String INTENT_PREFIX = PACKAGE_NAME + ".action."; + public static final String CUSTOM_CONTACT_DATA_MIME_TYPE = "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.key"; + public static final class Path { public static final String APP_DIR = Environment.getExternalStorageDirectory() + "/OpenKeychain"; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java index f911318a0..5d6a62f9c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java @@ -17,6 +17,8 @@ package org.sufficientlysecure.keychain; +import android.accounts.Account; +import android.accounts.AccountManager; import android.app.Application; import android.content.Context; import android.graphics.PorterDuff; @@ -24,6 +26,7 @@ import android.graphics.drawable.Drawable; import android.os.Environment; import org.spongycastle.jce.provider.BouncyCastleProvider; +import org.sufficientlysecure.keychain.helper.ContactHelper; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.PRNGFixes; @@ -76,6 +79,17 @@ public class KeychainApplication extends Application { brandGlowEffect(getApplicationContext(), getApplicationContext().getResources().getColor(R.color.emphasis)); + + setupAccountAsNeeded(this); + } + + public static void setupAccountAsNeeded(Context context) { + AccountManager manager = AccountManager.get(context); + Account[] accounts = manager.getAccountsByType(Constants.PACKAGE_NAME); + if (accounts == null || accounts.length == 0) { + Account dummy = new Account(context.getString(R.string.app_name), Constants.PACKAGE_NAME); + manager.addAccountExplicitly(dummy, null, null); + } } static void brandGlowEffect(Context context, int brandColor) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/ContactHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/ContactHelper.java index a92ea5408..f50ccf6f8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/ContactHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/ContactHelper.java @@ -19,8 +19,18 @@ package org.sufficientlysecure.keychain.helper; import android.accounts.Account; import android.accounts.AccountManager; -import android.content.Context; +import android.content.*; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; +import android.provider.ContactsContract; import android.util.Patterns; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.KeyRing; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.util.Log; import java.util.ArrayList; import java.util.HashSet; @@ -29,6 +39,15 @@ import java.util.Set; public class ContactHelper { + public static final String[] KEYS_TO_CONTACT_PROJECTION = new String[]{ + KeychainContract.KeyRings.USER_ID, + KeychainContract.KeyRings.FINGERPRINT, + KeychainContract.KeyRings.KEY_ID, + KeychainContract.KeyRings.MASTER_KEY_ID}; + public static final String[] RAW_CONTACT_ID_PROJECTION = new String[]{ContactsContract.RawContacts._ID}; + public static final String FIND_RAW_CONTACT_SELECTION = + ContactsContract.RawContacts.ACCOUNT_TYPE + "=? AND " + ContactsContract.RawContacts.SOURCE_ID + "=?"; + public static final List getMailAccounts(Context context) { final Account[] accounts = AccountManager.get(context).getAccounts(); final Set emailSet = new HashSet(); @@ -39,4 +58,92 @@ public class ContactHelper { } return new ArrayList(emailSet); } + + public static List getContactMails(Context context) { + ContentResolver resolver = context.getContentResolver(); + Cursor mailCursor = resolver.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI, + new String[]{ContactsContract.CommonDataKinds.Email.DATA}, + null, null, null); + if (mailCursor == null) return null; + + Set mails = new HashSet(); + while (mailCursor.moveToNext()) { + String email = mailCursor.getString(0); + if (email != null) { + mails.add(email); + } + } + mailCursor.close(); + return new ArrayList(mails); + } + + public static Uri dataUriFromContactUri(Context context, Uri contactUri) { + Cursor contactMasterKey = context.getContentResolver().query(contactUri, new String[]{ContactsContract.Data.DATA2}, null, null, null, null); + if (contactMasterKey != null) { + if (contactMasterKey.moveToNext()) { + return KeychainContract.KeyRings.buildGenericKeyRingUri(contactMasterKey.getLong(0)); + } + contactMasterKey.close(); + } + return null; + } + + public static void writeKeysToContacts(Context context) { + ContentResolver resolver = context.getContentResolver(); + Cursor cursor = resolver.query(KeychainContract.KeyRings.buildUnifiedKeyRingsUri(), KEYS_TO_CONTACT_PROJECTION, + null, null, null); + if (cursor != null) { + while (cursor.moveToNext()) { + String[] userId = KeyRing.splitUserId(cursor.getString(0)); + String fingerprint = PgpKeyHelper.convertFingerprintToHex(cursor.getBlob(1)); + String keyIdShort = PgpKeyHelper.convertKeyIdToHexShort(cursor.getLong(2)); + long masterKeyId = cursor.getLong(3); + int rawContactId = -1; + Cursor raw = resolver.query(ContactsContract.RawContacts.CONTENT_URI, RAW_CONTACT_ID_PROJECTION, + FIND_RAW_CONTACT_SELECTION, new String[]{Constants.PACKAGE_NAME, fingerprint}, null, null); + if (raw != null) { + if (raw.moveToNext()) { + rawContactId = raw.getInt(0); + } + raw.close(); + } + ArrayList ops = new ArrayList(); + if (rawContactId == -1) { + ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) + .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, context.getString(R.string.app_name)) + .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, Constants.PACKAGE_NAME) + .withValue(ContactsContract.RawContacts.SOURCE_ID, fingerprint) + .build()); + if (userId[0] != null) { + ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, userId[0]) + .build()); + } + if (userId[1] != null) { + ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.Email.DATA, userId[1]) + .build()); + } + ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, Constants.CUSTOM_CONTACT_DATA_MIME_TYPE) + .withValue(ContactsContract.Data.DATA1, String.format(context.getString(R.string.contact_show_key), keyIdShort)) + .withValue(ContactsContract.Data.DATA2, masterKeyId) + .build()); + } + try { + resolver.applyBatch(ContactsContract.AUTHORITY, ops); + } catch (RemoteException e) { + e.printStackTrace(); + } catch (OperationApplicationException e) { + e.printStackTrace(); + } + } + cursor.close(); + } + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/EmailKeyHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/EmailKeyHelper.java new file mode 100644 index 000000000..80f52f914 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/EmailKeyHelper.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * 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.helper; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Messenger; +import org.sufficientlysecure.keychain.keyimport.HkpKeyserver; +import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; +import org.sufficientlysecure.keychain.keyimport.Keyserver; +import org.sufficientlysecure.keychain.service.KeychainIntentService; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class EmailKeyHelper { + + public static void importContacts(Context context, Messenger messenger) { + importAll(context, messenger, ContactHelper.getContactMails(context)); + } + + public static void importAll(Context context, Messenger messenger, List mails) { + Set keys = new HashSet(); + for (String mail : mails) { + keys.addAll(getEmailKeys(context, mail)); + } + importKeys(context, messenger, new ArrayList(keys)); + } + + public static List getEmailKeys(Context context, String mail) { + Set keys = new HashSet(); + + // Try _hkp._tcp SRV record first + String[] mailparts = mail.split("@"); + if (mailparts.length == 2) { + HkpKeyserver hkp = HkpKeyserver.resolve(mailparts[1]); + if (hkp != null) { + keys.addAll(getEmailKeys(mail, hkp)); + } + } + + // Most users don't have the SRV record, so ask a default server as well + String[] servers = Preferences.getPreferences(context).getKeyServers(); + if (servers != null && servers.length != 0) { + HkpKeyserver hkp = new HkpKeyserver(servers[0]); + keys.addAll(getEmailKeys(mail, hkp)); + } + return new ArrayList(keys); + } + + private static void importKeys(Context context, Messenger messenger, List keys) { + Intent importIntent = new Intent(context, KeychainIntentService.class); + importIntent.setAction(KeychainIntentService.ACTION_DOWNLOAD_AND_IMPORT_KEYS); + Bundle importData = new Bundle(); + importData.putParcelableArrayList(KeychainIntentService.DOWNLOAD_KEY_LIST, + new ArrayList(keys)); + importIntent.putExtra(KeychainIntentService.EXTRA_DATA, importData); + importIntent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + + context.startService(importIntent); + } + + public static List getEmailKeys(String mail, Keyserver keyServer) { + Set keys = new HashSet(); + try { + for (ImportKeysListEntry key : keyServer.search(mail)) { + if (key.isRevoked() || key.isExpired()) continue; + for (String userId : key.getUserIds()) { + if (userId.toLowerCase().contains(mail.toLowerCase())) { + keys.add(key); + } + } + } + } catch (Keyserver.QueryFailedException ignored) { + } catch (Keyserver.QueryNeedsRepairException ignored) { + } + return new ArrayList(keys); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java index 5969455bd..2ec9e1c07 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java @@ -33,6 +33,10 @@ import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.pgp.PgpHelper; import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; import org.sufficientlysecure.keychain.util.Log; +import org.xbill.DNS.Lookup; +import org.xbill.DNS.Record; +import org.xbill.DNS.SRVRecord; +import org.xbill.DNS.Type; import java.io.IOException; import java.io.InputStream; @@ -45,6 +49,8 @@ import java.net.URLDecoder; import java.net.URLEncoder; import java.net.UnknownHostException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; @@ -75,6 +81,7 @@ public class HkpKeyserver extends Keyserver { private String mHost; private short mPort; + private boolean mSecure; /** * pub:%keyid%:%algo%:%keylen%:%creationdate%:%expirationdate%:%flags% @@ -109,7 +116,7 @@ public class HkpKeyserver extends Keyserver { */ public static final Pattern PUB_KEY_LINE = Pattern .compile("pub:([0-9a-fA-F]+):([0-9]+):([0-9]+):([0-9]+):([0-9]*):([rde]*)[ \n\r]*" // pub line - + "(uid:(.*):([0-9]+):([0-9]*):([rde]*))+", // one or more uid lines + + "((uid:([^:]*):([0-9]+):([0-9]*):([rde]*)[ \n\r]*)+)", // one or more uid lines Pattern.CASE_INSENSITIVE); /** @@ -137,10 +144,11 @@ public class HkpKeyserver extends Keyserver { * */ public static final Pattern UID_LINE = Pattern - .compile("uid:(.*):([0-9]+):([0-9]*):([rde]*)", + .compile("uid:([^:]*):([0-9]+):([0-9]*):([rde]*)", Pattern.CASE_INSENSITIVE); private static final short PORT_DEFAULT = 11371; + private static final short PORT_DEFAULT_HKPS = 443; /** * @param hostAndPort may be just @@ -151,31 +159,68 @@ public class HkpKeyserver extends Keyserver { public HkpKeyserver(String hostAndPort) { String host = hostAndPort; short port = PORT_DEFAULT; - final int colonPosition = hostAndPort.lastIndexOf(':'); - if (colonPosition > 0) { - host = hostAndPort.substring(0, colonPosition); - final String portStr = hostAndPort.substring(colonPosition + 1); - port = Short.decode(portStr); + boolean secure = false; + String[] parts = hostAndPort.split(":"); + if (parts.length > 1) { + if (!parts[0].contains(".")) { // This is not a domain or ip, so it must be a protocol name + if (parts[0].equalsIgnoreCase("hkps") || parts[0].equalsIgnoreCase("https")) { + secure = true; + port = PORT_DEFAULT_HKPS; + } else if (!parts[0].equalsIgnoreCase("hkp") && !parts[0].equalsIgnoreCase("http")) { + throw new IllegalArgumentException("Protocol " + parts[0] + " is unknown"); + } + host = parts[1]; + if (host.startsWith("//")) { // People tend to type https:// and hkps://, so we'll support that as well + host = host.substring(2); + } + if (parts.length > 2) { + port = Short.decode(parts[2]); + } + } else { + host = parts[0]; + port = Short.decode(parts[1]); + } } mHost = host; mPort = port; + mSecure = secure; } public HkpKeyserver(String host, short port) { + this(host, port, false); + } + + public HkpKeyserver(String host, short port, boolean secure) { mHost = host; mPort = port; + mSecure = secure; + } + + private String getUrlPrefix() { + return mSecure ? "https://" : "http://"; } private String query(String request) throws QueryFailedException, HttpError { - InetAddress ips[]; - try { - ips = InetAddress.getAllByName(mHost); - } catch (UnknownHostException e) { - throw new QueryFailedException(e.toString()); - } - for (int i = 0; i < ips.length; ++i) { + List urls = new ArrayList(); + if (mSecure) { + urls.add(getUrlPrefix() + mHost + ":" + mPort + request); + } else { + InetAddress ips[]; + try { + ips = InetAddress.getAllByName(mHost); + } catch (UnknownHostException e) { + throw new QueryFailedException(e.toString()); + } + for (InetAddress ip : ips) { + // Note: This is actually not HTTP 1.1 compliant, as we hide the real "Host" value, + // but Android's HTTPUrlConnection does not support any other way to set + // Socket's remote IP address... + urls.add(getUrlPrefix() + ip.getHostAddress() + ":" + mPort + request); + } + } + + for (String url : urls) { try { - String url = "http://" + ips[i].getHostAddress() + ":" + mPort + request; Log.d(Constants.TAG, "hkp keyserver query: " + url); URL realUrl = new URL(url); HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection(); @@ -238,6 +283,7 @@ public class HkpKeyserver extends Keyserver { while (matcher.find()) { final ImportKeysListEntry entry = new ImportKeysListEntry(); entry.setQuery(query); + entry.setOrigin(getUrlPrefix() + mHost + ":" + mPort); entry.setBitStrength(Integer.parseInt(matcher.group(3))); @@ -262,6 +308,7 @@ public class HkpKeyserver extends Keyserver { entry.setDate(tmpGreg.getTime()); entry.setRevoked(matcher.group(6).contains("r")); + entry.setExpired(matcher.group(6).contains("e")); ArrayList userIds = new ArrayList(); final String uidLines = matcher.group(7); @@ -290,7 +337,7 @@ public class HkpKeyserver extends Keyserver { public String get(String keyIdHex) throws QueryFailedException { HttpClient client = new DefaultHttpClient(); try { - String query = "http://" + mHost + ":" + mPort + + String query = getUrlPrefix() + mHost + ":" + mPort + "/pks/lookup?op=get&options=mr&search=" + keyIdHex; Log.d(Constants.TAG, "hkp keyserver get: " + query); HttpGet get = new HttpGet(query); @@ -319,7 +366,7 @@ public class HkpKeyserver extends Keyserver { public void add(String armoredKey) throws AddKeyException { HttpClient client = new DefaultHttpClient(); try { - String query = "http://" + mHost + ":" + mPort + "/pks/add"; + String query = getUrlPrefix() + mHost + ":" + mPort + "/pks/add"; HttpPost post = new HttpPost(query); Log.d(Constants.TAG, "hkp keyserver add: " + query); List nameValuePairs = new ArrayList(2); @@ -336,4 +383,36 @@ public class HkpKeyserver extends Keyserver { client.getConnectionManager().shutdown(); } } + + @Override + public String toString() { + return mHost + ":" + mPort; + } + + /** + * Tries to find a server responsible for a given domain + * + * @return A responsible Keyserver or null if not found. + */ + public static HkpKeyserver resolve(String domain) { + try { + Record[] records = new Lookup("_hkp._tcp." + domain, Type.SRV).run(); + if (records.length > 0) { + Arrays.sort(records, new Comparator() { + @Override + public int compare(Record lhs, Record rhs) { + if (!(lhs instanceof SRVRecord)) return 1; + if (!(rhs instanceof SRVRecord)) return -1; + return ((SRVRecord) lhs).getPriority() - ((SRVRecord) rhs).getPriority(); + } + }); + Record record = records[0]; // This is our best choice + if (record instanceof SRVRecord) { + return new HkpKeyserver(((SRVRecord) record).getTarget().toString(), (short) ((SRVRecord) record).getPort()); + } + } + } catch (Exception ignored) { + } + return null; + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysListEntry.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysListEntry.java index c64794bb6..7a7c89301 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysListEntry.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysListEntry.java @@ -36,6 +36,7 @@ public class ImportKeysListEntry implements Serializable, Parcelable { private long mKeyId; private String mKeyIdHex; private boolean mRevoked; + private boolean mExpired; private Date mDate; // TODO: not displayed private String mFingerprintHex; private int mBitStrength; @@ -44,6 +45,7 @@ public class ImportKeysListEntry implements Serializable, Parcelable { private String mPrimaryUserId; private String mExtraData; private String mQuery; + private String mOrigin; private boolean mSelected; @@ -57,6 +59,7 @@ public class ImportKeysListEntry implements Serializable, Parcelable { dest.writeStringList(mUserIds); dest.writeLong(mKeyId); dest.writeByte((byte) (mRevoked ? 1 : 0)); + dest.writeByte((byte) (mExpired ? 1 : 0)); dest.writeSerializable(mDate); dest.writeString(mFingerprintHex); dest.writeString(mKeyIdHex); @@ -65,6 +68,7 @@ public class ImportKeysListEntry implements Serializable, Parcelable { dest.writeByte((byte) (mSecretKey ? 1 : 0)); dest.writeByte((byte) (mSelected ? 1 : 0)); dest.writeString(mExtraData); + dest.writeString(mOrigin); } public static final Creator CREATOR = new Creator() { @@ -75,6 +79,7 @@ public class ImportKeysListEntry implements Serializable, Parcelable { source.readStringList(vr.mUserIds); vr.mKeyId = source.readLong(); vr.mRevoked = source.readByte() == 1; + vr.mExpired = source.readByte() == 1; vr.mDate = (Date) source.readSerializable(); vr.mFingerprintHex = source.readString(); vr.mKeyIdHex = source.readString(); @@ -83,6 +88,7 @@ public class ImportKeysListEntry implements Serializable, Parcelable { vr.mSecretKey = source.readByte() == 1; vr.mSelected = source.readByte() == 1; vr.mExtraData = source.readString(); + vr.mOrigin = source.readString(); return vr; } @@ -104,6 +110,14 @@ public class ImportKeysListEntry implements Serializable, Parcelable { this.mSelected = selected; } + public boolean isExpired() { + return mExpired; + } + + public void setExpired(boolean expired) { + this.mExpired = expired; + } + public long getKeyId() { return mKeyId; } @@ -196,6 +210,14 @@ public class ImportKeysListEntry implements Serializable, Parcelable { mQuery = query; } + public String getOrigin() { + return mOrigin; + } + + public void setOrigin(String origin) { + mOrigin = origin; + } + /** * Constructor for later querying from keyserver */ diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/KeybaseKeyserver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/KeybaseKeyserver.java index 145738e66..43557279f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/KeybaseKeyserver.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/KeybaseKeyserver.java @@ -31,6 +31,7 @@ import java.net.URLEncoder; import java.util.ArrayList; public class KeybaseKeyserver extends Keyserver { + public static final String ORIGIN = "keybase:keybase.io"; private String mQuery; @Override @@ -87,6 +88,7 @@ public class KeybaseKeyserver extends Keyserver { final ImportKeysListEntry entry = new ImportKeysListEntry(); entry.setQuery(mQuery); + entry.setOrigin(ORIGIN); String keybaseId = JWalk.getString(match, "components", "username", "val"); String fullName = JWalk.getString(match, "components", "full_name", "val"); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/Keyserver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/Keyserver.java index 868f543f0..842e7d922 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/Keyserver.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/Keyserver.java @@ -48,12 +48,12 @@ public abstract class Keyserver { private static final long serialVersionUID = -507574859137295530L; } - abstract List search(String query) throws QueryFailedException, + public abstract List search(String query) throws QueryFailedException, QueryNeedsRepairException; - abstract String get(String keyIdHex) throws QueryFailedException; + public abstract String get(String keyIdHex) throws QueryFailedException; - abstract void add(String armoredKey) throws AddKeyException; + public abstract void add(String armoredKey) throws AddKeyException; public static String readAll(InputStream in, String encoding) throws IOException { ByteArrayOutputStream raw = new ByteArrayOutputStream(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java new file mode 100644 index 000000000..8db9294df --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * 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.service; + +import android.accounts.Account; +import android.app.Service; +import android.content.AbstractThreadedSyncAdapter; +import android.content.ContentProviderClient; +import android.content.Intent; +import android.content.SyncResult; +import android.os.*; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.KeychainApplication; +import org.sufficientlysecure.keychain.helper.ContactHelper; +import org.sufficientlysecure.keychain.helper.EmailKeyHelper; +import org.sufficientlysecure.keychain.util.Log; + +public class ContactSyncAdapterService extends Service { + + private class ContactSyncAdapter extends AbstractThreadedSyncAdapter { + + public ContactSyncAdapter() { + super(ContactSyncAdapterService.this, true); + } + + @Override + public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, + final SyncResult syncResult) { + EmailKeyHelper.importContacts(getContext(), new Messenger(new Handler(Looper.getMainLooper(), + new Handler.Callback() { + @Override + public boolean handleMessage(Message msg) { + Bundle data = msg.getData(); + switch (msg.arg1) { + case KeychainIntentServiceHandler.MESSAGE_OKAY: + return true; + case KeychainIntentServiceHandler.MESSAGE_UPDATE_PROGRESS: + if (data.containsKey(KeychainIntentServiceHandler.DATA_PROGRESS) && + data.containsKey(KeychainIntentServiceHandler.DATA_PROGRESS_MAX)) { + Log.d(Constants.TAG, "Progress: " + + data.getInt(KeychainIntentServiceHandler.DATA_PROGRESS) + "/" + + data.getInt(KeychainIntentServiceHandler.DATA_PROGRESS_MAX)); + return false; + } + default: + Log.d(Constants.TAG, "Syncing... " + msg.toString()); + return false; + } + } + }))); + KeychainApplication.setupAccountAsNeeded(ContactSyncAdapterService.this); + ContactHelper.writeKeysToContacts(ContactSyncAdapterService.this); + } + } + + @Override + public IBinder onBind(Intent intent) { + return new ContactSyncAdapter().getSyncAdapterBinder(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/DummyAccountService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/DummyAccountService.java new file mode 100644 index 000000000..008502ce7 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/DummyAccountService.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * 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.service; + +import android.accounts.AbstractAccountAuthenticator; +import android.accounts.Account; +import android.accounts.AccountAuthenticatorResponse; +import android.accounts.NetworkErrorException; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.widget.Toast; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.util.Log; + +/** + * This service actually does nothing, it's sole task is to show a Toast if the use tries to create an account. + */ +public class DummyAccountService extends Service { + + private class Toaster { + private static final String TOAST_MESSAGE = "toast_message"; + private Context context; + private Handler handler = new Handler(new Handler.Callback() { + @Override + public boolean handleMessage(Message msg) { + Toast.makeText(context, msg.getData().getString(TOAST_MESSAGE), Toast.LENGTH_LONG).show(); + return true; + } + }); + + private Toaster(Context context) { + this.context = context; + } + + public void toast(int resourceId) { + toast(context.getString(resourceId)); + } + + public void toast(String message) { + Message msg = new Message(); + Bundle bundle = new Bundle(); + bundle.putString(TOAST_MESSAGE, message); + msg.setData(bundle); + handler.sendMessage(msg); + } + } + + private class Authenticator extends AbstractAccountAuthenticator { + + public Authenticator() { + super(DummyAccountService.this); + } + + @Override + public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { + Log.d(Constants.TAG, "DummyAccountService.editProperties"); + return null; + } + + @Override + public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, + String[] requiredFeatures, Bundle options) throws NetworkErrorException { + response.onResult(new Bundle()); + toaster.toast(R.string.info_no_manual_account_creation); + Log.d(Constants.TAG, "DummyAccountService.addAccount"); + return null; + } + + @Override + public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) + throws NetworkErrorException { + Log.d(Constants.TAG, "DummyAccountService.confirmCredentials"); + return null; + } + + @Override + public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, + Bundle options) throws NetworkErrorException { + Log.d(Constants.TAG, "DummyAccountService.getAuthToken"); + return null; + } + + @Override + public String getAuthTokenLabel(String authTokenType) { + Log.d(Constants.TAG, "DummyAccountService.getAuthTokenLabel"); + return null; + } + + @Override + public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, + Bundle options) throws NetworkErrorException { + Log.d(Constants.TAG, "DummyAccountService.updateCredentials"); + return null; + } + + @Override + public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) + throws NetworkErrorException { + Log.d(Constants.TAG, "DummyAccountService.hasFeatures"); + return null; + } + } + + private Toaster toaster; + + @Override + public IBinder onBind(Intent intent) { + toaster = new Toaster(this); + return new Authenticator().getIBinder(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java index 498b963f2..27f41e3d2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java @@ -32,6 +32,7 @@ import org.sufficientlysecure.keychain.helper.FileHelper; import org.sufficientlysecure.keychain.helper.OtherHelper; import org.sufficientlysecure.keychain.helper.Preferences; import org.sufficientlysecure.keychain.keyimport.HkpKeyserver; +import org.sufficientlysecure.keychain.keyimport.Keyserver; import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; import org.sufficientlysecure.keychain.keyimport.KeybaseKeyserver; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; @@ -734,49 +735,30 @@ public class KeychainIntentService extends IntentService } catch (Exception e) { sendErrorToHandler(e); } - } else if (ACTION_IMPORT_KEYBASE_KEYS.equals(action)) { - ArrayList entries = data.getParcelableArrayList(DOWNLOAD_KEY_LIST); - - try { - KeybaseKeyserver server = new KeybaseKeyserver(); - ArrayList keyRings = new ArrayList(entries.size()); - for (ImportKeysListEntry entry : entries) { - // the keybase handle is in userId(1) - String keybaseId = entry.getExtraData(); - byte[] downloadedKeyBytes = server.get(keybaseId).getBytes(); - - // save key bytes in entry object for doing the - // actual import afterwards - keyRings.add(new ParcelableKeyRing(downloadedKeyBytes)); - } - - Intent importIntent = new Intent(this, KeychainIntentService.class); - importIntent.setAction(ACTION_IMPORT_KEYRING); - Bundle importData = new Bundle(); - importData.putParcelableArrayList(IMPORT_KEY_LIST, keyRings); - importIntent.putExtra(EXTRA_DATA, importData); - importIntent.putExtra(EXTRA_MESSENGER, mMessenger); - - // now import it with this service - onHandleIntent(importIntent); - - // result is handled in ACTION_IMPORT_KEYRING - } catch (Exception e) { - sendErrorToHandler(e); - } - } else if (ACTION_DOWNLOAD_AND_IMPORT_KEYS.equals(action)) { + } else if (ACTION_DOWNLOAD_AND_IMPORT_KEYS.equals(action) || ACTION_IMPORT_KEYBASE_KEYS.equals(action)) { try { ArrayList entries = data.getParcelableArrayList(DOWNLOAD_KEY_LIST); // this downloads the keys and places them into the ImportKeysListEntry entries String keyServer = data.getString(DOWNLOAD_KEY_SERVER); - HkpKeyserver server = new HkpKeyserver(keyServer); ArrayList keyRings = new ArrayList(entries.size()); for (ImportKeysListEntry entry : entries) { + + Keyserver server; + if (entry.getOrigin() == null) { + server = new HkpKeyserver(keyServer); + } else if (KeybaseKeyserver.ORIGIN.equals(entry.getOrigin())) { + server = new KeybaseKeyserver(); + } else { + server = new HkpKeyserver(entry.getOrigin()); + } + // if available use complete fingerprint for get request byte[] downloadedKeyBytes; - if (entry.getFingerprintHex() != null) { + if (KeybaseKeyserver.ORIGIN.equals(entry.getOrigin())) { + downloadedKeyBytes = server.get(entry.getExtraData()).getBytes(); + } else if (entry.getFingerprintHex() != null) { downloadedKeyBytes = server.get("0x" + entry.getFingerprintHex()).getBytes(); } else { downloadedKeyBytes = server.get(entry.getKeyIdHex()).getBytes(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java index 9cc0f4667..5eb8ecb8d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java @@ -252,7 +252,7 @@ public class KeyListFragment extends LoaderFragment static final int INDEX_HAS_ANY_SECRET = 6; static final String ORDER = - KeyRings.HAS_ANY_SECRET + " DESC, " + KeyRings.USER_ID + " ASC"; + KeyRings.HAS_ANY_SECRET + " DESC, UPPER(" + KeyRings.USER_ID + ") ASC"; @Override @@ -592,7 +592,7 @@ public class KeyListFragment extends LoaderFragment String userId = mCursor.getString(KeyListFragment.INDEX_USER_ID); String headerText = convertView.getResources().getString(R.string.user_id_no_name); if (userId != null && userId.length() > 0) { - headerText = "" + userId.subSequence(0, 1).charAt(0); + headerText = "" + userId.charAt(0); } holder.mText.setText(headerText); holder.mCount.setVisibility(View.GONE); @@ -621,7 +621,7 @@ public class KeyListFragment extends LoaderFragment // otherwise, return the first character of the name as ID String userId = mCursor.getString(KeyListFragment.INDEX_USER_ID); if (userId != null && userId.length() > 0) { - return userId.charAt(0); + return Character.toUpperCase(userId.charAt(0)); } else { return Long.MAX_VALUE; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java index 7a0e83d9b..463c800d2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -20,6 +20,7 @@ package org.sufficientlysecure.keychain.ui; import android.annotation.TargetApi; import android.app.Activity; +import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; @@ -32,6 +33,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.provider.ContactsContract; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; @@ -47,6 +49,7 @@ import com.devspark.appmsg.AppMsg; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.ContactHelper; import org.sufficientlysecure.keychain.helper.ExportHelper; import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; @@ -124,7 +127,7 @@ public class ViewKeyActivity extends ActionBarActivity implements switchToTab = intent.getExtras().getInt(EXTRA_SELECTED_TAB); } - Uri dataUri = getIntent().getData(); + Uri dataUri = getDataUri(); if (dataUri == null) { Log.e(Constants.TAG, "Data missing. Should be Uri of key!"); finish(); @@ -214,6 +217,14 @@ public class ViewKeyActivity extends ActionBarActivity implements mSlidingTabLayout.setViewPager(mViewPager); } + private Uri getDataUri() { + Uri dataUri = getIntent().getData(); + if (dataUri != null && dataUri.getHost().equals(ContactsContract.AUTHORITY)) { + dataUri = ContactHelper.dataUriFromContactUri(this, dataUri); + } + return dataUri; + } + private void loadData(Uri dataUri) { mDataUri = dataUri; diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml index 82095a611..ad8abe78f 100644 --- a/OpenKeychain/src/main/res/values/strings.xml +++ b/OpenKeychain/src/main/res/values/strings.xml @@ -520,5 +520,7 @@ cannot sign Encoding error No encryption subkey available! + Do not create OpenKeychain-Accounts manually.\nFor more information, see Help. + Show key (%s) diff --git a/OpenKeychain/src/main/res/xml/account_desc.xml b/OpenKeychain/src/main/res/xml/account_desc.xml new file mode 100644 index 000000000..94ffdf40b --- /dev/null +++ b/OpenKeychain/src/main/res/xml/account_desc.xml @@ -0,0 +1,6 @@ + + + diff --git a/OpenKeychain/src/main/res/xml/custom_pgp_contacts_structure.xml b/OpenKeychain/src/main/res/xml/custom_pgp_contacts_structure.xml new file mode 100644 index 000000000..5f5f2be80 --- /dev/null +++ b/OpenKeychain/src/main/res/xml/custom_pgp_contacts_structure.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/OpenKeychain/src/main/res/xml/sync_adapter_desc.xml b/OpenKeychain/src/main/res/xml/sync_adapter_desc.xml new file mode 100644 index 000000000..d8fe60e91 --- /dev/null +++ b/OpenKeychain/src/main/res/xml/sync_adapter_desc.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/extern/dnsjava b/extern/dnsjava new file mode 160000 index 000000000..71c8a9e56 --- /dev/null +++ b/extern/dnsjava @@ -0,0 +1 @@ +Subproject commit 71c8a9e56b19b34907e7e2e810ca15b57e3edc2b diff --git a/settings.gradle b/settings.gradle index b0162875a..d7ca15f82 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,3 +11,4 @@ include ':extern:spongycastle:pg' include ':extern:spongycastle:pkix' include ':extern:spongycastle:prov' include ':extern:AppMsg:library' +include ':extern:dnsjava'