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