PgpEngine.java

  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}