Merge pull request #1513 from fiaxh/pgp_background_decryption

PGP messages background decryption
This commit is contained in:
Daniel Gultsch 2015-10-30 10:18:27 +01:00
commit 6a458b853c
11 changed files with 295 additions and 76 deletions

View file

@ -0,0 +1,162 @@
package eu.siacs.conversations.crypto;
import android.app.PendingIntent;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.UiCallback;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
public class PgpDecryptionService {
private final XmppConnectionService xmppConnectionService;
private final ConcurrentHashMap<String, List<Message>> messages = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Boolean> decryptingMessages = new ConcurrentHashMap<>();
private Boolean keychainLocked = false;
private final Object keychainLockedLock = new Object();
public PgpDecryptionService(XmppConnectionService xmppConnectionService) {
this.xmppConnectionService = xmppConnectionService;
}
public void add(Message message) {
if (isRunning()) {
decryptDirectly(message);
} else {
store(message);
}
}
public void addAll(List<Message> messagesList) {
if (!messagesList.isEmpty()) {
String conversationUuid = messagesList.get(0).getConversation().getUuid();
if (!messages.containsKey(conversationUuid)) {
List<Message> list = Collections.synchronizedList(new LinkedList<Message>());
messages.put(conversationUuid, list);
}
synchronized (messages.get(conversationUuid)) {
messages.get(conversationUuid).addAll(messagesList);
}
decryptAllMessages();
}
}
public void onKeychainUnlocked() {
synchronized (keychainLockedLock) {
keychainLocked = false;
}
decryptAllMessages();
}
public void onKeychainLocked() {
synchronized (keychainLockedLock) {
keychainLocked = true;
}
xmppConnectionService.updateConversationUi();
}
public void onOpenPgpServiceBound() {
decryptAllMessages();
}
public boolean isRunning() {
synchronized (keychainLockedLock) {
return !keychainLocked;
}
}
private void store(Message message) {
if (messages.containsKey(message.getConversation().getUuid())) {
messages.get(message.getConversation().getUuid()).add(message);
} else {
List<Message> messageList = Collections.synchronizedList(new LinkedList<Message>());
messageList.add(message);
messages.put(message.getConversation().getUuid(), messageList);
}
}
private void decryptAllMessages() {
for (String uuid : messages.keySet()) {
decryptMessages(uuid);
}
}
private void decryptMessages(final String uuid) {
synchronized (decryptingMessages) {
Boolean decrypting = decryptingMessages.get(uuid);
if ((decrypting != null && !decrypting) || decrypting == null) {
decryptingMessages.put(uuid, true);
decryptMessage(uuid);
}
}
}
private void decryptMessage(final String uuid) {
Message message = null;
synchronized (messages.get(uuid)) {
while (!messages.get(uuid).isEmpty()) {
if (messages.get(uuid).get(0).getEncryption() == Message.ENCRYPTION_PGP) {
if (isRunning()) {
message = messages.get(uuid).remove(0);
}
break;
} else {
messages.get(uuid).remove(0);
}
}
if (message != null && xmppConnectionService.getPgpEngine() != null) {
xmppConnectionService.getPgpEngine().decrypt(message, new UiCallback<Message>() {
@Override
public void userInputRequried(PendingIntent pi, Message message) {
messages.get(uuid).add(0, message);
decryptingMessages.put(uuid, false);
}
@Override
public void success(Message message) {
xmppConnectionService.updateConversationUi();
decryptMessage(uuid);
}
@Override
public void error(int error, Message message) {
message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
xmppConnectionService.updateConversationUi();
decryptMessage(uuid);
}
});
} else {
decryptingMessages.put(uuid, false);
}
}
}
private void decryptDirectly(final Message message) {
if (message.getEncryption() == Message.ENCRYPTION_PGP && xmppConnectionService.getPgpEngine() != null) {
xmppConnectionService.getPgpEngine().decrypt(message, new UiCallback<Message>() {
@Override
public void userInputRequried(PendingIntent pi, Message message) {
store(message);
}
@Override
public void success(Message message) {
xmppConnectionService.updateConversationUi();
xmppConnectionService.getNotificationService().updateNotification(false);
}
@Override
public void error(int error, Message message) {
message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
xmppConnectionService.updateConversationUi();
}
});
}
}
}

View file

