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.net.URL;
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.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
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 String mime = MimeUtils.guessMimeTypeFromExtension(originalExtension);
199 String path = outputFile.getName()+"."+originalExtension;
200 DownloadableFile fixedFile = mXmppConnectionService.getFileBackend().getFileForPath(path,mime);
201 if (fixedFile.getParentFile().mkdirs()) {
202 Log.d(Config.LOGTAG,"created parent directories for "+fixedFile.getAbsolutePath());
203 }
204 if (outputFile.renameTo(fixedFile)) {
205 Log.d(Config.LOGTAG, "renamed " + outputFile.getAbsolutePath() + " to " + fixedFile.getAbsolutePath());
206 message.setRelativeFilePath(path);
207 }
208 }
209 URL url = message.getFileParams().url;
210 mXmppConnectionService.getFileBackend().updateFileParams(message, url);
211 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
212 inputFile.delete();
213 mXmppConnectionService.updateMessage(message);
214 skipNotificationPush = true;
215 mXmppConnectionService.getFileBackend().updateMediaScanner(outputFile, () -> notifyIfPending(message));
216 break;
217 case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
218 synchronized (PgpDecryptionService.this) {
219 PendingIntent pendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
220 messages.addFirst(message);
221 currentMessage = null;
222 storePendingIntent(pendingIntent);
223 }
224 break;
225 case OpenPgpApi.RESULT_CODE_ERROR:
226 message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
227 mXmppConnectionService.updateMessage(message);
228 break;
229 }
230 } catch (final IOException e) {
231 message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
232 mXmppConnectionService.updateMessage(message);
233 }
234 }
235 }
236 if (!skipNotificationPush) {
237 notifyIfPending(message);
238 }
239 }
240
241 private synchronized void notifyIfPending(Message message) {
242 if (pendingNotifications.remove(message)) {
243 mXmppConnectionService.getNotificationService().push(message);
244 }
245 }
246
247 private void storePendingIntent(PendingIntent pendingIntent) {
248 this.pendingIntent = pendingIntent;
249 mXmppConnectionService.updateConversationUi();
250 }
251
252 public synchronized boolean hasPendingIntent(Conversation conversation) {
253 if (pendingIntent == null) {
254 return false;
255 } else {
256 for (Message message : messages) {
257 if (message.getConversation() == conversation) {
258 return true;
259 }
260 }
261 return false;
262 }
263 }
264
265 public PendingIntent getPendingIntent() {
266 return pendingIntent;
267 }
268
269 public boolean isConnected() {
270 return getOpenPgpApi() != null;
271 }
272}