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