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}