@ -50,6 +50,7 @@ public class PgpEngine {
@Override
public void onReturn(Intent result) {
notifyPgpDecryptionService(message.getContact().getAccount(), OpenPgpApi.ACTION_DECRYPT_VERIFY, result);
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
OpenPgpApi.RESULT_CODE_ERROR)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
@ -64,6 +65,7 @@ public class PgpEngine {
&& manager.getAutoAcceptFileSize() > 0) {
manager.createNewDownloadConnection(message);
}
mXmppConnectionService.updateMessage(message);
callback.success(message);
}
} catch (IOException e) {
@ -158,6 +160,7 @@ public class PgpEngine {
@Override
public void onReturn(Intent result) {
notifyPgpDecryptionService(message.getContact().getAccount(), OpenPgpApi.ACTION_ENCRYPT, result);
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
OpenPgpApi.RESULT_CODE_ERROR)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
@ -203,6 +206,7 @@ public class PgpEngine {
@Override
public void onReturn(Intent result) {
notifyPgpDecryptionService(message.getContact().getAccount(), OpenPgpApi.ACTION_ENCRYPT, result);
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
OpenPgpApi.RESULT_CODE_ERROR)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
@ -252,6 +256,7 @@ public class PgpEngine {
InputStream is = new ByteArrayInputStream(pgpSig.toString().getBytes());
ByteArrayOutputStream os = new ByteArrayOutputStream();
Intent result = api.executeApi(params, is, os);
notifyPgpDecryptionService(account, OpenPgpApi.ACTION_DECRYPT_VERIFY, result);
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
OpenPgpApi.RESULT_CODE_ERROR)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
@ -282,6 +287,7 @@ public class PgpEngine {
@Override
public void onReturn(Intent result) {
notifyPgpDecryptionService(account, OpenPgpApi.ACTION_SIGN, result);
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
StringBuilder signatureBuilder = new StringBuilder();
@ -368,4 +374,17 @@ public class PgpEngine {
return (PendingIntent) result
.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
}
private void notifyPgpDecryptionService(Account account, String action, final Intent result) {
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
if (OpenPgpApi.ACTION_SIGN.equals(action)) {
account.getPgpDecryptionService().onKeychainUnlocked();
}
break;
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
account.getPgpDecryptionService().onKeychainLocked();
break;
}
}
}

View file

@ -4,6 +4,7 @@ import android.content.ContentValues;
import android.database.Cursor;
import android.os.SystemClock;
import eu.siacs.conversations.crypto.PgpDecryptionService;
import net.java.otr4j.crypto.OtrCryptoEngineImpl;
import net.java.otr4j.crypto.OtrCryptoException;
@ -137,6 +138,7 @@ public class Account extends AbstractEntity {
protected boolean online = false;
private OtrService mOtrService = null;
private AxolotlService axolotlService = null;
private PgpDecryptionService pgpDecryptionService = null;
private XmppConnection xmppConnection = null;
private long mEndGracePeriod = 0L;
private String otrFingerprint;
@ -313,12 +315,17 @@ public class Account extends AbstractEntity {
if (xmppConnection != null) {
xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
}
this.pgpDecryptionService = new PgpDecryptionService(context);
}
public OtrService getOtrService() {
return this.mOtrService;
}
public PgpDecryptionService getPgpDecryptionService() {
return pgpDecryptionService;
}
public XmppConnection getXmppConnection() {
return this.xmppConnection;
}

View file

@ -777,6 +777,7 @@ public class Conversation extends AbstractEntity implements Blockable {
synchronized (this.messages) {
this.messages.addAll(index, messages);
}
account.getPgpDecryptionService().addAll(messages);
}
public void sort() {

View file

@ -3,6 +3,7 @@ package eu.siacs.conversations.parser;
import android.util.Log;
import android.util.Pair;
import eu.siacs.conversations.crypto.PgpDecryptionService;
import net.java.otr4j.session.Session;
import net.java.otr4j.session.SessionStatus;
@ -114,6 +115,13 @@ public class MessageParser extends AbstractParser implements
return finishedMessage;
}
private Message parsePGPChat(final Conversation conversation, String pgpEncrypted, int status) {
final Message message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
PgpDecryptionService pgpDecryptionService = conversation.getAccount().getPgpDecryptionService();
pgpDecryptionService.add(message);
return message;
}
private class Invite {
Jid jid;
String password;
@ -337,7 +345,7 @@ public class MessageParser extends AbstractParser implements
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
}
} else if (pgpEncrypted != null) {
message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
message = parsePGPChat(conversation, pgpEncrypted, status);
} else if (axolotlEncrypted != null) {
message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation, status);
if (message == null) {

View file

@ -177,7 +177,7 @@ public class NotificationService {
mBuilder.setColor(mXmppConnectionService.getResources().getColor(R.color.primary));
}
private void updateNotification(final boolean notify) {
public void updateNotification(final boolean notify) {
final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService
.getSystemService(Context.NOTIFICATION_SERVICE);
final SharedPreferences preferences = mXmppConnectionService.getPreferences();

View file

@ -37,6 +37,7 @@ import net.java.otr4j.session.SessionID;
import net.java.otr4j.session.SessionImpl;
import net.java.otr4j.session.SessionStatus;
import org.openintents.openpgp.IOpenPgpService;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpServiceConnection;
@ -659,7 +660,19 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
this.fileObserver.startWatching();
this.pgpServiceConnection = new OpenPgpServiceConnection(getApplicationContext(), "org.sufficientlysecure.keychain");
this.pgpServiceConnection = new OpenPgpServiceConnection(getApplicationContext(), "org.sufficientlysecure.keychain", new OpenPgpServiceConnection.OnBound() {
@Override
public void onBound(IOpenPgpService service) {
for (Account account : accounts) {
if (account.getPgpDecryptionService() != null) {
account.getPgpDecryptionService().onOpenPgpServiceBound();
}
}
}
@Override
public void onError(Exception e) { }
});
this.pgpServiceConnection.bindToService();
this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);

View file

@ -1194,8 +1194,7 @@ public class ConversationActivity extends XmppActivity
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
if (requestCode == REQUEST_DECRYPT_PGP) {
mConversationFragment.hideSnackbar();
mConversationFragment.updateMessages();
mConversationFragment.onActivityResult(requestCode, resultCode, data);
} else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) {
mPendingImageUris.clear();
mPendingImageUris.addAll(extractUriFromIntent(data));
@ -1240,6 +1239,9 @@ public class ConversationActivity extends XmppActivity
} else {
mPendingImageUris.clear();
mPendingFileUris.clear();
if (requestCode == ConversationActivity.REQUEST_DECRYPT_PGP) {
mConversationFragment.onActivityResult(requestCode, resultCode, data);
}
}
}

View file

@ -11,6 +11,7 @@ import android.content.Intent;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.text.InputType;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
@ -199,21 +200,47 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
}
}
};
private IntentSender askForPassphraseIntent = null;
private final int KEYCHAIN_UNLOCK_NOT_REQUIRED = 0;
private final int KEYCHAIN_UNLOCK_REQUIRED = 1;
private final int KEYCHAIN_UNLOCK_PENDING = 2;
private int keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
protected OnClickListener clickToDecryptListener = new OnClickListener() {
@Override
public void onClick(View v) {
if (activity.hasPgp() && askForPassphraseIntent != null) {
try {
getActivity().startIntentSenderForResult(
askForPassphraseIntent,
ConversationActivity.REQUEST_DECRYPT_PGP, null, 0,
0, 0);
askForPassphraseIntent = null;
} catch (SendIntentException e) {
//
if (keychainUnlock == KEYCHAIN_UNLOCK_REQUIRED
&& activity.hasPgp() && !conversation.getAccount().getPgpDecryptionService().isRunning()) {
keychainUnlock = KEYCHAIN_UNLOCK_PENDING;
updateSnackBar(conversation);
Message message = getLastPgpDecryptableMessage();
if (message != null) {
activity.xmppConnectionService.getPgpEngine().decrypt(message, new UiCallback<Message>() {
@Override
public void success(Message object) {
conversation.getAccount().getPgpDecryptionService().onKeychainUnlocked();
keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
}
@Override
public void error(int errorCode, Message object) {
keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
}
@Override
public void userInputRequried(PendingIntent pi, Message object) {
try {
activity.startIntentSenderForResult(pi.getIntentSender(),
ConversationActivity.REQUEST_DECRYPT_PGP, null, 0, 0, 0);
} catch (SendIntentException e) {
keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
updatePgpMessages();
}
}
});
}
} else {
keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
updatePgpMessages();
}
}
};
@ -224,8 +251,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
activity.verifyOtrSessionDialog(conversation, v);
}
};
private ConcurrentLinkedQueue<Message> mEncryptedMessages = new ConcurrentLinkedQueue<>();
private boolean mDecryptJobRunning = false;
private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() {
@Override
@ -629,7 +654,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
@Override
public void onStop() {
mDecryptJobRunning = false;
super.onStop();
if (this.conversation != null) {
final String msg = mEditMessage.getText().toString();
@ -661,10 +685,8 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
this.conversation.trim();
}
this.askForPassphraseIntent = null;
this.keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
this.conversation = conversation;
this.mDecryptJobRunning = false;
this.mEncryptedMessages.clear();
if (this.conversation.getMode() == Conversation.MODE_MULTI) {
this.conversation.setNextCounterpart(null);
}
@ -767,7 +789,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
default:
break;
}
} else if (askForPassphraseIntent != null) {
} else if (keychainUnlock == KEYCHAIN_UNLOCK_REQUIRED) {
showSnackbar(R.string.openpgp_messages_found, R.string.decrypt, clickToDecryptListener);
} else if (mode == Conversation.MODE_SINGLE
&& conversation.smpRequested()) {
@ -791,19 +813,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
}
final ConversationActivity activity = (ConversationActivity) getActivity();
if (this.conversation != null) {
updateSnackBar(this.conversation);
conversation.populateWithMessages(ConversationFragment.this.messageList);
for (final Message message : this.messageList) {
if (message.getEncryption() == Message.ENCRYPTION_PGP
&& (message.getStatus() == Message.STATUS_RECEIVED || message
.getStatus() >= Message.STATUS_SEND)
&& message.getTransferable() == null) {
if (!mEncryptedMessages.contains(message)) {
mEncryptedMessages.add(message);
}
}
}
decryptNext();
updatePgpMessages();
updateSnackBar(conversation);
updateStatusMessages();
this.messageListAdapter.notifyDataSetChanged();
updateChatMsgHint();
@ -815,46 +827,27 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
}
}
private void decryptNext() {
Message next = this.mEncryptedMessages.peek();
PgpEngine engine = activity.xmppConnectionService.getPgpEngine();
if (next != null && engine != null && !mDecryptJobRunning) {
mDecryptJobRunning = true;
engine.decrypt(next, new UiCallback<Message>() {
@Override
public void userInputRequried(PendingIntent pi, Message message) {
mDecryptJobRunning = false;
askForPassphraseIntent = pi.getIntentSender();
updateSnackBar(conversation);
public void updatePgpMessages() {
if (keychainUnlock != KEYCHAIN_UNLOCK_PENDING) {
if (getLastPgpDecryptableMessage() != null
&& !conversation.getAccount().getPgpDecryptionService().isRunning()) {
keychainUnlock = KEYCHAIN_UNLOCK_REQUIRED;
} else {
keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
}
}
}
@Override
public void success(Message message) {
mDecryptJobRunning = false;
try {
mEncryptedMessages.remove();
} catch (final NoSuchElementException ignored) {
@Nullable
private Message getLastPgpDecryptableMessage() {
for (final Message message : this.messageList) {
if (message.getEncryption() == Message.ENCRYPTION_PGP
&& (message.getStatus() == Message.STATUS_RECEIVED || message.getStatus() >= Message.STATUS_SEND)
&& message.getTransferable() == null) {
return message;
}
askForPassphraseIntent = null;
activity.xmppConnectionService.updateMessage(message);
}
@Override
public void error(int error, Message message) {
message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
mDecryptJobRunning = false;
try {
mEncryptedMessages.remove();
} catch (final NoSuchElementException ignored) {
}
activity.refreshUi();
}
});
}
return null;
}
private void messageSent() {
@ -1274,7 +1267,11 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
public void onActivityResult(int requestCode, int resultCode,
final Intent data) {
if (resultCode == Activity.RESULT_OK) {
if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_TEXT) {
if (requestCode == ConversationActivity.REQUEST_DECRYPT_PGP) {
activity.getSelectedConversation().getAccount().getPgpDecryptionService().onKeychainUnlocked();
keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
updatePgpMessages();
} else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_TEXT) {
final String body = mEditMessage.getText().toString();
Message message = new Message(conversation, body, conversation.getNextEncryption());
sendAxolotlMessage(message);
@ -1282,6 +1279,11 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
int choice = data.getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID);
activity.selectPresenceToAttachFile(choice, conversation.getNextEncryption());
}
} else {
if (requestCode == ConversationActivity.REQUEST_DECRYPT_PGP) {
keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
updatePgpMessages();
}
}
}

View file

@ -550,7 +550,11 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
if (activity.hasPgp()) {
displayInfoMessage(viewHolder,activity.getString(R.string.encrypted_message),darkBackground);
if (account.getPgpDecryptionService().isRunning()) {
displayInfoMessage(viewHolder, activity.getString(R.string.message_decrypting), darkBackground);
} else {
displayInfoMessage(viewHolder, activity.getString(R.string.pgp_message), darkBackground);
}
} else {
displayInfoMessage(viewHolder,activity.getString(R.string.install_openkeychain),darkBackground);
if (viewHolder != null) {

View file

@ -30,7 +30,8 @@
<string name="minutes_ago">%d mins ago</string>
<string name="unread_conversations">unread Conversations</string>
<string name="sending">sending…</string>
<string name="encrypted_message">Decrypting message. Please wait…</string>
<string name="message_decrypting">Decrypting message. Please wait…</string>
<string name="pgp_message">OpenPGP encrypted message</string>
<string name="nick_in_use">Nickname is already in use</string>
<string name="admin">Admin</string>
<string name="owner">Owner</string>