/* * Copyright (C) 2012-2014 Dominik Schürmann * Copyright (C) 2010-2014 Thialfihar * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.sufficientlysecure.keychain.operations; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.Proxy; import java.util.Collections; import java.util.concurrent.atomic.AtomicBoolean; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.support.annotation.NonNull; import android.text.TextUtils; import org.spongycastle.bcpg.ArmoredOutputStream; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.HkpKeyserver; import org.sufficientlysecure.keychain.keyimport.Keyserver.AddKeyException; import org.sufficientlysecure.keychain.operations.results.ExportResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.service.ExportKeyringParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.FileHelper; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; /** * An operation class which implements high level export * operations. * This class receives a source and/or destination of keys as input and performs * all steps for this export. * * @see org.sufficientlysecure.keychain.ui.adapter.ImportKeysAdapter#getSelectedEntries() * For the export operation, the input consists of a set of key ids and * either the name of a file or an output uri to write to. */ public class ExportOperation extends BaseOperation { public ExportOperation(Context context, ProviderHelper providerHelper, Progressable progressable) { super(context, providerHelper, progressable); } public ExportOperation(Context context, ProviderHelper providerHelper, Progressable progressable, AtomicBoolean cancelled) { super(context, providerHelper, progressable, cancelled); } public ExportResult uploadKeyRingToServer(HkpKeyserver server, CanonicalizedPublicKeyRing keyring, Proxy proxy) { return uploadKeyRingToServer(server, keyring.getUncachedKeyRing(), proxy); } public ExportResult uploadKeyRingToServer(HkpKeyserver server, UncachedKeyRing keyring, Proxy proxy) { mProgressable.setProgress(R.string.progress_uploading, 0, 1); ByteArrayOutputStream bos = new ByteArrayOutputStream(); ArmoredOutputStream aos = null; OperationLog log = new OperationLog(); log.add(LogType.MSG_EXPORT_UPLOAD_PUBLIC, 0, KeyFormattingUtils.convertKeyIdToHex( keyring.getPublicKey().getKeyId() )); try { aos = new ArmoredOutputStream(bos); keyring.encode(aos); aos.close(); String armoredKey = bos.toString("UTF-8"); server.add(armoredKey, proxy); log.add(LogType.MSG_EXPORT_UPLOAD_SUCCESS, 1); return new ExportResult(ExportResult.RESULT_OK, log); } catch (IOException e) { Log.e(Constants.TAG, "IOException", e); log.add(LogType.MSG_EXPORT_ERROR_KEY, 1); return new ExportResult(ExportResult.RESULT_ERROR, log); } catch (AddKeyException e) { Log.e(Constants.TAG, "AddKeyException", e); log.add(LogType.MSG_EXPORT_ERROR_UPLOAD, 1); return new ExportResult(ExportResult.RESULT_ERROR, log); } finally { mProgressable.setProgress(R.string.progress_uploading, 1, 1); try { if (aos != null) { aos.close(); } bos.close(); } catch (IOException e) { // this is just a finally thing, no matter if it doesn't work out. } } } public ExportResult exportToFile(long[] masterKeyIds, boolean exportSecret, String outputFile) { OperationLog log = new OperationLog(); if (masterKeyIds != null) { log.add(LogType.MSG_EXPORT, 0, masterKeyIds.length); } else { log.add(LogType.MSG_EXPORT_ALL, 0); } // do we have a file name? if (outputFile == null) { log.add(LogType.MSG_EXPORT_ERROR_NO_FILE, 1); return new ExportResult(ExportResult.RESULT_ERROR, log); } // check if storage is ready if (!FileHelper.isStorageMounted(outputFile)) { log.add(LogType.MSG_EXPORT_ERROR_STORAGE, 1); return new ExportResult(ExportResult.RESULT_ERROR, log); } try { OutputStream outStream = new FileOutputStream(outputFile); try { ExportResult result = exportKeyRings(log, masterKeyIds, exportSecret, outStream); if (result.cancelled()) { //noinspection ResultOfMethodCallIgnored new File(outputFile).delete(); } return result; } finally { outStream.close(); } } catch (IOException e) { log.add(LogType.MSG_EXPORT_ERROR_FOPEN, 1); return new ExportResult(ExportResult.RESULT_ERROR, log); } } public ExportResult exportToUri(long[] masterKeyIds, boolean exportSecret, Uri outputUri) { OperationLog log = new OperationLog(); if (masterKeyIds != null) { log.add(LogType.MSG_EXPORT, 0, masterKeyIds.length); } else { log.add(LogType.MSG_EXPORT_ALL, 0); } // do we have a file name? if (outputUri == null) { log.add(LogType.MSG_EXPORT_ERROR_NO_URI, 1); return new ExportResult(ExportResult.RESULT_ERROR, log); } try { OutputStream outStream = mProviderHelper.getContentResolver().openOutputStream (outputUri); return exportKeyRings(log, masterKeyIds, exportSecret, outStream); } catch (FileNotFoundException e) { log.add(LogType.MSG_EXPORT_ERROR_URI_OPEN, 1); return new ExportResult(ExportResult.RESULT_ERROR, log); } } ExportResult exportKeyRings(OperationLog log, long[] masterKeyIds, boolean exportSecret, OutputStream outStream) { /* TODO isn't this checked above, with the isStorageMounted call? if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { log.add(LogType.MSG_EXPORT_ERROR_STORAGE, 1); return new ExportResult(ExportResult.RESULT_ERROR, log); } */ if (!BufferedOutputStream.class.isInstance(outStream)) { outStream = new BufferedOutputStream(outStream); } int okSecret = 0, okPublic = 0, progress = 0; Cursor cursor = null; try { String selection = null, selectionArgs[] = null; if (masterKeyIds != null) { // convert long[] to String[] selectionArgs = new String[masterKeyIds.length]; for (int i = 0; i < masterKeyIds.length; i++) { selectionArgs[i] = Long.toString(masterKeyIds[i]); } // generates ?,?,? as placeholders for selectionArgs String placeholders = TextUtils.join(",", Collections.nCopies(masterKeyIds.length, "?")); // put together selection string selection = Tables.KEYS + "." + KeyRings.MASTER_KEY_ID + " IN (" + placeholders + ")"; } cursor = mProviderHelper.getContentResolver().query( KeyRings.buildUnifiedKeyRingsUri(), new String[]{ KeyRings.MASTER_KEY_ID, KeyRings.PUBKEY_DATA, KeyRings.PRIVKEY_DATA, KeyRings.HAS_ANY_SECRET }, selection, selectionArgs, Tables.KEYS + "." + KeyRings.MASTER_KEY_ID ); if (cursor == null || !cursor.moveToFirst()) { log.add(LogType.MSG_EXPORT_ERROR_DB, 1); return new ExportResult(ExportResult.RESULT_ERROR, log, okPublic, okSecret); } int numKeys = cursor.getCount(); updateProgress( mContext.getResources().getQuantityString(R.plurals.progress_exporting_key, numKeys), 0, numKeys); // For each public masterKey id while (!cursor.isAfterLast()) { long keyId = cursor.getLong(0); ArmoredOutputStream arOutStream = null; // Create an output stream try { arOutStream = new ArmoredOutputStream(outStream); log.add(LogType.MSG_EXPORT_PUBLIC, 1, KeyFormattingUtils.beautifyKeyId(keyId)); byte[] data = cursor.getBlob(1); CanonicalizedKeyRing ring = UncachedKeyRing.decodeFromData(data).canonicalize(log, 2, true); ring.encode(arOutStream); okPublic += 1; } catch (PgpGeneralException e) { log.add(LogType.MSG_EXPORT_ERROR_KEY, 2); updateProgress(progress++, numKeys); continue; } finally { // make sure this is closed if (arOutStream != null) { arOutStream.close(); } arOutStream = null; } if (exportSecret && cursor.getInt(3) > 0) { try { arOutStream = new ArmoredOutputStream(outStream); // export secret key part log.add(LogType.MSG_EXPORT_SECRET, 2, KeyFormattingUtils.beautifyKeyId (keyId)); byte[] data = cursor.getBlob(2); CanonicalizedKeyRing ring = UncachedKeyRing.decodeFromData(data).canonicalize(log, 2, true); ring.encode(arOutStream); okSecret += 1; } catch (PgpGeneralException e) { log.add(LogType.MSG_EXPORT_ERROR_KEY, 2); updateProgress(progress++, numKeys); continue; } finally { // make sure this is closed if (arOutStream != null) { arOutStream.close(); } } } updateProgress(progress++, numKeys); cursor.moveToNext(); } updateProgress(R.string.progress_done, numKeys, numKeys); } catch (IOException e) { log.add(LogType.MSG_EXPORT_ERROR_IO, 1); return new ExportResult(ExportResult.RESULT_ERROR, log, okPublic, okSecret); } finally { // Make sure the stream is closed if (outStream != null) try { outStream.close(); } catch (Exception e) { Log.e(Constants.TAG, "error closing stream", e); } if (cursor != null) { cursor.close(); } } log.add(LogType.MSG_EXPORT_SUCCESS, 1); return new ExportResult(ExportResult.RESULT_OK, log, okPublic, okSecret); } @NonNull public ExportResult execute(ExportKeyringParcel exportInput, CryptoInputParcel cryptoInput) { switch (exportInput.mExportType) { case UPLOAD_KEYSERVER: { Proxy proxy; if (cryptoInput.getParcelableProxy() == null) { // explicit proxy not set if (!OrbotHelper.isOrbotInRequiredState(mContext)) { return new ExportResult(null, RequiredInputParcel.createOrbotRequiredOperation(), cryptoInput); } proxy = Preferences.getPreferences(mContext).getProxyPrefs() .parcelableProxy.getProxy(); } else { proxy = cryptoInput.getParcelableProxy().getProxy(); } HkpKeyserver hkpKeyserver = new HkpKeyserver(exportInput.mKeyserver); try { if (exportInput.mCanonicalizedPublicKeyringUri != null) { CanonicalizedPublicKeyRing keyring = mProviderHelper.getCanonicalizedPublicKeyRing( exportInput.mCanonicalizedPublicKeyringUri); return uploadKeyRingToServer(hkpKeyserver, keyring, proxy); } else { return uploadKeyRingToServer(hkpKeyserver, exportInput.mUncachedKeyRing, proxy); } } catch (ProviderHelper.NotFoundException e) { Log.e(Constants.TAG, "error uploading key", e); return new ExportResult(ExportResult.RESULT_ERROR, new OperationLog()); } } case EXPORT_FILE: { return exportToFile(exportInput.mMasterKeyIds, exportInput.mExportSecret, exportInput.mOutputFile); } case EXPORT_URI: { return exportToUri(exportInput.mMasterKeyIds, exportInput.mExportSecret, exportInput.mOutputUri); } default: { // can never happen, all enum types must be handled above throw new AssertionError("must not happen, this is a bug!"); } } } }