basic pgp support.

Daniel Gultsch created

Change summary

gen/de/gultsch/chat/R.java                              |  62 +-
res/layout/fragment_conversation.xml                    |   1 
res/layout/message_recieved.xml                         |   1 
res/layout/message_sent.xml                             |   1 
res/menu/manageaccounts_context.xml                     |   6 
res/values/strings.xml                                  |   2 
src/de/gultsch/chat/crypto/OtrEngine.java               |   2 
src/de/gultsch/chat/crypto/PgpEngine.java               | 148 ++++++++
src/de/gultsch/chat/entities/Account.java               |   9 
src/de/gultsch/chat/entities/Contact.java               |  20 +
src/de/gultsch/chat/entities/Message.java               |   6 
src/de/gultsch/chat/services/XmppConnectionService.java | 118 ++++++
src/de/gultsch/chat/ui/ConversationActivity.java        |  19 
src/de/gultsch/chat/ui/ConversationFragment.java        | 193 +++++++++-
src/de/gultsch/chat/ui/DialogContactDetails.java        |   2 
src/de/gultsch/chat/ui/ManageAccountActivity.java       |  30 +
src/de/gultsch/chat/utils/MessageParser.java            |  15 
17 files changed, 558 insertions(+), 77 deletions(-)

Detailed changes

gen/de/gultsch/chat/R.java 🔗

