aboutsummaryrefslogtreecommitdiffstats
path: root/OpenKeychain/src/main/java/org/sufficientlysecure
diff options
context:
space:
mode:
Diffstat (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure')
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/LinkedVerifyResult.java49
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java15
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java1
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedUserAttribute.java14
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedCookieResource.java138
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedIdentity.java75
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedResource.java98
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/RawLinkedIdentity.java45
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/DnsResource.java103
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/GenericHttpsResource.java103
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/TwitterResource.java124
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/UnknownResource.java21
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java5
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java14
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java21
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java23
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java8
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java3
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java13
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java86
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsAdapter.java201
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java13
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep1Fragment.java133
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep2Fragment.java362
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java132
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java368
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java134
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep2Fragment.java152
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep3Fragment.java235
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdSelectFragment.java91
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdViewFragment.java45
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdWizard.java106
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/HttpsPrefixedText.java38
36 files changed, 2920 insertions, 55 deletions
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/LinkedVerifyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/LinkedVerifyResult.java
new file mode 100644
index 000000000..42e9ec3f0
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/LinkedVerifyResult.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.operations.results;
+
+import android.os.Parcel;
+
+public class LinkedVerifyResult extends OperationResult {
+
+ public LinkedVerifyResult(int result, OperationLog log) {
+ super(result, log);
+ }
+
+ /** Construct from a parcel - trivial because we have no extra data. */
+ public LinkedVerifyResult(Parcel source) {
+ super(source);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ }
+
+ public static Creator<LinkedVerifyResult> CREATOR = new Creator<LinkedVerifyResult>() {
+ public LinkedVerifyResult createFromParcel(final Parcel source) {
+ return new LinkedVerifyResult(source);
+ }
+
+ public LinkedVerifyResult[] newArray(final int size) {
+ return new LinkedVerifyResult[size];
+ }
+ };
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java
index 54cd9b1b4..a856a4fc3 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java
@@ -763,6 +763,21 @@ public abstract class OperationResult implements Parcelable {
MSG_DEL_OK (LogLevel.OK, R.plurals.msg_del_ok),
MSG_DEL_FAIL (LogLevel.WARN, R.plurals.msg_del_fail),
+ MSG_LV (LogLevel.START, R.string.msg_lv),
+ MSG_LV_MATCH (LogLevel.DEBUG, R.string.msg_lv_match),
+ MSG_LV_MATCH_ERROR (LogLevel.ERROR, R.string.msg_lv_match_error),
+ MSG_LV_FP_OK (LogLevel.DEBUG, R.string.msg_lv_fp_ok),
+ MSG_LV_FP_ERROR (LogLevel.ERROR, R.string.msg_lv_fp_error),
+ MSG_LV_NONCE_OK (LogLevel.OK, R.string.msg_lv_nonce_ok),
+ MSG_LV_NONCE_ERROR (LogLevel.ERROR, R.string.msg_lv_nonce_error),
+
+ MSG_LV_FETCH (LogLevel.DEBUG, R.string.msg_lv_fetch),
+ MSG_LV_FETCH_REDIR (LogLevel.DEBUG, R.string.msg_lv_fetch_redir),
+ MSG_LV_FETCH_OK (LogLevel.DEBUG, R.string.msg_lv_fetch_ok),
+ MSG_LV_FETCH_ERROR (LogLevel.ERROR, R.string.msg_lv_fetch_error),
+ MSG_LV_FETCH_ERROR_URL (LogLevel.ERROR, R.string.msg_lv_fetch_error_url),
+ MSG_LV_FETCH_ERROR_IO (LogLevel.ERROR, R.string.msg_lv_fetch_error_io),
+
//export log
MSG_EXPORT_LOG(LogLevel.START,R.string.msg_export_log_start),
MSG_EXPORT_LOG_EXPORT_ERROR_NO_FILE(LogLevel.ERROR,R.string.msg_export_log_error_no_file),
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java
index 9276cba10..0173a1d83 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java
@@ -361,4 +361,5 @@ public class UncachedPublicKey {
return calendar.getTime();
}
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedUserAttribute.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedUserAttribute.java
index 8e23d36d9..49e4d9793 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedUserAttribute.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedUserAttribute.java
@@ -1,4 +1,8 @@
/*
+<<<<<<< HEAD
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+=======
+>>>>>>> development
* Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
*
* This program is free software: you can redistribute it and/or modify
@@ -37,6 +41,7 @@ public class WrappedUserAttribute implements Serializable {
public static final int UAT_NONE = 0;
public static final int UAT_IMAGE = UserAttributeSubpacketTags.IMAGE_ATTRIBUTE;
+ public static final int UAT_LINKED_ID = 100;
private PGPUserAttributeSubpacketVector mVector;
@@ -109,6 +114,15 @@ public class WrappedUserAttribute implements Serializable {
}
+ public byte[][] getSubpackets() {
+ UserAttributeSubpacket[] subpackets = mVector.toSubpacketArray();
+ byte[][] ret = new byte[subpackets.length][];
+ for (int i = 0; i < subpackets.length; i++) {
+ ret[i] = subpackets[i].getData();
+ }
+ return ret;
+ }
+
private void readObjectNoData() throws ObjectStreamException {
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedCookieResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedCookieResource.java
new file mode 100644
index 000000000..b21f482b7
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedCookieResource.java
@@ -0,0 +1,138 @@
+package org.sufficientlysecure.keychain.pgp.linked;
+
+import android.content.Context;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.pgp.linked.resources.GenericHttpsResource;
+import org.sufficientlysecure.keychain.pgp.linked.resources.UnknownResource;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public abstract class LinkedCookieResource extends LinkedResource {
+
+ protected LinkedCookieResource(Set<String> flags, HashMap<String, String> params, URI uri) {
+ super(flags, params, uri);
+ }
+
+ public URI toUri () {
+
+ StringBuilder b = new StringBuilder();
+ b.append("pgpid+cookie:");
+
+ // add flags
+ if (mFlags != null) {
+ boolean first = true;
+ for (String flag : mFlags) {
+ if (!first) {
+ b.append(";");
+ }
+ first = false;
+ b.append(flag);
+ }
+ }
+
+ // add parameters
+ if (mParams != null) {
+ boolean first = true;
+ for (Entry<String, String> stringStringEntry : mParams.entrySet()) {
+ if (!first) {
+ b.append(";");
+ }
+ first = false;
+ b.append(stringStringEntry.getKey()).append("=").append(stringStringEntry.getValue());
+ }
+ }
+
+ b.append("@");
+ b.append(mSubUri);
+
+ return URI.create(b.toString());
+
+ }
+
+ public URI getSubUri () {
+ return mSubUri;
+ }
+
+ public static String generate (Context context, byte[] fingerprint, int nonce) {
+ return String.format("\"[Verifying my PGP key: openpgp4fpr:%s#%08x]\"",
+ KeyFormattingUtils.convertFingerprintToHex(fingerprint), nonce);
+ }
+
+ public static String generatePreview () {
+ return "[Verifying my PGP key: openpgp4fpr:0x…]";
+ }
+
+ public LinkedVerifyResult verify(byte[] fingerprint, int nonce) {
+
+ OperationLog log = new OperationLog();
+ log.add(LogType.MSG_LV, 0);
+
+ // Try to fetch resource. Logs for itself
+ String res = fetchResource(log, 1);
+ if (res == null) {
+ // if this is null, an error was recorded in fetchResource above
+ return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
+ }
+
+ Log.d(Constants.TAG, "Resource data: '" + res + "'");
+
+ return verifyString(log, 1, res, nonce, fingerprint);
+
+ }
+
+ protected abstract String fetchResource (OperationLog log, int indent);
+
+ protected Matcher matchResource (OperationLog log, int indent, String res) {
+ return magicPattern.matcher(res);
+ }
+
+ protected LinkedVerifyResult verifyString (OperationLog log, int indent,
+ String res,
+ int nonce, byte[] fingerprint) {
+
+ log.add(LogType.MSG_LV_MATCH, indent);
+ Matcher match = matchResource(log, indent+1, res);
+ if (!match.find()) {
+ log.add(LogType.MSG_LV_MATCH_ERROR, 2);
+ return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
+ }
+
+ String candidateFp = match.group(1).toLowerCase();
+ try {
+ int nonceCandidate = Integer.parseInt(match.group(2).toLowerCase(), 16);
+
+ if (nonce != nonceCandidate) {
+ log.add(LogType.MSG_LV_NONCE_ERROR, indent);
+ return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
+ }
+ } catch (NumberFormatException e) {
+ log.add(LogType.MSG_LV_NONCE_ERROR, indent);
+ return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
+ }
+
+ String fp = KeyFormattingUtils.convertFingerprintToHex(fingerprint);
+
+ if (!fp.equals(candidateFp)) {
+ log.add(LogType.MSG_LV_FP_ERROR, indent);
+ return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
+ }
+ log.add(LogType.MSG_LV_FP_OK, indent);
+
+ log.add(LogType.MSG_LV_NONCE_OK, indent);
+ return new LinkedVerifyResult(LinkedVerifyResult.RESULT_OK, log);
+
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedIdentity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedIdentity.java
new file mode 100644
index 000000000..f06a23681
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedIdentity.java
@@ -0,0 +1,75 @@
+package org.sufficientlysecure.keychain.pgp.linked;
+
+import org.spongycastle.bcpg.UserAttributeSubpacket;
+import org.spongycastle.util.Strings;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+public class LinkedIdentity extends RawLinkedIdentity {
+
+ public final LinkedResource mResource;
+
+ protected LinkedIdentity(int nonce, URI uri, LinkedResource resource) {
+ super(nonce, uri);
+ if (resource == null) {
+ throw new AssertionError("resource must not be null in a LinkedIdentity!");
+ }
+ mResource = resource;
+ }
+
+ public static RawLinkedIdentity fromAttributeData(byte[] data) throws IOException {
+ WrappedUserAttribute att = WrappedUserAttribute.fromData(data);
+
+ byte[][] subpackets = att.getSubpackets();
+ if (subpackets.length >= 1) {
+ return fromSubpacketData(subpackets[0]);
+ }
+
+ throw new IOException("no subpacket data");
+ }
+
+ /** This method parses a linked id from a UserAttributeSubpacket, or returns null if the
+ * subpacket can not be parsed as a valid linked id.
+ */
+ static RawLinkedIdentity fromAttributeSubpacket(UserAttributeSubpacket subpacket) {
+ if (subpacket.getType() != 100) {
+ return null;
+ }
+
+ byte[] data = subpacket.getData();
+
+ return fromSubpacketData(data);
+
+ }
+
+ static RawLinkedIdentity fromSubpacketData(byte[] data) {
+
+ try {
+ int nonce = ByteBuffer.wrap(data).getInt();
+ String uriStr = Strings.fromUTF8ByteArray(Arrays.copyOfRange(data, 4, data.length));
+ URI uri = URI.create(uriStr);
+
+ LinkedResource res = LinkedResource.fromUri(uri);
+ if (res == null) {
+ return new RawLinkedIdentity(nonce, uri);
+ }
+
+ return new LinkedIdentity(nonce, uri, res);
+
+ } catch (IllegalArgumentException e) {
+ Log.e(Constants.TAG, "error parsing uri in (suspected) linked id packet");
+ return null;
+ }
+ }
+
+ public static RawLinkedIdentity fromResource (LinkedCookieResource res, int nonce) {
+ return new RawLinkedIdentity(nonce, res.toUri());
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedResource.java
new file mode 100644
index 000000000..59ffbfc45
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/LinkedResource.java
@@ -0,0 +1,98 @@
+package org.sufficientlysecure.keychain.pgp.linked;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.pgp.linked.resources.DnsResource;
+import org.sufficientlysecure.keychain.pgp.linked.resources.GenericHttpsResource;
+import org.sufficientlysecure.keychain.pgp.linked.resources.UnknownResource;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+public abstract class LinkedResource {
+
+ protected final URI mSubUri;
+ protected final Set<String> mFlags;
+ protected final HashMap<String,String> mParams;
+
+ static Pattern magicPattern =
+ Pattern.compile("\\[Verifying my PGP key: openpgp4fpr:([a-zA-Z0-9]+)#([a-zA-Z0-9]+)\\]");
+
+ protected LinkedResource(Set<String> flags, HashMap<String, String> params, URI uri) {
+ mFlags = flags;
+ mParams = params;
+ mSubUri = uri;
+ }
+
+ public abstract URI toUri();
+
+ public Set<String> getFlags () {
+ return new HashSet<>(mFlags);
+ }
+
+ public HashMap<String,String> getParams () {
+ return new HashMap<>(mParams);
+ }
+
+ protected static LinkedCookieResource fromUri (URI uri) {
+
+ if (!"pgpid+cookie".equals(uri.getScheme())) {
+ Log.e(Constants.TAG, "unknown uri scheme in (suspected) linked id packet");
+ return null;
+ }
+
+ if (!uri.isOpaque()) {
+ Log.e(Constants.TAG, "non-opaque uri in (suspected) linked id packet");
+ return null;
+ }
+
+ String specific = uri.getSchemeSpecificPart();
+ if (!specific.contains("@")) {
+ Log.e(Constants.TAG, "unknown uri scheme in linked id packet");
+ return null;
+ }
+
+ String[] pieces = specific.split("@", 2);
+ URI subUri = URI.create(pieces[1]);
+
+ Set<String> flags = new HashSet<String>();
+ HashMap<String,String> params = new HashMap<String,String>();
+ if (!pieces[0].isEmpty()) {
+ String[] rawParams = pieces[0].split(";");
+ for (String param : rawParams) {
+ String[] p = param.split("=", 2);
+ if (p.length == 1) {
+ flags.add(param);
+ } else {
+ params.put(p[0], p[1]);
+ }
+ }
+ }
+
+ return findResourceType(flags, params, subUri);
+
+ }
+
+ protected static LinkedCookieResource findResourceType (Set<String> flags,
+ HashMap<String,String> params,
+ URI subUri) {
+
+ LinkedCookieResource res;
+
+ res = GenericHttpsResource.create(flags, params, subUri);
+ if (res != null) {
+ return res;
+ }
+ res = DnsResource.create(flags, params, subUri);
+ if (res != null) {
+ return res;
+ }
+
+ return null;
+
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/RawLinkedIdentity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/RawLinkedIdentity.java
new file mode 100644
index 000000000..bfde3c3b9
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/RawLinkedIdentity.java
@@ -0,0 +1,45 @@
+package org.sufficientlysecure.keychain.pgp.linked;
+
+import org.spongycastle.util.Strings;
+import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+
+/** The RawLinkedIdentity contains raw parsed data from a Linked Identity subpacket. */
+public class RawLinkedIdentity {
+
+ public final int mNonce;
+ public final URI mUri;
+
+ protected RawLinkedIdentity(int nonce, URI uri) {
+ mNonce = nonce;
+ mUri = uri;
+ }
+
+ public byte[] getEncoded() {
+ byte[] uriData = Strings.toUTF8ByteArray(mUri.toASCIIString());
+
+ ByteBuffer buf = ByteBuffer.allocate(4 + uriData.length);
+
+ buf.putInt(mNonce);
+ buf.put(uriData);
+
+ return buf.array();
+ }
+
+ public WrappedUserAttribute toUserAttribute () {
+ return WrappedUserAttribute.fromSubpacket(WrappedUserAttribute.UAT_LINKED_ID, getEncoded());
+ }
+
+ public static int generateNonce() {
+ // TODO make this actually random
+ // byte[] data = new byte[4];
+ // new SecureRandom().nextBytes(data);
+ // return Hex.toHexString(data);
+
+ // debug for now
+ return 1234567;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/DnsResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/DnsResource.java
new file mode 100644
index 000000000..d5b8a0345
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/DnsResource.java
@@ -0,0 +1,103 @@
+package org.sufficientlysecure.keychain.pgp.linked.resources;
+
+import android.content.Context;
+
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedCookieResource;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.measite.minidns.Client;
+import de.measite.minidns.DNSMessage;
+import de.measite.minidns.Question;
+import de.measite.minidns.Record;
+import de.measite.minidns.Record.CLASS;
+import de.measite.minidns.Record.TYPE;
+import de.measite.minidns.record.TXT;
+
+public class DnsResource extends LinkedCookieResource {
+
+ final static Pattern magicPattern =
+ Pattern.compile("pgpid\\+cookie=([a-zA-Z0-9]+)(?:#|;)([a-zA-Z0-9]+)");
+
+ String mFqdn;
+ CLASS mClass;
+ TYPE mType;
+
+ DnsResource(Set<String> flags, HashMap<String, String> params, URI uri,
+ String fqdn, CLASS clazz, TYPE type) {
+ super(flags, params, uri);
+
+ mFqdn = fqdn;
+ mClass = clazz;
+ mType = type;
+ }
+
+ public static String generateText (Context context, byte[] fingerprint, int nonce) {
+
+ return String.format("pgpid+cookie=%s;%08x",
+ KeyFormattingUtils.convertFingerprintToHex(fingerprint), nonce);
+
+ }
+
+ public static DnsResource createNew (String domain) {
+ HashSet<String> flags = new HashSet<String>();
+ HashMap<String,String> params = new HashMap<String,String>();
+ URI uri = URI.create("dns:" + domain);
+ return create(flags, params, uri);
+ }
+
+ public static DnsResource create(Set<String> flags, HashMap<String,String> params, URI uri) {
+ if ( ! ("dns".equals(uri.getScheme())
+ && (flags == null || flags.isEmpty())
+ && (params == null || params.isEmpty()))) {
+ return null;
+ }
+
+ //
+ String spec = uri.getSchemeSpecificPart();
+ // If there are // at the beginning, this includes an authority - we don't support those!
+ if (spec.startsWith("//")) {
+ return null;
+ }
+
+ String[] pieces = spec.split("\\?", 2);
+ // In either case, part before a ? is the fqdn
+ String fqdn = pieces[0];
+ // There may be a query part
+ if (pieces.length > 1) {
+ // TODO parse CLASS and TYPE query paramters
+ }
+
+ CLASS clazz = CLASS.IN;
+ TYPE type = TYPE.TXT;
+
+ return new DnsResource(flags, params, uri, fqdn, clazz, type);
+ }
+
+ public String getFqdn() {
+ return mFqdn;
+ }
+
+ @Override
+ protected String fetchResource (OperationLog log, int indent) {
+
+ Client c = new Client();
+ DNSMessage msg = c.query(new Question(mFqdn, mType, mClass));
+ Record aw = msg.getAnswers()[0];
+ TXT txt = (TXT) aw.getPayload();
+ return txt.getText().toLowerCase();
+
+ }
+
+ @Override
+ protected Matcher matchResource(OperationLog log, int indent, String res) {
+ return magicPattern.matcher(res);
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/GenericHttpsResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/GenericHttpsResource.java
new file mode 100644
index 000000000..ba94bae75
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/GenericHttpsResource.java
@@ -0,0 +1,103 @@
+package org.sufficientlysecure.keychain.pgp.linked.resources;
+
+import android.content.Context;
+
+import com.textuality.keybase.lib.Search;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedCookieResource;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.net.ssl.HttpsURLConnection;
+
+public class GenericHttpsResource extends LinkedCookieResource {
+
+ GenericHttpsResource(Set<String> flags, HashMap<String,String> params, URI uri) {
+ super(flags, params, uri);
+ }
+
+ public static String generateText (Context context, byte[] fingerprint, int nonce) {
+ String cookie = LinkedCookieResource.generate(context, fingerprint, nonce);
+
+ return String.format(context.getResources().getString(R.string.linked_id_generic_text),
+ cookie, "0x" + KeyFormattingUtils.convertFingerprintToHex(fingerprint).substring(24));
+ }
+
+ @Override
+ protected String fetchResource (OperationLog log, int indent) {
+
+ log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString());
+ indent += 1;
+
+ try {
+
+ HttpsURLConnection conn = null;
+ URL url = mSubUri.toURL();
+ int status = 0;
+ int redirects = 0;
+
+ while (redirects < 5) {
+ conn = (HttpsURLConnection) url.openConnection();
+ conn.addRequestProperty("User-Agent", "OpenKeychain");
+ conn.setConnectTimeout(5000);
+ conn.setReadTimeout(25000);
+ conn.connect();
+ status = conn.getResponseCode();
+ if (status == 301) {
+ redirects++;
+ url = new URL(conn.getHeaderFields().get("Location").get(0));
+ log.add(LogType.MSG_LV_FETCH_REDIR, indent, url.toString());
+ } else {
+ break;
+ }
+ }
+
+ if (status >= 200 && status < 300) {
+ log.add(LogType.MSG_LV_FETCH_OK, indent, Integer.toString(status));
+ return Search.snarf(conn.getInputStream());
+ } else {
+ // log verbose output to logcat
+ Log.e(Constants.TAG, Search.snarf(conn.getErrorStream()));
+ log.add(LogType.MSG_LV_FETCH_ERROR, indent, Integer.toString(status));
+ return null;
+ }
+
+ } catch (MalformedURLException e) {
+ log.add(LogType.MSG_LV_FETCH_ERROR_URL, indent);
+ return null;
+ } catch (IOException e) {
+ log.add(LogType.MSG_LV_FETCH_ERROR_IO, indent);
+ return null;
+ }
+
+ }
+
+ public static GenericHttpsResource createNew (URI uri) {
+ HashSet<String> flags = new HashSet<String>();
+ flags.add("generic");
+ HashMap<String,String> params = new HashMap<String,String>();
+ return create(flags, params, uri);
+ }
+
+ public static GenericHttpsResource create(Set<String> flags, HashMap<String,String> params, URI uri) {
+ if ( ! ("https".equals(uri.getScheme())
+ && flags != null && flags.size() == 1 && flags.contains("generic")
+ && (params == null || params.isEmpty()))) {
+ return null;
+ }
+ return new GenericHttpsResource(flags, params, uri);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/TwitterResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/TwitterResource.java
new file mode 100644
index 000000000..f6ec4f97a
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/TwitterResource.java
@@ -0,0 +1,124 @@
+package org.sufficientlysecure.keychain.pgp.linked.resources;
+
+import android.content.Context;
+import android.util.Base64;
+
+import com.textuality.keybase.lib.JWalk;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.params.BasicHttpParams;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedCookieResource;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.Set;
+
+public class TwitterResource extends LinkedCookieResource {
+
+ TwitterResource(Set<String> flags, HashMap<String,String> params, URI uri) {
+ super(flags, params, uri);
+ }
+
+ public static String generateText (Context context, byte[] fingerprint, int nonce) {
+ // nothing special here for now, might change this later
+ return LinkedCookieResource.generate(context, fingerprint, nonce);
+ }
+
+ private String getTwitterStream(String screenName) {
+ String results = null;
+
+ // Step 1: Encode consumer key and secret
+ try {
+ // URL encode the consumer key and secret
+ String urlApiKey = URLEncoder.encode("6IhPnWbYxASAoAzH2QaUtHD0J", "UTF-8");
+ String urlApiSecret = URLEncoder.encode("L0GnuiOnapWbSBbQtLIqtpeS5BTtvh06dmoMoKQfHQS8UwHuWm", "UTF-8");
+
+ // Concatenate the encoded consumer key, a colon character, and the
+ // encoded consumer secret
+ String combined = urlApiKey + ":" + urlApiSecret;
+
+ // Base64 encode the string
+ String base64Encoded = Base64.encodeToString(combined.getBytes(), Base64.NO_WRAP);
+
+ // Step 2: Obtain a bearer token
+ HttpPost httpPost = new HttpPost("https://api.twitter.com/oauth2/token");
+ httpPost.setHeader("Authorization", "Basic " + base64Encoded);
+ httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
+ httpPost.setEntity(new StringEntity("grant_type=client_credentials"));
+ JSONObject rawAuthorization = new JSONObject(getResponseBody(httpPost));
+ String auth = JWalk.getString(rawAuthorization, "access_token");
+
+ // Applications should verify that the value associated with the
+ // token_type key of the returned object is bearer
+ if (auth != null && JWalk.getString(rawAuthorization, "token_type").equals("bearer")) {
+
+ // Step 3: Authenticate API requests with bearer token
+ HttpGet httpGet =
+ new HttpGet("https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=" + screenName);
+
+ // construct a normal HTTPS request and include an Authorization
+ // header with the value of Bearer <>
+ httpGet.setHeader("Authorization", "Bearer " + auth);
+ httpGet.setHeader("Content-Type", "application/json");
+ // update the results with the body of the response
+ results = getResponseBody(httpGet);
+ }
+ } catch (UnsupportedEncodingException ex) {
+ } catch (JSONException ex) {
+ } catch (IllegalStateException ex1) {
+ }
+ return results;
+ }
+
+ private static String getResponseBody(HttpRequestBase request) {
+ StringBuilder sb = new StringBuilder();
+ try {
+
+ DefaultHttpClient httpClient = new DefaultHttpClient(new BasicHttpParams());
+ HttpResponse response = httpClient.execute(request);
+ int statusCode = response.getStatusLine().getStatusCode();
+ String reason = response.getStatusLine().getReasonPhrase();
+
+ if (statusCode == 200) {
+
+ HttpEntity entity = response.getEntity();
+ InputStream inputStream = entity.getContent();
+
+ BufferedReader bReader = new BufferedReader(
+ new InputStreamReader(inputStream, "UTF-8"), 8);
+ String line = null;
+ while ((line = bReader.readLine()) != null) {
+ sb.append(line);
+ }
+ } else {
+ sb.append(reason);
+ }
+ } catch (UnsupportedEncodingException ex) {
+ } catch (ClientProtocolException ex1) {
+ } catch (IOException ex2) {
+ }
+ return sb.toString();
+ }
+
+ @Override
+ protected String fetchResource(OperationLog log, int indent) {
+ return getTwitterStream("Valodim");
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/UnknownResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/UnknownResource.java
new file mode 100644
index 000000000..f29ab5b39
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/linked/resources/UnknownResource.java
@@ -0,0 +1,21 @@
+package org.sufficientlysecure.keychain.pgp.linked.resources;
+
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedCookieResource;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Set;
+
+public class UnknownResource extends LinkedCookieResource {
+
+ public UnknownResource(Set<String> flags, HashMap<String,String> params, URI uri) {
+ super(flags, params, uri);
+ }
+
+ @Override
+ protected String fetchResource(OperationLog log, int indent) {
+ return null;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java
index 5856589c4..a7ca613d7 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java
@@ -106,6 +106,7 @@ public class KeychainContract {
public static final String PATH_PUBLIC = "public";
public static final String PATH_SECRET = "secret";
public static final String PATH_USER_IDS = "user_ids";
+ public static final String PATH_LINKED_IDS = "linked_ids";
public static final String PATH_KEYS = "keys";
public static final String PATH_CERTS = "certs";
@@ -261,6 +262,10 @@ public class KeychainContract {
public static Uri buildUserIdsUri(Uri uri) {
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_USER_IDS).build();
}
+
+ public static Uri buildLinkedIdsUri(Uri uri) {
+ return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_LINKED_IDS).build();
+ }
}
public static class ApiApps implements ApiAppsColumns, BaseColumns {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java
index b88cd6006..4a162989f 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java
@@ -53,7 +53,7 @@ import java.io.IOException;
*/
public class KeychainDatabase extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "openkeychain.db";
- private static final int DATABASE_VERSION = 8;
+ private static final int DATABASE_VERSION = 9;
static Boolean apgHack = false;
private Context mContext;
@@ -61,7 +61,7 @@ public class KeychainDatabase extends SQLiteOpenHelper {
String KEY_RINGS_PUBLIC = "keyrings_public";
String KEY_RINGS_SECRET = "keyrings_secret";
String KEYS = "keys";
- String USER_PACKETS = "user_ids";
+ String USER_PACKETS = "user_packets";
String CERTS = "certs";
String API_APPS = "api_apps";
String API_ACCOUNTS = "api_accounts";
@@ -119,8 +119,7 @@ public class KeychainDatabase extends SQLiteOpenHelper {
+ UserPacketsColumns.IS_REVOKED + " INTEGER, "
+ UserPacketsColumns.RANK+ " INTEGER, "
- + "PRIMARY KEY(" + UserPacketsColumns.MASTER_KEY_ID + ", " + UserPacketsColumns.USER_ID + "), "
- + "UNIQUE (" + UserPacketsColumns.MASTER_KEY_ID + ", " + UserPacketsColumns.RANK + "), "
+ + "PRIMARY KEY(" + UserPacketsColumns.MASTER_KEY_ID + ", " + UserPacketsColumns.RANK + "), "
+ "FOREIGN KEY(" + UserPacketsColumns.MASTER_KEY_ID + ") REFERENCES "
+ Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE"
+ ")";
@@ -267,6 +266,13 @@ public class KeychainDatabase extends SQLiteOpenHelper {
} catch (Exception e) {
// never mind, the column probably already existed
}
+ case 9:
+ // tbale name for user_ids changed to user_packets
+ db.execSQL("DROP TABLE IF EXISTS certs");
+ db.execSQL("DROP TABLE IF EXISTS user_ids");
+ db.execSQL(CREATE_USER_PACKETS);
+ db.execSQL(CREATE_CERTS);
+
}
// always do consolidate after upgrade
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java
index 2601c1f69..31ed89d67 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java
@@ -62,6 +62,7 @@ public class KeychainProvider extends ContentProvider {
private static final int KEY_RING_SECRET = 204;
private static final int KEY_RING_CERTS = 205;
private static final int KEY_RING_CERTS_SPECIFIC = 206;
+ private static final int KEY_RING_LINKED_IDS = 207;
private static final int API_APPS = 301;
private static final int API_APPS_BY_PACKAGE_NAME = 302;
@@ -127,6 +128,7 @@ public class KeychainProvider extends ContentProvider {
* key_rings/_/unified
* key_rings/_/keys
* key_rings/_/user_ids
+ * key_rings/_/linked_ids
* key_rings/_/public
* key_rings/_/secret
* key_rings/_/certs
@@ -143,6 +145,9 @@ public class KeychainProvider extends ContentProvider {
+ KeychainContract.PATH_USER_IDS,
KEY_RING_USER_IDS);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
+ + KeychainContract.PATH_LINKED_IDS,
+ KEY_RING_LINKED_IDS);
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
+ KeychainContract.PATH_PUBLIC,
KEY_RING_PUBLIC);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
@@ -469,7 +474,8 @@ public class KeychainProvider extends ContentProvider {
}
case KEY_RINGS_USER_IDS:
- case KEY_RING_USER_IDS: {
+ case KEY_RING_USER_IDS:
+ case KEY_RING_LINKED_IDS: {
HashMap<String, String> projectionMap = new HashMap<>();
projectionMap.put(UserPackets._ID, Tables.USER_PACKETS + ".oid AS _id");
projectionMap.put(UserPackets.MASTER_KEY_ID, Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID);
@@ -494,13 +500,14 @@ public class KeychainProvider extends ContentProvider {
groupBy = Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID
+ ", " + Tables.USER_PACKETS + "." + UserPackets.RANK;
- // for now, we only respect user ids here, so TYPE must be NULL
- // TODO expand with KEY_RING_USER_PACKETS query type which lifts this restriction
- qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL");
+ if (match == KEY_RING_LINKED_IDS) {
+ qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.TYPE + " = 100");
+ } else {
+ qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL");
+ }
// If we are searching for a particular keyring's ids, add where
- if (match == KEY_RING_USER_IDS) {
- // TODO remove with the thing above
+ if (match == KEY_RING_USER_IDS || match == KEY_RING_LINKED_IDS) {
qb.appendWhere(" AND ");
qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " = ");
qb.appendWhereEscapeString(uri.getPathSegments().get(1));
@@ -699,7 +706,7 @@ public class KeychainProvider extends ContentProvider {
)) {
throw new AssertionError("Incorrect type for user packet! This is a bug!");
}
- if (((Number)values.get(UserPacketsColumns.RANK)).intValue() == 0 && values.get(UserPacketsColumns.USER_ID) == null) {
+ if (values.get(UserPacketsColumns.RANK) == 0 && values.get(UserPacketsColumns.USER_ID) == null) {
throw new AssertionError("Rank 0 user packet must be a user id!");
}
db.insertOrThrow(Tables.USER_PACKETS, null, values);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java
index d947ae053..2ff4803fb 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java
@@ -30,6 +30,12 @@ import android.support.v4.util.LongSparseArray;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
+import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize;
+import org.sufficientlysecure.keychain.util.Preferences;
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
import org.sufficientlysecure.keychain.operations.ImportExportOperation;
import org.sufficientlysecure.keychain.operations.results.ConsolidateResult;
@@ -1076,9 +1082,8 @@ public class ProviderHelper {
log.add(LogType.MSG_CON_SAVE_SECRET, indent);
indent += 1;
- final Cursor cursor = mContentResolver.query(KeyRings.buildUnifiedKeyRingsUri(), new String[]{
- KeyRings.PRIVKEY_DATA, KeyRings.FINGERPRINT, KeyRings.HAS_ANY_SECRET
- }, KeyRings.HAS_ANY_SECRET + " = 1", null, null);
+ final Cursor cursor = mContentResolver.query(KeyRingData.buildSecretKeyRingUri(),
+ new String[]{ KeyRingData.KEY_RING_DATA }, null, null, null);
if (cursor == null) {
log.add(LogType.MSG_CON_ERROR_DB, indent);
@@ -1100,8 +1105,7 @@ public class ProviderHelper {
if (cursor.isAfterLast()) {
return false;
}
- ring = new ParcelableKeyRing(KeyFormattingUtils.convertFingerprintToHex(cursor.getBlob(1)), cursor.getBlob(0)
- );
+ ring = new ParcelableKeyRing(cursor.getBlob(0));
cursor.moveToNext();
return true;
}
@@ -1138,9 +1142,9 @@ public class ProviderHelper {
log.add(LogType.MSG_CON_SAVE_PUBLIC, indent);
indent += 1;
- final Cursor cursor = mContentResolver.query(KeyRings.buildUnifiedKeyRingsUri(), new String[]{
- KeyRings.PUBKEY_DATA, KeyRings.FINGERPRINT
- }, null, null, null);
+ final Cursor cursor = mContentResolver.query(
+ KeyRingData.buildPublicKeyRingUri(),
+ new String[]{ KeyRingData.KEY_RING_DATA }, null, null, null);
if (cursor == null) {
log.add(LogType.MSG_CON_ERROR_DB, indent);
@@ -1162,8 +1166,7 @@ public class ProviderHelper {
if (cursor.isAfterLast()) {
return false;
}
- ring = new ParcelableKeyRing(KeyFormattingUtils.convertFingerprintToHex(cursor.getBlob(1)), cursor.getBlob(0)
- );
+ ring = new ParcelableKeyRing(cursor.getBlob(0));
cursor.moveToNext();
return true;
}
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 d95701458..0ddf0c76d 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java
@@ -48,6 +48,14 @@ import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
import org.sufficientlysecure.keychain.operations.results.DeleteResult;
import org.sufficientlysecure.keychain.operations.results.EditKeyResult;
import org.sufficientlysecure.keychain.operations.results.ExportResult;
+import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
+import org.sufficientlysecure.keychain.operations.results.CertifyResult;
+import org.sufficientlysecure.keychain.util.FileHelper;
+import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize;
+import org.sufficientlysecure.keychain.util.Preferences;
+import org.sufficientlysecure.keychain.keyimport.HkpKeyserver;
+import org.sufficientlysecure.keychain.keyimport.Keyserver;
+import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java
index 25ca6e8fd..005b60ce0 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java
@@ -328,7 +328,7 @@ public class EditKeyFragment extends LoaderFragment implements
case LOADER_ID_USER_IDS: {
Uri baseUri = UserPackets.buildUserIdsUri(mDataUri);
return new CursorLoader(getActivity(), baseUri,
- UserIdsAdapter.USER_IDS_PROJECTION, null, null, null);
+ UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null);
}
case LOADER_ID_SUBKEYS: {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java
index bb669f6b8..29621662f 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java
@@ -237,6 +237,9 @@ public class PassphraseDialogActivity extends FragmentActivity {
case DIVERT_TO_CARD:
message = getString(R.string.yubikey_pin_for, userId);
break;
+ // special case: empty passphrase just returns the empty passphrase
+ case PASSPHRASE_EMPTY:
+ finishCaching("");
default:
message = "This should not happen!";
break;
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 1b4fc503c..678ecd289 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
@@ -72,6 +72,7 @@ import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.KeychainIntentService;
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.ui.linked.LinkedIdWizard;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State;
@@ -282,7 +283,7 @@ public class ViewKeyActivity extends BaseActivity implements
// Add the fragment to the 'fragment_container' FrameLayout
// NOTE: We use commitAllowingStateLoss() to prevent weird crashes!
getSupportFragmentManager().beginTransaction()
- .replace(R.id.view_key_fragment, frag)
+ .replace(R.id.view_key_fragment, frag, "main")
.commitAllowingStateLoss();
// do it immediately!
getSupportFragmentManager().executePendingTransactions();
@@ -336,6 +337,12 @@ public class ViewKeyActivity extends BaseActivity implements
certifyFingeprint(mDataUri);
return true;
}
+ case R.id.menu_key_view_add_linked_identity: {
+ Intent intent = new Intent(this, LinkedIdWizard.class);
+ intent.setData(mDataUri);
+ startActivity(intent);
+ return true;
+ }
}
} catch (ProviderHelper.NotFoundException e) {
Notify.showNotify(this, R.string.error_key_not_found, Notify.Style.ERROR);
@@ -348,6 +355,10 @@ public class ViewKeyActivity extends BaseActivity implements
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem editKey = menu.findItem(R.id.menu_key_view_edit);
editKey.setVisible(mIsSecret);
+
+ MenuItem addLinked = menu.findItem(R.id.menu_key_view_add_linked_identity);
+ addLinked.setVisible(mIsSecret);
+
MenuItem certifyFingerprint = menu.findItem(R.id.menu_key_view_certify_fingerprint);
certifyFingerprint.setVisible(!mIsSecret && !mIsVerified && !mIsExpired && !mIsRevoked);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java
index 7bfebaf62..ad437f924 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java
@@ -133,7 +133,7 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements
case LOADER_ID_USER_IDS: {
Uri baseUri = UserPackets.buildUserIdsUri(mDataUri);
return new CursorLoader(getActivity(), baseUri,
- UserIdsAdapter.USER_IDS_PROJECTION, null, null, null);
+ UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null);
}
default:
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java
index 628970b27..20386b372 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java
@@ -20,20 +20,29 @@ package org.sufficientlysecure.keychain.ui;
import android.database.Cursor;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
+import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
+import android.support.v7.widget.CardView;
+import android.transition.Explode;
+import android.transition.Fade;
+import android.transition.Transition;
+import android.transition.TransitionInflater;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
import org.sufficientlysecure.keychain.provider.KeychainContract;
+import org.sufficientlysecure.keychain.ui.adapter.LinkedIdsAdapter;
import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter;
import org.sufficientlysecure.keychain.ui.dialog.UserIdInfoDialogFragment;
import org.sufficientlysecure.keychain.util.Log;
@@ -49,10 +58,14 @@ public class ViewKeyFragment extends LoaderFragment implements
private static final int LOADER_ID_UNIFIED = 0;
private static final int LOADER_ID_USER_IDS = 1;
+ private static final int LOADER_ID_LINKED_IDS = 2;
private UserIdsAdapter mUserIdsAdapter;
+ private LinkedIdsAdapter mLinkedIdsAdapter;
private Uri mDataUri;
+ private ListView mLinkedIds;
+ private CardView mLinkedIdsCard;
/**
* Creates new instance of this fragment
@@ -73,6 +86,11 @@ public class ViewKeyFragment extends LoaderFragment implements
View view = inflater.inflate(R.layout.view_key_fragment, getContainer());
mUserIds = (ListView) view.findViewById(R.id.view_key_user_ids);
+ mLinkedIdsCard = (CardView) view.findViewById(R.id.card_linked_ids);
+
+ mLinkedIds = (ListView) view.findViewById(R.id.view_key_linked_ids);
+ mLinkedIdsAdapter = new LinkedIdsAdapter(getActivity(), null, 0);
+ mLinkedIds.setAdapter(mLinkedIdsAdapter);
mUserIds.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
@@ -80,10 +98,34 @@ public class ViewKeyFragment extends LoaderFragment implements
showUserIdInfo(position);
}
});
+ mLinkedIds.setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ showLinkedId(position);
+ }
+ });
return root;
}
+ private void showLinkedId(final int position) {
+ Fragment frag = mLinkedIdsAdapter.getLinkedIdFragment(position);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ Transition trans = TransitionInflater.from(getActivity())
+ .inflateTransition(R.transition.linked_id_card_trans);
+ // setSharedElementReturnTransition(trans);
+ setExitTransition(new Fade());
+ frag.setSharedElementEnterTransition(trans);
+ }
+
+ getFragmentManager().beginTransaction()
+ .replace(R.id.view_key_fragment, frag)
+ .addSharedElement(mLinkedIdsCard, "card_linked_ids")
+ .addToBackStack(null)
+ .commit();
+ }
+
private void showUserIdInfo(final int position) {
if (!mIsSecret) {
final boolean isRevoked = mUserIdsAdapter.getIsRevoked(position);
@@ -118,34 +160,18 @@ public class ViewKeyFragment extends LoaderFragment implements
// These are the rows that we will retrieve.
static final String[] UNIFIED_PROJECTION = new String[]{
KeychainContract.KeyRings._ID,
- KeychainContract.KeyRings.MASTER_KEY_ID,
- KeychainContract.KeyRings.USER_ID,
- KeychainContract.KeyRings.IS_REVOKED,
- KeychainContract.KeyRings.IS_EXPIRED,
- KeychainContract.KeyRings.VERIFIED,
KeychainContract.KeyRings.HAS_ANY_SECRET,
- KeychainContract.KeyRings.FINGERPRINT,
- KeychainContract.KeyRings.HAS_ENCRYPT
};
- static final int INDEX_MASTER_KEY_ID = 1;
- static final int INDEX_USER_ID = 2;
- static final int INDEX_IS_REVOKED = 3;
- static final int INDEX_IS_EXPIRED = 4;
- static final int INDEX_VERIFIED = 5;
- static final int INDEX_HAS_ANY_SECRET = 6;
- static final int INDEX_FINGERPRINT = 7;
- static final int INDEX_HAS_ENCRYPT = 8;
+ static final int INDEX_HAS_ANY_SECRET = 1;
private void loadData(Uri dataUri) {
mDataUri = dataUri;
Log.i(Constants.TAG, "mDataUri: " + mDataUri);
- // Prepare the loaders. Either re-connect with an existing ones,
- // or start new ones.
- // TODO Is this loader the same as the one in the activity?
getLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this);
+ getLoaderManager().initLoader(LOADER_ID_LINKED_IDS, null, this);
}
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
@@ -159,23 +185,23 @@ public class ViewKeyFragment extends LoaderFragment implements
case LOADER_ID_USER_IDS:
return UserIdsAdapter.createLoader(getActivity(), mDataUri);
+ case LOADER_ID_LINKED_IDS:
+ return LinkedIdsAdapter.createLoader(getActivity(), mDataUri);
+
default:
return null;
}
}
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
- /* TODO better error handling? May cause problems when a key is deleted,
- * because the notification triggers faster than the activity closes.
- */
- // Avoid NullPointerExceptions...
- if (data.getCount() == 0) {
- return;
- }
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
switch (loader.getId()) {
case LOADER_ID_UNIFIED: {
+ // Avoid NullPointerExceptions...
+ if (data.getCount() == 0) {
+ return;
+ }
if (data.moveToFirst()) {
mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0;
@@ -194,6 +220,11 @@ public class ViewKeyFragment extends LoaderFragment implements
break;
}
+ case LOADER_ID_LINKED_IDS: {
+ mLinkedIdsCard.setVisibility(data.getCount() > 0 ? View.VISIBLE : View.GONE);
+ mLinkedIdsAdapter.swapCursor(data);
+ break;
+ }
}
setContentShown(true);
}
@@ -208,6 +239,11 @@ public class ViewKeyFragment extends LoaderFragment implements
mUserIdsAdapter.swapCursor(null);
break;
}
+ case LOADER_ID_LINKED_IDS: {
+ mLinkedIdsCard.setVisibility(View.GONE);
+ mLinkedIdsAdapter.swapCursor(null);
+ break;
+ }
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsAdapter.java
new file mode 100644
index 000000000..329b95ebc
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsAdapter.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2014-2015 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.adapter;
+
+import android.app.Activity;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.support.v4.app.Fragment;
+import android.support.v4.content.CursorLoader;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedIdentity;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedResource;
+import org.sufficientlysecure.keychain.pgp.linked.RawLinkedIdentity;
+import org.sufficientlysecure.keychain.pgp.linked.resources.DnsResource;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
+import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State;
+
+import java.io.IOException;
+import java.util.WeakHashMap;
+
+
+public class LinkedIdsAdapter extends UserAttributesAdapter {
+ protected LayoutInflater mInflater;
+ WeakHashMap<Integer,RawLinkedIdentity> mLinkedIdentityCache = new WeakHashMap<>();
+
+ public LinkedIdsAdapter(Context context, Cursor c, int flags) {
+ super(context, c, flags);
+ mInflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+
+ RawLinkedIdentity id = getItem(cursor.getPosition());
+ ViewHolder holder = (ViewHolder) view.getTag();
+
+ int isVerified = cursor.getInt(INDEX_VERIFIED);
+ switch (isVerified) {
+ case Certs.VERIFIED_SECRET:
+ KeyFormattingUtils.setStatusImage(mContext, holder.vVerified,
+ null, State.VERIFIED, KeyFormattingUtils.DEFAULT_COLOR);
+ break;
+ case Certs.VERIFIED_SELF:
+ KeyFormattingUtils.setStatusImage(mContext, holder.vVerified,
+ null, State.UNVERIFIED, KeyFormattingUtils.DEFAULT_COLOR);
+ break;
+ default:
+ KeyFormattingUtils.setStatusImage(mContext, holder.vVerified,
+ null, State.INVALID, KeyFormattingUtils.DEFAULT_COLOR);
+ break;
+ }
+
+ if (holder instanceof ViewHolderNonRaw) {
+ ((ViewHolderNonRaw) holder).setData(mContext, (LinkedIdentity) id);
+ }
+
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ RawLinkedIdentity id = getItem(position);
+
+ if (id instanceof LinkedIdentity) {
+ LinkedResource res = ((LinkedIdentity) id).mResource;
+ if (res instanceof DnsResource) {
+ return 1;
+ }
+ }
+
+ return 0;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 2;
+ }
+
+ @Override
+ public RawLinkedIdentity getItem(int position) {
+ RawLinkedIdentity ret = mLinkedIdentityCache.get(position);
+ if (ret != null) {
+ return ret;
+ }
+
+ Cursor c = getCursor();
+ c.moveToPosition(position);
+
+ byte[] data = c.getBlob(INDEX_ATTRIBUTE_DATA);
+ try {
+ ret = LinkedIdentity.fromAttributeData(data);
+ mLinkedIdentityCache.put(position, ret);
+ return ret;
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "could not read linked identity subpacket data", e);
+ return null;
+ }
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ int type = getItemViewType(cursor.getPosition());
+ switch(type) {
+ case 0: {
+ View v = mInflater.inflate(R.layout.linked_id_item_unknown, null);
+ ViewHolder holder = new ViewHolder(v);
+ v.setTag(holder);
+ return v;
+ }
+ case 1: {
+ View v = mInflater.inflate(R.layout.linked_id_item_dns, null);
+ ViewHolder holder = new ViewHolderDns(v);
+ v.setTag(holder);
+ return v;
+ }
+ default:
+ throw new AssertionError("all cases must be covered in LinkedIdsAdapter.newView!");
+ }
+ }
+
+ // don't show revoked user ids, irrelevant for average users
+ public static final String LINKED_IDS_WHERE = UserPackets.IS_REVOKED + " = 0";
+
+ public static CursorLoader createLoader(Activity activity, Uri dataUri) {
+ Uri baseUri = UserPackets.buildLinkedIdsUri(dataUri);
+ return new CursorLoader(activity, baseUri,
+ UserIdsAdapter.USER_PACKETS_PROJECTION, LINKED_IDS_WHERE, null, null);
+ }
+
+ public Fragment getLinkedIdFragment(int position) {
+ RawLinkedIdentity id = getItem(position);
+
+ return LinkedIdViewFragment.newInstance(id);
+ }
+
+ static class ViewHolder {
+ ImageView vVerified;
+
+ ViewHolder(View view) {
+ vVerified = (ImageView) view.findViewById(R.id.user_id_item_certified);
+ }
+ }
+
+ static abstract class ViewHolderNonRaw extends ViewHolder {
+ ViewHolderNonRaw(View view) {
+ super(view);
+ }
+
+ abstract void setData(Context context, LinkedIdentity id);
+ }
+
+ static class ViewHolderDns extends ViewHolderNonRaw {
+ TextView vFqdn;
+
+ ViewHolderDns(View view) {
+ super(view);
+
+ vFqdn = (TextView) view.findViewById(R.id.linked_id_dns_fqdn);
+ }
+
+ @Override
+ void setData(Context context, LinkedIdentity id) {
+ DnsResource res = (DnsResource) id.mResource;
+ vFqdn.setText(res.getFqdn());
+ }
+
+ }
+
+ @Override
+ public void notifyDataSetChanged() {
+ mLinkedIdentityCache.clear();
+ super.notifyDataSetChanged();
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java
index 457083770..345ccccc0 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java
@@ -8,10 +8,11 @@ import android.view.View;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
public abstract class UserAttributesAdapter extends CursorAdapter {
- public static final String[] USER_IDS_PROJECTION = new String[]{
+ public static final String[] USER_PACKETS_PROJECTION = new String[]{
UserPackets._ID,
UserPackets.TYPE,
UserPackets.USER_ID,
+ UserPackets.ATTRIBUTE_DATA,
UserPackets.RANK,
UserPackets.VERIFIED,
UserPackets.IS_PRIMARY,
@@ -20,10 +21,11 @@ public abstract class UserAttributesAdapter extends CursorAdapter {
protected static final int INDEX_ID = 0;
protected static final int INDEX_TYPE = 1;
protected static final int INDEX_USER_ID = 2;
- protected static final int INDEX_RANK = 3;
- protected static final int INDEX_VERIFIED = 4;
- protected static final int INDEX_IS_PRIMARY = 5;
- protected static final int INDEX_IS_REVOKED = 6;
+ protected static final int INDEX_ATTRIBUTE_DATA = 3;
+ protected static final int INDEX_RANK = 4;
+ protected static final int INDEX_VERIFIED = 5;
+ protected static final int INDEX_IS_PRIMARY = 6;
+ protected static final int INDEX_IS_REVOKED = 7;
public UserAttributesAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
@@ -46,4 +48,5 @@ public abstract class UserAttributesAdapter extends CursorAdapter {
mCursor.moveToPosition(position);
return mCursor.getInt(INDEX_VERIFIED);
}
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java
index 3486f1516..1cf3f4d38 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java
@@ -188,7 +188,7 @@ public class UserIdsAdapter extends UserAttributesAdapter {
public static CursorLoader createLoader(Activity activity, Uri dataUri) {
Uri baseUri = UserPackets.buildUserIdsUri(dataUri);
return new CursorLoader(activity, baseUri,
- UserIdsAdapter.USER_IDS_PROJECTION, USER_IDS_WHERE, null, null);
+ UserIdsAdapter.USER_PACKETS_PROJECTION, USER_IDS_WHERE, null, null);
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep1Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep1Fragment.java
new file mode 100644
index 000000000..26b0a0539
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep1Fragment.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.linked;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Patterns;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.linked.RawLinkedIdentity;
+import org.sufficientlysecure.keychain.pgp.linked.resources.DnsResource;
+
+public class LinkedIdCreateDnsStep1Fragment extends Fragment {
+
+ LinkedIdWizard mLinkedIdWizard;
+
+ EditText mEditDns;
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static LinkedIdCreateDnsStep1Fragment newInstance() {
+ LinkedIdCreateDnsStep1Fragment frag = new LinkedIdCreateDnsStep1Fragment();
+
+ Bundle args = new Bundle();
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mLinkedIdWizard = (LinkedIdWizard) getActivity();
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.linked_create_dns_fragment_step1, container, false);
+
+ view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ String uri = mEditDns.getText().toString();
+
+ if (!checkUri(uri)) {
+ mEditDns.setError("Please enter a valid domain name!");
+ return;
+ }
+
+ int proofNonce = RawLinkedIdentity.generateNonce();
+ String proofText = DnsResource.generateText(getActivity(),
+ mLinkedIdWizard.mFingerprint, proofNonce);
+
+ LinkedIdCreateDnsStep2Fragment frag =
+ LinkedIdCreateDnsStep2Fragment.newInstance(uri, proofNonce, proofText);
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+
+ }
+ });
+
+ view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
+ }
+ });
+
+ mEditDns = (EditText) view.findViewById(R.id.linked_create_dns_domain);
+
+ mEditDns.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ String uri = editable.toString();
+ if (uri.length() > 0) {
+ if (checkUri(uri)) {
+ mEditDns.setCompoundDrawablesWithIntrinsicBounds(0, 0,
+ R.drawable.uid_mail_ok, 0);
+ } else {
+ mEditDns.setCompoundDrawablesWithIntrinsicBounds(0, 0,
+ R.drawable.uid_mail_bad, 0);
+ }
+ } else {
+ // remove drawable if email is empty
+ mEditDns.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
+ }
+ }
+ });
+
+ mEditDns.setText("test.mugenguild.com");
+
+ return view;
+ }
+
+ private static boolean checkUri(String uri) {
+ return Patterns.DOMAIN_NAME.matcher(uri).matches();
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep2Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep2Fragment.java
new file mode 100644
index 000000000..3b0860fa9
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep2Fragment.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.linked;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.graphics.PorterDuff;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Message;
+import android.os.Messenger;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult;
+import org.sufficientlysecure.keychain.operations.results.OperationResult;
+import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedIdentity;
+import org.sufficientlysecure.keychain.pgp.linked.RawLinkedIdentity;
+import org.sufficientlysecure.keychain.pgp.linked.resources.DnsResource;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
+import org.sufficientlysecure.keychain.ui.PassphraseDialogActivity;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+import org.sufficientlysecure.keychain.ui.util.Notify.Style;
+import org.sufficientlysecure.keychain.util.FileHelper;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+
+public class LinkedIdCreateDnsStep2Fragment extends Fragment {
+
+ private static final int REQUEST_CODE_OUTPUT = 0x00007007;
+ private static final int REQUEST_CODE_PASSPHRASE = 0x00007008;
+
+ public static final String DOMAIN = "domain", NONCE = "nonce", TEXT = "text";
+
+ LinkedIdWizard mLinkedIdWizard;
+
+ TextView mTextView;
+ ImageView mVerifyImage;
+ View mVerifyProgress;
+ TextView mVerifyStatus;
+
+ String mResourceDomain;
+ int mResourceNonce;
+ String mResourceString;
+
+ // This is a resource, set AFTER it has been verified
+ DnsResource mVerifiedResource = null;
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static LinkedIdCreateDnsStep2Fragment newInstance
+ (String uri, int proofNonce, String proofText) {
+
+ LinkedIdCreateDnsStep2Fragment frag = new LinkedIdCreateDnsStep2Fragment();
+
+ Bundle args = new Bundle();
+ args.putString(DOMAIN, uri);
+ args.putInt(NONCE, proofNonce);
+ args.putString(TEXT, proofText);
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.linked_create_dns_fragment_step2, container, false);
+
+ mResourceDomain = getArguments().getString(DOMAIN);
+ mResourceNonce = getArguments().getInt(NONCE);
+ mResourceString = getArguments().getString(TEXT);
+
+ view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startCertify();
+ }
+ });
+
+ view.findViewById(R.id.back_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
+ }
+ });
+
+ mVerifyImage = (ImageView) view.findViewById(R.id.verify_image);
+ mVerifyProgress = view.findViewById(R.id.verify_progress);
+ mVerifyStatus = (TextView) view.findViewById(R.id.verify_status);
+
+ view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofSend();
+ }
+ });
+
+ view.findViewById(R.id.button_save).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofSave();
+ }
+ });
+
+ view.findViewById(R.id.button_verify).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofVerify();
+ }
+ });
+
+ mTextView = (TextView) view.findViewById(R.id.linked_create_dns_text);
+ mTextView.setText(mResourceString);
+
+ setVerifyProgress(false, null);
+ mVerifyStatus.setText(R.string.linked_verify_pending);
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mLinkedIdWizard = (LinkedIdWizard) getActivity();
+ }
+
+ public void setVerifyProgress(boolean on, Boolean success) {
+ mVerifyProgress.setVisibility(on ? View.VISIBLE : View.GONE);
+ mVerifyImage.setVisibility(on ? View.GONE : View.VISIBLE);
+ if (success == null) {
+ mVerifyStatus.setText(R.string.linked_verifying);
+ mVerifyImage.setImageResource(R.drawable.status_signature_unverified_cutout_24dp);
+ mVerifyImage.setColorFilter(getResources().getColor(R.color.tertiary_text_light),
+ PorterDuff.Mode.SRC_IN);
+ } else if (success) {
+ mVerifyStatus.setText(R.string.linked_verify_success);
+ mVerifyImage.setImageResource(R.drawable.status_signature_verified_cutout_24dp);
+ mVerifyImage.setColorFilter(getResources().getColor(R.color.android_green_dark),
+ PorterDuff.Mode.SRC_IN);
+ } else {
+ mVerifyStatus.setText(R.string.linked_verify_error);
+ mVerifyImage.setImageResource(R.drawable.status_signature_unknown_cutout_24dp);
+ mVerifyImage.setColorFilter(getResources().getColor(R.color.android_red_dark),
+ PorterDuff.Mode.SRC_IN);
+ }
+ }
+
+ private void proofSend () {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, mResourceString);
+ sendIntent.setType("text/plain");
+ startActivity(sendIntent);
+ }
+
+ private void proofSave () {
+ String state = Environment.getExternalStorageState();
+ if (!Environment.MEDIA_MOUNTED.equals(state)) {
+ Notify.showNotify(getActivity(), "External storage not available!", Style.ERROR);
+ return;
+ }
+
+ String targetName = "pgpkey.txt";
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ File targetFile = new File(Constants.Path.APP_DIR, targetName);
+ FileHelper.saveFile(this, getString(R.string.title_decrypt_to_file),
+ getString(R.string.specify_file_to_decrypt_to), targetFile, REQUEST_CODE_OUTPUT);
+ } else {
+ FileHelper.saveDocument(this, "text/plain", targetName, REQUEST_CODE_OUTPUT);
+ }
+ }
+
+ private void saveFile(Uri uri) {
+ try {
+ PrintWriter out =
+ new PrintWriter(getActivity().getContentResolver().openOutputStream(uri));
+ out.print(mResourceString);
+ if (out.checkError()) {
+ Notify.showNotify(getActivity(), "Error writing file!", Style.ERROR);
+ }
+ } catch (FileNotFoundException e) {
+ Notify.showNotify(getActivity(), "File could not be opened for writing!", Style.ERROR);
+ e.printStackTrace();
+ }
+ }
+
+ public void proofVerify() {
+ setVerifyProgress(true, null);
+
+ final DnsResource resource = DnsResource.createNew(mResourceDomain);
+
+ new AsyncTask<Void,Void,LinkedVerifyResult>() {
+
+ @Override
+ protected LinkedVerifyResult doInBackground(Void... params) {
+ return resource.verify(mLinkedIdWizard.mFingerprint, mResourceNonce);
+ }
+
+ @Override
+ protected void onPostExecute(LinkedVerifyResult result) {
+ super.onPostExecute(result);
+ if (result.success()) {
+ setVerifyProgress(false, true);
+ mVerifiedResource = resource;
+ } else {
+ setVerifyProgress(false, false);
+ mVerifiedResource = resource;
+ // on error, show error message
+ result.createNotify(getActivity()).show();
+ }
+ }
+ }.execute();
+
+ }
+
+ public void startCertify() {
+
+ if (mVerifiedResource == null) {
+ Notify.showNotify(getActivity(), R.string.linked_need_verify, Style.ERROR);
+ return;
+ }
+
+ Intent intent = new Intent(getActivity(), PassphraseDialogActivity.class);
+ intent.putExtra(PassphraseDialogActivity.EXTRA_SUBKEY_ID, mLinkedIdWizard.mMasterKeyId);
+ startActivityForResult(intent, REQUEST_CODE_PASSPHRASE);
+
+ }
+
+ public void certifyLinkedIdentity (String passphrase) {
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(
+ getActivity(),
+ getString(R.string.progress_saving),
+ ProgressDialog.STYLE_HORIZONTAL,
+ true) {
+ public void handleMessage(Message message) {
+ // handle messages by standard KeychainIntentServiceHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+
+ // get returned data bundle
+ Bundle returnData = message.getData();
+ if (returnData == null) {
+ return;
+ }
+ final OperationResult result =
+ returnData.getParcelable(OperationResult.EXTRA_RESULT);
+ if (result == null) {
+ return;
+ }
+
+ // if bad -> display here!
+ if (!result.success()) {
+ result.createNotify(getActivity()).show();
+ return;
+ }
+
+ result.createNotify(getActivity()).show();
+
+ // if good -> finish, return result to showkey and display there!
+ // Intent intent = new Intent();
+ // intent.putExtra(OperationResult.EXTRA_RESULT, result);
+ // getActivity().setResult(EditKeyActivity.RESULT_OK, intent);
+
+ // AffirmationCreateHttpsStep3Fragment frag =
+ // AffirmationCreateHttpsStep3Fragment.newInstance(
+ // mResourceDomain, mResourceNonce, mResourceString);
+
+ // mAffirmationWizard.loadFragment(null, frag, AffirmationWizard.FRAG_ACTION_TO_RIGHT);
+
+ }
+ }
+ };
+
+ SaveKeyringParcel skp =
+ new SaveKeyringParcel(mLinkedIdWizard.mMasterKeyId, mLinkedIdWizard.mFingerprint);
+
+ WrappedUserAttribute ua =
+ LinkedIdentity.fromResource(mVerifiedResource, mResourceNonce).toUserAttribute();
+
+ skp.mAddUserAttribute.add(ua);
+
+ // Send all information needed to service to import key in other thread
+ Intent intent = new Intent(getActivity(), KeychainIntentService.class);
+ intent.setAction(KeychainIntentService.ACTION_EDIT_KEYRING);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+ data.putString(KeychainIntentService.EDIT_KEYRING_PASSPHRASE, passphrase);
+ data.putParcelable(KeychainIntentService.EDIT_KEYRING_PARCEL, skp);
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(getActivity());
+
+ // start service with intent
+ getActivity().startService(intent);
+
+ }
+
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ // For saving a file
+ case REQUEST_CODE_OUTPUT:
+ if (data == null) {
+ return;
+ }
+ Uri uri = data.getData();
+ saveFile(uri);
+ break;
+ case REQUEST_CODE_PASSPHRASE:
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ String passphrase =
+ data.getStringExtra(PassphraseDialogActivity.MESSAGE_DATA_PASSPHRASE);
+ certifyLinkedIdentity(passphrase);
+ }
+ break;
+ }
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java
new file mode 100644
index 000000000..78ca4cfe9
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.linked;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Patterns;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.linked.RawLinkedIdentity;
+import org.sufficientlysecure.keychain.pgp.linked.resources.GenericHttpsResource;
+
+public class LinkedIdCreateHttpsStep1Fragment extends Fragment {
+
+ LinkedIdWizard mLinkedIdWizard;
+
+ EditText mEditUri;
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static LinkedIdCreateHttpsStep1Fragment newInstance() {
+ LinkedIdCreateHttpsStep1Fragment frag = new LinkedIdCreateHttpsStep1Fragment();
+
+ Bundle args = new Bundle();
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mLinkedIdWizard = (LinkedIdWizard) getActivity();
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.linked_create_https_fragment_step1, container, false);
+
+ view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ String uri = "https://" + mEditUri.getText();
+
+ if (!checkUri(uri)) {
+ return;
+ }
+
+ int proofNonce = RawLinkedIdentity.generateNonce();
+ String proofText = GenericHttpsResource.generateText(getActivity(),
+ mLinkedIdWizard.mFingerprint, proofNonce);
+
+ LinkedIdCreateHttpsStep2Fragment frag =
+ LinkedIdCreateHttpsStep2Fragment.newInstance(uri, proofNonce, proofText);
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+
+ }
+ });
+
+ view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
+ }
+ });
+
+ mEditUri = (EditText) view.findViewById(R.id.linked_create_https_uri);
+
+ mEditUri.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ String uri = "https://" + editable;
+ if (uri.length() > 0) {
+ if (checkUri(uri)) {
+ mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0,
+ R.drawable.uid_mail_ok, 0);
+ } else {
+ mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0,
+ R.drawable.uid_mail_bad, 0);
+ }
+ } else {
+ // remove drawable if email is empty
+ mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
+ }
+ }
+ });
+
+ mEditUri.setText("mugenguild.com/pgpkey.txt");
+
+ return view;
+ }
+
+ private static boolean checkUri(String uri) {
+ return Patterns.WEB_URL.matcher(uri).matches();
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java
new file mode 100644
index 000000000..7d2f3dfdb
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.linked;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.graphics.PorterDuff;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Message;
+import android.os.Messenger;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult;
+import org.sufficientlysecure.keychain.operations.results.OperationResult;
+import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
+import org.sufficientlysecure.keychain.pgp.linked.LinkedIdentity;
+import org.sufficientlysecure.keychain.pgp.linked.RawLinkedIdentity;
+import org.sufficientlysecure.keychain.pgp.linked.resources.GenericHttpsResource;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
+import org.sufficientlysecure.keychain.ui.PassphraseDialogActivity;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+import org.sufficientlysecure.keychain.ui.util.Notify.Style;
+import org.sufficientlysecure.keychain.util.FileHelper;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+public class LinkedIdCreateHttpsStep2Fragment extends Fragment {
+
+ private static final int REQUEST_CODE_OUTPUT = 0x00007007;
+ private static final int REQUEST_CODE_PASSPHRASE = 0x00007008;
+
+ public static final String URI = "uri", NONCE = "nonce", TEXT = "text";
+
+ LinkedIdWizard mLinkedIdWizard;
+
+ EditText mEditUri;
+ ImageView mVerifyImage;
+ View mVerifyProgress;
+ TextView mVerifyStatus;
+
+ String mResourceUri;
+ int mResourceNonce;
+ String mResourceString;
+
+ // This is a resource, set AFTER it has been verified
+ GenericHttpsResource mVerifiedResource = null;
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static LinkedIdCreateHttpsStep2Fragment newInstance
+ (String uri, int proofNonce, String proofText) {
+
+ LinkedIdCreateHttpsStep2Fragment frag = new LinkedIdCreateHttpsStep2Fragment();
+
+ Bundle args = new Bundle();
+ args.putString(URI, uri);
+ args.putInt(NONCE, proofNonce);
+ args.putString(TEXT, proofText);
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.linked_create_https_fragment_step2, container, false);
+
+ mResourceUri = getArguments().getString(URI);
+ mResourceNonce = getArguments().getInt(NONCE);
+ mResourceString = getArguments().getString(TEXT);
+
+ view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startCertify();
+ }
+ });
+
+ view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
+ }
+ });
+
+ mVerifyImage = (ImageView) view.findViewById(R.id.verify_image);
+ mVerifyProgress = view.findViewById(R.id.verify_progress);
+ mVerifyStatus = (TextView) view.findViewById(R.id.verify_status);
+
+ view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofSend();
+ }
+ });
+
+ view.findViewById(R.id.button_save).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofSave();
+ }
+ });
+
+ view.findViewById(R.id.button_verify).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofVerify();
+ }
+ });
+
+ mEditUri = (EditText) view.findViewById(R.id.linked_create_https_uri);
+ mEditUri.setText(mResourceUri);
+
+ setVerifyProgress(false, null);
+ mVerifyStatus.setText(R.string.linked_verify_pending);
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mLinkedIdWizard = (LinkedIdWizard) getActivity();
+ }
+
+ public void setVerifyProgress(boolean on, Boolean success) {
+ mVerifyProgress.setVisibility(on ? View.VISIBLE : View.GONE);
+ mVerifyImage.setVisibility(on ? View.GONE : View.VISIBLE);
+ if (success == null) {
+ mVerifyStatus.setText(R.string.linked_verifying);
+ mVerifyImage.setImageResource(R.drawable.status_signature_unverified_cutout_24dp);
+ mVerifyImage.setColorFilter(getResources().getColor(R.color.tertiary_text_light),
+ PorterDuff.Mode.SRC_IN);
+ } else if (success) {
+ mVerifyStatus.setText(R.string.linked_verify_success);
+ mVerifyImage.setImageResource(R.drawable.status_signature_verified_cutout_24dp);
+ mVerifyImage.setColorFilter(getResources().getColor(R.color.android_green_dark),
+ PorterDuff.Mode.SRC_IN);
+ } else {
+ mVerifyStatus.setText(R.string.linked_verify_error);
+ mVerifyImage.setImageResource(R.drawable.status_signature_unknown_cutout_24dp);
+ mVerifyImage.setColorFilter(getResources().getColor(R.color.android_red_dark),
+ PorterDuff.Mode.SRC_IN);
+ }
+ }
+
+ private void proofSend () {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, mResourceString);
+ sendIntent.setType("text/plain");
+ startActivity(sendIntent);
+ }
+
+ private void proofSave () {
+ String state = Environment.getExternalStorageState();
+ if (!Environment.MEDIA_MOUNTED.equals(state)) {
+ Notify.showNotify(getActivity(), "External storage not available!", Style.ERROR);
+ return;
+ }
+
+ String targetName = "pgpkey.txt";
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ File targetFile = new File(Constants.Path.APP_DIR, targetName);
+ FileHelper.saveFile(this, getString(R.string.title_decrypt_to_file),
+ getString(R.string.specify_file_to_decrypt_to), targetFile, REQUEST_CODE_OUTPUT);
+ } else {
+ FileHelper.saveDocument(this, "text/plain", targetName, REQUEST_CODE_OUTPUT);
+ }
+ }
+
+ private void saveFile(Uri uri) {
+ try {
+ PrintWriter out =
+ new PrintWriter(getActivity().getContentResolver().openOutputStream(uri));
+ out.print(mResourceString);
+ if (out.checkError()) {
+ Notify.showNotify(getActivity(), "Error writing file!", Style.ERROR);
+ }
+ } catch (FileNotFoundException e) {
+ Notify.showNotify(getActivity(), "File could not be opened for writing!", Style.ERROR);
+ e.printStackTrace();
+ }
+ }
+
+ public void proofVerify() {
+ setVerifyProgress(true, null);
+
+ try {
+ final GenericHttpsResource resource = GenericHttpsResource.createNew(new URI(mResourceUri));
+
+ new AsyncTask<Void,Void,LinkedVerifyResult>() {
+
+ @Override
+ protected LinkedVerifyResult doInBackground(Void... params) {
+ return resource.verify(mLinkedIdWizard.mFingerprint, mResourceNonce);
+ }
+
+ @Override
+ protected void onPostExecute(LinkedVerifyResult result) {
+ super.onPostExecute(result);
+ if (result.success()) {
+ setVerifyProgress(false, true);
+ mVerifiedResource = resource;
+ } else {
+ setVerifyProgress(false, false);
+ // on error, show error message
+ result.createNotify(getActivity()).show();
+ }
+ }
+ }.execute();
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ public void startCertify() {
+
+ if (mVerifiedResource == null) {
+ Notify.showNotify(getActivity(), R.string.linked_need_verify, Notify.Style.ERROR);
+ return;
+ }
+
+ Intent intent = new Intent(getActivity(), PassphraseDialogActivity.class);
+ intent.putExtra(PassphraseDialogActivity.EXTRA_SUBKEY_ID, mLinkedIdWizard.mMasterKeyId);
+ startActivityForResult(intent, REQUEST_CODE_PASSPHRASE);
+
+ }
+
+ public void certifyLinkedIdentity (String passphrase) {
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(
+ getActivity(),
+ getString(R.string.progress_saving),
+ ProgressDialog.STYLE_HORIZONTAL,
+ true) {
+ public void handleMessage(Message message) {
+ // handle messages by standard KeychainIntentServiceHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+
+ // get returned data bundle
+ Bundle returnData = message.getData();
+ if (returnData == null) {
+ return;
+ }
+ final OperationResult result =
+ returnData.getParcelable(OperationResult.EXTRA_RESULT);
+ if (result == null) {
+ return;
+ }
+
+ // if bad -> display here!
+ if (!result.success()) {
+ result.createNotify(getActivity()).show();
+ return;
+ }
+
+ result.createNotify(getActivity()).show();
+
+ // if good -> finish, return result to showkey and display there!
+ // Intent intent = new Intent();
+ // intent.putExtra(OperationResult.EXTRA_RESULT, result);
+ // getActivity().setResult(EditKeyActivity.RESULT_OK, intent);
+
+ // AffirmationCreateHttpsStep3Fragment frag =
+ // AffirmationCreateHttpsStep3Fragment.newInstance(
+ // mResourceUri, mResourceNonce, mResourceString);
+
+ // mAffirmationWizard.loadFragment(null, frag, AffirmationWizard.FRAG_ACTION_TO_RIGHT);
+
+ }
+ }
+ };
+
+ SaveKeyringParcel skp =
+ new SaveKeyringParcel(mLinkedIdWizard.mMasterKeyId, mLinkedIdWizard.mFingerprint);
+
+ WrappedUserAttribute ua =
+ LinkedIdentity.fromResource(mVerifiedResource, mResourceNonce).toUserAttribute();
+
+ skp.mAddUserAttribute.add(ua);
+
+ // Send all information needed to service to import key in other thread
+ Intent intent = new Intent(getActivity(), KeychainIntentService.class);
+ intent.setAction(KeychainIntentService.ACTION_EDIT_KEYRING);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+ data.putString(KeychainIntentService.EDIT_KEYRING_PASSPHRASE, passphrase);
+ data.putParcelable(KeychainIntentService.EDIT_KEYRING_PARCEL, skp);
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(getActivity());
+
+ // start service with intent
+ getActivity().startService(intent);
+
+ }
+
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ // For saving a file
+ case REQUEST_CODE_OUTPUT:
+ if (data == null) {
+ return;
+ }
+ Uri uri = data.getData();
+ saveFile(uri);
+ break;
+ case REQUEST_CODE_PASSPHRASE:
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ String passphrase =
+ data.getStringExtra(PassphraseDialogActivity.MESSAGE_DATA_PASSPHRASE);
+ certifyLinkedIdentity(passphrase);
+ }
+ break;
+ }
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java
new file mode 100644
index 000000000..e966fd71f
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.linked;
+
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.linked.RawLinkedIdentity;
+import org.sufficientlysecure.keychain.pgp.linked.resources.TwitterResource;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class LinkedIdCreateTwitterStep1Fragment extends Fragment {
+
+ LinkedIdWizard mLinkedIdWizard;
+
+ EditText mEditHandle;
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static LinkedIdCreateTwitterStep1Fragment newInstance() {
+ LinkedIdCreateTwitterStep1Fragment frag = new LinkedIdCreateTwitterStep1Fragment();
+
+ Bundle args = new Bundle();
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mLinkedIdWizard = (LinkedIdWizard) getActivity();
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.linked_create_twitter_fragment_step1, container, false);
+
+ view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ final String handle = mEditHandle.getText().toString();
+
+ new AsyncTask<Void,Void,Boolean>() {
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ return true; // checkHandle(handle);
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ super.onPostExecute(result);
+
+ if (result == null) {
+ Notify.showNotify(getActivity(), "Connection error while checking username!", Notify.Style.ERROR);
+ return;
+ }
+
+ if (!result) {
+ Notify.showNotify(getActivity(), "This handle does not exist on Twitter!", Notify.Style.ERROR);
+ return;
+ }
+
+ int proofNonce = RawLinkedIdentity.generateNonce();
+ String proofText = TwitterResource.generateText(getActivity(),
+ mLinkedIdWizard.mFingerprint, proofNonce);
+
+ LinkedIdCreateTwitterStep2Fragment frag =
+ LinkedIdCreateTwitterStep2Fragment.newInstance(handle, proofNonce, proofText);
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+ }
+ }.execute();
+
+ }
+ });
+
+ view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
+ }
+ });
+
+ mEditHandle = (EditText) view.findViewById(R.id.linked_create_twitter_handle);
+ mEditHandle.setText("Valodim");
+
+ return view;
+ }
+
+ private static Boolean checkHandle(String handle) {
+ try {
+ HttpURLConnection nection =
+ (HttpURLConnection) new URL("https://twitter.com/" + handle).openConnection();
+ nection.setRequestMethod("HEAD");
+ return nection.getResponseCode() == 200;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep2Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep2Fragment.java
new file mode 100644
index 000000000..837b84d40
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep2Fragment.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.linked;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.linked.resources.TwitterResource;
+
+public class LinkedIdCreateTwitterStep2Fragment extends Fragment {
+
+ public static final String HANDLE = "uri", NONCE = "nonce", TEXT = "text";
+
+ LinkedIdWizard mLinkedIdWizard;
+
+ EditText mEditTweetCustom, mEditTweetPreview;
+ ImageView mVerifyImage;
+ View mVerifyProgress;
+ TextView mVerifyStatus, mEditTweetTextLen;
+
+ String mResourceHandle;
+ String mResourceNonce, mResourceString;
+ String mCookiePreview;
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static LinkedIdCreateTwitterStep2Fragment newInstance
+ (String handle, int proofNonce, String proofText) {
+
+ LinkedIdCreateTwitterStep2Fragment frag = new LinkedIdCreateTwitterStep2Fragment();
+
+ Bundle args = new Bundle();
+ args.putString(HANDLE, handle);
+ args.putInt(NONCE, proofNonce);
+ args.putString(TEXT, proofText);
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.linked_create_twitter_fragment_step2, container, false);
+
+ mCookiePreview = TwitterResource.generatePreview();
+
+ mResourceHandle = getArguments().getString(HANDLE);
+ mResourceNonce = getArguments().getString(NONCE);
+ mResourceString = getArguments().getString(TEXT);
+
+ view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ LinkedIdCreateTwitterStep3Fragment frag =
+ LinkedIdCreateTwitterStep3Fragment.newInstance(mResourceHandle,
+ mResourceNonce, mResourceString,
+ mEditTweetCustom.getText().toString());
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+ }
+ });
+
+ view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
+ }
+ });
+
+ mVerifyImage = (ImageView) view.findViewById(R.id.verify_image);
+ mVerifyProgress = view.findViewById(R.id.verify_progress);
+ mVerifyStatus = (TextView) view.findViewById(R.id.verify_status);
+
+ mEditTweetPreview = (EditText) view.findViewById(R.id.linked_create_twitter_preview);
+ mEditTweetPreview.setText(mCookiePreview);
+
+ mEditTweetCustom = (EditText) view.findViewById(R.id.linked_create_twitter_custom);
+ mEditTweetCustom.setFilters(new InputFilter[] {
+ new InputFilter.LengthFilter(139 - mResourceString.length())
+ });
+
+ mEditTweetTextLen = (TextView) view.findViewById(R.id.linked_create_twitter_textlen);
+ mEditTweetTextLen.setText(mResourceString.length() + "/140");
+
+ mEditTweetCustom.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ if (editable != null && editable.length() > 0) {
+ String str = editable + " " + mCookiePreview;
+ mEditTweetPreview.setText(str);
+
+ mEditTweetTextLen.setText(
+ (editable.length() + mResourceString.length() + 1) + "/140");
+ mEditTweetTextLen.setTextColor(getResources().getColor(str.length() == 140
+ ? R.color.android_red_dark
+ : R.color.primary_dark_material_light));
+
+
+ } else {
+ mEditTweetPreview.setText(mCookiePreview);
+ mEditTweetTextLen.setText(mResourceString.length() + "/140");
+ }
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mLinkedIdWizard = (LinkedIdWizard) getActivity();
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep3Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep3Fragment.java
new file mode 100644
index 000000000..d298e8251
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep3Fragment.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.linked;
+
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.PorterDuff;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+
+import java.util.List;
+
+public class LinkedIdCreateTwitterStep3Fragment extends Fragment {
+
+ public static final String HANDLE = "uri", NONCE = "nonce", TEXT = "text", CUSTOM = "custom";
+
+ LinkedIdWizard mLinkedIdWizard;
+
+ EditText mEditTweetPreview;
+ ImageView mVerifyImage;
+ View mVerifyProgress;
+ TextView mVerifyStatus;
+
+ String mResourceHandle, mCustom, mFullString;
+ String mResourceNonce, mResourceString;
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static LinkedIdCreateTwitterStep3Fragment newInstance
+ (String handle, String proofNonce, String proofText, String customText) {
+
+ LinkedIdCreateTwitterStep3Fragment frag = new LinkedIdCreateTwitterStep3Fragment();
+
+ Bundle args = new Bundle();
+ args.putString(HANDLE, handle);
+ args.putString(NONCE, proofNonce);
+ args.putString(TEXT, proofText);
+ args.putString(CUSTOM, customText);
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.linked_create_twitter_fragment_step3, container, false);
+
+ mResourceHandle = getArguments().getString(HANDLE);
+ mResourceNonce = getArguments().getString(NONCE);
+ mResourceString = getArguments().getString(TEXT);
+ mCustom = getArguments().getString(CUSTOM);
+
+ mFullString = mCustom.isEmpty() ? mResourceString : (mCustom + " " + mResourceString);
+
+ mVerifyImage = (ImageView) view.findViewById(R.id.verify_image);
+ mVerifyProgress = view.findViewById(R.id.verify_progress);
+ mVerifyStatus = (TextView) view.findViewById(R.id.verify_status);
+
+ mEditTweetPreview = (EditText) view.findViewById(R.id.linked_create_twitter_preview);
+ mEditTweetPreview.setText(mFullString);
+
+ view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
+ }
+ });
+
+ view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofSend();
+ }
+ });
+
+ view.findViewById(R.id.button_share).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofShare();
+ }
+ });
+
+ view.findViewById(R.id.button_verify).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofVerify();
+ }
+ });
+
+ setVerifyProgress(false, null);
+ mVerifyStatus.setText(R.string.linked_verify_pending);
+
+
+ view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ // AffirmationCreateHttpsStep2Fragment frag =
+ // AffirmationCreateHttpsStep2Fragment.newInstance();
+
+ // mAffirmationWizard.loadFragment(null, frag, AffirmationWizard.FRAG_ACTION_TO_RIGHT);
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mLinkedIdWizard = (LinkedIdWizard) getActivity();
+ }
+
+ public void setVerifyProgress(boolean on, Boolean success) {
+ mVerifyProgress.setVisibility(on ? View.VISIBLE : View.GONE);
+ mVerifyImage.setVisibility(on ? View.GONE : View.VISIBLE);
+ if (success == null) {
+ mVerifyStatus.setText(R.string.linked_verifying);
+ mVerifyImage.setImageResource(R.drawable.status_signature_unverified_cutout_24dp);
+ mVerifyImage.setColorFilter(getResources().getColor(R.color.tertiary_text_light),
+ PorterDuff.Mode.SRC_IN);
+ } else if (success) {
+ mVerifyStatus.setText(R.string.linked_verify_success);
+ mVerifyImage.setImageResource(R.drawable.status_signature_verified_cutout_24dp);
+ mVerifyImage.setColorFilter(getResources().getColor(R.color.android_green_dark),
+ PorterDuff.Mode.SRC_IN);
+ } else {
+ mVerifyStatus.setText(R.string.linked_verify_error);
+ mVerifyImage.setImageResource(R.drawable.status_signature_unknown_cutout_24dp);
+ mVerifyImage.setColorFilter(getResources().getColor(R.color.android_red_dark),
+ PorterDuff.Mode.SRC_IN);
+ }
+ }
+
+ public void proofVerify() {
+ setVerifyProgress(true, null);
+
+ /*
+ try {
+ final TwitterResource resource = TwitterResource.createNew(new URI(mResourceHandle));
+
+ new AsyncTask<Void,Void,LinkedVerifyResult>() {
+
+ @Override
+ protected LinkedVerifyResult doInBackground(Void... params) {
+ return resource.verify(mAffirmationWizard.mFingerprint, mResourceNonce);
+ }
+
+ @Override
+ protected void onPostExecute(LinkedVerifyResult result) {
+ super.onPostExecute(result);
+ if (result.success()) {
+ setVerifyProgress(false, true);
+ } else {
+ setVerifyProgress(false, false);
+ // on error, show error message
+ result.createNotify(getActivity()).show();
+ }
+ }
+ }.execute();
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ }
+ */
+
+ }
+
+ private void proofShare() {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, mFullString);
+ sendIntent.setType("text/plain");
+ startActivity(sendIntent);
+ }
+
+ private void proofSend() {
+
+ Intent tweetIntent = new Intent(Intent.ACTION_SEND);
+ tweetIntent.putExtra(Intent.EXTRA_TEXT, mFullString);
+ tweetIntent.setType("text/plain");
+
+ PackageManager packManager = getActivity().getPackageManager();
+ List<ResolveInfo> resolvedInfoList = packManager.queryIntentActivities(tweetIntent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+
+ boolean resolved = false;
+ for(ResolveInfo resolveInfo : resolvedInfoList){
+ if(resolveInfo.activityInfo.packageName.startsWith("com.twitter.android")) {
+ tweetIntent.setClassName(
+ resolveInfo.activityInfo.packageName,
+ resolveInfo.activityInfo.name );
+ resolved = true;
+ break;
+ }
+ }
+
+ if (resolved) {
+ startActivity(tweetIntent);
+ } else {
+ Notify.showNotify(getActivity(),
+ "Twitter app is not installed, please use the send intent!",
+ Notify.Style.ERROR);
+ }
+
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdSelectFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdSelectFragment.java
new file mode 100644
index 000000000..abe7dbaf1
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdSelectFragment.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.linked;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.sufficientlysecure.keychain.R;
+
+public class LinkedIdSelectFragment extends Fragment {
+
+ LinkedIdWizard mLinkedIdWizard;
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static LinkedIdSelectFragment newInstance() {
+ LinkedIdSelectFragment frag = new LinkedIdSelectFragment();
+
+ Bundle args = new Bundle();
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.linked_select_fragment, container, false);
+
+ view.findViewById(R.id.linked_create_https_button)
+ .setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LinkedIdCreateHttpsStep1Fragment frag =
+ LinkedIdCreateHttpsStep1Fragment.newInstance();
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+ }
+ });
+
+ view.findViewById(R.id.linked_create_dns_button)
+ .setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LinkedIdCreateDnsStep1Fragment frag =
+ LinkedIdCreateDnsStep1Fragment.newInstance();
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+ }
+ });
+
+ view.findViewById(R.id.linked_create_twitter_button)
+ .setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LinkedIdCreateTwitterStep1Fragment frag =
+ LinkedIdCreateTwitterStep1Fragment.newInstance();
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mLinkedIdWizard = (LinkedIdWizard) getActivity();
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdViewFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdViewFragment.java
new file mode 100644
index 000000000..0eb0dcd45
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdViewFragment.java
@@ -0,0 +1,45 @@
+package org.sufficientlysecure.keychain.ui.linked;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v7.widget.CardView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.linked.RawLinkedIdentity;
+
+
+public class LinkedIdViewFragment extends Fragment {
+
+ private CardView mLinkedIdsCard;
+
+ public static Fragment newInstance(RawLinkedIdentity id) {
+ LinkedIdViewFragment frag = new LinkedIdViewFragment();
+
+ Bundle args = new Bundle();
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) {
+ View root = inflater.inflate(R.layout.linked_id_view_fragment, null);
+
+ mLinkedIdsCard = (CardView) root.findViewById(R.id.card_linked_ids);
+
+ root.findViewById(R.id.back_button).setClickable(true);
+ root.findViewById(R.id.back_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ getFragmentManager().popBackStack();
+ }
+ });
+
+ return root;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdWizard.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdWizard.java
new file mode 100644
index 000000000..161efc8fb
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdWizard.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.linked;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v7.app.ActionBarActivity;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
+import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
+import org.sufficientlysecure.keychain.provider.KeychainContract;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.util.Log;
+
+public class LinkedIdWizard extends ActionBarActivity {
+
+ public static final int FRAG_ACTION_START = 0;
+ public static final int FRAG_ACTION_TO_RIGHT = 1;
+ public static final int FRAG_ACTION_TO_LEFT = 2;
+
+ long mMasterKeyId;
+ byte[] mFingerprint;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.create_key_activity);
+
+ try {
+ Uri uri = getIntent().getData();
+ uri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(uri);
+ CachedPublicKeyRing ring = new ProviderHelper(this).getCachedPublicKeyRing(uri);
+ if (!ring.hasAnySecret()) {
+ Log.e(Constants.TAG, "Linked Identities can only be added to secret keys!");
+ finish();
+ return;
+ }
+
+ mMasterKeyId = ring.extractOrGetMasterKeyId();
+ mFingerprint = ring.getFingerprint();
+ } catch (PgpKeyNotFoundException e) {
+ Log.e(Constants.TAG, "Invalid uri given, key does not exist!");
+ finish();
+ return;
+ }
+
+ // pass extras into fragment
+ LinkedIdSelectFragment frag = LinkedIdSelectFragment.newInstance();
+ loadFragment(null, frag, FRAG_ACTION_START);
+ }
+
+ public void loadFragment(Bundle savedInstanceState, Fragment fragment, int action) {
+ // However, if we're being restored from a previous state,
+ // then we don't need to do anything and should return or else
+ // we could end up with overlapping fragments.
+ if (savedInstanceState != null) {
+ return;
+ }
+
+ // Add the fragment to the 'fragment_container' FrameLayout
+ // NOTE: We use commitAllowingStateLoss() to prevent weird crashes!
+ FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
+
+ switch (action) {
+ case FRAG_ACTION_START:
+ transaction.setCustomAnimations(0, 0);
+ transaction.replace(R.id.create_key_fragment_container, fragment)
+ .commitAllowingStateLoss();
+ break;
+ case FRAG_ACTION_TO_LEFT:
+ getSupportFragmentManager().popBackStackImmediate();
+ break;
+ case FRAG_ACTION_TO_RIGHT:
+ transaction.setCustomAnimations(R.anim.frag_slide_in_from_right, R.anim.frag_slide_out_to_left,
+ R.anim.frag_slide_in_from_left, R.anim.frag_slide_out_to_right);
+ transaction.addToBackStack(null);
+ transaction.replace(R.id.create_key_fragment_container, fragment)
+ .commitAllowingStateLoss();
+ break;
+
+ }
+ // do it immediately!
+ getSupportFragmentManager().executePendingTransactions();
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/HttpsPrefixedText.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/HttpsPrefixedText.java
new file mode 100644
index 000000000..292343eb7
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/HttpsPrefixedText.java
@@ -0,0 +1,38 @@
+package org.sufficientlysecure.keychain.ui.widget;
+
+import android.content.Context;
+import android.graphics.*;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.widget.EditText;
+
+/** */
+public class HttpsPrefixedText extends EditText {
+
+ private String mPrefix; // can be hardcoded for demo purposes
+ private Rect mPrefixRect = new Rect();
+
+ public HttpsPrefixedText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mPrefix = "https://";
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ getPaint().getTextBounds(mPrefix, 0, mPrefix.length(), mPrefixRect);
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ protected void onDraw(@NonNull Canvas canvas) {
+ super.onDraw(canvas);
+ canvas.drawText(mPrefix, super.getCompoundPaddingLeft(), getBaseline(), getPaint());
+ }
+
+ @Override
+ public int getCompoundPaddingLeft() {
+ return super.getCompoundPaddingLeft() + mPrefixRect.width();
+ }
+
+} \ No newline at end of file