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