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