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