aboutsummaryrefslogtreecommitdiffstats
path: root/OpenKeychain
diff options
context:
space:
mode:
Diffstat (limited to 'OpenKeychain')
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java45
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java27
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenNfcActivity.java72
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FidesmoInstallDialog.java59
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FidesmoPgpInstallDialog.java63
-rw-r--r--OpenKeychain/src/main/res/values/strings.xml23
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/InteropTest.java274
m---------OpenKeychain/src/test/resources/openpgp-interop0
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