1package eu.siacs.conversations.crypto;
2
3import android.app.PendingIntent;
4import android.content.Intent;
5import android.util.Log;
6import androidx.annotation.StringRes;
7import com.google.common.base.Joiner;
8import com.google.common.base.Splitter;
9import com.google.common.base.Strings;
10import eu.siacs.conversations.Config;
11import eu.siacs.conversations.R;
12import eu.siacs.conversations.entities.Account;
13import eu.siacs.conversations.entities.Contact;
14import eu.siacs.conversations.entities.Conversation;
15import eu.siacs.conversations.entities.DownloadableFile;
16import eu.siacs.conversations.entities.Message;
17import eu.siacs.conversations.persistance.FileBackend;
18import eu.siacs.conversations.services.XmppConnectionService;
19import eu.siacs.conversations.ui.UiCallback;
20import eu.siacs.conversations.utils.AsciiArmor;
21import java.io.ByteArrayInputStream;
22import java.io.ByteArrayOutputStream;
23import java.io.FileInputStream;
24import java.io.FileOutputStream;
25import java.io.IOException;
26import java.io.InputStream;
27import java.io.OutputStream;
28import java.util.ArrayList;
29import org.openintents.openpgp.OpenPgpError;
30import org.openintents.openpgp.OpenPgpSignatureResult;
31import org.openintents.openpgp.util.OpenPgpApi;
32import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
33
34public class PgpEngine {
35 private final OpenPgpApi api;
36 private final XmppConnectionService mXmppConnectionService;
37
38 public PgpEngine(OpenPgpApi api, XmppConnectionService service) {
39 this.api = api;
40 this.mXmppConnectionService = service;
41 }
42
43 private static void logError(Account account, OpenPgpError error) {
44 if (error != null) {
45 error.describeContents();
46 Log.d(
47 Config.LOGTAG,
48 account.getJid().asBareJid().toString()
49 + ": OpenKeychain error '"
50 + error.getMessage()
51 + "' code="
52 + error.getErrorId()
53 + " class="
54 + error.getClass().getName());
55 } else {
56 Log.d(
57 Config.LOGTAG,
58 account.getJid().asBareJid().toString()
59 + ": OpenKeychain error with no message");
60 }
61 }
62
63 public void encrypt(final Message message, final UiCallback<Message> callback) {
64 Intent params = new Intent();
65 params.setAction(OpenPgpApi.ACTION_ENCRYPT);
66 final Conversation conversation = (Conversation) message.getConversation();
67 if (conversation.getMode() == Conversation.MODE_SINGLE) {
68 long[] keys = {
69 conversation.getContact().getPgpKeyId(), conversation.getAccount().getPgpId()
70 };
71 params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keys);
72 } else {
73 params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, conversation.getMucOptions().getPgpKeyIds());
74 }
75
76 if (!message.needsUploading()) {
77 params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
78 String body;
79 if (message.hasFileOnRemoteHost()) {
80 body = message.getFileParams().url;
81 } else {
82 body = message.getBody();
83 }
84 InputStream is = new ByteArrayInputStream(body.getBytes());
85 final OutputStream os = new ByteArrayOutputStream();
86 api.executeApiAsync(
87 params,
88 is,
89 os,
90 result -> {
91 switch (result.getIntExtra(
92 OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
93 case OpenPgpApi.RESULT_CODE_SUCCESS:
94 try {
95 os.flush();
96 final ArrayList<String> encryptedMessageBody =
97 new ArrayList<>();
98 final String[] lines = os.toString().split("\n");
99 for (int i = 2; i < lines.length - 1; ++i) {
100 if (!lines[i].contains("Version")) {
101 encryptedMessageBody.add(lines[i].trim());
102 }
103 }
104 message.setEncryptedBody(
105 Joiner.on('\n').join(encryptedMessageBody));
106 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
107 mXmppConnectionService.sendMessage(message);
108 callback.success(message);
109 } catch (IOException e) {
110 callback.error(R.string.openpgp_error, message);
111 }
112
113 break;
114 case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
115 callback.userInputRequired(
116 result.getParcelableExtra(OpenPgpApi.RESULT_INTENT),
117 message);
118 break;
119 case OpenPgpApi.RESULT_CODE_ERROR:
120 OpenPgpError error =
121 result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
122 String errorMessage = error != null ? error.getMessage() : null;
123 @StringRes final int res;
124 if (errorMessage != null
125 && errorMessage.startsWith("Bad key for encryption")) {
126 res = R.string.bad_key_for_encryption;
127 } else {
128 res = R.string.openpgp_error;
129 }
130 logError(conversation.getAccount(), error);
131 callback.error(res, message);
132 break;
133 }
134 });
135 } else {
136 try {
137 DownloadableFile inputFile =
138 this.mXmppConnectionService.getFileBackend().getFile(message, true);
139 DownloadableFile outputFile =
140 this.mXmppConnectionService.getFileBackend().getFile(message, false);
141 outputFile.getParentFile().mkdirs();
142 outputFile.createNewFile();
143 final InputStream is = new FileInputStream(inputFile);
144 final OutputStream os = new FileOutputStream(outputFile);
145 api.executeApiAsync(
146 params,
147 is,
148 os,
149 result -> {
150 switch (result.getIntExtra(
151 OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
152 case OpenPgpApi.RESULT_CODE_SUCCESS:
153 try {
154 os.flush();
155 } catch (IOException ignored) {
156 // ignored
157 }
158 FileBackend.close(os);
159 mXmppConnectionService.sendMessage(message);
160 callback.success(message);
161 break;
162 case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
163 callback.userInputRequired(
164 result.getParcelableExtra(OpenPgpApi.RESULT_INTENT),
165 message);
166 break;
167 case OpenPgpApi.RESULT_CODE_ERROR:
168 logError(
169 conversation.getAccount(),
170 result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
171 callback.error(R.string.openpgp_error, message);
172 break;
173 }
174 });
175 } catch (final IOException e) {
176 callback.error(R.string.openpgp_error, message);
177 }
178 }
179 }
180
181 public long fetchKeyId(final Account account, final String status, final String signature) {
182 if (signature == null || api == null) {
183 return 0;
184 }
185 final Intent params = new Intent();
186 params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
187 try {
188 params.putExtra(OpenPgpApi.RESULT_DETACHED_SIGNATURE, AsciiArmor.decode(signature));
189 } catch (final Exception e) {
190 Log.d(Config.LOGTAG, "unable to parse signature", e);
191 return 0;
192 }
193 final InputStream is = new ByteArrayInputStream(Strings.nullToEmpty(status).getBytes());
194 final ByteArrayOutputStream os = new ByteArrayOutputStream();
195 final Intent result = api.executeApi(params, is, os);
196 switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
197 case OpenPgpApi.RESULT_CODE_SUCCESS:
198 final OpenPgpSignatureResult sigResult =
199 result.getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE);
200 // TODO unsure that sigResult.getResult() is either 1, 2 or 3
201 if (sigResult != null) {
202 return sigResult.getKeyId();
203 } else {
204 return 0;
205 }
206 case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
207 return 0;
208 case OpenPgpApi.RESULT_CODE_ERROR:
209 logError(account, result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
210 return 0;
211 }
212 return 0;
213 }
214
215 public void chooseKey(final Account account, final UiCallback<Account> callback) {
216 Intent p = new Intent();
217 p.setAction(OpenPgpApi.ACTION_GET_SIGN_KEY_ID);
218 api.executeApiAsync(
219 p,
220 null,
221 null,
222 result -> {
223 switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
224 case OpenPgpApi.RESULT_CODE_SUCCESS:
225 callback.success(account);
226 return;
227 case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
228 callback.userInputRequired(
229 result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), account);
230 return;
231 case OpenPgpApi.RESULT_CODE_ERROR:
232 logError(account, result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
233 callback.error(R.string.openpgp_error, account);
234 }
235 });
236 }
237
238 public void generateSignature(
239 Intent intent,
240 final Account account,
241 String status,
242 final UiCallback<String> callback) {
243 if (account.getPgpId() == 0) {
244 return;
245 }
246 Intent params = intent == null ? new Intent() : intent;
247 params.setAction(OpenPgpApi.ACTION_CLEARTEXT_SIGN);
248 params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
249 params.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, account.getPgpId());
250 InputStream is = new ByteArrayInputStream(status.getBytes());
251 final OutputStream os = new ByteArrayOutputStream();
252 Log.d(
253 Config.LOGTAG,
254 account.getJid().asBareJid() + ": signing status message \"" + status + "\"");
255 api.executeApiAsync(
256 params,
257 is,
258 os,
259 result -> {
260 switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
261 case OpenPgpApi.RESULT_CODE_SUCCESS:
262 final ArrayList<String> signature = new ArrayList<>();
263 try {
264 os.flush();
265 boolean sig = false;
266 for (final String line : Splitter.on('\n').split(os.toString())) {
267 if (sig) {
268 if (line.contains("END PGP SIGNATURE")) {
269 sig = false;
270 } else {
271 if (!line.contains("Version")) {
272 signature.add(line.trim());
273 }
274 }
275 }
276 if (line.contains("BEGIN PGP SIGNATURE")) {
277 sig = true;
278 }
279 }
280 } catch (IOException e) {
281 callback.error(R.string.openpgp_error, null);
282 return;
283 }
284 callback.success(Joiner.on('\n').join(signature));
285 return;
286 case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
287 callback.userInputRequired(
288 result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), status);
289 return;
290 case OpenPgpApi.RESULT_CODE_ERROR:
291 OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
292 if (error != null
293 && "signing subkey not found!".equals(error.getMessage())) {
294 callback.error(0, null);
295 } else {
296 logError(account, error);
297 callback.error(R.string.unable_to_connect_to_keychain, null);
298 }
299 }
300 });
301 }
302
303 public void hasKey(final Contact contact, final UiCallback<Contact> callback) {
304 Intent params = new Intent();
305 params.setAction(OpenPgpApi.ACTION_GET_KEY);
306 params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId());
307 api.executeApiAsync(
308 params,
309 null,
310 null,
311 new IOpenPgpCallback() {
312
313 @Override
314 public void onReturn(Intent result) {
315 switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
316 case OpenPgpApi.RESULT_CODE_SUCCESS:
317 callback.success(contact);
318 return;
319 case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
320 callback.userInputRequired(
321 result.getParcelableExtra(OpenPgpApi.RESULT_INTENT),
322 contact);
323 return;
324 case OpenPgpApi.RESULT_CODE_ERROR:
325 logError(
326 contact.getAccount(),
327 result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
328 callback.error(R.string.openpgp_error, contact);
329 }
330 }
331 });
332 }
333
334 public PendingIntent getIntentForKey(long pgpKeyId) {
335 Intent params = new Intent();
336 params.setAction(OpenPgpApi.ACTION_GET_KEY);
337 params.putExtra(OpenPgpApi.EXTRA_KEY_ID, pgpKeyId);
338 final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
339 final ByteArrayInputStream inputStream = new ByteArrayInputStream(new byte[0]);
340 Intent result = api.executeApi(params, inputStream, outputStream);
341 return result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
342 }
343}