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							if (body == null) {
152								throw new IOException("body was null");
153							}
154							message.setBody(body);
155							message.setEncryption(Message.ENCRYPTION_DECRYPTED);
156							final HttpConnectionManager manager = mXmppConnectionService.getHttpConnectionManager();
157							if (message.trusted()
158									&& message.treatAsDownloadable()
159									&& manager.getAutoAcceptFileSize() > 0) {
160								manager.createNewDownloadConnection(message);
161							}
162						} catch (IOException e) {
163							message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
164						}
165						mXmppConnectionService.updateMessage(message);
166						break;
167					case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
168						synchronized (PgpDecryptionService.this) {
169							PendingIntent pendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
170							messages.addFirst(message);
171							currentMessage = null;
172							storePendingIntent(pendingIntent);
173						}
174						break;
175					case OpenPgpApi.RESULT_CODE_ERROR:
176						message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
177						mXmppConnectionService.updateMessage(message);
178						break;
179				}
180			} else if (message.isFileOrImage()) {
181				try {
182					final DownloadableFile inputFile = mXmppConnectionService.getFileBackend().getFile(message, false);
183					final DownloadableFile outputFile = mXmppConnectionService.getFileBackend().getFile(message, true);
184					if (outputFile.getParentFile().mkdirs()) {
185						Log.d(Config.LOGTAG,"created parent directories for "+outputFile.getAbsolutePath());
186					}
187					outputFile.createNewFile();
188					InputStream is = new FileInputStream(inputFile);
189					OutputStream os = new FileOutputStream(outputFile);
190					Intent result = getOpenPgpApi().executeApi(params, is, os);
191					switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
192						case OpenPgpApi.RESULT_CODE_SUCCESS:
193							OpenPgpMetadata openPgpMetadata = result.getParcelableExtra(OpenPgpApi.RESULT_METADATA);
194							String originalFilename = openPgpMetadata.getFilename();
195							String originalExtension = originalFilename == null ? null : MimeUtils.extractRelevantExtension(originalFilename);
196							if (originalExtension != null && MimeUtils.extractRelevantExtension(outputFile.getName()) == null) {
197								Log.d(Config.LOGTAG,"detected original filename during pgp decryption");
198								final String mime = MimeUtils.guessMimeTypeFromExtension(originalExtension);
199								final String filename = outputFile.getName()+"."+originalExtension;
200								final File fixedFile = mXmppConnectionService.getFileBackend().getStorageLocation(filename,mime);
201								if (fixedFile.getParentFile().mkdirs()) {
202									Log.d(Config.LOGTAG,"created parent directories for "+fixedFile.getAbsolutePath());
203								}
204								synchronized (mXmppConnectionService.FILENAMES_TO_IGNORE_DELETION) {
205									mXmppConnectionService.FILENAMES_TO_IGNORE_DELETION.add(outputFile.getAbsolutePath());
206								}
207								if (outputFile.renameTo(fixedFile)) {
208									Log.d(Config.LOGTAG, "renamed " + outputFile.getAbsolutePath() + " to " + fixedFile.getAbsolutePath());
209									message.setRelativeFilePath(fixedFile.getAbsolutePath());
210								}
211							}
212							final String url = message.getFileParams().url;
213							mXmppConnectionService.getFileBackend().updateFileParams(message, url);
214							message.setEncryption(Message.ENCRYPTION_DECRYPTED);
215							mXmppConnectionService.updateMessage(message);
216							if (!inputFile.delete()) {
217								Log.w(Config.LOGTAG,"unable to delete pgp encrypted source file "+inputFile.getAbsolutePath());
218							}
219							skipNotificationPush = true;
220							mXmppConnectionService.getFileBackend().updateMediaScanner(outputFile, () -> notifyIfPending(message));
221							break;
222						case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
223							synchronized (PgpDecryptionService.this) {
224								PendingIntent pendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
225								messages.addFirst(message);
226								currentMessage = null;
227								storePendingIntent(pendingIntent);
228							}
229							break;
230						case OpenPgpApi.RESULT_CODE_ERROR:
231							message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
232							mXmppConnectionService.updateMessage(message);
233							break;
234					}
235				} catch (final IOException e) {
236					message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
237					mXmppConnectionService.updateMessage(message);
238				}
239			}
240		}
241		if (!skipNotificationPush) {
242			notifyIfPending(message);
243		}
244	}
245
246	private synchronized void notifyIfPending(Message message) {
247		if (pendingNotifications.remove(message)) {
248			mXmppConnectionService.getNotificationService().push(message);
249		}
250	}
251
252	private void storePendingIntent(PendingIntent pendingIntent) {
253		this.pendingIntent = pendingIntent;
254		mXmppConnectionService.updateConversationUi();
255	}
256
257	public synchronized boolean hasPendingIntent(Conversation conversation) {
258		if (pendingIntent == null) {
259			return false;
260		} else {
261			for (Message message : messages) {
262				if (message.getConversation() == conversation) {
263					return true;
264				}
265			}
266			return false;
267		}
268	}
269
270	public PendingIntent getPendingIntent() {
271		return pendingIntent;
272	}
273
274	public boolean isConnected() {
275		return getOpenPgpApi() != null;
276	}
277}