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 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}