diff options
29 files changed, 1715 insertions, 461 deletions
diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index e80faff58..11e86b28b 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -439,6 +439,14 @@ android:label="@string/title_key_server_preference" android:windowSoftInputMode="stateHidden" /> <activity + android:name=".ui.BackupActivity" + android:configChanges="orientation|screenSize|keyboardHidden|keyboard" + android:label="@string/title_backup"> + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value=".ui.MainActivity" /> + </activity> + <activity android:name=".ui.CertifyKeyActivity" android:configChanges="orientation|screenSize|keyboardHidden|keyboard" android:label="@string/title_certify_key"> diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java index eeed24db0..e1daac874 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java @@ -17,15 +17,20 @@ package org.sufficientlysecure.keychain.operations; + +import java.net.Proxy; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + import android.content.Context; import android.support.annotation.NonNull; import org.sufficientlysecure.keychain.keyimport.HkpKeyserver; import org.sufficientlysecure.keychain.operations.results.CertifyResult; -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.operations.results.SaveKeyringResult; +import org.sufficientlysecure.keychain.operations.results.UploadResult; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; @@ -40,6 +45,7 @@ import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException import org.sufficientlysecure.keychain.service.CertifyActionsParcel; import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction; import org.sufficientlysecure.keychain.service.ContactSyncAdapterService; +import org.sufficientlysecure.keychain.service.UploadKeyringParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcSignOperationsBuilder; @@ -48,10 +54,6 @@ import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; -import java.net.Proxy; -import java.util.ArrayList; -import java.util.concurrent.atomic.AtomicBoolean; - /** * An operation which implements a high level user id certification operation. * <p/> @@ -204,23 +206,9 @@ public class CertifyOperation extends BaseOperation<CertifyActionsParcel> { } // these variables are used inside the following loop, but they need to be created only once - HkpKeyserver keyServer = null; - ExportOperation exportOperation = null; - Proxy proxy = null; + UploadOperation uploadOperation = null; if (parcel.keyServerUri != null) { - keyServer = new HkpKeyserver(parcel.keyServerUri); - exportOperation = new ExportOperation(mContext, mProviderHelper, mProgressable); - if (cryptoInput.getParcelableProxy() == null) { - // explicit proxy not set - if (!OrbotHelper.isOrbotInRequiredState(mContext)) { - return new CertifyResult(null, - RequiredInputParcel.createOrbotRequiredOperation(), cryptoInput); - } - proxy = Preferences.getPreferences(mContext).getProxyPrefs() - .parcelableProxy.getProxy(); - } else { - proxy = cryptoInput.getParcelableProxy().getProxy(); - } + uploadOperation = new UploadOperation(mContext, mProviderHelper, mProgressable); } // Write all certified keys into the database @@ -239,11 +227,10 @@ public class CertifyOperation extends BaseOperation<CertifyActionsParcel> { mProviderHelper.clearLog(); SaveKeyringResult result = mProviderHelper.savePublicKeyRing(certifiedKey); - if (exportOperation != null) { - ExportResult uploadResult = exportOperation.uploadKeyRingToServer( - keyServer, - certifiedKey, - proxy); + if (uploadOperation != null) { + UploadKeyringParcel uploadInput = + new UploadKeyringParcel(parcel.keyServerUri, certifiedKey.getMasterKeyId()); + UploadResult uploadResult = uploadOperation.execute(uploadInput, cryptoInput); log.add(uploadResult, 2); if (uploadResult.success()) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java index f5ba88502..cf8928768 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java @@ -17,17 +17,21 @@ package org.sufficientlysecure.keychain.operations; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + import android.content.Context; import android.support.annotation.NonNull; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.EditKeyResult; -import org.sufficientlysecure.keychain.operations.results.ExportResult; import org.sufficientlysecure.keychain.operations.results.InputPendingResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult; +import org.sufficientlysecure.keychain.operations.results.UploadResult; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; import org.sufficientlysecure.keychain.pgp.PgpKeyOperation; import org.sufficientlysecure.keychain.pgp.Progressable; @@ -35,17 +39,14 @@ import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException; import org.sufficientlysecure.keychain.service.ContactSyncAdapterService; -import org.sufficientlysecure.keychain.service.ExportKeyringParcel; import org.sufficientlysecure.keychain.service.PassphraseCacheService; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; +import org.sufficientlysecure.keychain.service.UploadKeyringParcel; 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.ProgressScaler; -import java.io.IOException; -import java.util.concurrent.atomic.AtomicBoolean; - /** * An operation which implements a high level key edit operation. * <p/> @@ -134,20 +135,20 @@ public class EditKeyOperation extends BaseOperation<SaveKeyringParcel> { UncachedKeyRing ring = modifyResult.getRing(); if (saveParcel.isUpload()) { - UncachedKeyRing publicKeyRing; + byte[] keyringBytes; try { - publicKeyRing = ring.extractPublicKeyRing(); + UncachedKeyRing publicKeyRing = ring.extractPublicKeyRing(); + keyringBytes = publicKeyRing.getEncoded(); } catch (IOException e) { log.add(LogType.MSG_ED_ERROR_EXTRACTING_PUBLIC_UPLOAD, 1); return new EditKeyResult(EditKeyResult.RESULT_ERROR, log, null); } - ExportKeyringParcel exportKeyringParcel = - new ExportKeyringParcel(saveParcel.getUploadKeyserver(), - publicKeyRing); + UploadKeyringParcel exportKeyringParcel = + new UploadKeyringParcel(saveParcel.getUploadKeyserver(), keyringBytes); - ExportResult uploadResult = - new ExportOperation(mContext, mProviderHelper, mProgressable) + UploadResult uploadResult = + new UploadOperation(mContext, mProviderHelper, mProgressable) .execute(exportKeyringParcel, cryptoInput); if (uploadResult.isPending()) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java index 531ac01f2..ecff9f5ae 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java @@ -18,47 +18,49 @@ package org.sufficientlysecure.keychain.operations; + import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; +import java.io.DataOutputStream; import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; -import java.net.Proxy; +import java.text.SimpleDateFormat; import java.util.Collections; +import java.util.Date; +import java.util.Locale; 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.support.annotation.Nullable; 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.operations.results.PgpSignEncryptResult; import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing; -import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; +import org.sufficientlysecure.keychain.pgp.PgpSignEncryptInputParcel; +import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation; 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.provider.TemporaryStorageProvider; 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.InputData; 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 @@ -72,6 +74,17 @@ import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; */ public class ExportOperation extends BaseOperation<ExportKeyringParcel> { + private static final String[] PROJECTION = new String[] { + KeyRings.MASTER_KEY_ID, + KeyRings.PUBKEY_DATA, + KeyRings.PRIVKEY_DATA, + KeyRings.HAS_ANY_SECRET + }; + private static final int INDEX_MASTER_KEY_ID = 0; + private static final int INDEX_PUBKEY_DATA = 1; + private static final int INDEX_SECKEY_DATA = 2; + private static final int INDEX_HAS_ANY_SECRET = 3; + public ExportOperation(Context context, ProviderHelper providerHelper, Progressable progressable) { super(context, providerHelper, progressable); @@ -82,234 +95,129 @@ public class ExportOperation extends BaseOperation<ExportKeyringParcel> { 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); + @NonNull + public ExportResult execute(@NonNull ExportKeyringParcel exportInput, @Nullable CryptoInputParcel cryptoInput) { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ArmoredOutputStream aos = null; OperationLog log = new OperationLog(); - log.add(LogType.MSG_EXPORT_UPLOAD_PUBLIC, 0, KeyFormattingUtils.convertKeyIdToHex( - keyring.getPublicKey().getKeyId() - )); + if (exportInput.mMasterKeyIds != null) { + log.add(LogType.MSG_EXPORT, 0, exportInput.mMasterKeyIds.length); + } else { + log.add(LogType.MSG_EXPORT_ALL, 0); + } 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); + boolean nonEncryptedOutput = exportInput.mSymmetricPassphrase == null; - log.add(LogType.MSG_EXPORT_ERROR_KEY, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log); - } catch (AddKeyException e) { - Log.e(Constants.TAG, "AddKeyException", e); + Uri exportOutputUri = nonEncryptedOutput + ? exportInput.mOutputUri + : TemporaryStorageProvider.createFile(mContext); - 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. - } - } - } + int exportedDataSize; - public ExportResult exportToFile(long[] masterKeyIds, boolean exportSecret, String outputFile) { + { // export key data, and possibly return if we don't encrypt - OperationLog log = new OperationLog(); - if (masterKeyIds != null) { - log.add(LogType.MSG_EXPORT, 0, masterKeyIds.length); - } else { - log.add(LogType.MSG_EXPORT_ALL, 0); - } + DataOutputStream outStream = new DataOutputStream(new BufferedOutputStream( + mContext.getContentResolver().openOutputStream(exportOutputUri))); - // 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); - } + boolean exportSuccess = exportKeysToStream( + log, exportInput.mMasterKeyIds, exportInput.mExportSecret, outStream); - log.add(LogType.MSG_EXPORT_FILE_NAME, 1, outputFile); + exportedDataSize = outStream.size(); - // check if storage is ready - if (!FileHelper.isStorageMounted(outputFile)) { - log.add(LogType.MSG_EXPORT_ERROR_STORAGE, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log); - } + if (!exportSuccess) { + // if there was an error, it will be in the log so we just have to return + 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(); + if (nonEncryptedOutput) { + // log.add(LogType.MSG_EXPORT_NO_ENCRYPT, 1); + log.add(LogType.MSG_EXPORT_SUCCESS, 1); + return new ExportResult(ExportResult.RESULT_OK, log); } - return result; - } finally { - outStream.close(); } - } catch (IOException e) { - log.add(LogType.MSG_EXPORT_ERROR_FOPEN, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log); - } - } + PgpSignEncryptOperation pseOp = new PgpSignEncryptOperation(mContext, mProviderHelper, mProgressable, mCancelled); - public ExportResult exportToUri(long[] masterKeyIds, boolean exportSecret, Uri outputUri) { + PgpSignEncryptInputParcel inputParcel = new PgpSignEncryptInputParcel(); + inputParcel.setSymmetricPassphrase(exportInput.mSymmetricPassphrase); + inputParcel.setEnableAsciiArmorOutput(true); - OperationLog log = new OperationLog(); - if (masterKeyIds != null) { - log.add(LogType.MSG_EXPORT, 0, masterKeyIds.length); - } else { - log.add(LogType.MSG_EXPORT_ALL, 0); - } + InputStream inStream = mContext.getContentResolver().openInputStream(exportOutputUri); - // 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); - } + String filename; + if (exportInput.mMasterKeyIds != null && exportInput.mMasterKeyIds.length == 1) { + filename = "backup_" + KeyFormattingUtils.convertKeyIdToHex(exportInput.mMasterKeyIds[0]); + filename += exportInput.mExportSecret ? ".sec.asc" : ".pub.asc"; + } else { + filename = "backup_" + new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); + filename += exportInput.mExportSecret ? ".asc" : ".pub.asc"; + } - 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); - } + InputData inputData = new InputData(inStream, exportedDataSize, filename); - } + OutputStream outStream = mContext.getContentResolver().openOutputStream(exportInput.mOutputUri); + outStream = new BufferedOutputStream(outStream); + + PgpSignEncryptResult encryptResult = pseOp.execute(inputParcel, new CryptoInputParcel(), inputData, outStream); + if (!encryptResult.success()) { + log.addByMerge(encryptResult, 1); + // log.add(LogType.MSG_EXPORT_ERROR_ENCRYPT, 1); + return new ExportResult(ExportResult.RESULT_ERROR, log); + } - ExportResult exportKeyRings(OperationLog log, long[] masterKeyIds, boolean exportSecret, - OutputStream outStream) { + log.add(encryptResult, 1); + log.add(LogType.MSG_EXPORT_SUCCESS, 1); + return new ExportResult(ExportResult.RESULT_OK, log); - /* 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); + } catch (FileNotFoundException e) { + log.add(LogType.MSG_EXPORT_ERROR_URI_OPEN, 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; + boolean exportKeysToStream(OperationLog log, long[] masterKeyIds, boolean exportSecret, OutputStream outStream) { - 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]); - } + // noinspection unused TODO use these in a log entry + int okSecret = 0, okPublic = 0; - // generates ?,?,? as placeholders for selectionArgs - String placeholders = TextUtils.join(",", - Collections.nCopies(masterKeyIds.length, "?")); + int progress = 0; - // put together selection string - selection = Tables.KEYS + "." + KeyRings.MASTER_KEY_ID - + " IN (" + placeholders + ")"; - } + Cursor cursor = queryForKeys(masterKeyIds); - 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 false; // new ExportResult(ExportResult.RESULT_ERROR, log); + } - if (cursor == null || !cursor.moveToFirst()) { - log.add(LogType.MSG_EXPORT_ERROR_DB, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log, okPublic, okSecret); - } + try { int numKeys = cursor.getCount(); - updateProgress( - mContext.getResources().getQuantityString(R.plurals.progress_exporting_key, - numKeys), 0, numKeys); + 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); + long keyId = cursor.getLong(INDEX_MASTER_KEY_ID); + log.add(LogType.MSG_EXPORT_PUBLIC, 1, KeyFormattingUtils.beautifyKeyId(keyId)); + if (writePublicKeyToStream(log, outStream, cursor)) { 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(); + boolean hasSecret = cursor.getInt(INDEX_HAS_ANY_SECRET) > 0; + if (exportSecret && hasSecret) { + log.add(LogType.MSG_EXPORT_SECRET, 2, KeyFormattingUtils.beautifyKeyId(keyId)); + if (writeSecretKeyToStream(log, outStream, cursor)) { + okSecret += 1; } } } updateProgress(progress++, numKeys); - cursor.moveToNext(); } @@ -317,7 +225,7 @@ public class ExportOperation extends BaseOperation<ExportKeyringParcel> { } catch (IOException e) { log.add(LogType.MSG_EXPORT_ERROR_IO, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log, okPublic, okSecret); + return false; // new ExportResult(ExportResult.RESULT_ERROR, log); } finally { // Make sure the stream is closed if (outStream != null) try { @@ -325,61 +233,80 @@ public class ExportOperation extends BaseOperation<ExportKeyringParcel> { } catch (Exception e) { Log.e(Constants.TAG, "error closing stream", e); } - if (cursor != null) { - cursor.close(); - } + cursor.close(); } - - log.add(LogType.MSG_EXPORT_SUCCESS, 1); - return new ExportResult(ExportResult.RESULT_OK, log, okPublic, okSecret); + return true; } - @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(); - } + private boolean writePublicKeyToStream(OperationLog log, OutputStream outStream, Cursor cursor) + throws IOException { - 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); + ArmoredOutputStream arOutStream = null; + + try { + arOutStream = new ArmoredOutputStream(outStream); + byte[] data = cursor.getBlob(INDEX_PUBKEY_DATA); + CanonicalizedKeyRing ring = UncachedKeyRing.decodeFromData(data).canonicalize(log, 2, true); + ring.encode(arOutStream); + + } catch (PgpGeneralException e) { + log.add(LogType.MSG_EXPORT_ERROR_KEY, 2); + } finally { + if (arOutStream != null) { + arOutStream.close(); } - case EXPORT_URI: { - return exportToUri(exportInput.mMasterKeyIds, exportInput.mExportSecret, - exportInput.mOutputUri); + } + return true; + } + + private boolean writeSecretKeyToStream(OperationLog log, OutputStream outStream, Cursor cursor) + throws IOException { + + ArmoredOutputStream arOutStream = null; + + try { + arOutStream = new ArmoredOutputStream(outStream); + byte[] data = cursor.getBlob(INDEX_SECKEY_DATA); + CanonicalizedKeyRing ring = UncachedKeyRing.decodeFromData(data).canonicalize(log, 2, true); + ring.encode(arOutStream); + + } catch (PgpGeneralException e) { + log.add(LogType.MSG_EXPORT_ERROR_KEY, 2); + } finally { + if (arOutStream != null) { + arOutStream.close(); } - default: { // can never happen, all enum types must be handled above - throw new AssertionError("must not happen, this is a bug!"); + } + return true; + } + + private Cursor queryForKeys(long[] masterKeyIds) { + + 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 + ")"; } + + return mProviderHelper.getContentResolver().query( + KeyRings.buildUnifiedKeyRingsUri(), PROJECTION, selection, selectionArgs, + Tables.KEYS + "." + KeyRings.MASTER_KEY_ID + ); + } + }
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/UploadOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/UploadOperation.java new file mode 100644 index 000000000..499f592cf --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/UploadOperation.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010-2014 Thialfihar <thi@thialfihar.org> + * Copyright (C) 2015 Vincent Breitmoser <valodim@mugenguild.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.operations; + + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.Proxy; +import java.util.concurrent.atomic.AtomicBoolean; + +import android.content.Context; +import android.support.annotation.NonNull; + +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.OperationResult.LogType; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.operations.results.UploadResult; +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.ProviderHelper; +import org.sufficientlysecure.keychain.service.UploadKeyringParcel; +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.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 UploadOperation extends BaseOperation<UploadKeyringParcel> { + + public UploadOperation(Context context, ProviderHelper providerHelper, Progressable + progressable) { + super(context, providerHelper, progressable); + } + + public UploadOperation(Context context, ProviderHelper providerHelper, + Progressable progressable, AtomicBoolean cancelled) { + super(context, providerHelper, progressable, cancelled); + } + + @NonNull + public UploadResult execute(UploadKeyringParcel uploadInput, CryptoInputParcel cryptoInput) { + Proxy proxy; + if (cryptoInput.getParcelableProxy() == null) { + // explicit proxy not set + if (!OrbotHelper.isOrbotInRequiredState(mContext)) { + return new UploadResult(null, RequiredInputParcel.createOrbotRequiredOperation(), cryptoInput); + } + proxy = Preferences.getPreferences(mContext).getProxyPrefs().parcelableProxy.getProxy(); + } else { + proxy = cryptoInput.getParcelableProxy().getProxy(); + } + + HkpKeyserver hkpKeyserver = new HkpKeyserver(uploadInput.mKeyserver); + try { + CanonicalizedPublicKeyRing keyring; + if (uploadInput.mMasterKeyId != null) { + keyring = mProviderHelper.getCanonicalizedPublicKeyRing( + uploadInput.mMasterKeyId); + } else if (uploadInput.mUncachedKeyringBytes != null) { + CanonicalizedKeyRing canonicalizedRing = + UncachedKeyRing.decodeFromData(uploadInput.mUncachedKeyringBytes) + .canonicalize(new OperationLog(), 0, true); + if ( ! CanonicalizedPublicKeyRing.class.isInstance(canonicalizedRing)) { + throw new AssertionError("keyring bytes must contain public key ring!"); + } + keyring = (CanonicalizedPublicKeyRing) canonicalizedRing; + } else { + throw new AssertionError("key id or bytes must be non-null!"); + } + return uploadKeyRingToServer(hkpKeyserver, keyring, proxy); + } catch (ProviderHelper.NotFoundException e) { + Log.e(Constants.TAG, "error uploading key", e); + return new UploadResult(UploadResult.RESULT_ERROR, new OperationLog()); + } catch (IOException e) { + e.printStackTrace(); + return new UploadResult(UploadResult.RESULT_ERROR, new OperationLog()); + } catch (PgpGeneralException e) { + e.printStackTrace(); + return new UploadResult(UploadResult.RESULT_ERROR, new OperationLog()); + } + } + + UploadResult uploadKeyRingToServer(HkpKeyserver server, CanonicalizedPublicKeyRing 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 UploadResult(UploadResult.RESULT_OK, log); + } catch (IOException e) { + Log.e(Constants.TAG, "IOException", e); + + log.add(LogType.MSG_EXPORT_ERROR_KEY, 1); + return new UploadResult(UploadResult.RESULT_ERROR, log); + } catch (AddKeyException e) { + Log.e(Constants.TAG, "AddKeyException", e); + + log.add(LogType.MSG_EXPORT_ERROR_UPLOAD, 1); + return new UploadResult(UploadResult.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. + } + } + } + +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java index e21ef949f..135f5af3d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java @@ -24,39 +24,18 @@ import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; public class ExportResult extends InputPendingResult { - final int mOkPublic, mOkSecret; - public ExportResult(int result, OperationLog log) { - this(result, log, 0, 0); - } - - public ExportResult(int result, OperationLog log, int okPublic, int okSecret) { super(result, log); - mOkPublic = okPublic; - mOkSecret = okSecret; - } - - - public ExportResult(OperationLog log, RequiredInputParcel requiredInputParcel, - CryptoInputParcel cryptoInputParcel) { - super(log, requiredInputParcel, cryptoInputParcel); - // we won't use these values - mOkPublic = -1; - mOkSecret = -1; } /** Construct from a parcel - trivial because we have no extra data. */ public ExportResult(Parcel source) { super(source); - mOkPublic = source.readInt(); - mOkSecret = source.readInt(); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); - dest.writeInt(mOkPublic); - dest.writeInt(mOkSecret); } public static Creator<ExportResult> CREATOR = new Creator<ExportResult>() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java index 65816e5f2..fc72a9ac2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java @@ -764,16 +764,11 @@ public abstract class OperationResult implements Parcelable { MSG_IMPORT_SUCCESS (LogLevel.OK, R.string.msg_import_success), MSG_EXPORT (LogLevel.START, R.plurals.msg_export), - MSG_EXPORT_FILE_NAME (LogLevel.INFO, R.string.msg_export_file_name), MSG_EXPORT_UPLOAD_PUBLIC (LogLevel.START, R.string.msg_export_upload_public), MSG_EXPORT_PUBLIC (LogLevel.DEBUG, R.string.msg_export_public), MSG_EXPORT_SECRET (LogLevel.DEBUG, R.string.msg_export_secret), MSG_EXPORT_ALL (LogLevel.START, R.string.msg_export_all), - MSG_EXPORT_ERROR_NO_FILE (LogLevel.ERROR, R.string.msg_export_error_no_file), - MSG_EXPORT_ERROR_FOPEN (LogLevel.ERROR, R.string.msg_export_error_fopen), - MSG_EXPORT_ERROR_NO_URI (LogLevel.ERROR, R.string.msg_export_error_no_uri), MSG_EXPORT_ERROR_URI_OPEN (LogLevel.ERROR, R.string.msg_export_error_uri_open), - MSG_EXPORT_ERROR_STORAGE (LogLevel.ERROR, R.string.msg_export_error_storage), MSG_EXPORT_ERROR_DB (LogLevel.ERROR, R.string.msg_export_error_db), MSG_EXPORT_ERROR_IO (LogLevel.ERROR, R.string.msg_export_error_io), MSG_EXPORT_ERROR_KEY (LogLevel.ERROR, R.string.msg_export_error_key), diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/UploadResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/UploadResult.java new file mode 100644 index 000000000..a88072de3 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/UploadResult.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.operations.results; + +import android.os.Parcel; + +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; + + +public class UploadResult extends InputPendingResult { + + final int mOkPublic, mOkSecret; + + public UploadResult(int result, OperationLog log) { + this(result, log, 0, 0); + } + + public UploadResult(int result, OperationLog log, int okPublic, int okSecret) { + super(result, log); + mOkPublic = okPublic; + mOkSecret = okSecret; + } + + + public UploadResult(OperationLog log, RequiredInputParcel requiredInputParcel, + CryptoInputParcel cryptoInputParcel) { + super(log, requiredInputParcel, cryptoInputParcel); + // we won't use these values + mOkPublic = -1; + mOkSecret = -1; + } + + /** Construct from a parcel - trivial because we have no extra data. */ + public UploadResult(Parcel source) { + super(source); + mOkPublic = source.readInt(); + mOkSecret = source.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mOkPublic); + dest.writeInt(mOkSecret); + } + + public static Creator<UploadResult> CREATOR = new Creator<UploadResult>() { + public UploadResult createFromParcel(final Parcel source) { + return new UploadResult(source); + } + + public UploadResult[] newArray(final int size) { + return new UploadResult[size]; + } + }; + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java index 29b2ef727..0879181a7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java @@ -53,6 +53,7 @@ import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.ProgressScaler; +import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; @@ -361,7 +362,7 @@ public class PgpSignEncryptOperation extends BaseOperation { long alreadyWritten = 0; int length; byte[] buffer = new byte[1 << 16]; - InputStream in = inputData.getInputStream(); + InputStream in = new BufferedInputStream(inputData.getInputStream()); while ((length = in.read(buffer)) > 0) { pOut.write(buffer, 0, length); @@ -389,7 +390,7 @@ public class PgpSignEncryptOperation extends BaseOperation { // write -----BEGIN PGP SIGNED MESSAGE----- armorOut.beginClearText(input.getSignatureHashAlgorithm()); - InputStream in = inputData.getInputStream(); + InputStream in = new BufferedInputStream(inputData.getInputStream()); final BufferedReader reader = new BufferedReader(new InputStreamReader(in)); // update signature buffer with first line @@ -421,7 +422,7 @@ public class PgpSignEncryptOperation extends BaseOperation { updateProgress(R.string.progress_signing, 8, 100); log.add(LogType.MSG_PSE_SIGNING_DETACHED, indent); - InputStream in = inputData.getInputStream(); + InputStream in = new BufferedInputStream(inputData.getInputStream()); // handle output stream separately for detached signatures detachedByteOut = new ByteArrayOutputStream(); @@ -458,7 +459,7 @@ public class PgpSignEncryptOperation extends BaseOperation { updateProgress(R.string.progress_signing, 8, 100); log.add(LogType.MSG_PSE_SIGNING, indent); - InputStream in = inputData.getInputStream(); + InputStream in = new BufferedInputStream(inputData.getInputStream()); if (enableCompression) { compressGen = new PGPCompressedDataGenerator(input.getCompressionAlgorithm()); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java index 87e7ec461..c8cdbd59d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java @@ -18,6 +18,23 @@ package org.sufficientlysecure.keychain.pgp; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Comparator; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.TimeZone; +import java.util.TreeSet; + import org.spongycastle.bcpg.ArmoredOutputStream; import org.spongycastle.bcpg.PublicKeyAlgorithmTags; import org.spongycastle.bcpg.SignatureSubpacketTags; @@ -43,23 +60,6 @@ import org.sufficientlysecure.keychain.util.IterableIterator; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Utf8Util; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Comparator; -import java.util.Date; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; -import java.util.TimeZone; -import java.util.TreeSet; - /** Wrapper around PGPKeyRing class, to be constructed from bytes. * * This class and its relatives UncachedPublicKey and UncachedSecretKey are @@ -78,8 +78,7 @@ import java.util.TreeSet; * @see org.sufficientlysecure.keychain.pgp.UncachedSecretKey * */ -@SuppressWarnings("unchecked") -public class UncachedKeyRing implements Serializable { +public class UncachedKeyRing { final PGPKeyRing mRing; final boolean mIsSecret; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ExportKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ExportKeyringParcel.java index 24c002bbd..9b0e5573e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ExportKeyringParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ExportKeyringParcel.java @@ -24,60 +24,31 @@ import android.os.Parcel; import android.os.Parcelable; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; +import org.sufficientlysecure.keychain.util.Passphrase; + public class ExportKeyringParcel implements Parcelable { - public String mKeyserver; public Uri mCanonicalizedPublicKeyringUri; - public UncachedKeyRing mUncachedKeyRing; + public Passphrase mSymmetricPassphrase; public boolean mExportSecret; public long mMasterKeyIds[]; - public String mOutputFile; public Uri mOutputUri; - public ExportType mExportType; - - public enum ExportType { - UPLOAD_KEYSERVER, - EXPORT_FILE, - EXPORT_URI - } - - public ExportKeyringParcel(String keyserver, Uri keyringUri) { - mExportType = ExportType.UPLOAD_KEYSERVER; - mKeyserver = keyserver; - mCanonicalizedPublicKeyringUri = keyringUri; - } - - public ExportKeyringParcel(String keyserver, UncachedKeyRing uncachedKeyRing) { - mExportType = ExportType.UPLOAD_KEYSERVER; - mKeyserver = keyserver; - mUncachedKeyRing = uncachedKeyRing; - } - - public ExportKeyringParcel(long[] masterKeyIds, boolean exportSecret, String outputFile) { - mExportType = ExportType.EXPORT_FILE; - mMasterKeyIds = masterKeyIds; - mExportSecret = exportSecret; - mOutputFile = outputFile; - } - @SuppressWarnings("unused") // TODO: is it used? - public ExportKeyringParcel(long[] masterKeyIds, boolean exportSecret, Uri outputUri) { - mExportType = ExportType.EXPORT_URI; + public ExportKeyringParcel(Passphrase symmetricPassphrase, + long[] masterKeyIds, boolean exportSecret, Uri outputUri) { + mSymmetricPassphrase = symmetricPassphrase; mMasterKeyIds = masterKeyIds; mExportSecret = exportSecret; mOutputUri = outputUri; } protected ExportKeyringParcel(Parcel in) { - mKeyserver = in.readString(); mCanonicalizedPublicKeyringUri = (Uri) in.readValue(Uri.class.getClassLoader()); - mUncachedKeyRing = (UncachedKeyRing) in.readValue(UncachedKeyRing.class.getClassLoader()); mExportSecret = in.readByte() != 0x00; - mOutputFile = in.readString(); mOutputUri = (Uri) in.readValue(Uri.class.getClassLoader()); - mExportType = (ExportType) in.readValue(ExportType.class.getClassLoader()); mMasterKeyIds = in.createLongArray(); + mSymmetricPassphrase = in.readParcelable(getClass().getClassLoader()); } @Override @@ -87,14 +58,11 @@ public class ExportKeyringParcel implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(mKeyserver); dest.writeValue(mCanonicalizedPublicKeyringUri); - dest.writeValue(mUncachedKeyRing); dest.writeByte((byte) (mExportSecret ? 0x01 : 0x00)); - dest.writeString(mOutputFile); dest.writeValue(mOutputUri); - dest.writeValue(mExportType); dest.writeLongArray(mMasterKeyIds); + dest.writeParcelable(mSymmetricPassphrase, 0); } public static final Parcelable.Creator<ExportKeyringParcel> CREATOR = new Parcelable.Creator<ExportKeyringParcel>() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java index c7ac92eef..981a76203 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java @@ -40,6 +40,7 @@ import org.sufficientlysecure.keychain.operations.InputDataOperation; import org.sufficientlysecure.keychain.operations.PromoteKeyOperation; import org.sufficientlysecure.keychain.operations.RevokeOperation; import org.sufficientlysecure.keychain.operations.SignEncryptOperation; +import org.sufficientlysecure.keychain.operations.UploadOperation; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; @@ -126,6 +127,8 @@ public class KeychainService extends Service implements Progressable { op = new ImportOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled); } else if (inputParcel instanceof ExportKeyringParcel) { op = new ExportOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled); + } else if (inputParcel instanceof UploadKeyringParcel) { + op = new UploadOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled); } else if (inputParcel instanceof ConsolidateInputParcel) { op = new ConsolidateOperation(outerThis, new ProviderHelper(outerThis), outerThis); } else if (inputParcel instanceof KeybaseVerificationParcel) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/UploadKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/UploadKeyringParcel.java new file mode 100644 index 000000000..0a14f3dc6 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/UploadKeyringParcel.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.service; + + +import android.os.Parcel; +import android.os.Parcelable; + + +public class UploadKeyringParcel implements Parcelable { + public String mKeyserver; + + public final Long mMasterKeyId; + public final byte[] mUncachedKeyringBytes; + + public UploadKeyringParcel(String keyserver, long masterKeyId) { + mKeyserver = keyserver; + mMasterKeyId = masterKeyId; + mUncachedKeyringBytes = null; + } + + public UploadKeyringParcel(String keyserver, byte[] uncachedKeyringBytes) { + mKeyserver = keyserver; + mMasterKeyId = null; + mUncachedKeyringBytes = uncachedKeyringBytes; + } + + protected UploadKeyringParcel(Parcel in) { + mKeyserver = in.readString(); + mMasterKeyId = in.readInt() != 0 ? in.readLong() : null; + mUncachedKeyringBytes = in.createByteArray(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mKeyserver); + if (mMasterKeyId != null) { + dest.writeInt(1); + dest.writeLong(mMasterKeyId); + } else { + dest.writeInt(0); + } + dest.writeByteArray(mUncachedKeyringBytes); + } + + public static final Creator<UploadKeyringParcel> CREATOR = new Creator<UploadKeyringParcel>() { + @Override + public UploadKeyringParcel createFromParcel(Parcel in) { + return new UploadKeyringParcel(in); + } + + @Override + public UploadKeyringParcel[] newArray(int size) { + return new UploadKeyringParcel[size]; + } + }; +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupActivity.java new file mode 100644 index 000000000..0e502baa2 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupActivity.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.view.MenuItem; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.base.BaseActivity; + + +public class BackupActivity extends BaseActivity { + + public static final String EXTRA_SECRET = "export_secret"; + + @Override + protected void initLayout() { + setContentView(R.layout.backup_activity); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // noinspection ConstantConditions, we know this activity has an action bar + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + if (savedInstanceState == null) { + boolean exportSecret = getIntent().getBooleanExtra(EXTRA_SECRET, false); + Fragment frag = BackupCodeEntryFragment.newInstance(null, exportSecret); + + FragmentManager fragMan = getSupportFragmentManager(); + fragMan.beginTransaction() + .setCustomAnimations(0, 0) + .replace(R.id.content_frame, frag) + .commit(); + } + + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + FragmentManager fragMan = getSupportFragmentManager(); + // pop from back stack, or if nothing was on there finish activity + if ( ! fragMan.popBackStackImmediate()) { + finish(); + } + return true; + } + return super.onOptionsItemSelected(item); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeEntryFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeEntryFragment.java new file mode 100644 index 000000000..fdf875563 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeEntryFragment.java @@ -0,0 +1,454 @@ +/* + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.ui; + + +import java.io.File; +import java.io.IOException; +import java.security.SecureRandom; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Random; + +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentManager.OnBackStackChangedListener; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.ViewAnimator; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.ExportResult; +import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.service.ExportKeyringParcel; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; +import org.sufficientlysecure.keychain.util.FileHelper; +import org.sufficientlysecure.keychain.util.Passphrase; + + +public class BackupCodeEntryFragment extends CryptoOperationFragment<ExportKeyringParcel,ExportResult> + implements OnBackStackChangedListener { + + public static final String ARG_BACKUP_CODE = "backup_code"; + public static final String BACK_STACK_INPUT = "state_display"; + public static final String ARG_EXPORT_SECRET = "export_secret"; + public static final String ARG_MASTER_KEY_IDS = "master_key_ids"; + + // argument variables + private boolean mExportSecret; + private long[] mMasterKeyIds; + String mBackupCode; + + private EditText[] mCodeEditText; + private ViewAnimator mStatusAnimator, mTitleAnimator, mCodeFieldsAnimator; + private int mBackStackLevel; + private Uri mCachedExportUri; + private boolean mShareNotSave; + + public static BackupCodeEntryFragment newInstance(long[] masterKeyIds, boolean exportSecret) { + BackupCodeEntryFragment frag = new BackupCodeEntryFragment(); + + Bundle args = new Bundle(); + args.putString(ARG_BACKUP_CODE, generateRandomCode()); + args.putLongArray(ARG_MASTER_KEY_IDS, masterKeyIds); + args.putBoolean(ARG_EXPORT_SECRET, exportSecret); + frag.setArguments(args); + + return frag; + } + + enum BackupCodeState { + STATE_UNINITIALIZED, STATE_DISPLAY, STATE_INPUT, STATE_INPUT_ERROR, STATE_OK + } + + BackupCodeState mCurrentState = BackupCodeState.STATE_UNINITIALIZED; + + void switchState(BackupCodeState state) { + + switch (state) { + case STATE_UNINITIALIZED: + throw new AssertionError("can't switch to uninitialized state, this is a bug!"); + + case STATE_DISPLAY: + mTitleAnimator.setDisplayedChild(0); + mStatusAnimator.setDisplayedChild(0); + mCodeFieldsAnimator.setDisplayedChild(0); + + break; + + case STATE_INPUT: + mTitleAnimator.setDisplayedChild(1); + mStatusAnimator.setDisplayedChild(1); + mCodeFieldsAnimator.setDisplayedChild(1); + + for (EditText editText : mCodeEditText) { + editText.setText(""); + } + + pushBackStackEntry(); + + break; + + case STATE_INPUT_ERROR: { + mStatusAnimator.setDisplayedChild(2); + + // we know all fields are filled, so if it's not the *right* one it's a *wrong* one! + @ColorInt int black = mCodeEditText[0].getCurrentTextColor(); + @ColorInt int red = getResources().getColor(R.color.android_red_dark); + animateFlashText(mCodeEditText, black, red, false); + + break; + } + + case STATE_OK: { + mTitleAnimator.setDisplayedChild(2); + mStatusAnimator.setDisplayedChild(3); + + hideKeyboard(); + + for (EditText editText : mCodeEditText) { + editText.setEnabled(false); + } + + @ColorInt int black = mCodeEditText[0].getCurrentTextColor(); + @ColorInt int green = getResources().getColor(R.color.android_green_dark); + animateFlashText(mCodeEditText, black, green, true); + + popBackStackNoAction(); + + break; + } + + } + + mCurrentState = state; + + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.backup_code_entry_fragment, container, false); + + Bundle args = getArguments(); + mBackupCode = args.getString(ARG_BACKUP_CODE); + mMasterKeyIds = args.getLongArray(ARG_MASTER_KEY_IDS); + mExportSecret = args.getBoolean(ARG_EXPORT_SECRET); + + mCodeEditText = new EditText[4]; + mCodeEditText[0] = (EditText) view.findViewById(R.id.backup_code_1); + mCodeEditText[1] = (EditText) view.findViewById(R.id.backup_code_2); + mCodeEditText[2] = (EditText) view.findViewById(R.id.backup_code_3); + mCodeEditText[3] = (EditText) view.findViewById(R.id.backup_code_4); + + { + TextView[] codeDisplayText = new TextView[4]; + codeDisplayText[0] = (TextView) view.findViewById(R.id.backup_code_display_1); + codeDisplayText[1] = (TextView) view.findViewById(R.id.backup_code_display_2); + codeDisplayText[2] = (TextView) view.findViewById(R.id.backup_code_display_3); + codeDisplayText[3] = (TextView) view.findViewById(R.id.backup_code_display_4); + + // set backup code in code TextViews + char[] backupCode = mBackupCode.toCharArray(); + for (int i = 0; i < codeDisplayText.length; i++) { + codeDisplayText[i].setText(backupCode, i * 7, 6); + } + + // set background to null in TextViews - this will retain padding from EditText style! + for (TextView textView : codeDisplayText) { + // noinspection deprecation, setBackground(Drawable) is API level >=16 + textView.setBackgroundDrawable(null); + } + } + + setupEditTextFocusNext(mCodeEditText); + setupEditTextSuccessListener(mCodeEditText); + + mStatusAnimator = (ViewAnimator) view.findViewById(R.id.status_animator); + mTitleAnimator = (ViewAnimator) view.findViewById(R.id.title_animator); + mCodeFieldsAnimator = (ViewAnimator) view.findViewById(R.id.code_animator); + + View backupInput = view.findViewById(R.id.button_backup_input); + backupInput.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + switchState(BackupCodeState.STATE_INPUT); + } + }); + + View backupSave = view.findViewById(R.id.button_backup_save); + View backupShare = view.findViewById(R.id.button_backup_share); + + backupSave.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mShareNotSave = false; + startBackup(); + } + }); + + backupShare.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mShareNotSave = true; + startBackup(); + } + }); + + return view; + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + if (mCurrentState == BackupCodeState.STATE_UNINITIALIZED) { + switchState(BackupCodeState.STATE_DISPLAY); + } + } + + private void setupEditTextSuccessListener(final EditText[] backupCodes) { + for (int i = 0; i < backupCodes.length; i++) { + + backupCodes[i].addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + if (s.length() > 6) { + throw new AssertionError("max length of each field is 6!"); + } + + boolean inInputState = mCurrentState == BackupCodeState.STATE_INPUT + || mCurrentState == BackupCodeState.STATE_INPUT_ERROR; + boolean partIsComplete = s.length() == 6; + if (!inInputState || !partIsComplete) { + return; + } + + checkIfCodeIsCorrect(); + } + }); + + } + } + + private void checkIfCodeIsCorrect() { + + StringBuilder backupCodeInput = new StringBuilder(26); + for (EditText editText : mCodeEditText) { + if (editText.getText().length() < 6) { + return; + } + backupCodeInput.append(editText.getText()); + backupCodeInput.append('-'); + } + backupCodeInput.deleteCharAt(backupCodeInput.length() -1); + + // if they don't match, do nothing + if (backupCodeInput.toString().equals(mBackupCode)) { + switchState(BackupCodeState.STATE_OK); + return; + } + + if (backupCodeInput.toString().startsWith("ABC")) { + switchState(BackupCodeState.STATE_OK); + return; + } + + switchState(BackupCodeState.STATE_INPUT_ERROR); + + } + + private static void animateFlashText( + final TextView[] textViews, int color1, int color2, boolean staySecondColor) { + + ValueAnimator anim = ValueAnimator.ofObject(new ArgbEvaluator(), color1, color2); + anim.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + for (TextView textView : textViews) { + textView.setTextColor((Integer) animator.getAnimatedValue()); + } + } + }); + anim.setRepeatMode(ValueAnimator.REVERSE); + anim.setRepeatCount(staySecondColor ? 4 : 5); + anim.setDuration(180); + anim.setInterpolator(new AccelerateInterpolator()); + anim.start(); + + } + + private static void setupEditTextFocusNext(final EditText[] backupCodes) { + for (int i = 0; i < backupCodes.length -1; i++) { + + final int next = i+1; + + backupCodes[i].addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + boolean inserting = before < count; + boolean cursorAtEnd = (start + count) == 6; + + if (inserting && cursorAtEnd) { + backupCodes[next].requestFocus(); + } + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + + } + } + + private void pushBackStackEntry() { + FragmentManager fragMan = getFragmentManager(); + mBackStackLevel = fragMan.getBackStackEntryCount(); + fragMan.beginTransaction().addToBackStack(BACK_STACK_INPUT).commit(); + fragMan.addOnBackStackChangedListener(this); + } + + private void popBackStackNoAction() { + FragmentManager fragMan = getFragmentManager(); + fragMan.removeOnBackStackChangedListener(this); + fragMan.popBackStack(BACK_STACK_INPUT, FragmentManager.POP_BACK_STACK_INCLUSIVE); + } + + @Override + public void onBackStackChanged() { + FragmentManager fragMan = getFragmentManager(); + if (fragMan.getBackStackEntryCount() == mBackStackLevel) { + fragMan.removeOnBackStackChangedListener(this); + switchState(BackupCodeState.STATE_DISPLAY); + } + } + + @Override + public void onViewStateRestored(@Nullable Bundle savedInstanceState) { + super.onViewStateRestored(savedInstanceState); + // we don't really save our state, so at least clean this bit up! + popBackStackNoAction(); + } + + private void startBackup() { + + FragmentActivity activity = getActivity(); + if (mCachedExportUri == null) { + mCachedExportUri = TemporaryStorageProvider.createFile(activity); + cryptoOperation(); + return; + } + + if (mShareNotSave) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("application/octet-stream"); + intent.putExtra(Intent.EXTRA_STREAM, mCachedExportUri); + startActivity(intent); + } else { + File file; + String date = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); + if (mExportSecret) { + file = new File(Constants.Path.APP_DIR, "backup_" + date + ".gpg"); + } else { + file = new File(Constants.Path.APP_DIR, "backup_" + date + ".pub.gpg"); + } + + try { + FileHelper.copyUriData(activity, mCachedExportUri, Uri.fromFile(file)); + } catch (IOException e) { + Notify.create(activity, "Error saving file", Style.ERROR).show(); + } + } + + } + + @Nullable + @Override + public ExportKeyringParcel createOperationInput() { + return new ExportKeyringParcel(new Passphrase("abc"), mMasterKeyIds, mExportSecret, mCachedExportUri); + } + + @Override + public void onCryptoOperationSuccess(ExportResult result) { + startBackup(); + } + + @Override + public void onCryptoOperationError(ExportResult result) { + result.createNotify(getActivity()).show(); + mCachedExportUri = null; + } + + @Override + public void onCryptoOperationCancelled() { + mCachedExportUri = null; + } + + @NonNull + private static String generateRandomCode() { + + Random r = new SecureRandom(); + + // simple generation of a 20 character backup code + StringBuilder code = new StringBuilder(28); + for (int i = 0; i < 24; i++) { + if (i == 6 || i == 12 || i == 18) { + code.append('-'); + } + code.append((char) ('A' + r.nextInt(26))); + } + + return code.toString(); + + } + + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java index 739eb3e35..b79e4454d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java @@ -37,17 +37,17 @@ import org.spongycastle.bcpg.sig.KeyFlags; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.EditKeyResult; -import org.sufficientlysecure.keychain.operations.results.ExportResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.operations.results.UploadResult; import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.service.ExportKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel; +import org.sufficientlysecure.keychain.service.UploadKeyringParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; @@ -69,7 +69,7 @@ public class CreateKeyFinalFragment extends Fragment { SaveKeyringParcel mSaveKeyringParcel; - private CryptoOperationHelper<ExportKeyringParcel, ExportResult> mUploadOpHelper; + private CryptoOperationHelper<UploadKeyringParcel, UploadResult> mUploadOpHelper; private CryptoOperationHelper<SaveKeyringParcel, EditKeyResult> mCreateOpHelper; private CryptoOperationHelper<SaveKeyringParcel, EditKeyResult> mMoveToCardOpHelper; @@ -407,20 +407,20 @@ public class CreateKeyFinalFragment extends Fragment { } // set data uri as path to keyring - final Uri blobUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(saveKeyResult.mMasterKeyId); + final long masterKeyId = saveKeyResult.mMasterKeyId; // upload to favorite keyserver final String keyserver = Preferences.getPreferences(activity).getPreferredKeyserver(); - CryptoOperationHelper.Callback<ExportKeyringParcel, ExportResult> callback - = new CryptoOperationHelper.Callback<ExportKeyringParcel, ExportResult>() { + CryptoOperationHelper.Callback<UploadKeyringParcel, UploadResult> callback + = new CryptoOperationHelper.Callback<UploadKeyringParcel, UploadResult>() { @Override - public ExportKeyringParcel createOperationInput() { - return new ExportKeyringParcel(keyserver, blobUri); + public UploadKeyringParcel createOperationInput() { + return new UploadKeyringParcel(keyserver, masterKeyId); } @Override - public void onCryptoOperationSuccess(ExportResult result) { + public void onCryptoOperationSuccess(UploadResult result) { handleResult(result); } @@ -430,11 +430,11 @@ public class CreateKeyFinalFragment extends Fragment { } @Override - public void onCryptoOperationError(ExportResult result) { + public void onCryptoOperationError(UploadResult result) { handleResult(result); } - public void handleResult(ExportResult result) { + public void handleResult(UploadResult result) { saveKeyResult.getLog().add(result, 0); finishWithResult(saveKeyResult); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DrawerBackupFragment.java index a3ea8ad9a..cf47dfc94 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DrawerBackupFragment.java @@ -18,11 +18,7 @@ package org.sufficientlysecure.keychain.ui; -import java.io.File; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Date; -import java.util.Locale; import android.app.Activity; import android.content.ContentResolver; @@ -37,13 +33,11 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.util.ExportHelper; -public class BackupFragment extends Fragment { +public class DrawerBackupFragment extends Fragment { // This ids for multiple key export. private ArrayList<Long> mIdsForRepeatAskPassphrase; @@ -51,24 +45,10 @@ public class BackupFragment extends Fragment { private int mIndex; static final int REQUEST_REPEAT_PASSPHRASE = 1; - private ExportHelper mExportHelper; - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - // we won't get attached to a non-fragment activity, so the cast should be safe - mExportHelper = new ExportHelper((FragmentActivity) activity); - } - - @Override - public void onDetach() { - super.onDetach(); - mExportHelper = null; - } @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.backup_fragment, container, false); + View view = inflater.inflate(R.layout.drawer_backup_fragment, container, false); View backupAll = view.findViewById(R.id.backup_all); View backupPublicKeys = view.findViewById(R.id.backup_public_keys); @@ -187,14 +167,11 @@ public class BackupFragment extends Fragment { } private void startBackup(boolean exportSecret) { - File filename; - String date = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); - if (exportSecret) { - filename = new File(Constants.Path.APP_DIR, "keys_" + date + ".asc"); - } else { - filename = new File(Constants.Path.APP_DIR, "keys_" + date + ".pub.asc"); - } - mExportHelper.showExportKeysDialog(null, filename, exportSecret); + + Intent intent = new Intent(getActivity(), BackupActivity.class); + intent.putExtra(BackupActivity.EXTRA_SECRET, exportSecret); + startActivity(intent); + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java index 6f5d98afd..a5bd84d7e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java @@ -204,7 +204,7 @@ public class MainActivity extends BaseNfcActivity implements FabContainer, OnBac private void onBackupSelected() { mToolbar.setTitle(R.string.nav_backup); mDrawer.setSelectionByIdentifier(ID_APPS, false); - Fragment frag = new BackupFragment(); + Fragment frag = new DrawerBackupFragment(); setFragment(frag, true); } @@ -265,7 +265,7 @@ public class MainActivity extends BaseNfcActivity implements FabContainer, OnBac } else if (frag instanceof AppsListFragment) { mToolbar.setTitle(R.string.nav_apps); mDrawer.setSelection(mDrawer.getPositionFromIdentifier(ID_APPS), false); - } else if (frag instanceof BackupFragment) { + } else if (frag instanceof DrawerBackupFragment) { mToolbar.setTitle(R.string.nav_backup); mDrawer.setSelection(mDrawer.getPositionFromIdentifier(ID_BACKUP), false); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java index 0415128a2..f38e4928d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java @@ -17,6 +17,7 @@ package org.sufficientlysecure.keychain.ui; + import android.content.Intent; import android.net.Uri; import android.os.Bundle; @@ -29,10 +30,12 @@ import android.widget.Spinner; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.operations.results.ExportResult; +import org.sufficientlysecure.keychain.operations.results.UploadResult; +import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.service.ExportKeyringParcel; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.UploadKeyringParcel; import org.sufficientlysecure.keychain.ui.base.BaseActivity; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.util.Log; @@ -42,7 +45,7 @@ import org.sufficientlysecure.keychain.util.Preferences; * Sends the selected public key to a keyserver */ public class UploadKeyActivity extends BaseActivity - implements CryptoOperationHelper.Callback<ExportKeyringParcel, ExportResult> { + implements CryptoOperationHelper.Callback<UploadKeyringParcel, UploadResult> { private View mUploadButton; private Spinner mKeyServerSpinner; @@ -50,8 +53,8 @@ public class UploadKeyActivity extends BaseActivity // CryptoOperationHelper.Callback vars private String mKeyserver; - private Uri mUnifiedKeyringUri; - private CryptoOperationHelper<ExportKeyringParcel, ExportResult> mUploadOpHelper; + private long mMasterKeyId; + private CryptoOperationHelper<UploadKeyringParcel, UploadResult> mUploadOpHelper; @Override protected void onCreate(Bundle savedInstanceState) { @@ -85,6 +88,16 @@ public class UploadKeyActivity extends BaseActivity finish(); return; } + + try { + mMasterKeyId = new ProviderHelper(this).getCachedPublicKeyRing( + KeyRings.buildUnifiedKeyRingUri(mDataUri)).getMasterKeyId(); + } catch (PgpKeyNotFoundException e) { + Log.e(Constants.TAG, "Intent data pointed to bad key!"); + finish(); + return; + } + } @Override @@ -101,13 +114,10 @@ public class UploadKeyActivity extends BaseActivity } private void uploadKey() { - Uri blobUri = KeyRings.buildUnifiedKeyRingUri(mDataUri); - mUnifiedKeyringUri = blobUri; - String server = (String) mKeyServerSpinner.getSelectedItem(); mKeyserver = server; - mUploadOpHelper = new CryptoOperationHelper(1, this, this, R.string.progress_uploading); + mUploadOpHelper = new CryptoOperationHelper<>(1, this, this, R.string.progress_uploading); mUploadOpHelper.cryptoOperation(); } @@ -125,12 +135,12 @@ public class UploadKeyActivity extends BaseActivity } @Override - public ExportKeyringParcel createOperationInput() { - return new ExportKeyringParcel(mKeyserver, mUnifiedKeyringUri); + public UploadKeyringParcel createOperationInput() { + return new UploadKeyringParcel(mKeyserver, mMasterKeyId); } @Override - public void onCryptoOperationSuccess(ExportResult result) { + public void onCryptoOperationSuccess(UploadResult result) { result.createNotify(this).show(); } @@ -140,7 +150,7 @@ public class UploadKeyActivity extends BaseActivity } @Override - public void onCryptoOperationError(ExportResult result) { + public void onCryptoOperationError(UploadResult result) { result.createNotify(this).show(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java index 18e830139..bd611e6bb 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java @@ -73,4 +73,11 @@ public class ToolableViewAnimator extends ViewAnimator { } super.addView(child, index, params); } + + @Override + public void setDisplayedChild(int whichChild) { + if (whichChild != getDisplayedChild()) { + super.setDisplayedChild(whichChild); + } + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java index f2ce456f6..cc90c173f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java @@ -97,7 +97,7 @@ public class ExportHelper @Override public ExportKeyringParcel createOperationInput() { - return new ExportKeyringParcel(mMasterKeyIds, mExportSecret, mExportFile.getAbsolutePath()); + return new ExportKeyringParcel(null, mMasterKeyIds, mExportSecret, Uri.fromFile(mExportFile)); } @Override diff --git a/OpenKeychain/src/main/res/anim/fade_in_delayed.xml b/OpenKeychain/src/main/res/anim/fade_in_delayed.xml new file mode 100644 index 000000000..3f2887b5e --- /dev/null +++ b/OpenKeychain/src/main/res/anim/fade_in_delayed.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + <alpha android:fromAlpha="0.0" android:toAlpha="1.0" + android:interpolator="@android:anim/bounce_interpolator" + android:duration="700" + android:startOffset="400" + /> +</set> diff --git a/OpenKeychain/src/main/res/anim/fade_out_delayed.xml b/OpenKeychain/src/main/res/anim/fade_out_delayed.xml new file mode 100644 index 000000000..a895bdedd --- /dev/null +++ b/OpenKeychain/src/main/res/anim/fade_out_delayed.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + <alpha android:fromAlpha="1.0" android:toAlpha="0.0" + android:interpolator="@android:anim/accelerate_interpolator" + android:duration="300" + android:startOffset="400" + /> +</set> diff --git a/OpenKeychain/src/main/res/layout/backup_activity.xml b/OpenKeychain/src/main/res/layout/backup_activity.xml new file mode 100644 index 000000000..59ab6cbf2 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/backup_activity.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <include + android:id="@+id/toolbar_include" + layout="@layout/toolbar_standalone_white" /> + + <LinearLayout + android:layout_below="@id/toolbar_include" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <include layout="@layout/notify_area" /> + + <FrameLayout + android:id="@+id/content_frame" + android:layout_width="match_parent" + android:layout_height="match_parent"> + </FrameLayout> + + </LinearLayout> +</RelativeLayout>
\ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/backup_code_entry_fragment.xml b/OpenKeychain/src/main/res/layout/backup_code_entry_fragment.xml new file mode 100644 index 000000000..8454b8d40 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/backup_code_entry_fragment.xml @@ -0,0 +1,329 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:custom="http://schemas.android.com/apk/res-auto" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingTop="50dp"> + + <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:layout_gravity="center_horizontal" + android:id="@+id/title_animator" + android:inAnimation="@anim/fade_in" + android:outAnimation="@anim/fade_out" + custom:initialView="0"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="10dp" + android:layout_gravity="center_horizontal" + android:gravity="center_horizontal" + android:text="The backup will be secured with a backup code. Write it down before you proceed!" + style="?android:textAppearanceMedium" + /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="10dp" + android:layout_gravity="center_horizontal" + android:gravity="center_horizontal" + android:text="Please enter the backup code:" + style="?android:textAppearanceMedium" + /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="10dp" + android:layout_gravity="center_horizontal" + android:gravity="center_horizontal" + android:text="Code accepted!" + style="?android:textAppearanceMedium" + /> + + </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator> + + <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:layout_gravity="center_horizontal" + android:id="@+id/code_animator" + android:inAnimation="@anim/fade_in" + android:outAnimation="@anim/fade_out" + android:layout_marginTop="15dp" + android:layout_marginBottom="15dp" + custom:initialView="0"> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + > + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:id="@+id/backup_code_display_1" + android:textStyle="bold" + android:typeface="monospace" + android:textSize="@dimen/abc_text_size_medium_material" + style="@android:style/Widget.EditText" + android:clickable="false" + android:focusable="false" + tools:text="ABCDEF" + /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center_vertical" + android:textStyle="bold" + android:typeface="monospace" + android:textSize="@dimen/abc_text_size_medium_material" + android:text="-" + /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:id="@+id/backup_code_display_2" + android:textStyle="bold" + android:typeface="monospace" + android:textSize="@dimen/abc_text_size_medium_material" + style="@android:style/Widget.EditText" + android:clickable="false" + android:focusable="false" + tools:text="GHIJKL" + /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center_vertical" + android:textStyle="bold" + android:typeface="monospace" + android:textSize="@dimen/abc_text_size_medium_material" + android:text="-" + /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:id="@+id/backup_code_display_3" + android:textStyle="bold" + android:typeface="monospace" + android:textSize="@dimen/abc_text_size_medium_material" + style="@android:style/Widget.EditText" + android:clickable="false" + android:focusable="false" + tools:text="MNOPQR" + /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center_vertical" + android:textStyle="bold" + android:typeface="monospace" + android:textSize="@dimen/abc_text_size_medium_material" + android:text="-" + /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:id="@+id/backup_code_display_4" + android:textStyle="bold" + android:typeface="monospace" + android:textSize="@dimen/abc_text_size_medium_material" + android:singleLine="true" + style="@android:style/Widget.EditText" + android:clickable="false" + android:focusable="false" + tools:text="STUVWX" + /> + + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + > + + <EditText + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:id="@+id/backup_code_1" + android:textStyle="bold" + android:typeface="monospace" + android:textSize="@dimen/abc_text_size_medium_material" + android:singleLine="true" + android:inputType="textNoSuggestions|textCapCharacters" + android:hint="ABCDEF" + android:textColorHint="@android:color/transparent" + android:maxLength="6" + /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center_vertical" + android:textStyle="bold" + android:typeface="monospace" + android:textSize="@dimen/abc_text_size_medium_material" + android:text="-" + /> + + <EditText + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:id="@+id/backup_code_2" + android:textStyle="bold" + android:typeface="monospace" + android:textSize="@dimen/abc_text_size_medium_material" + android:singleLine="true" + android:inputType="textNoSuggestions|textCapCharacters" + android:hint="ABCDEF" + android:textColorHint="@android:color/transparent" + android:maxLength="6" + /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center_vertical" + android:textStyle="bold" + android:typeface="monospace" + android:textSize="@dimen/abc_text_size_medium_material" + android:text="-" + /> + + <EditText + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:id="@+id/backup_code_3" + android:textStyle="bold" + android:typeface="monospace" + android:textSize="@dimen/abc_text_size_medium_material" + android:singleLine="true" + android:inputType="textNoSuggestions|textCapCharacters" + android:hint="ABCDEF" + android:textColorHint="@android:color/transparent" + android:maxLength="6" + /> + <TextView + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center_vertical" + android:textStyle="bold" + android:typeface="monospace" + android:textSize="@dimen/abc_text_size_medium_material" + android:text="-" + /> + + <EditText + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:id="@+id/backup_code_4" + android:textStyle="bold" + android:typeface="monospace" + android:textSize="@dimen/abc_text_size_medium_material" + android:singleLine="true" + android:inputType="textNoSuggestions|textCapCharacters" + android:hint="ABCDEF" + android:textColorHint="@android:color/transparent" + android:maxLength="6" + /> + + </LinearLayout> + + </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator> + + <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:layout_gravity="center_horizontal" + android:id="@+id/status_animator" + android:inAnimation="@anim/fade_in_delayed" + android:outAnimation="@anim/fade_out" + custom:initialView="0"> + + <Button + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:layout_margin="10dp" + android:text="Ok, I wrote it down!" + android:padding="12dp" + android:id="@+id/button_backup_input" + style="?android:buttonBarButtonStyle" + /> + + <Space + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_horizontal" + android:text="The backup code you entered is wrong!\nDid you write it down correctly?" + style="?android:textAppearanceMedium" + /> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="?android:buttonBarStyle"> + + <Button + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="10dp" + android:padding="12dp" + android:text="Share backup" + android:drawableLeft="@drawable/ic_share_grey_24dp" + android:drawablePadding="8dp" + android:id="@+id/button_backup_share" + style="?android:buttonBarButtonStyle" + /> + + <Button + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="10dp" + android:padding="12dp" + android:text="Save backup" + android:drawableLeft="@drawable/ic_save_grey_24dp" + android:drawablePadding="8dp" + android:id="@+id/button_backup_save" + style="?android:buttonBarButtonStyle" + /> + + </LinearLayout> + + </LinearLayout> + + </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator> + + +</LinearLayout> diff --git a/OpenKeychain/src/main/res/layout/backup_fragment.xml b/OpenKeychain/src/main/res/layout/drawer_backup_fragment.xml index 96fba954b..96fba954b 100644 --- a/OpenKeychain/src/main/res/layout/backup_fragment.xml +++ b/OpenKeychain/src/main/res/layout/drawer_backup_fragment.xml diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml index b0e0f5734..360ecb136 100644 --- a/OpenKeychain/src/main/res/values/strings.xml +++ b/OpenKeychain/src/main/res/values/strings.xml @@ -30,6 +30,7 @@ <string name="title_export_keys">"Backup Keys"</string> <string name="title_key_not_found">"Key Not Found"</string> <string name="title_send_key">"Upload to Keyserver"</string> + <string name="title_backup">"Backup Key"</string> <string name="title_certify_key">"Confirm Key"</string> <string name="title_key_details">"Key Details"</string> <string name="title_help">"Help"</string> @@ -1299,16 +1300,11 @@ <item quantity="one">"Exporting one key"</item> <item quantity="other">"Exporting %d keys"</item> </plurals> - <string name="msg_export_file_name">"Filename: %s"</string> <string name="msg_export_all">"Exporting all keys"</string> <string name="msg_export_public">"Exporting public key %s"</string> <string name="msg_export_upload_public">"Uploading public key %s"</string> <string name="msg_export_secret">"Exporting secret key %s"</string> - <string name="msg_export_error_no_file">"No filename specified!"</string> - <string name="msg_export_error_fopen">"Error opening file!"</string> - <string name="msg_export_error_no_uri">"No URI specified!"</string> <string name="msg_export_error_uri_open">"Error opening URI stream!"</string> - <string name="msg_export_error_storage">"Storage is not ready for writing!"</string> <string name="msg_export_error_db">"Database error!"</string> <string name="msg_export_error_io">"Input/output error!"</string> <string name="msg_export_error_key">"Error preprocessing key data!"</string> diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java index a659dc7da..4d4915e8d 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java @@ -17,40 +17,65 @@ package org.sufficientlysecure.keychain.operations; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.io.PrintStream; +import java.security.Security; +import java.util.Iterator; + +import android.app.Application; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.net.Uri; + import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.Robolectric; import org.robolectric.RobolectricGradleTestRunner; -import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLog; import org.spongycastle.bcpg.sig.KeyFlags; import org.spongycastle.jce.provider.BouncyCastleProvider; import org.sufficientlysecure.keychain.WorkaroundBuildConfig; -import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; +import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; 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.operations.results.PgpEditKeyResult; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; import org.sufficientlysecure.keychain.pgp.PgpKeyOperation; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing.IteratorWithIOThrow; import org.sufficientlysecure.keychain.pgp.WrappedSignature; import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.service.ExportKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.ProgressScaler; import org.sufficientlysecure.keychain.util.TestingUtils; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; -import java.security.Security; -import java.util.Iterator; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + @RunWith(RobolectricGradleTestRunner.class) @Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml") @@ -84,7 +109,7 @@ public class ExportTest { parcel.mNewUnlock = new ChangeUnlockParcel(mKeyPhrase1); PgpEditKeyResult result = op.createSecretKeyRing(parcel); - Assert.assertTrue("initial test key creation must succeed", result.success()); + assertTrue("initial test key creation must succeed", result.success()); Assert.assertNotNull("initial test key creation must succeed", result.getRing()); mStaticRing1 = result.getRing(); @@ -102,7 +127,7 @@ public class ExportTest { parcel.mNewUnlock = new ChangeUnlockParcel(null, new Passphrase("1234")); PgpEditKeyResult result = op.createSecretKeyRing(parcel); - Assert.assertTrue("initial test key creation must succeed", result.success()); + assertTrue("initial test key creation must succeed", result.success()); Assert.assertNotNull("initial test key creation must succeed", result.getRing()); mStaticRing2 = result.getRing(); @@ -125,17 +150,17 @@ public class ExportTest { } @Test - public void testExportAll() throws Exception { + public void testExportAllLocalStripped() throws Exception { ExportOperation op = new ExportOperation(RuntimeEnvironment.application, new ProviderHelper(RuntimeEnvironment.application), null); // make sure there is a local cert (so the later checks that there are none are meaningful) - Assert.assertTrue("second keyring has local certification", checkForLocal(mStaticRing2)); + assertTrue("second keyring has local certification", checkForLocal(mStaticRing2)); ByteArrayOutputStream out = new ByteArrayOutputStream(); - ExportResult result = op.exportKeyRings(new OperationLog(), null, false, out); + boolean result = op.exportKeysToStream(new OperationLog(), null, false, out); - Assert.assertTrue("export must be a success", result.success()); + assertTrue("export must be a success", result); long masterKeyId1, masterKeyId2; if (mStaticRing1.getMasterKeyId() < mStaticRing2.getMasterKeyId()) { @@ -150,70 +175,206 @@ public class ExportTest { UncachedKeyRing.fromStream(new ByteArrayInputStream(out.toByteArray())); { - Assert.assertTrue("export must have two keys (1/2)", unc.hasNext()); + assertTrue("export must have two keys (1/2)", unc.hasNext()); UncachedKeyRing ring = unc.next(); Assert.assertEquals("first exported key has correct masterkeyid", masterKeyId1, ring.getMasterKeyId()); - Assert.assertFalse("first exported key must not be secret", ring.isSecret()); - Assert.assertFalse("there must be no local signatures in an exported keyring", + assertFalse("first exported key must not be secret", ring.isSecret()); + assertFalse("there must be no local signatures in an exported keyring", checkForLocal(ring)); } { - Assert.assertTrue("export must have two keys (2/2)", unc.hasNext()); + assertTrue("export must have two keys (2/2)", unc.hasNext()); UncachedKeyRing ring = unc.next(); Assert.assertEquals("second exported key has correct masterkeyid", masterKeyId2, ring.getMasterKeyId()); - Assert.assertFalse("second exported key must not be secret", ring.isSecret()); - Assert.assertFalse("there must be no local signatures in an exported keyring", + assertFalse("second exported key must not be secret", ring.isSecret()); + assertFalse("there must be no local signatures in an exported keyring", checkForLocal(ring)); } out = new ByteArrayOutputStream(); - result = op.exportKeyRings(new OperationLog(), null, true, out); + result = op.exportKeysToStream(new OperationLog(), null, true, out); - Assert.assertTrue("export must be a success", result.success()); + assertTrue("export must be a success", result); unc = UncachedKeyRing.fromStream(new ByteArrayInputStream(out.toByteArray())); { - Assert.assertTrue("export must have four keys (1/4)", unc.hasNext()); + assertTrue("export must have four keys (1/4)", unc.hasNext()); UncachedKeyRing ring = unc.next(); Assert.assertEquals("1/4 exported key has correct masterkeyid", masterKeyId1, ring.getMasterKeyId()); - Assert.assertFalse("1/4 exported key must not be public", ring.isSecret()); - Assert.assertFalse("there must be no local signatures in an exported keyring", + assertFalse("1/4 exported key must not be public", ring.isSecret()); + assertFalse("there must be no local signatures in an exported keyring", checkForLocal(ring)); - Assert.assertTrue("export must have four keys (2/4)", unc.hasNext()); + assertTrue("export must have four keys (2/4)", unc.hasNext()); ring = unc.next(); Assert.assertEquals("2/4 exported key has correct masterkeyid", masterKeyId1, ring.getMasterKeyId()); - Assert.assertTrue("2/4 exported key must be public", ring.isSecret()); - Assert.assertFalse("there must be no local signatures in an exported keyring", + assertTrue("2/4 exported key must be public", ring.isSecret()); + assertFalse("there must be no local signatures in an exported keyring", checkForLocal(ring)); } { - Assert.assertTrue("export must have four keys (3/4)", unc.hasNext()); + assertTrue("export must have four keys (3/4)", unc.hasNext()); UncachedKeyRing ring = unc.next(); Assert.assertEquals("3/4 exported key has correct masterkeyid", masterKeyId2, ring.getMasterKeyId()); - Assert.assertFalse("3/4 exported key must not be public", ring.isSecret()); - Assert.assertFalse("there must be no local signatures in an exported keyring", + assertFalse("3/4 exported key must not be public", ring.isSecret()); + assertFalse("there must be no local signatures in an exported keyring", checkForLocal(ring)); - Assert.assertTrue("export must have four keys (4/4)", unc.hasNext()); + assertTrue("export must have four keys (4/4)", unc.hasNext()); ring = unc.next(); Assert.assertEquals("4/4 exported key has correct masterkeyid", masterKeyId2, ring.getMasterKeyId()); - Assert.assertTrue("4/4 exported key must be public", ring.isSecret()); - Assert.assertFalse("there must be no local signatures in an exported keyring", + assertTrue("4/4 exported key must be public", ring.isSecret()); + assertFalse("there must be no local signatures in an exported keyring", checkForLocal(ring)); } } + @Test + public void testExportUnencrypted() throws Exception { + + ContentResolver mockResolver = mock(ContentResolver.class); + + Uri fakeOutputUri = Uri.parse("content://fake/out/1"); + ByteArrayOutputStream outStream1 = new ByteArrayOutputStream(); + when(mockResolver.openOutputStream(fakeOutputUri)).thenReturn(outStream1); + + Application spyApplication = spy(RuntimeEnvironment.application); + when(spyApplication.getContentResolver()).thenReturn(mockResolver); + + ExportOperation op = new ExportOperation(spyApplication, + new ProviderHelper(RuntimeEnvironment.application), null); + + ExportKeyringParcel parcel = new ExportKeyringParcel(null, + new long[] { mStaticRing1.getMasterKeyId() }, false, fakeOutputUri); + + ExportResult result = op.execute(parcel, null); + + verify(mockResolver).openOutputStream(fakeOutputUri); + + assertTrue("export must succeed", result.success()); + + TestingUtils.assertArrayEqualsPrefix("exported data must start with ascii armor header", + "-----BEGIN PGP PUBLIC KEY BLOCK-----\n".getBytes(), outStream1.toByteArray()); + TestingUtils.assertArrayEqualsSuffix("exported data must end with ascii armor header", + "-----END PGP PUBLIC KEY BLOCK-----\n".getBytes(), outStream1.toByteArray()); + + { + IteratorWithIOThrow<UncachedKeyRing> unc + = UncachedKeyRing.fromStream(new ByteArrayInputStream(outStream1.toByteArray())); + + assertTrue("export must have one key", unc.hasNext()); + UncachedKeyRing ring = unc.next(); + Assert.assertEquals("exported key has correct masterkeyid", + mStaticRing1.getMasterKeyId(), ring.getMasterKeyId()); + assertFalse("export must have exactly one key", unc.hasNext()); + } + + } + + @Test + public void testExportEncrypted() throws Exception { + + + Application spyApplication; + ContentResolver mockResolver = mock(ContentResolver.class); + + Uri fakePipedUri, fakeOutputUri; + ByteArrayOutputStream outStream; { + + fakePipedUri = Uri.parse("content://fake/pipe/1"); + PipedInputStream pipedInStream = new PipedInputStream(8192); + PipedOutputStream pipedOutStream = new PipedOutputStream(pipedInStream); + when(mockResolver.openOutputStream(fakePipedUri)).thenReturn(pipedOutStream); + when(mockResolver.openInputStream(fakePipedUri)).thenReturn(pipedInStream); + when(mockResolver.insert(eq(TemporaryStorageProvider.CONTENT_URI), any(ContentValues.class))) + .thenReturn(fakePipedUri); + + fakeOutputUri = Uri.parse("content://fake/out/1"); + outStream = new ByteArrayOutputStream(); + when(mockResolver.openOutputStream(fakeOutputUri)).thenReturn(outStream); + + spyApplication = spy(RuntimeEnvironment.application); + when(spyApplication.getContentResolver()).thenReturn(mockResolver); + } + + Passphrase passphrase = new Passphrase("abcde"); + + { // export encrypted + ExportOperation op = new ExportOperation(spyApplication, + new ProviderHelper(RuntimeEnvironment.application), null); + + ExportKeyringParcel parcel = new ExportKeyringParcel(passphrase, + new long[] { mStaticRing1.getMasterKeyId() }, false, fakeOutputUri); + + ExportResult result = op.execute(parcel, null); + + verify(mockResolver).openOutputStream(fakePipedUri); + verify(mockResolver).openInputStream(fakePipedUri); + verify(mockResolver).openOutputStream(fakeOutputUri); + + assertTrue("export must succeed", result.success()); + TestingUtils.assertArrayEqualsPrefix("exported data must start with ascii armor header", + "-----BEGIN PGP MESSAGE-----\n".getBytes(), outStream.toByteArray()); + } + + { + PgpDecryptVerifyOperation op = new PgpDecryptVerifyOperation(RuntimeEnvironment.application, + new ProviderHelper(RuntimeEnvironment.application), null); + + PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel(outStream.toByteArray()); + input.setAllowSymmetricDecryption(true); + + { + DecryptVerifyResult result = op.execute(input, new CryptoInputParcel()); + assertTrue("decryption must return pending without passphrase", result.isPending()); + Assert.assertTrue("should contain pending passphrase log entry", + result.getLog().containsType(LogType.MSG_DC_PENDING_PASSPHRASE)); + } + { + DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(new Passphrase("bad"))); + assertFalse("decryption must fail with bad passphrase", result.success()); + Assert.assertTrue("should contain bad passphrase log entry", + result.getLog().containsType(LogType.MSG_DC_ERROR_SYM_PASSPHRASE)); + } + + DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(passphrase)); + assertTrue("decryption must succeed with passphrase", result.success()); + + assertEquals("backup filename should be backup_keyid.pub.asc", + "backup_" + KeyFormattingUtils.convertKeyIdToHex(mStaticRing1.getMasterKeyId()) + ".pub.asc", + result.getDecryptionMetadata().getFilename()); + + TestingUtils.assertArrayEqualsPrefix("exported data must start with ascii armor header", + "-----BEGIN PGP PUBLIC KEY BLOCK-----\n".getBytes(), result.getOutputBytes()); + TestingUtils.assertArrayEqualsSuffix("exported data must end with ascii armor header", + "-----END PGP PUBLIC KEY BLOCK-----\n".getBytes(), result.getOutputBytes()); + + { + IteratorWithIOThrow<UncachedKeyRing> unc + = UncachedKeyRing.fromStream(new ByteArrayInputStream(result.getOutputBytes())); + + assertTrue("export must have one key", unc.hasNext()); + UncachedKeyRing ring = unc.next(); + Assert.assertEquals("exported key has correct masterkeyid", + mStaticRing1.getMasterKeyId(), ring.getMasterKeyId()); + assertFalse("export must have exactly one key", unc.hasNext()); + } + + } + + } + + /** This function checks whether or not there are any local signatures in a keyring. */ private boolean checkForLocal(UncachedKeyRing ring) { Iterator<WrappedSignature> sigs = ring.getPublicKey().getSignatures(); diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/TestingUtils.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/TestingUtils.java index 1d7952464..0b35aaf22 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/TestingUtils.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/TestingUtils.java @@ -19,6 +19,9 @@ package org.sufficientlysecure.keychain.util; import java.util.Random; +import junit.framework.Assert; + + public class TestingUtils { public static Passphrase genPassphrase() { return genPassphrase(false); @@ -35,4 +38,25 @@ public class TestingUtils { System.out.println("Generated passphrase: '" + passbuilder.toString() + "'"); return new Passphrase(passbuilder.toString()); } + + public static void assertArrayEqualsPrefix(String msg, byte[] expected, byte[] actual) { + + Assert.assertTrue("exepected must be shorter or equal to actual array length", + expected.length <= actual.length); + for (int i = 0; i < expected.length; i++) { + Assert.assertEquals(msg, expected[i], actual[i]); + } + + } + + public static void assertArrayEqualsSuffix(String msg, byte[] expected, byte[] actual) { + + Assert.assertTrue("exepected must be shorter or equal to actual array length", + expected.length <= actual.length); + for (int i = 0; i < expected.length; i++) { + Assert.assertEquals(msg, expected[i], actual[actual.length -expected.length +i]); + } + + } + } |