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