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