PgpDecryptionService.java

  1package eu.siacs.conversations.crypto;
  2
  3import android.app.PendingIntent;
  4import android.content.Intent;
  5import android.util.Log;
  6
  7import org.openintents.openpgp.OpenPgpMetadata;
  8import org.openintents.openpgp.util.OpenPgpApi;
  9
 10import java.io.ByteArrayInputStream;
 11import java.io.ByteArrayOutputStream;
 12import java.io.File;
 13import java.io.FileInputStream;
 14import java.io.FileOutputStream;
 15import java.io.IOException;
 16import java.io.InputStream;
 17import java.io.OutputStream;
 18import java.util.ArrayDeque;
 19import java.util.HashSet;
 20import java.util.List;
 21
 22import eu.siacs.conversations.Config;
 23import eu.siacs.conversations.entities.Conversation;
 24import eu.siacs.conversations.entities.DownloadableFile;
 25import eu.siacs.conversations.entities.Message;
 26import eu.siacs.conversations.http.HttpConnectionManager;
 27import eu.siacs.conversations.services.XmppConnectionService;
 28import eu.siacs.conversations.utils.MimeUtils;
 29
 30public class PgpDecryptionService {
 31
 32	protected final ArrayDeque<Message> messages = new ArrayDeque<>();
 33	protected final HashSet<Message> pendingNotifications = new HashSet<>();
 34	private final XmppConnectionService mXmppConnectionService;
 35	private OpenPgpApi openPgpApi = null;
 36	private Message currentMessage;
 37	private PendingIntent pendingIntent;
 38	private Intent userInteractionResult;
 39
 40
 41	public PgpDecryptionService(XmppConnectionService service) {
 42		this.mXmppConnectionService = service;
 43	}
 44
 45	public synchronized boolean decrypt(final Message message, boolean notify) {
 46		messages.add(message);
 47		if (notify && pendingIntent == null) {
 48			pendingNotifications.add(message);
 49			continueDecryption();
 50			return false;
 51		} else {
 52			continueDecryption();
 53			return notify;
 54		}
 55	}
 56
 57	public synchronized void decrypt(final List<Message> list) {
 58		for (Message message : list) {
 59			if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 60				messages.add(message);
 61			}
 62		}
 63		continueDecryption();
 64	}
 65
 66	public synchronized void discard(List<Message> discards) {
 67		this.messages.removeAll(discards);
 68		this.pendingNotifications.removeAll(discards);
 69	}
 70
 71	public synchronized void discard(Message message) {
 72		this.messages.remove(message);
 73		this.pendingNotifications.remove(message);
 74	}
 75
 76	public void giveUpCurrentDecryption() {
 77		Message message;
 78		synchronized (this) {
 79			if (currentMessage != null) {
 80				return;
 81			}
 82			message = messages.peekFirst();
 83			if (message == null) {
 84				return;
 85			}
 86			discard(message);
 87		}
 88		synchronized (message) {
 89			if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 90				message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
 91			}
 92		}
 93		mXmppConnectionService.updateMessage(message, false);
 94		continueDecryption(true);
 95	}
 96
 97	protected synchronized void decryptNext() {
 98		if (pendingIntent == null
 99				&& getOpenPgpApi() != null
100				&& (currentMessage = messages.poll()) != null) {
101			new Thread(new Runnable() {
102				@Override
103				public void run() {
104					executeApi(currentMessage);
105					decryptNext();
106				}
107			}).start();
108		}
109	}
110
111	public synchronized void continueDecryption(boolean resetPending) {
112		if (resetPending) {
113			this.pendingIntent = null;
114		}
115		continueDecryption();
116	}
117
118	public synchronized void continueDecryption(Intent userInteractionResult) {
119		this.pendingIntent = null;
120		this.userInteractionResult = userInteractionResult;
121		continueDecryption();
122	}
123
124	public synchronized void continueDecryption() {
125		if (currentMessage == null) {
126			decryptNext();
127		}
128	}
129
130	private synchronized OpenPgpApi getOpenPgpApi() {
131		if (openPgpApi == null) {
132			this.openPgpApi = mXmppConnectionService.getOpenPgpApi();
133		}
134		return this.openPgpApi;
135	}
136
137	private void executeApi(Message message) {
138		boolean skipNotificationPush = false;
139		synchronized (message) {
140			Intent params = userInteractionResult != null ? userInteractionResult : new Intent();
141			params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
142			if (message.getType() == Message.TYPE_TEXT) {
143				InputStream is = new ByteArrayInputStream(message.getBody().getBytes());
144				final OutputStream os = new ByteArrayOutputStream();
145				Intent result = getOpenPgpApi().executeApi(params, is, os);
146				switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
147					case OpenPgpApi.RESULT_CODE_SUCCESS:
148						try {
149							os.flush();
150							final String body = os.toString();
151							message.setBody(body);
152							message.setEncryption(Message.ENCRYPTION_DECRYPTED);
153							final HttpConnectionManager manager = mXmppConnectionService.getHttpConnectionManager();
154							if (message.trusted()
155									&& message.treatAsDownloadable()
156									&& manager.getAutoAcceptFileSize() > 0) {
157								manager.createNewDownloadConnection(message);
158							}
159						} catch (IOException e) {
160							message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
161						}
162						mXmppConnectionService.updateMessage(message);
163						break;
164					case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
165						synchronized (PgpDecryptionService.this) {
166							PendingIntent pendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
167							messages.addFirst(message);
168							currentMessage = null;
169							storePendingIntent(pendingIntent);
170						}
171						break;
172					case OpenPgpApi.RESULT_CODE_ERROR:
173						message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
174						mXmppConnectionService.updateMessage(message);
175						break;
176				}
177			} else if (message.isFileOrImage()) {
178				try {
179					final DownloadableFile inputFile = mXmppConnectionService.getFileBackend().getFile(message, false);
180					final DownloadableFile outputFile = mXmppConnectionService.getFileBackend().getFile(message, true);
181					if (outputFile.getParentFile().mkdirs()) {
182						Log.d(Config.LOGTAG,"created parent directories for "+outputFile.getAbsolutePath());
183					}
184					outputFile.createNewFile();
185					InputStream is = new FileInputStream(inputFile);
186					OutputStream os = new FileOutputStream(outputFile);
187					Intent result = getOpenPgpApi().executeApi(params, is, os);
188					switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
189						case OpenPgpApi.RESULT_CODE_SUCCESS:
190							OpenPgpMetadata openPgpMetadata = result.getParcelableExtra(OpenPgpApi.RESULT_METADATA);
191							String originalFilename = openPgpMetadata.getFilename();
192							String originalExtension = originalFilename == null ? null : MimeUtils.extractRelevantExtension(originalFilename);
193							if (originalExtension != null && MimeUtils.extractRelevantExtension(outputFile.getName()) == null) {
194								Log.d(Config.LOGTAG,"detected original filename during pgp decryption");
195								final String mime = MimeUtils.guessMimeTypeFromExtension(originalExtension);
196								final String filename = outputFile.getName()+"."+originalExtension;
197								final File fixedFile = mXmppConnectionService.getFileBackend().getStorageLocation(filename,mime);
198								if (fixedFile.getParentFile().mkdirs()) {
199									Log.d(Config.LOGTAG,"created parent directories for "+fixedFile.getAbsolutePath());
200								}
201								synchronized (mXmppConnectionService.FILENAMES_TO_IGNORE_DELETION) {
202									mXmppConnectionService.FILENAMES_TO_IGNORE_DELETION.add(outputFile.getAbsolutePath());
203								}
204								if (outputFile.renameTo(fixedFile)) {
205									Log.d(Config.LOGTAG, "renamed " + outputFile.getAbsolutePath() + " to " + fixedFile.getAbsolutePath());
206									message.setRelativeFilePath(fixedFile.getAbsolutePath());
207								}
208							}
209							final String url = message.getFileParams().url;
210							message.setEncryption(Message.ENCRYPTION_DECRYPTED);
211							mXmppConnectionService.getFileBackend().updateFileParams(message, url);
212							mXmppConnectionService.updateMessage(message);
213							if (!inputFile.delete()) {
214								Log.w(Config.LOGTAG,"unable to delete pgp encrypted source file "+inputFile.getAbsolutePath());
215							}
216							skipNotificationPush = true;
217							mXmppConnectionService.getFileBackend().updateMediaScanner(outputFile, () -> notifyIfPending(message));
218							break;
219						case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
220							synchronized (PgpDecryptionService.this) {
221								PendingIntent pendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
222								messages.addFirst(message);
223								currentMessage = null;
224								storePendingIntent(pendingIntent);
225							}
226							break;
227						case OpenPgpApi.RESULT_CODE_ERROR:
228							message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
229							mXmppConnectionService.updateMessage(message);
230							break;
231					}
232				} catch (final IOException e) {
233					message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
234					mXmppConnectionService.updateMessage(message);
235				}
236			}
237		}
238		if (!skipNotificationPush) {
239			notifyIfPending(message);
240		}
241	}
242
243	private synchronized void notifyIfPending(Message message) {
244		if (pendingNotifications.remove(message)) {
245			mXmppConnectionService.getNotificationService().push(message);
246		}
247	}
248
249	private void storePendingIntent(PendingIntent pendingIntent) {
250		this.pendingIntent = pendingIntent;
251		mXmppConnectionService.updateConversationUi();
252	}
253
254	public synchronized boolean hasPendingIntent(Conversation conversation) {
255		if (pendingIntent == null) {
256			return false;
257		} else {
258			for (Message message : messages) {
259				if (message.getConversation() == conversation) {
260					return true;
261				}
262			}
263			return false;
264		}
265	}
266
267	public PendingIntent getPendingIntent() {
268		return pendingIntent;
269	}
270
271	public boolean isConnected() {
272		return getOpenPgpApi() != null;
273	}
274}