@@ -9,9 +9,9 @@ package de.gultsch.chat;
 
 public final class R {
     public static final class array {
-        public static final int conversation_encryption_type_entries=0x7f050000;
-        public static final int conversation_encryption_type_values=0x7f050001;
-        public static final int manage_account_options=0x7f050002;
+        public static final int conversation_encryption_type_entries=0x7f060000;
+        public static final int conversation_encryption_type_values=0x7f060001;
+        public static final int manage_account_options=0x7f060002;
     }
     public static final class attr {
     }
@@ -22,24 +22,25 @@ public final class R {
          screen margins) for sw720dp devices (e.g. 10" tablets) in landscape here.
     
          */
-        public static final int activity_horizontal_margin=0x7f060000;
-        public static final int activity_vertical_margin=0x7f060001;
+        public static final int activity_horizontal_margin=0x7f070000;
+        public static final int activity_vertical_margin=0x7f070001;
     }
     public static final class drawable {
         public static final int es_slidingpane_shadow=0x7f020000;
         public static final int ic_action_add=0x7f020001;
         public static final int ic_action_add_person=0x7f020002;
-        public static final int ic_action_delete=0x7f020003;
-        public static final int ic_action_refresh=0x7f020004;
-        public static final int ic_action_secure=0x7f020005;
-        public static final int ic_action_send=0x7f020006;
-        public static final int ic_action_send_now=0x7f020007;
-        public static final int ic_action_unsecure=0x7f020008;
-        public static final int ic_launcher=0x7f020009;
-        public static final int ic_profile=0x7f02000a;
-        public static final int message_border=0x7f02000b;
-        public static final int notification=0x7f02000c;
-        public static final int section_header=0x7f02000d;
+        public static final int ic_action_cancel_launchersize=0x7f020003;
+        public static final int ic_action_delete=0x7f020004;
+        public static final int ic_action_refresh=0x7f020005;
+        public static final int ic_action_secure=0x7f020006;
+        public static final int ic_action_send=0x7f020007;
+        public static final int ic_action_send_now=0x7f020008;
+        public static final int ic_action_unsecure=0x7f020009;
+        public static final int ic_launcher=0x7f02000a;
+        public static final int ic_profile=0x7f02000b;
+        public static final int message_border=0x7f02000c;
+        public static final int notification=0x7f02000d;
+        public static final int section_header=0x7f02000e;
     }
     public static final class id {
         public static final int account_confirm_password_desc=0x7f0a001c;
@@ -57,9 +58,10 @@ public final class R {
         public static final int action_add_account=0x7f0a0034;
         public static final int action_archive=0x7f0a002e;
         public static final int action_details=0x7f0a002d;
-        public static final int action_refresh_contacts=0x7f0a0038;
+        public static final int action_refresh_contacts=0x7f0a0039;
         public static final int action_security=0x7f0a002c;
         public static final int action_settings=0x7f0a0030;
+        public static final int announce_pgp=0x7f0a0038;
         public static final int contactList=0x7f0a0006;
         public static final int contact_display_name=0x7f0a0008;
         public static final int contact_jid=0x7f0a0009;
@@ -123,17 +125,21 @@ public final class R {
         public static final int newconversation=0x7f090004;
     }
     public static final class string {
-        public static final int action_accounts=0x7f070003;
-        public static final int action_add=0x7f070002;
-        public static final int action_add_account=0x7f070007;
-        public static final int action_archive=0x7f070004;
-        public static final int action_details=0x7f070005;
-        public static final int action_secure=0x7f070006;
-        public static final int action_settings=0x7f070001;
-        public static final int app_name=0x7f070000;
-        public static final int just_now=0x7f070009;
-        public static final int sending=0x7f07000a;
-        public static final int title_activity_new_conversation=0x7f070008;
+        public static final int action_accounts=0x7f050005;
+        public static final int action_add=0x7f050004;
+        public static final int action_add_account=0x7f050009;
+        public static final int action_archive=0x7f050006;
+        public static final int action_details=0x7f050007;
+        public static final int action_secure=0x7f050008;
+        public static final int action_settings=0x7f050003;
+        public static final int announce_pgp=0x7f05000d;
+        public static final int app_name=0x7f050002;
+        public static final int encrypted_message=0x7f05000e;
+        public static final int just_now=0x7f05000b;
+        public static final int openpgp_install_openkeychain_via=0x7f050001;
+        public static final int openpgp_list_preference_none=0x7f050000;
+        public static final int sending=0x7f05000c;
+        public static final int title_activity_new_conversation=0x7f05000a;
     }
     public static final class style {
         /** 

res/layout/message_recieved.xml 🔗

@@ -24,6 +24,7 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="Hi, how are you?"
+        android:autoLink="all"
         android:textSize="16sp"
         android:id="@+id/message_body"
         android:textColor="#333333"/>

res/layout/message_sent.xml 🔗

@@ -26,6 +26,7 @@
         android:text="Hi, how are you?"
         android:textSize="16sp"
         android:id="@+id/message_body"
+        android:autoLink="all"
         android:textColor="#333333"/>
     <TextView
         android:layout_width="wrap_content"

res/menu/manageaccounts_context.xml 🔗

@@ -15,5 +15,11 @@
         android:title="Enable"
         android:showAsAction="always"
         android:visible="false"/>
+    
+        <item
+        android:id="@+id/announce_pgp"
+        android:orderInCategory="75"
+        android:showAsAction="never"
+        android:title="@string/announce_pgp" />
 
 </menu>

res/values/strings.xml 🔗

@@ -12,4 +12,6 @@
     <string name="title_activity_new_conversation">New Conversation</string>
     <string name="just_now">just now</string>
     <string name="sending">sending&#8230;</string>
+    <string name="announce_pgp">Renew PGP announcement</string>
+    <string name="encrypted_message">This message is encrypted. Click to decrypt.</string>
 </resources>

src/de/gultsch/chat/crypto/OtrEngine.java 🔗

@@ -93,8 +93,6 @@ public class OtrEngine implements OtrEngineHost {
 			e.printStackTrace();
 		} catch (InvalidKeySpecException e) {
 			e.printStackTrace();
-		} catch (JSONException e) {
-			e.printStackTrace();
 		}
 		
 	}

src/de/gultsch/chat/crypto/PgpEngine.java 🔗

@@ -0,0 +1,148 @@
+package de.gultsch.chat.crypto;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+
+import org.openintents.openpgp.OpenPgpError;
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.openintents.openpgp.util.OpenPgpApi;
+import org.openintents.openpgp.util.OpenPgpConstants;
+
+import android.app.PendingIntent;
+import android.os.Bundle;
+import android.util.Log;
+
+public class PgpEngine {
+	private OpenPgpApi api;
+
+	public PgpEngine(OpenPgpApi api) {
+		this.api = api;
+	}
+
+	public String decrypt(String message) throws UserInputRequiredException,
+			OpenPgpException {
+		InputStream is = new ByteArrayInputStream(message.getBytes());
+		ByteArrayOutputStream os = new ByteArrayOutputStream();
+		Bundle result = api.decryptAndVerify(is, os);
+		switch (result.getInt(OpenPgpConstants.RESULT_CODE)) {
+		case OpenPgpConstants.RESULT_CODE_SUCCESS:
+			return os.toString();
+		case OpenPgpConstants.RESULT_CODE_USER_INTERACTION_REQUIRED:
+			throw new UserInputRequiredException(
+					(PendingIntent) result
+							.getParcelable(OpenPgpConstants.RESULT_INTENT));
+		case OpenPgpConstants.RESULT_CODE_ERROR:
+			throw new OpenPgpException(
+					(OpenPgpError) result
+							.getParcelable(OpenPgpConstants.RESULT_ERRORS));
+		default:
+			return null;
+		}
+	}
+
+	public String encrypt(long keyId, String message) {
+		Bundle params = new Bundle();
+		params.putBoolean(OpenPgpConstants.PARAMS_REQUEST_ASCII_ARMOR, true);
+		long[] keyIds = { keyId };
+		params.putLongArray(OpenPgpConstants.PARAMS_KEY_IDS, keyIds);
+
+		InputStream is = new ByteArrayInputStream(message.getBytes());
+		ByteArrayOutputStream os = new ByteArrayOutputStream();
+		Bundle result = api.encrypt(params, is, os);
+		StringBuilder encryptedMessageBody = new StringBuilder();
+		String[] lines = os.toString().split("\n");
+		for (int i = 3; i < lines.length - 1; ++i) {
+			encryptedMessageBody.append(lines[i].trim());
+		}
+		return encryptedMessageBody.toString();
+	}
+
+	public long fetchKeyId(String status, String signature)
+			throws OpenPgpException {
+		StringBuilder pgpSig = new StringBuilder();
+		pgpSig.append("-----BEGIN PGP SIGNED MESSAGE-----");
+		pgpSig.append('\n');
+		pgpSig.append("Hash: SHA1");
+		pgpSig.append('\n');
+		pgpSig.append('\n');
+		pgpSig.append(status);
+		pgpSig.append('\n');
+		pgpSig.append("-----BEGIN PGP SIGNATURE-----");
+		pgpSig.append('\n');
+		pgpSig.append('\n');
+		pgpSig.append(signature.replace("\n", "").trim());
+		pgpSig.append('\n');
+		pgpSig.append("-----END PGP SIGNATURE-----");
+		Bundle params = new Bundle();
+		params.putBoolean(OpenPgpConstants.PARAMS_REQUEST_ASCII_ARMOR, true);
+		InputStream is = new ByteArrayInputStream(pgpSig.toString().getBytes());
+		ByteArrayOutputStream os = new ByteArrayOutputStream();
+		Bundle result = api.decryptAndVerify(params, is, os);
+		switch (result.getInt(OpenPgpConstants.RESULT_CODE)) {
+		case OpenPgpConstants.RESULT_CODE_SUCCESS:
+			OpenPgpSignatureResult sigResult = result
+					.getParcelable(OpenPgpConstants.RESULT_SIGNATURE);
+			return sigResult.getKeyId();
+		case OpenPgpConstants.RESULT_CODE_USER_INTERACTION_REQUIRED:
+			break;
+		case OpenPgpConstants.RESULT_CODE_ERROR:
+			throw new OpenPgpException(
+					(OpenPgpError) result
+							.getParcelable(OpenPgpConstants.RESULT_ERRORS));
+		}
+		return 0;
+	}
+
+	public String generateSignature(String status)
+			throws UserInputRequiredException {
+		Bundle params = new Bundle();
+		params.putBoolean(OpenPgpConstants.PARAMS_REQUEST_ASCII_ARMOR, true);
+		InputStream is = new ByteArrayInputStream(status.getBytes());
+		ByteArrayOutputStream os = new ByteArrayOutputStream();
+		Bundle result = api.sign(params, is, os);
+		StringBuilder signatureBuilder = new StringBuilder();
+		switch (result.getInt(OpenPgpConstants.RESULT_CODE)) {
+		case OpenPgpConstants.RESULT_CODE_SUCCESS:
+			String[] lines = os.toString().split("\n");
+			for (int i = 7; i < lines.length - 1; ++i) {
+				signatureBuilder.append(lines[i].trim());
+			}
+			break;
+		case OpenPgpConstants.RESULT_CODE_USER_INTERACTION_REQUIRED:
+			UserInputRequiredException exception = new UserInputRequiredException(
+					(PendingIntent) result
+							.getParcelable(OpenPgpConstants.RESULT_INTENT));
+			throw exception;
+		case OpenPgpConstants.RESULT_CODE_ERROR:
+			break;
+		}
+		return signatureBuilder.toString();
+	}
+
+	public class UserInputRequiredException extends Exception {
+		private static final long serialVersionUID = -6913480043269132016L;
+		private PendingIntent pi;
+
+		public UserInputRequiredException(PendingIntent pi) {
+			this.pi = pi;
+		}
+
+		public PendingIntent getPendingIntent() {
+			return this.pi;
+		}
+	}
+
+	public class OpenPgpException extends Exception {
+		private static final long serialVersionUID = -7324789703473056077L;
+		private OpenPgpError error;
+
+		public OpenPgpException(OpenPgpError openPgpError) {
+			this.error = openPgpError;
+		}
+
+		public OpenPgpError getOpenPgpError() {
+			return this.error;
+		}
+	}
+}

src/de/gultsch/chat/entities/Account.java 🔗

@@ -137,8 +137,13 @@ public class Account  extends AbstractEntity{
 		return keys;
 	}
 	
-	public void setKey(String keyName, String keyValue) throws JSONException {
-		this.keys.put(keyName, keyValue);
+	public boolean setKey(String keyName, String keyValue) {
+		try {
+			this.keys.put(keyName, keyValue);
+			return true;
+		} catch (JSONException e) {
+			return false;
+		}
 	}
 
 	@Override

src/de/gultsch/chat/entities/Contact.java 🔗

@@ -221,6 +221,26 @@ public class Contact extends AbstractEntity implements Serializable {
 		}
 	}
 	
+	public void setPgpKeyId(long keyId) {
+		try {
+			this.keys.put("pgp_keyid", keyId);
+		} catch (JSONException e) {
+			
+		}
+	}
+	
+	public long getPgpKeyId() {
+		if (this.keys.has("pgp_keyid")) {
+			try {
+				return this.keys.getLong("pgp_keyid");
+			} catch (JSONException e) {
+				return 0;
+			}
+		} else {
+			return 0;
+		}
+	}
+	
 	public void setSubscriptionOption(int option) {
 		this.subscription |= 1 << option;
 	}

src/de/gultsch/chat/entities/Message.java 🔗

@@ -17,6 +17,7 @@ public class Message extends AbstractEntity {
 	public static final int ENCRYPTION_NONE = 0;
 	public static final int ENCRYPTION_PGP = 1;
 	public static final int ENCRYPTION_OTR = 2;
+	public static final int ENCRYPTION_DECRYPTED = 3;
 
 	public static String CONVERSATION = "conversationUuid";
 	public static String COUNTERPART = "counterpart";
@@ -136,5 +137,8 @@ public class Message extends AbstractEntity {
 	public void setEncryption(int encryption) {
 		this.encryption = encryption;
 	}
-	
+
+	public void setBody(String body) {
+		this.body = body;
+	}
 }

src/de/gultsch/chat/services/XmppConnectionService.java 🔗

@@ -1,5 +1,16 @@
 package de.gultsch.chat.services;
 
+import java.io.BufferedOutputStream;
+import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
@@ -8,11 +19,21 @@ import java.util.Hashtable;
 import java.util.List;
 import java.util.Set;
 
+import org.json.JSONException;
+import org.openintents.openpgp.IOpenPgpService;
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.openintents.openpgp.util.OpenPgpApi;
+import org.openintents.openpgp.util.OpenPgpServiceConnection;
+import org.openintents.openpgp.OpenPgpError;
+
 import net.java.otr4j.OtrException;
 import net.java.otr4j.session.Session;
 import net.java.otr4j.session.SessionImpl;
 import net.java.otr4j.session.SessionStatus;
 
+import de.gultsch.chat.crypto.PgpEngine;
+import de.gultsch.chat.crypto.PgpEngine.OpenPgpException;
+import de.gultsch.chat.crypto.PgpEngine.UserInputRequiredException;
 import de.gultsch.chat.entities.Account;
 import de.gultsch.chat.entities.Contact;
 import de.gultsch.chat.entities.Conversation;
@@ -46,7 +67,9 @@ import android.database.DatabaseUtils;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
 import android.os.PowerManager;
+import android.os.RemoteException;
 import android.preference.PreferenceManager;
 import android.provider.ContactsContract;
 import android.util.Log;
@@ -84,7 +107,12 @@ public class XmppConnectionService extends Service {
 			Message message = null;
 			boolean notify = false;
 			if ((packet.getType() == MessagePacket.TYPE_CHAT)) {
-				if (packet.hasChild("body")
+				String pgpBody = MessageParser.getPgpBody(packet);
+				if (pgpBody != null) {
+					message = MessageParser.parsePgpChat(pgpBody, packet,
+							account, service);
+					notify = false;
+				} else if (packet.hasChild("body")
 						&& (packet.getBody().startsWith("?OTR"))) {
 					message = MessageParser.parseOtrChat(packet, account,
 							service);
@@ -164,6 +192,13 @@ public class XmppConnectionService extends Service {
 				if (convChangedListener != null) {
 					convChangedListener.onConversationListChanged();
 				}
+				if (account.getKeys().has("pgp_signature")) {
+					try {
+						sendPgpPresence(account, account.getKeys().getString("pgp_signature"));
+					} catch (JSONException e) {
+						//
+					}
+				}
 			}
 		}
 	};
@@ -195,6 +230,18 @@ public class XmppConnectionService extends Service {
 				} else if (show.getContent().equals("dnd")) {
 					contact.updatePresence(fromParts[1], Presences.DND);
 				}
+				Element x = packet.findChild("x");
+				if ((x != null)
+						&& (x.getAttribute("xmlns").equals("jabber:x:signed"))) {
+					try {
+						Log.d(LOGTAG,"pgp signature for contact" +packet.getAttribute("from"));
+						contact.setPgpKeyId(getPgpEngine().fetchKeyId(packet.findChild("status")
+								.getContent(), x.getContent()));
+						databaseBackend.updateContact(contact);
+					} catch (OpenPgpException e) {
+						Log.d(LOGTAG,"faulty pgp. just ignore");
+					}
+				}
 				databaseBackend.updateContact(contact);
 			} else if (type.equals("unavailable")) {
 				if (fromParts.length != 2) {
@@ -242,6 +289,23 @@ public class XmppConnectionService extends Service {
 		}
 	};
 
+	private OpenPgpServiceConnection pgpServiceConnection;
+	private PgpEngine mPgpEngine = null;
+
+	public PgpEngine getPgpEngine() {
+		if (pgpServiceConnection.isBound()) {
+			if (this.mPgpEngine == null) {
+				this.mPgpEngine = new PgpEngine(new OpenPgpApi(
+						getApplicationContext(),
+						pgpServiceConnection.getService()));
+			}
+			return mPgpEngine;
+		} else {
+			return null;
+		}
+
+	}
+
 	private void processRosterItems(Account account, Element elements) {
 		for (Element item : elements.getChildren()) {
 			if (item.getName().equals("item")) {
@@ -307,6 +371,9 @@ public class XmppConnectionService extends Service {
 
 		getContentResolver().registerContentObserver(
 				ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
+		this.pgpServiceConnection = new OpenPgpServiceConnection(
+				getApplicationContext(), "org.sufficientlysecure.keychain");
+		this.pgpServiceConnection.bindToService();
 	}
 
 	@Override
@@ -332,7 +399,8 @@ public class XmppConnectionService extends Service {
 		return connection;
 	}
 
-	public void sendMessage(Account account, Message message, String presence) {
+	public void sendMessage(Message message, String presence) {
+		Account account = message.getConversation().getAccount();
 		Conversation conv = message.getConversation();
 		boolean saveInDb = false;
 		boolean addToConversation = false;
@@ -352,6 +420,25 @@ public class XmppConnectionService extends Service {
 				}
 				saveInDb = true;
 				addToConversation = true;
+			} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+				long keyId = message.getConversation().getContact()
+						.getPgpKeyId();
+				packet = new MessagePacket();
+				packet.setType(MessagePacket.TYPE_CHAT);
+				packet.setFrom(message.getConversation().getAccount()
+						.getFullJid());
+				packet.setTo(message.getCounterpart());
+				packet.setBody("This is an XEP-0027 encryted message");
+				Element x = new Element("x");
+				x.setAttribute("xmlns", "jabber:x:encrypted");
+				x.setContent(this.getPgpEngine().encrypt(keyId,
+						message.getBody()));
+				packet.addChild(x);
+				account.getXmppConnection().sendMessagePacket(packet);
+				message.setStatus(Message.STATUS_SEND);
+				message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+				saveInDb = true;
+				addToConversation = true;
 			} else {
 				// don't encrypt
 				if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
@@ -777,6 +864,10 @@ public class XmppConnectionService extends Service {
 		databaseBackend.updateContact(contact);
 	}
 
+	public void updateMessage(Message message) {
+		databaseBackend.updateMessage(message);
+	}
+
 	public void createContact(Contact contact) {
 		SharedPreferences sharedPref = PreferenceManager
 				.getDefaultSharedPreferences(getApplicationContext());
@@ -841,4 +932,27 @@ public class XmppConnectionService extends Service {
 		Log.d(LOGTAG, packet.toString());
 		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
 	}
+	
+	public void sendPgpPresence(Account account, String signature) {
+		PresencePacket packet = new PresencePacket();
+		packet.setAttribute("from", account.getFullJid());
+		Element status = new Element("status");
+		status.setContent("online");
+		packet.addChild(status);
+		Element x = new Element("x");
+		x.setAttribute("xmlns", "jabber:x:signed");
+		x.setContent(signature);
+		packet.addChild(x);
+		account.getXmppConnection().sendPresencePacket(packet);
+	}
+
+	public void generatePgpAnnouncement(Account account)
+			throws PgpEngine.UserInputRequiredException {
+		if (account.getStatus() == Account.STATUS_ONLINE) {
+			String signature = getPgpEngine().generateSignature("online");
+			account.setKey("pgp_signature", signature);
+			databaseBackend.updateAccount(account);
+			sendPgpPresence(account, signature);
+		}
+	}
 }

src/de/gultsch/chat/ui/ConversationActivity.java 🔗

@@ -5,8 +5,13 @@ import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.openintents.openpgp.util.OpenPgpConstants;
+
 import de.gultsch.chat.R;
 import de.gultsch.chat.R.id;
+import de.gultsch.chat.crypto.PgpEngine;
+import de.gultsch.chat.crypto.PgpEngine.UserInputRequiredException;
 import de.gultsch.chat.entities.Account;
 import de.gultsch.chat.entities.Contact;
 import de.gultsch.chat.entities.Conversation;
@@ -20,6 +25,7 @@ import android.app.NotificationManager;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.content.IntentSender.SendIntentException;
 import android.graphics.Typeface;
 import android.support.v4.widget.SlidingPaneLayout;
 import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener;
@@ -45,7 +51,8 @@ public class ConversationActivity extends XmppActivity {
 	public static final String VIEW_CONVERSATION = "viewConversation";
 	public static final String CONVERSATION = "conversationUuid";
 	
-	public static final int INSERT_CONTACT = 0x9889;
+	public static final int REQUEST_SEND_MESSAGE = 0x75441;
+	public static final int REQUEST_DECRYPT_PGP = 0x76783;
 
 	protected SlidingPaneLayout spl;
 
@@ -354,6 +361,9 @@ public class ConversationActivity extends XmppActivity {
 				case Message.ENCRYPTION_PGP:
 					popup.getMenu().findItem(R.id.encryption_choice_pgp).setChecked(true);
 					break;
+				case Message.ENCRYPTION_DECRYPTED:
+					popup.getMenu().findItem(R.id.encryption_choice_pgp).setChecked(true);
+					break;
 				default:
 					popup.getMenu().findItem(R.id.encryption_choice_none).setChecked(true);
 					break;
@@ -459,11 +469,4 @@ public class ConversationActivity extends XmppActivity {
 			}
 		}
 	}
-	
-	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-		if (requestCode==INSERT_CONTACT) {
-			Log.d("xmppService","contact inserted");
-			this.contactInserted  = true;
-		}
-	}
 }

src/de/gultsch/chat/ui/ConversationFragment.java 🔗

@@ -3,17 +3,16 @@ package de.gultsch.chat.ui;
 import java.io.FileNotFoundException;
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Hashtable;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
 
-import javax.crypto.spec.PSource;
-
-import net.java.otr4j.OtrException;
 import net.java.otr4j.session.SessionStatus;
 
 import de.gultsch.chat.R;
+import de.gultsch.chat.crypto.PgpEngine.OpenPgpException;
+import de.gultsch.chat.crypto.PgpEngine.UserInputRequiredException;
 import de.gultsch.chat.entities.Contact;
 import de.gultsch.chat.entities.Conversation;
 import de.gultsch.chat.entities.Message;
@@ -23,18 +22,20 @@ import de.gultsch.chat.utils.UIHelper;
 import android.app.AlertDialog;
 import android.app.Fragment;
 import android.content.DialogInterface;
+import android.content.IntentSender;
 import android.content.SharedPreferences;
+import android.content.IntentSender.SendIntentException;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Typeface;
 import android.net.Uri;
+import android.os.AsyncTask;
 import android.os.Bundle;
 import android.preference.PreferenceManager;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.OnClickListener;
-import android.view.View.OnLayoutChangeListener;
 import android.view.ViewGroup;
 import android.widget.ArrayAdapter;
 import android.widget.EditText;
@@ -42,6 +43,7 @@ import android.widget.LinearLayout;
 import android.widget.ListView;
 import android.widget.ImageButton;
 import android.widget.ImageView;
+import android.widget.ProgressBar;
 import android.widget.TextView;
 
 public class ConversationFragment extends Fragment {
@@ -54,27 +56,45 @@ public class ConversationFragment extends Fragment {
 	protected Contact contact;
 	protected BitmapCache mBitmapCache = new BitmapCache();
 
+	protected String queuedPqpMessage = null;
+
 	private EditText chatMsg;
-	
+
 	protected Bitmap selfBitmap;
+	
+	private IntentSender askForPassphraseIntent = null;
 
 	private OnClickListener sendMsgListener = new OnClickListener() {
 
 		@Override
 		public void onClick(View v) {
-			ConversationActivity activity = (ConversationActivity) getActivity();
-			final XmppConnectionService xmppService = activity.xmppConnectionService;
 			if (chatMsg.getText().length() < 1)
 				return;
 			Message message = new Message(conversation, chatMsg.getText()
 					.toString(), conversation.nextMessageEncryption);
 			if (conversation.nextMessageEncryption == Message.ENCRYPTION_OTR) {
 				sendOtrMessage(message);
+			} else if (conversation.nextMessageEncryption == Message.ENCRYPTION_PGP) {
+				sendPgpMessage(message);
 			} else {
 				sendPlainTextMessage(message);
 			}
 		}
 	};
+	protected OnClickListener clickToDecryptListener = new OnClickListener() {
+		
+		@Override
+		public void onClick(View v) {
+			Log.d("gultsch","clicked to decrypt");
+			if (askForPassphraseIntent!=null) {
+				try {
+					getActivity().startIntentSenderForResult(askForPassphraseIntent, ConversationActivity.REQUEST_DECRYPT_PGP, null, 0, 0, 0);
+				} catch (SendIntentException e) {
+					Log.d("gultsch","couldnt fire intent");
+				}
+			}
+		}
+	};
 
 	public void updateChatMsgHint() {
 		if (conversation.getMode() == Conversation.MODE_MULTI) {
@@ -89,6 +109,10 @@ public class ConversationFragment extends Fragment {
 				break;
 			case Message.ENCRYPTION_PGP:
 				chatMsg.setHint("Send openPGP encryted messeage");
+				break;
+			case Message.ENCRYPTION_DECRYPTED:
+				chatMsg.setHint("Send openPGP encryted messeage");
+				break;
 			default:
 				break;
 			}
@@ -107,9 +131,9 @@ public class ConversationFragment extends Fragment {
 		ImageButton sendButton = (ImageButton) view
 				.findViewById(R.id.textSendButton);
 		sendButton.setOnClickListener(this.sendMsgListener);
-
-		messagesView = (ListView) view.findViewById(R.id.messages_view);
 		
+		messagesView = (ListView) view.findViewById(R.id.messages_view);
+
 		messageListAdapter = new ArrayAdapter<Message>(this.getActivity()
 				.getApplicationContext(), R.layout.message_sent,
 				this.messageList) {
@@ -155,23 +179,28 @@ public class ConversationFragment extends Fragment {
 						viewHolder.imageView = (ImageView) view
 								.findViewById(R.id.message_photo);
 						if (item.getConversation().getMode() == Conversation.MODE_SINGLE) {
-							Uri uri = item.getConversation().getProfilePhotoUri();
+							Uri uri = item.getConversation()
+									.getProfilePhotoUri();
 							if (uri != null) {
 								viewHolder.imageView
 										.setImageBitmap(mBitmapCache.get(item
-												.getConversation().getName(), uri));
+												.getConversation().getName(),
+												uri));
 							} else {
 								viewHolder.imageView
 										.setImageBitmap(mBitmapCache.get(item
-												.getConversation().getName(), null));
+												.getConversation().getName(),
+												null));
 							}
 						}
 						break;
 					case ERROR:
-						view = (View) inflater.inflate(R.layout.message_error, null);
+						view = (View) inflater.inflate(R.layout.message_error,
+								null);
 						viewHolder.imageView = (ImageView) view
 								.findViewById(R.id.message_photo);
-						viewHolder.imageView.setImageBitmap(mBitmapCache.getError());
+						viewHolder.imageView.setImageBitmap(mBitmapCache
+								.getError());
 						break;
 					default:
 						viewHolder = null;
@@ -186,7 +215,7 @@ public class ConversationFragment extends Fragment {
 					viewHolder = (ViewHolder) view.getTag();
 				}
 				if (type == RECIEVED) {
-					 if (item.getConversation().getMode() == Conversation.MODE_MULTI) {
+					if (item.getConversation().getMode() == Conversation.MODE_MULTI) {
 						if (item.getCounterpart() != null) {
 							viewHolder.imageView.setImageBitmap(mBitmapCache
 									.get(item.getCounterpart(), null));
@@ -199,7 +228,15 @@ public class ConversationFragment extends Fragment {
 				}
 				String body = item.getBody();
 				if (body != null) {
-					viewHolder.messageBody.setText(body.trim());
+					if (item.getEncryption() == Message.ENCRYPTION_PGP) {
+						viewHolder.messageBody.setText(getString(R.string.encrypted_message));
+						viewHolder.messageBody.setTextColor(0xff33B5E5);
+						viewHolder.messageBody.setOnClickListener(clickToDecryptListener);
+					} else {
+						viewHolder.messageBody.setText(body.trim());
+						viewHolder.messageBody.setTextColor(0xff000000);
+						viewHolder.messageBody.setOnClickListener(null);
+					}
 				}
 				if (item.getStatus() == Message.STATUS_UNSEND) {
 					viewHolder.time.setTypeface(null, Typeface.ITALIC);
@@ -236,7 +273,7 @@ public class ConversationFragment extends Fragment {
 
 		if (showPhoneSelfContactPicture) {
 			Uri selfiUri = PhoneHelper.getSefliUri(getActivity());
-			if (selfiUri!=null) {
+			if (selfiUri != null) {
 				try {
 					self = BitmapFactory.decodeStream(getActivity()
 							.getContentResolver().openInputStream(selfiUri));
@@ -245,7 +282,7 @@ public class ConversationFragment extends Fragment {
 				}
 			}
 		}
-		if (self==null){
+		if (self == null) {
 			self = UIHelper.getUnknownContactPicture(conversation.getAccount()
 					.getJid(), 200);
 		}
@@ -280,17 +317,38 @@ public class ConversationFragment extends Fragment {
 				activity.updateConversationList();
 			}
 		}
+		if (queuedPqpMessage != null) {
+			this.conversation.nextMessageEncryption = Message.ENCRYPTION_PGP;
+			Message message = new Message(conversation, queuedPqpMessage,
+					Message.ENCRYPTION_PGP);
+			sendPgpMessage(message);
+		}
 	}
 
 	public void updateMessages() {
 		ConversationActivity activity = (ConversationActivity) getActivity();
+		List<Message> encryptedMessages = new LinkedList<Message>();
+		for(Message message : this.conversation.getMessages()) {
+			if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+				encryptedMessages.add(message);
+			}
+		}
+		if (encryptedMessages.size() > 0) {
+			DecryptMessage task = new DecryptMessage();
+			Message[] msgs = new Message[encryptedMessages.size()];
+			task.execute(encryptedMessages.toArray(msgs));
+		}
 		this.messageList.clear();
 		this.messageList.addAll(this.conversation.getMessages());
 		this.messageListAdapter.notifyDataSetChanged();
 		if (messageList.size() >= 1) {
 			int latestEncryption = this.conversation.getLatestMessage()
 					.getEncryption();
-			conversation.nextMessageEncryption = latestEncryption;
+			if (latestEncryption== Message.ENCRYPTION_DECRYPTED) {
+				conversation.nextMessageEncryption = Message.ENCRYPTION_PGP;
+			} else {
+				conversation.nextMessageEncryption = latestEncryption;
+			}
 			makeFingerprintWarning(latestEncryption);
 		}
 		getActivity().invalidateOptionsMenu();
@@ -339,17 +397,47 @@ public class ConversationFragment extends Fragment {
 
 	protected void sendPlainTextMessage(Message message) {
 		ConversationActivity activity = (ConversationActivity) getActivity();
-		activity.xmppConnectionService.sendMessage(conversation.getAccount(),
-				message, null);
+		activity.xmppConnectionService.sendMessage(message, null);
 		chatMsg.setText("");
 	}
 
+	protected void sendPgpMessage(final Message message) {
+		ConversationActivity activity = (ConversationActivity) getActivity();
+		final XmppConnectionService xmppService = activity.xmppConnectionService;
+		Contact contact = message.getConversation().getContact();
+		if (contact.getPgpKeyId() != 0) {
+			xmppService.sendMessage(message, null);
+			chatMsg.setText("");
+		} else {
+			AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+			builder.setTitle("No openPGP key found");
+			builder.setIconAttribute(android.R.attr.alertDialogIcon);
+			builder.setMessage("There is no openPGP key assoziated with this contact");
+			builder.setNegativeButton("Cancel", null);
+			builder.setPositiveButton("Send plain text",
+					new DialogInterface.OnClickListener() {
+
+						@Override
+						public void onClick(DialogInterface dialog, int which) {
+							conversation.nextMessageEncryption = Message.ENCRYPTION_NONE;
+							message.setEncryption(Message.ENCRYPTION_NONE);
+							xmppService.sendMessage(message, null);
+							chatMsg.setText("");
+						}
+					});
+			builder.create().show();
+		}
+	}
+
+	public void resendPgpMessage(String msg) {
+		this.queuedPqpMessage = msg;
+	}
+
 	protected void sendOtrMessage(final Message message) {
 		ConversationActivity activity = (ConversationActivity) getActivity();
 		final XmppConnectionService xmppService = activity.xmppConnectionService;
 		if (conversation.hasValidOtrSession()) {
-			activity.xmppConnectionService.sendMessage(
-					conversation.getAccount(), message, null);
+			activity.xmppConnectionService.sendMessage(message, null);
 			chatMsg.setText("");
 		} else {
 			Hashtable<String, Integer> presences;
@@ -372,17 +460,15 @@ public class ConversationFragment extends Fragment {
 									int which) {
 								conversation.nextMessageEncryption = Message.ENCRYPTION_NONE;
 								message.setEncryption(Message.ENCRYPTION_NONE);
-								xmppService.sendMessage(
-										conversation.getAccount(), message,
-										null);
+								xmppService.sendMessage(message, null);
 								chatMsg.setText("");
 							}
 						});
 				builder.setNegativeButton("Cancel", null);
 				builder.create().show();
 			} else if (presences.size() == 1) {
-				xmppService.sendMessage(conversation.getAccount(), message,
-						(String) presences.keySet().toArray()[0]);
+				xmppService.sendMessage(message, (String) presences.keySet()
+						.toArray()[0]);
 				chatMsg.setText("");
 			} else {
 				AlertDialog.Builder builder = new AlertDialog.Builder(
@@ -396,8 +482,7 @@ public class ConversationFragment extends Fragment {
 							@Override
 							public void onClick(DialogInterface dialog,
 									int which) {
-								xmppService.sendMessage(
-										conversation.getAccount(), message,
+								xmppService.sendMessage(message,
 										presencesArray[which]);
 								chatMsg.setText("");
 							}
@@ -438,7 +523,7 @@ public class ConversationFragment extends Fragment {
 				return bm;
 			}
 		}
-		
+
 		public Bitmap getError() {
 			if (error == null) {
 				error = UIHelper.getErrorPicture(200);
@@ -446,4 +531,48 @@ public class ConversationFragment extends Fragment {
 			return error;
 		}
 	}
+	
+	class DecryptMessage extends AsyncTask<Message, Void, Boolean> {
+
+		@Override
+		protected Boolean doInBackground(Message... params) {
+			XmppActivity activity = (XmppActivity) getActivity();
+			askForPassphraseIntent = null;
+			for(int i = 0; i < params.length; ++i) {
+				if (params[i].getEncryption() == Message.ENCRYPTION_PGP) {
+					String body = params[i].getBody();
+					String decrypted = null;
+					try {
+						if (activity==null) {
+							return false;
+						}
+						Log.d("gultsch","calling to decrypt message id #"+params[i].getUuid());
+						decrypted = activity.xmppConnectionService.getPgpEngine().decrypt(body);
+					} catch (UserInputRequiredException e) {
+						askForPassphraseIntent = e.getPendingIntent().getIntentSender();
+						return false;
+		
+					} catch (OpenPgpException e) {
+						Log.d("gultsch","error decrypting pgp");
+					}
+					if (decrypted!=null) {
+						params[i].setBody(decrypted);
+						params[i].setEncryption(Message.ENCRYPTION_DECRYPTED);
+						activity.xmppConnectionService.updateMessage(params[i]);
+					}
+					if (activity!=null) {
+						activity.runOnUiThread(new Runnable() {
+							
+							@Override
+							public void run() {
+								messageListAdapter.notifyDataSetChanged();
+							}
+						});
+					}
+				}
+			}
+			return true;
+		}
+		
+	}
 }

src/de/gultsch/chat/ui/DialogContactDetails.java 🔗

@@ -63,7 +63,7 @@ public class DialogContactDetails extends DialogFragment {
 			intent.putExtra(Intents.Insert.IM_HANDLE,contact.getJid());
 			intent.putExtra(Intents.Insert.IM_PROTOCOL,CommonDataKinds.Im.PROTOCOL_JABBER);
 			intent.putExtra("finishActivityOnSaveCompleted", true);
-			getActivity().startActivityForResult(intent,ConversationActivity.INSERT_CONTACT);
+			getActivity().startActivityForResult(intent,0);
 			mDetailsDialog.dismiss();
 		}
 	};

src/de/gultsch/chat/ui/ManageAccountActivity.java 🔗

@@ -4,6 +4,8 @@ import java.util.ArrayList;
 import java.util.List;
 
 import de.gultsch.chat.R;
+import de.gultsch.chat.crypto.PgpEngine;
+import de.gultsch.chat.crypto.PgpEngine.UserInputRequiredException;
 import de.gultsch.chat.entities.Account;
 import de.gultsch.chat.ui.EditAccount.EditAccountListener;
 import android.app.Activity;
@@ -12,6 +14,7 @@ import android.content.Context;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
+import android.content.IntentSender.SendIntentException;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.ActionMode;
@@ -31,6 +34,8 @@ import android.widget.TextView;
 
 public class ManageAccountActivity extends XmppActivity implements ActionMode.Callback {
 
+	public static final int REQUEST_ANNOUNCE_PGP = 0x73731;
+	
 	protected boolean isActionMode = false;
 	protected ActionMode actionMode;
 	protected Account selectedAccountForActionMode = null;
@@ -231,6 +236,17 @@ public class ManageAccountActivity extends XmppActivity implements ActionMode.Ca
 			});
 			builder.setNegativeButton("Cancel",null);
 			builder.create().show();
+		} else if (item.getItemId()==R.id.announce_pgp) {
+			mode.finish();
+			try {
+				xmppConnectionService.generatePgpAnnouncement(selectedAccountForActionMode);
+			} catch (PgpEngine.UserInputRequiredException e) {
+				try {
+					startIntentSenderForResult(e.getPendingIntent().getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
+				} catch (SendIntentException e1) {
+					Log.d("gultsch","sending intent failed");
+				}
+			}
 		}
 		return true;
 	}
@@ -279,4 +295,18 @@ public class ManageAccountActivity extends XmppActivity implements ActionMode.Ca
             }
         });
 	}
+	
+	 @Override
+	 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+		 super.onActivityResult(requestCode, resultCode, data);
+		 if (resultCode == RESULT_OK) {
+			if (requestCode == REQUEST_ANNOUNCE_PGP) {
+				 try {
+					xmppConnectionService.generatePgpAnnouncement(selectedAccountForActionMode);
+				} catch (UserInputRequiredException e) {
+					Log.d("gultsch","already came back. ignoring");
+				}
+			 }
+		 }
+	 }
 }

src/de/gultsch/chat/utils/MessageParser.java 🔗

@@ -23,6 +23,12 @@ public class MessageParser {
 		return new Message(conversation, packet.getFrom(), body, Message.ENCRYPTION_NONE, Message.STATUS_RECIEVED);
 	}
 	
+	public static Message parsePgpChat(String pgpBody, MessagePacket packet, Account account, XmppConnectionService service) {
+		String[] fromParts = packet.getFrom().split("/");
+		Conversation conversation = service.findOrCreateConversation(account, fromParts[0],false);
+		return new Message(conversation, packet.getFrom(), pgpBody, Message.ENCRYPTION_PGP, Message.STATUS_RECIEVED);
+	}
+	
 	public static Message parseOtrChat(MessagePacket packet, Account account, XmppConnectionService service) {
 		String[] fromParts = packet.getFrom().split("/");
 		Conversation conversation = service.findOrCreateConversation(account, fromParts[0],false);
@@ -131,4 +137,13 @@ public class MessageParser {
 	}
 	return new Message(conversation, packet.getFrom(), displayError, Message.ENCRYPTION_NONE, Message.STATUS_ERROR);
 	}
+
+	public static String getPgpBody(MessagePacket packet) {
+		for(Element child : packet.getChildren()) {
+			if (child.getName().equals("x")&&child.getAttribute("xmlns").equals("jabber:x:encrypted")) {
+				return child.getContent();
+			}
+		}
+		return null;
+	}
 }