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.FileInputStream;
13import java.io.FileOutputStream;
14import java.io.IOException;
15import java.io.InputStream;
16import java.io.OutputStream;
17import java.util.ArrayDeque;
18import java.util.HashSet;
19import java.util.List;
20
21import eu.siacs.conversations.Config;
22import eu.siacs.conversations.entities.Conversation;
23import eu.siacs.conversations.entities.DownloadableFile;
24import eu.siacs.conversations.entities.Message;
25import eu.siacs.conversations.http.HttpConnectionManager;
26import eu.siacs.conversations.services.XmppConnectionService;
27import eu.siacs.conversations.utils.MimeUtils;
28
29public class PgpDecryptionService {
30
31 protected final ArrayDeque<Message> messages = new ArrayDeque<>();
32 protected final HashSet<Message> pendingNotifications = new HashSet<>();
33 private final XmppConnectionService mXmppConnectionService;
34 private OpenPgpApi openPgpApi = null;
35 private Message currentMessage;
36 private PendingIntent pendingIntent;
37 private Intent userInteractionResult;
38
39
40 public PgpDecryptionService(XmppConnectionService service) {
41 this.mXmppConnectionService = service;
42 }
43
44 public synchronized boolean decrypt(final Message message, boolean notify) {
45 messages.add(message);
46 if (notify && pendingIntent == null) {
47 pendingNotifications.add(message);
48 continueDecryption();
49 return false;
50 } else {
51 continueDecryption();
52 return notify;
53 }
54 }
55
56 public synchronized void decrypt(final List<Message> list) {
57 for (Message message : list) {
58 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
59 messages.add(message);
60 }
61 }
62 continueDecryption();
63 }
64
65 public synchronized void discard(List<Message> discards) {
66 this.messages.removeAll(discards);
67 this.pendingNotifications.removeAll(discards);
68 }
69
70 public synchronized void discard(Message message) {
71 this.messages.remove(message);
72 this.pendingNotifications.remove(message);
73 }
74
75 public void giveUpCurrentDecryption() {
76 Message message;
77 synchronized (this) {
78 if (currentMessage != null) {
79 return;
80 }
81 message = messages.peekFirst();
82 if (message == null) {
83 return;
84 }
85 discard(message);
86 }
87 synchronized (message) {
88 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
89 message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
90 }
91 }
92 mXmppConnectionService.updateMessage(message, false);
93 continueDecryption(true);
94 }
95
96 protected synchronized void decryptNext() {
97 if (pendingIntent == null
98 && getOpenPgpApi() != null
99 && (currentMessage = messages.poll()) != null) {
100 new Thread(new Runnable() {
101 @Override
102 public void run() {
103 executeApi(currentMessage);
104 decryptNext();
105 }
106 }).start();
107 }
108 }
109
110 public synchronized void continueDecryption(boolean resetPending) {
111 if (resetPending) {
112 this.pendingIntent = null;
113 }
114 continueDecryption();
115 }
116
117 public synchronized void continueDecryption(Intent userInteractionResult) {
118 this.pendingIntent = null;
119 this.userInteractionResult = userInteractionResult;
120 continueDecryption();
121 }
122
123 public synchronized void continueDecryption() {
124 if (currentMessage == null) {
125 decryptNext();
126 }
127 }
128
129 private synchronized OpenPgpApi getOpenPgpApi() {
130 if (openPgpApi == null) {
131 this.openPgpApi = mXmppConnectionService.getOpenPgpApi();
132 }
133 return this.openPgpApi;
134 }
135
136 private void executeApi(Message message) {
137 boolean skipNotificationPush = false;
138 synchronized (message) {
139 Intent params = userInteractionResult != null ? userInteractionResult : new Intent();
140 params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
141 if (message.getType() == Message.TYPE_TEXT) {
142 InputStream is = new ByteArrayInputStream(message.getBody().getBytes());
143 final OutputStream os = new ByteArrayOutputStream();
144 Intent result = getOpenPgpApi().executeApi(params, is, os);
145 switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
146 case OpenPgpApi.RESULT_CODE_SUCCESS:
147 try {
148 os.flush();
149 final String body = os.toString();
150 if (body == null) {
151 throw new IOException("body was null");
152 }
153 message.setBody(body);
154 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
155 final HttpConnectionManager manager = mXmppConnectionService.getHttpConnectionManager();
156 if (message.trusted()
157 && message.treatAsDownloadable()
158 && manager.getAutoAcceptFileSize() > 0) {
159 manager.createNewDownloadConnection(message);
160 }
161 } catch (IOException e) {
162 message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
163 }
164 mXmppConnectionService.updateMessage(message);
165 break;
166 case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
167 synchronized (PgpDecryptionService.this) {
168 PendingIntent pendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
169 messages.addFirst(message);
170 currentMessage = null;
171 storePendingIntent(pendingIntent);
172 }
173 break;
174 case OpenPgpApi.RESULT_CODE_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 String mime = MimeUtils.guessMimeTypeFromExtension(originalExtension);
198 String path = outputFile.getName()+"."+originalExtension;
199 DownloadableFile fixedFile = mXmppConnectionService.getFileBackend().getFileForPath(path,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(path);
209 }
210 }
211 final String url = message.getFileParams().url;
212 mXmppConnectionService.getFileBackend().updateFileParams(message, url);
213 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
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}