diff options
Diffstat (limited to 'OpenKeychain/src')
8 files changed, 536 insertions, 27 deletions
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 c967a5abc..af09cf235 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java @@ -250,8 +250,21 @@ public class UncachedKeyRing { * - Remove all non-verifying self-certificates * - Remove all "future" self-certificates * - Remove all certificates flagged as "local" - * - Remove all certificates which are superseded by a newer one on the same target, - * including revocations with later re-certifications. + * - For UID certificates, remove all certificates which are + * superseded by a newer one on the same target, including + * revocations with later re-certifications. + * - For subkey certifications, remove all certificates which + * are superseded by a newer one on the same target, unless + * it encounters a revocation certificate. The revocation + * certificate is considered to permanently revoke the key, + * even if contains later re-certifications. + * This is the "behavior in practice" used by (e.g.) GnuPG, and + * the rationale for both can be found as comments in the GnuPG + * source. + * UID signatures: + * https://github.com/mtigas/gnupg/blob/50c98c7ed6b542857ee2f902eca36cda37407737/g10/getkey.c#L1668-L1674 + * Subkey signatures: + * https://github.com/mtigas/gnupg/blob/50c98c7ed6b542857ee2f902eca36cda37407737/g10/getkey.c#L1990-L1997 * - Remove all certificates in other positions if not of known type: * - key revocation signatures on the master key * - subkey binding signatures for subkeys @@ -278,8 +291,21 @@ public class UncachedKeyRing { * - Remove all non-verifying self-certificates * - Remove all "future" self-certificates * - Remove all certificates flagged as "local" - * - Remove all certificates which are superseded by a newer one on the same target, - * including revocations with later re-certifications. + * - For UID certificates, remove all certificates which are + * superseded by a newer one on the same target, including + * revocations with later re-certifications. + * - For subkey certifications, remove all certificates which + * are superseded by a newer one on the same target, unless + * it encounters a revocation certificate. The revocation + * certificate is considered to permanently revoke the key, + * even if contains later re-certifications. + * This is the "behavior in practice" used by (e.g.) GnuPG, and + * the rationale for both can be found as comments in the GnuPG + * source. + * UID signatures: + * https://github.com/mtigas/gnupg/blob/50c98c7ed6b542857ee2f902eca36cda37407737/g10/getkey.c#L1668-L1674 + * Subkey signatures: + * https://github.com/mtigas/gnupg/blob/50c98c7ed6b542857ee2f902eca36cda37407737/g10/getkey.c#L1990-L1997 * - Remove all certificates in other positions if not of known type: * - key revocation signatures on the master key * - subkey binding signatures for subkeys @@ -950,12 +976,6 @@ public class UncachedKeyRing { } selfCert = zert; - // if this is newer than a possibly existing revocation, drop that one - if (revocation != null && selfCert.getCreationTime().after(revocation.getCreationTime())) { - log.add(LogType.MSG_KC_SUB_REVOKE_DUP, indent); - redundantCerts += 1; - revocation = null; - } // it must be a revocation, then (we made sure above) } else { @@ -974,8 +994,9 @@ public class UncachedKeyRing { continue; } - // if there is a certification that is newer than this revocation, don't bother - if (selfCert != null && selfCert.getCreationTime().after(cert.getCreationTime())) { + // If we already have a newer revocation cert, skip this one. + if (revocation != null && + revocation.getCreationTime().after(cert.getCreationTime())) { log.add(LogType.MSG_KC_SUB_REVOKE_DUP, indent); redundantCerts += 1; continue; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java index 15c83d4dc..c3a122388 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java @@ -539,13 +539,26 @@ public class OpenPgpService extends Service { result.putExtra(OpenPgpApi.RESULT_SIGNATURE, signatureResult); - if (signatureResult.getResult() == OpenPgpSignatureResult.RESULT_KEY_MISSING) { - // If signature is unknown we return an _additional_ PendingIntent - // to retrieve the missing key - result.putExtra(OpenPgpApi.RESULT_INTENT, getKeyserverPendingIntent(data, signatureResult.getKeyId())); - } else { - // If signature key is known, return PendingIntent to show key - result.putExtra(OpenPgpApi.RESULT_INTENT, getShowKeyPendingIntent(signatureResult.getKeyId())); + switch (signatureResult.getResult()) { + case OpenPgpSignatureResult.RESULT_KEY_MISSING: { + // If signature key is missing we return a PendingIntent to retrieve the key + result.putExtra(OpenPgpApi.RESULT_INTENT, getKeyserverPendingIntent(data, signatureResult.getKeyId())); + break; + } + case OpenPgpSignatureResult.RESULT_VALID_CONFIRMED: + case OpenPgpSignatureResult.RESULT_VALID_UNCONFIRMED: + case OpenPgpSignatureResult.RESULT_INVALID_KEY_REVOKED: + case OpenPgpSignatureResult.RESULT_INVALID_KEY_EXPIRED: + case OpenPgpSignatureResult.RESULT_INVALID_INSECURE: { + // If signature key is known, return PendingIntent to show key + result.putExtra(OpenPgpApi.RESULT_INTENT, getShowKeyPendingIntent(signatureResult.getKeyId())); + break; + } + default: + case OpenPgpSignatureResult.RESULT_NO_SIGNATURE: + case OpenPgpSignatureResult.RESULT_INVALID_SIGNATURE: { + // no key id -> no PendingIntent + } } if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) < 5) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenNfcActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenNfcActivity.java index 1a04bcf43..52b439a0d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenNfcActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenNfcActivity.java @@ -29,6 +29,7 @@ import android.app.Activity; import android.app.PendingIntent; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.nfc.NfcAdapter; import android.nfc.Tag; import android.nfc.TagLostException; @@ -36,6 +37,7 @@ import android.nfc.tech.IsoDep; import android.os.AsyncTask; import android.os.Bundle; +import nordpol.Apdu; import nordpol.android.TagDispatcher; import nordpol.android.AndroidCard; import nordpol.android.OnDiscoveredTagListener; @@ -59,6 +61,8 @@ import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ui.CreateKeyActivity; import org.sufficientlysecure.keychain.ui.PassphraseDialogActivity; import org.sufficientlysecure.keychain.ui.ViewKeyActivity; +import org.sufficientlysecure.keychain.ui.dialog.FidesmoInstallDialog; +import org.sufficientlysecure.keychain.ui.dialog.FidesmoPgpInstallDialog; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.Notify.Style; @@ -71,6 +75,10 @@ public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implemen public static final String EXTRA_TAG_HANDLING_ENABLED = "tag_handling_enabled"; + // Fidesmo constants + private static final String FIDESMO_APPS_AID_PREFIX = "A000000617"; + private static final String FIDESMO_APP_PACKAGE = "com.fidesmo.sec.android"; + protected Passphrase mPin; protected Passphrase mAdminPin; protected boolean mPw1ValidForMultipleSignatures; @@ -309,6 +317,20 @@ public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implemen onNfcError(getString(R.string.security_token_error_unknown)); break; } + // 6A82 app not installed on security token! + case 0x6A82: { + if (isFidesmoDevice()) { + // Check if the Fidesmo app is installed + if (isAndroidAppInstalled(FIDESMO_APP_PACKAGE)) { + promptFidesmoPgpInstall(); + } else { + promptFidesmoAppInstall(); + } + } else { // Other (possibly) compatible hardware + onNfcError(getString(R.string.security_token_error_pgp_app_not_installed)); + } + break; + } default: { onNfcError(getString(R.string.security_token_error, e.getMessage())); break; @@ -372,7 +394,6 @@ public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implemen mPin = input.getPassphrase(); break; } - default: super.onActivityResult(requestCode, resultCode, data); } @@ -984,4 +1005,53 @@ public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implemen } + private boolean isFidesmoDevice() { + if (isNfcConnected()) { // Check if we can still talk to the card + try { + // By trying to select any apps that have the Fidesmo AID prefix we can + // see if it is a Fidesmo device or not + byte[] mSelectResponse = mIsoCard.transceive(Apdu.select(FIDESMO_APPS_AID_PREFIX)); + // Compare the status returned by our select with the OK status code + return Apdu.hasStatus(mSelectResponse, Apdu.OK_APDU); + } catch (IOException e) { + Log.e(Constants.TAG, "Card communication failed!", e); + } + } + return false; + } + + /** + * Ask user if she wants to install PGP onto her Fidesmo device + */ + private void promptFidesmoPgpInstall() { + FidesmoPgpInstallDialog mFidesmoPgpInstallDialog = new FidesmoPgpInstallDialog(); + mFidesmoPgpInstallDialog.show(getSupportFragmentManager(), "mFidesmoPgpInstallDialog"); + } + + /** + * Show a Dialog to the user informing that Fidesmo App must be installed and with option + * to launch the Google Play store. + */ + private void promptFidesmoAppInstall() { + FidesmoInstallDialog mFidesmoInstallDialog = new FidesmoInstallDialog(); + mFidesmoInstallDialog.show(getSupportFragmentManager(), "mFidesmoInstallDialog"); + } + + /** + * Use the package manager to detect if an application is installed on the phone + * @param uri an URI identifying the application's package + * @return 'true' if the app is installed + */ + private boolean isAndroidAppInstalled(String uri) { + PackageManager mPackageManager = getPackageManager(); + boolean mAppInstalled = false; + try { + mPackageManager.getPackageInfo(uri, PackageManager.GET_ACTIVITIES); + mAppInstalled = true; + } catch (PackageManager.NameNotFoundException e) { + Log.e(Constants.TAG, "App not installed on Android device"); + mAppInstalled = false; + } + return mAppInstalled; + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FidesmoInstallDialog.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FidesmoInstallDialog.java new file mode 100644 index 000000000..76934c5d4 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FidesmoInstallDialog.java @@ -0,0 +1,59 @@ +package org.sufficientlysecure.keychain.ui.dialog; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; + +import org.sufficientlysecure.keychain.R; + +public class FidesmoInstallDialog extends DialogFragment { + + // URLs for Google Play app and to install apps via browser + private final static String PLAY_STORE_URI = "market://details?id="; + private final static String PLAY_STORE_VIA_BROWSER_URI = "http://play.google.com/store/apps/details?id="; + + // Fidesmo constants + private static final String FIDESMO_APP_PACKAGE = "com.fidesmo.sec.android"; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + CustomAlertDialogBuilder mCustomAlertDialogBuilder = new CustomAlertDialogBuilder(getActivity()); + mCustomAlertDialogBuilder.setTitle(getString(R.string.prompt_fidesmo_app_install_title)); + mCustomAlertDialogBuilder.setMessage(getString(R.string.prompt_fidesmo_app_install_message)); + mCustomAlertDialogBuilder.setPositiveButton( + getString(R.string.prompt_fidesmo_app_install_button_positive), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dismiss(); + startPlayStoreFidesmoAppActivity(); + } + }); + mCustomAlertDialogBuilder.setNegativeButton( + getString(R.string.prompt_fidesmo_app_install_button_negative), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dismiss(); + } + }); + + return mCustomAlertDialogBuilder.show(); + } + + private void startPlayStoreFidesmoAppActivity() { + try { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(PLAY_STORE_URI + + FIDESMO_APP_PACKAGE))); + } catch (android.content.ActivityNotFoundException exception) { + // if the Google Play app is not installed, call the browser + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(PLAY_STORE_VIA_BROWSER_URI + + FIDESMO_APP_PACKAGE))); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FidesmoPgpInstallDialog.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FidesmoPgpInstallDialog.java new file mode 100644 index 000000000..cdf6e5c7c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FidesmoPgpInstallDialog.java @@ -0,0 +1,63 @@ +package org.sufficientlysecure.keychain.ui.dialog; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.util.Log; + +public class FidesmoPgpInstallDialog extends DialogFragment { + + // Fidesmo constants + private static final String FIDESMO_SERVICE_DELIVERY_CARD_ACTION = "com.fidesmo.sec.DELIVER_SERVICE"; + private static final String FIDESMO_SERVICE_URI = "https://api.fidesmo.com/service/"; + private static final String FIDESMO_PGP_APPLICATION_ID = "0cdc651e"; + private static final String FIDESMO_PGP_SERVICE_ID = "OKC-install"; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + CustomAlertDialogBuilder mCustomAlertDialogBuilder = new CustomAlertDialogBuilder(getActivity()); + mCustomAlertDialogBuilder.setTitle(getString(R.string.prompt_fidesmo_pgp_install_title)); + mCustomAlertDialogBuilder.setMessage(getString(R.string.prompt_fidesmo_pgp_install_message)); + mCustomAlertDialogBuilder.setPositiveButton( + getString(R.string.prompt_fidesmo_pgp_install_button_positive), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dismiss(); + startFidesmoPgpAppletActivity(); + } + }); + mCustomAlertDialogBuilder.setNegativeButton( + getString(R.string.prompt_fidesmo_pgp_install_button_negative), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dismiss(); + } + }); + + return mCustomAlertDialogBuilder.show(); + } + + private void startFidesmoPgpAppletActivity() { + try { + // Call the Fidesmo app with the PGP applet as parameter to + // send the user straight to it + final String mPgpInstallServiceUrl = FIDESMO_SERVICE_URI + FIDESMO_PGP_APPLICATION_ID + + "/" + FIDESMO_PGP_SERVICE_ID; + Intent mPgpServiceIntent = new Intent(FIDESMO_SERVICE_DELIVERY_CARD_ACTION, + Uri.parse(mPgpInstallServiceUrl)); + startActivity(mPgpServiceIntent); + } catch (IllegalArgumentException e) { + Log.e(Constants.TAG, "Error when parsing URI"); + } + } +} diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml index 021b684a4..d15f66bd0 100644 --- a/OpenKeychain/src/main/res/values/strings.xml +++ b/OpenKeychain/src/main/res/values/strings.xml @@ -1708,17 +1708,26 @@ <string name="title_edit_identities">"Edit Identities"</string> <string name="title_edit_subkeys">"Edit Subkeys"</string> <string name="btn_search_for_query">"Search for\n'%s'"</string> - <string name="cache_ttl_lock_screen">"until Screen Off"</string> - <string name="cache_ttl_ten_minutes">"for Ten Minutes"</string> - <string name="cache_ttl_thirty_minutes">"for Thirty Minutes"</string> - <string name="cache_ttl_one_hour">"for One Hour"</string> - <string name="cache_ttl_three_hours">"for Three Hours"</string> - <string name="cache_ttl_one_day">"for One Day"</string> - <string name="cache_ttl_three_days">"for Three Days"</string> + <string name="cache_ttl_lock_screen">"until screen off"</string> + <string name="cache_ttl_ten_minutes">"for ten minutes"</string> + <string name="cache_ttl_thirty_minutes">"for thirty minutes"</string> + <string name="cache_ttl_one_hour">"for one hour"</string> + <string name="cache_ttl_three_hours">"for three hours"</string> + <string name="cache_ttl_one_day">"for one day"</string> + <string name="cache_ttl_three_days">"for three days"</string> <string name="cache_ttl_forever">"forever"</string> <string name="settings_cache_select_three">"Pick up to three."</string> <string name="settings_cache_ttl_at_least_one">"At least one item must be selected!"</string> <string name="settings_cache_ttl_max_three">"Can\'t select more than three items!"</string> <string name="remember">"Remember"</string> + <string name="security_token_error_pgp_app_not_installed">"No PGP app was found on the security token"</string> + <string name="prompt_fidesmo_pgp_install_title">"Install PGP?"</string> + <string name="prompt_fidesmo_pgp_install_message">"There was no PGP app available on your Fidesmo device."</string> + <string name="prompt_fidesmo_pgp_install_button_positive">"Install"</string> + <string name="prompt_fidesmo_pgp_install_button_negative">"Cancel"</string> + <string name="prompt_fidesmo_app_install_title">"Install Fidesmo?"</string> + <string name="prompt_fidesmo_app_install_message">"To install PGP you need the Fidesmo Android app."</string> + <string name="prompt_fidesmo_app_install_button_positive">"Install"</string> + <string name="prompt_fidesmo_app_install_button_negative">"Cancel"</string> </resources> diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/InteropTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/InteropTest.java new file mode 100644 index 000000000..b48c5ac4e --- /dev/null +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/InteropTest.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * 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.provider; + +import android.net.Uri; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.openintents.openpgp.OpenPgpMetadata; +import org.openintents.openpgp.OpenPgpSignatureResult; +import org.robolectric.RobolectricGradleTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.spongycastle.jce.provider.BouncyCastleProvider; +import org.sufficientlysecure.keychain.WorkaroundBuildConfig; +import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing; +import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey; +import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.util.InputData; +import org.sufficientlysecure.keychain.util.Passphrase; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.net.URL; +import java.security.Security; +import java.util.ArrayList; + +@RunWith(RobolectricGradleTestRunner.class) +@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml") +public class InteropTest { + + @BeforeClass + public static void setUpOnce() throws Exception { + Security.insertProviderAt(new BouncyCastleProvider(), 1); + } + + @Test + public void testInterop() throws Exception { + URL baseURL = InteropTest.class.getResource("/openpgp-interop/testcases"); + Assert.assertNotNull(baseURL); + File baseFile = new File(baseURL.toURI()); + walkTests(baseFile); + } + + private void walkTests(File root) throws Exception { + File children[] = root.listFiles(); + if (children == null) { + return; + } + for (File child : children) { + if (child.getName().startsWith(".")) { + continue; + } + if (child.isDirectory()) { + walkTests(child); + } else if (child.getName().endsWith(".json")) { + runTest(child); + } + } + } + + private void runTest(File base) throws Exception { + JSONObject config = new JSONObject(asString(base)); + String testType = config.getString("type"); + if (testType.equals("import")) { + runImportTest(config, base); + } else if (testType.equals("decrypt")) { + runDecryptTest(config, base); + } else { + Assert.fail(base + ": unexpected test type"); + } + } + + private static final String asString(File json) throws Exception { + return new String(asBytes(json), "utf-8"); + } + + private static final byte[] asBytes(File f) throws Exception { + FileInputStream fin = null; + try { + fin = new FileInputStream(f); + byte data[] = new byte[fin.available()]; + fin.read(data); + return data; + } finally { + close(fin); + } + } + + private void runDecryptTest(JSONObject config, File base) throws Exception { + File root = base.getParentFile(); + String baseName = getBaseName(base); + CanonicalizedPublicKeyRing verify; + if (config.has("verifyKey")) { + verify = (CanonicalizedPublicKeyRing) + readRingFromFile(new File(root, config.getString("verifyKey"))); + } else { + verify = null; + } + + CanonicalizedSecretKeyRing decrypt = (CanonicalizedSecretKeyRing) + readRingFromFile(new File(root, config.getString("decryptKey"))); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayInputStream in = + new ByteArrayInputStream(asBytes(new File(root, baseName + ".asc"))); + + InputData data = new InputData(in, in.available()); + + Passphrase pass = new Passphrase(config.getString("passphrase")); + + PgpDecryptVerifyOperation op = makeOperation(base.toString(), pass, decrypt, verify); + PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel(); + CryptoInputParcel cip = new CryptoInputParcel(pass); + DecryptVerifyResult result = op.execute(input, cip, data, out); + byte[] plaintext = config.getString("textcontent").getBytes("utf-8"); + String filename = config.getString("filename"); + Assert.assertTrue(base + ": decryption must succeed", result.success()); + byte[] decrypted = out.toByteArray(); + Assert.assertArrayEquals(base + ": plaintext should be correct", decrypted, plaintext); + if (verify != null) { + // Certain keys are too short, so we check appropriately. + int code = result.getSignatureResult().getResult(); + Assert.assertTrue(base + ": should have a signature", + (code == OpenPgpSignatureResult.RESULT_INVALID_INSECURE) || + (code == OpenPgpSignatureResult.RESULT_VALID_UNCONFIRMED)); + } + OpenPgpMetadata metadata = result.getDecryptionMetadata(); + Assert.assertEquals(base + ": filesize must be correct", + decrypted.length, metadata.getOriginalSize()); + Assert.assertEquals(base + ": filename must be correct", + filename, metadata.getFilename()); + } + + private void runImportTest(JSONObject config, File base) throws Exception { + File root = base.getParentFile(); + String baseName = getBaseName(base); + CanonicalizedKeyRing pkr = + readRingFromFile(new File(root, baseName + ".asc")); + + // Check we have the correct uids. + ArrayList<String> expected = new ArrayList<String>(); + JSONArray uids = config.getJSONArray("expected_uids"); + for (int i = 0; i < uids.length(); i++) { + expected.add(uids.getString(i)); + } + check(base + ": incorrect uids", expected, pkr.getUnorderedUserIds()); + + // Check we have the correct main and subkey fingerprints. + expected.clear(); + expected.add(config.getString("expected_fingerprint")); + JSONArray subkeys = config.optJSONArray("expected_subkeys"); + if (subkeys != null) { + for (int i = 0; i < subkeys.length(); i++) { + expected.add(subkeys.getJSONObject(i).getString("expected_fingerprint")); + } + } + ArrayList<String> actual = new ArrayList<String>(); + for (CanonicalizedPublicKey pk: pkr.publicKeyIterator()) { + if (pk.isValid()) { + actual.add(KeyFormattingUtils.convertFingerprintToHex(pk.getFingerprint())); + } + } + check(base + ": incorrect fingerprints", expected, actual); + } + + private void check(String msg, ArrayList<String> a, ArrayList<String> b) { + Assert.assertEquals(msg, a.size(), b.size()); + for (int i = 0; i < a.size(); i++) { + Assert.assertEquals(msg, a.get(i), b.get(i)); + } + } + + UncachedKeyRing readUncachedRingFromFile(File path) throws Exception { + BufferedInputStream bin = null; + try { + bin = new BufferedInputStream(new FileInputStream(path)); + return UncachedKeyRing.fromStream(bin).next(); + } finally { + close(bin); + } + } + + CanonicalizedKeyRing readRingFromFile(File path) throws Exception { + UncachedKeyRing ukr = readUncachedRingFromFile(path); + OperationLog log = new OperationLog(); + return ukr.canonicalize(log, 0); + } + + private static final void close(Closeable v) { + if (v != null) { + try { + v.close(); + } catch (Throwable any) { + } + } + } + + private static final String getBaseName(File base) { + String name = base.getName(); + return name.substring(0, name.length() - ".json".length()); + } + + private PgpDecryptVerifyOperation makeOperation(final String msg, final Passphrase passphrase, + final CanonicalizedSecretKeyRing decrypt, final CanonicalizedPublicKeyRing verify) + throws Exception { + + final long decryptId = decrypt.getEncryptId(); + final Uri decryptUri = KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(decryptId); + final Uri verifyUri = verify != null ? + KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(verify.getMasterKeyId()) : null; + + ProviderHelper helper = new ProviderHelper(RuntimeEnvironment.application) { + @Override + public CanonicalizedPublicKeyRing getCanonicalizedPublicKeyRing(Uri q) + throws NotFoundException { + Assert.assertEquals(msg + ": query should be for verification key", + q, verifyUri); + return verify; + } + @Override + public CanonicalizedSecretKeyRing getCanonicalizedSecretKeyRing(Uri q) + throws NotFoundException { + Assert.assertEquals(msg + ": query should be for the decryption key", + q, decryptUri); + return decrypt; + } + }; + + return new PgpDecryptVerifyOperation(RuntimeEnvironment.application, helper, null) { + @Override + public Passphrase getCachedPassphrase(long masterKeyId, long subKeyId) + throws NoSecretKeyException { + Assert.assertEquals(msg + ": passphrase should be for the secret key", + masterKeyId, decrypt.getMasterKeyId()); + Assert.assertEquals(msg + ": passphrase should refer to the decryption subkey", + subKeyId, decryptId); + return passphrase; + } + }; + } +} diff --git a/OpenKeychain/src/test/resources/openpgp-interop b/OpenKeychain/src/test/resources/openpgp-interop new file mode 160000 +Subproject 1cf03918f0ec839015b340b1a89b12114b90c46 |