/* * 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 . */ 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 expected = new ArrayList(); 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 actual = new ArrayList(); 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 a, ArrayList 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; } }; } }