Use new officially supported way for local unit tests, many dependencies upgraded for this, temporary disabled separate debug builds

This commit is contained in:
Dominik Schürmann
2015-06-11 00:05:13 +02:00
parent 05fcbcae7b
commit d16b09b2a6
131 changed files with 151 additions and 234 deletions

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
*
* 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.pgp;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.sufficientlysecure.keychain.BuildConfig;
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
public class KeyRingTest {
@Test
public void splitCompleteUserIdShouldReturnAll3Components() throws Exception {
KeyRing.UserId info = KeyRing.splitUserId("Max Mustermann (this is a comment) <max@example.com>");
Assert.assertEquals("Max Mustermann", info.name);
Assert.assertEquals("this is a comment", info.comment);
Assert.assertEquals("max@example.com", info.email);
}
@Test
public void splitUserIdWithAllButCommentShouldReturnNameAndEmail() throws Exception {
KeyRing.UserId info = KeyRing.splitUserId("Max Mustermann <max@example.com>");
Assert.assertEquals("Max Mustermann", info.name);
Assert.assertNull(info.comment);
Assert.assertEquals("max@example.com", info.email);
}
@Test
public void splitUserIdWithAllButEmailShouldReturnNameAndComment() throws Exception {
KeyRing.UserId info = KeyRing.splitUserId("Max Mustermann (this is a comment)");
Assert.assertEquals(info.name, "Max Mustermann");
Assert.assertEquals(info.comment, "this is a comment");
Assert.assertNull(info.email);
}
}

View File

@@ -0,0 +1,587 @@
/*
* 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.pgp;
import org.junit.Assert;
import org.junit.Before;
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.*;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLog;
import org.spongycastle.bcpg.sig.KeyFlags;
import org.spongycastle.jce.provider.BouncyCastleProvider;
import org.spongycastle.openpgp.PGPEncryptedData;
import org.sufficientlysecure.keychain.BuildConfig;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
import org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.RequiredInputType;
import org.sufficientlysecure.keychain.support.KeyringTestingHelper;
import org.sufficientlysecure.keychain.util.InputData;
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.HashSet;
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
public class PgpEncryptDecryptTest {
static Passphrase mPassphrase = TestingUtils.genPassphrase(true);
static UncachedKeyRing mStaticRing1, mStaticRing2;
static Passphrase mKeyPhrase1 = TestingUtils.genPassphrase(true);
static Passphrase mKeyPhrase2 = TestingUtils.genPassphrase(true);
static PrintStream oldShadowStream;
@BeforeClass
public static void setUpOnce() throws Exception {
Security.insertProviderAt(new BouncyCastleProvider(), 1);
oldShadowStream = ShadowLog.stream;
// ShadowLog.stream = System.out;
PgpKeyOperation op = new PgpKeyOperation(null);
{
SaveKeyringParcel parcel = new SaveKeyringParcel();
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
Algorithm.RSA, 1024, null, KeyFlags.CERTIFY_OTHER, 0L));
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
Algorithm.DSA, 1024, null, KeyFlags.SIGN_DATA, 0L));
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
Algorithm.ELGAMAL, 1024, null, KeyFlags.ENCRYPT_COMMS, 0L));
parcel.mAddUserIds.add("bloom");
parcel.mNewUnlock = new ChangeUnlockParcel(mKeyPhrase1);
PgpEditKeyResult result = op.createSecretKeyRing(parcel);
Assert.assertTrue("initial test key creation must succeed", result.success());
Assert.assertNotNull("initial test key creation must succeed", result.getRing());
mStaticRing1 = result.getRing();
}
{
SaveKeyringParcel parcel = new SaveKeyringParcel();
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
Algorithm.RSA, 1024, null, KeyFlags.CERTIFY_OTHER, 0L));
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
Algorithm.DSA, 1024, null, KeyFlags.SIGN_DATA, 0L));
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
Algorithm.ELGAMAL, 1024, null, KeyFlags.ENCRYPT_COMMS, 0L));
parcel.mAddUserIds.add("belle");
parcel.mNewUnlock = new ChangeUnlockParcel(mKeyPhrase2);
PgpEditKeyResult result = op.createSecretKeyRing(parcel);
Assert.assertTrue("initial test key creation must succeed", result.success());
Assert.assertNotNull("initial test key creation must succeed", result.getRing());
mStaticRing2 = result.getRing();
}
}
@Before
public void setUp() {
ProviderHelper providerHelper = new ProviderHelper(RuntimeEnvironment.application);
// don't log verbosely here, we're not here to test imports
ShadowLog.stream = oldShadowStream;
providerHelper.saveSecretKeyRing(mStaticRing1, new ProgressScaler());
providerHelper.saveSecretKeyRing(mStaticRing2, new ProgressScaler());
// ok NOW log verbosely!
ShadowLog.stream = System.out;
}
@Test
public void testSymmetricEncryptDecrypt() {
String plaintext = "dies ist ein plaintext ☭" + TestingUtils.genPassphrase(true);
byte[] ciphertext;
{ // encrypt data with a given passphrase
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(plaintext.getBytes());
PgpSignEncryptOperation op = new PgpSignEncryptOperation(RuntimeEnvironment.application,
new ProviderHelper(RuntimeEnvironment.application), null);
InputData data = new InputData(in, in.available());
PgpSignEncryptInputParcel b = new PgpSignEncryptInputParcel();
b.setSymmetricPassphrase(mPassphrase);
b.setSymmetricEncryptionAlgorithm(PGPEncryptedData.AES_128);
PgpSignEncryptResult result = op.execute(b, new CryptoInputParcel(), data, out);
Assert.assertTrue("encryption must succeed", result.success());
ciphertext = out.toByteArray();
}
{ // decryption with same passphrase should yield the same result
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
InputData data = new InputData(in, in.available());
PgpDecryptVerify op = new PgpDecryptVerify(RuntimeEnvironment.application,
new ProviderHelper(RuntimeEnvironment.application), null);
PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
input.setAllowSymmetricDecryption(true);
DecryptVerifyResult result = op.execute(
input, new CryptoInputParcel(mPassphrase), data, out);
Assert.assertTrue("decryption must succeed", result.success());
Assert.assertArrayEquals("decrypted ciphertext should equal plaintext",
out.toByteArray(), plaintext.getBytes());
Assert.assertNull("signature should be an error", result.getSignatureResult());
OpenPgpMetadata metadata = result.getDecryptMetadata();
Assert.assertEquals("filesize must be correct",
out.toByteArray().length, metadata.getOriginalSize());
}
{ // decryption with a bad passphrase should fail
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
InputData data = new InputData(in, in.available());
PgpDecryptVerify op = new PgpDecryptVerify(RuntimeEnvironment.application,
new ProviderHelper(RuntimeEnvironment.application), null);
PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
input.setAllowSymmetricDecryption(true);
DecryptVerifyResult result = op.execute(input,
new CryptoInputParcel(new Passphrase(new String(mPassphrase.getCharArray()) + "x")),
data, out);
Assert.assertFalse("decryption must fail", result.success());
Assert.assertEquals("decrypted plaintext should be empty", 0, out.size());
Assert.assertNull("signature should be an error", result.getSignatureResult());
}
{ // decryption with an unset passphrase should fail
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
InputData data = new InputData(in, in.available());
PgpDecryptVerify op = new PgpDecryptVerify(RuntimeEnvironment.application,
new ProviderHelper(RuntimeEnvironment.application), null);
PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
input.setAllowSymmetricDecryption(true);
DecryptVerifyResult result = op.execute(input,
new CryptoInputParcel(), data, out);
Assert.assertFalse("decryption must fail", result.success());
Assert.assertEquals("decrypted plaintext should be empty", 0, out.size());
Assert.assertNull("signature should be an error", result.getSignatureResult());
}
{ // decryption if symmetric decryption isn't allowed should fail
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
InputData data = new InputData(in, in.available());
PgpDecryptVerify op = new PgpDecryptVerify(RuntimeEnvironment.application,
new ProviderHelper(RuntimeEnvironment.application), null);
PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
input.setAllowSymmetricDecryption(false);
DecryptVerifyResult result = op.execute(input,
new CryptoInputParcel(), data, out);
Assert.assertFalse("decryption must fail", result.success());
Assert.assertEquals("decrypted plaintext should be empty", 0, out.size());
Assert.assertNull("signature should be an error", result.getSignatureResult());
}
}
@Test
public void testAsymmetricEncryptDecrypt() {
String plaintext = "dies ist ein plaintext ☭" + TestingUtils.genPassphrase(true);
byte[] ciphertext;
{ // encrypt data with key
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(plaintext.getBytes());
PgpSignEncryptOperation op = new PgpSignEncryptOperation(RuntimeEnvironment.application,
new ProviderHelper(RuntimeEnvironment.application), null);
InputData data = new InputData(in, in.available());
PgpSignEncryptInputParcel input = new PgpSignEncryptInputParcel();
input.setEncryptionMasterKeyIds(new long[] { mStaticRing1.getMasterKeyId() });
input.setSymmetricEncryptionAlgorithm(PGPEncryptedData.AES_128);
PgpSignEncryptResult result = op.execute(input, new CryptoInputParcel(), data, out);
Assert.assertTrue("encryption must succeed", result.success());
ciphertext = out.toByteArray();
}
{ // decryption with provided passphrase should yield the same result
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
InputData data = new InputData(in, in.available());
PgpDecryptVerify op = operationWithFakePassphraseCache(null, null, null);
PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(mKeyPhrase1), data, out);
Assert.assertTrue("decryption with provided passphrase must succeed", result.success());
Assert.assertArrayEquals("decrypted ciphertext with provided passphrase should equal plaintext",
out.toByteArray(), plaintext.getBytes());
Assert.assertNull("signature be empty", result.getSignatureResult());
OpenPgpMetadata metadata = result.getDecryptMetadata();
Assert.assertEquals("filesize must be correct",
out.toByteArray().length, metadata.getOriginalSize());
}
{ // decryption with passphrase cached should succeed
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
InputData data = new InputData(in, in.available());
PgpDecryptVerify op = operationWithFakePassphraseCache(
mKeyPhrase1, mStaticRing1.getMasterKeyId(), null);
PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(), data, out);
Assert.assertTrue("decryption with cached passphrase must succeed", result.success());
Assert.assertArrayEquals("decrypted ciphertext with cached passphrase should equal plaintext",
out.toByteArray(), plaintext.getBytes());
Assert.assertNull("signature should be empty", result.getSignatureResult());
}
{ // decryption with no passphrase provided should return status pending
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
InputData data = new InputData(in, in.available());
PgpDecryptVerify op = operationWithFakePassphraseCache(
null, mStaticRing1.getMasterKeyId(), null);
PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(), data, out);
Assert.assertFalse("decryption with no passphrase must return pending", result.success());
Assert.assertTrue("decryption with no passphrase should return pending", result.isPending());
Assert.assertEquals("decryption with no passphrase should return pending passphrase",
RequiredInputType.PASSPHRASE, result.getRequiredInputParcel().mType);
}
}
@Test
public void testMultiAsymmetricEncryptDecrypt() {
String plaintext = "dies ist ein plaintext ☭" + TestingUtils.genPassphrase(true);
byte[] ciphertext;
{ // encrypt data with a given passphrase
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(plaintext.getBytes());
PgpSignEncryptOperation op = new PgpSignEncryptOperation(RuntimeEnvironment.application,
new ProviderHelper(RuntimeEnvironment.application), null);
InputData data = new InputData(in, in.available());
PgpSignEncryptInputParcel b = new PgpSignEncryptInputParcel();
b.setEncryptionMasterKeyIds(new long[] {
mStaticRing1.getMasterKeyId(),
mStaticRing2.getMasterKeyId()
});
b.setSymmetricEncryptionAlgorithm(PGPEncryptedData.AES_128);
PgpSignEncryptResult result = op.execute(b, new CryptoInputParcel(), data, out);
Assert.assertTrue("encryption must succeed", result.success());
ciphertext = out.toByteArray();
}
{ // decryption with passphrase cached should succeed for the first key
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
InputData data = new InputData(in, in.available());
PgpDecryptVerify op = operationWithFakePassphraseCache(
mKeyPhrase1, mStaticRing1.getMasterKeyId(), null);
PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(), data, out);
Assert.assertTrue("decryption with cached passphrase must succeed for the first key", result.success());
Assert.assertArrayEquals("decrypted ciphertext with cached passphrase should equal plaintext",
out.toByteArray(), plaintext.getBytes());
Assert.assertNull("signature should be empty", result.getSignatureResult());
OpenPgpMetadata metadata = result.getDecryptMetadata();
Assert.assertEquals("filesize must be correct",
out.toByteArray().length, metadata.getOriginalSize());
}
{ // decryption should succeed if key is allowed
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
InputData data = new InputData(in, in.available());
// allow only the second to decrypt
HashSet<Long> allowed = new HashSet<>();
allowed.add(mStaticRing2.getMasterKeyId());
// provide passphrase for the second, and check that the first is never asked for!
PgpDecryptVerify op = operationWithFakePassphraseCache(
mKeyPhrase2, mStaticRing2.getMasterKeyId(), null);
PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
input.setAllowedKeyIds(allowed);
DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(), data, out);
Assert.assertTrue("decryption with cached passphrase must succeed for allowed key", result.success());
Assert.assertArrayEquals("decrypted ciphertext with cached passphrase should equal plaintext",
out.toByteArray(), plaintext.getBytes());
Assert.assertTrue("other key was skipped", result.getLog().containsType(LogType.MSG_DC_ASKIP_NOT_ALLOWED));
Assert.assertNull("signature should be empty", result.getSignatureResult());
}
{ // decryption should fail if no key is allowed
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
InputData data = new InputData(in, in.available());
// provide passphrase for the second, and check that the first is never asked for!
PgpDecryptVerify op = operationWithFakePassphraseCache(
mKeyPhrase2, mStaticRing2.getMasterKeyId(), null);
PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
input.setAllowedKeyIds(new HashSet<Long>());
DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(), data, out);
Assert.assertFalse("decryption must fail if no key allowed", result.success());
Assert.assertEquals("decryption must fail with key disllowed status",
DecryptVerifyResult.RESULT_KEY_DISALLOWED, result.getResult());
}
{ // decryption with passphrase cached should succeed for the other key if first is gone
// delete first key from database
new ProviderHelper(RuntimeEnvironment.application).getContentResolver().delete(
KeyRingData.buildPublicKeyRingUri(mStaticRing1.getMasterKeyId()), null, null
);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
InputData data = new InputData(in, in.available());
PgpDecryptVerify op = operationWithFakePassphraseCache(
mKeyPhrase2, mStaticRing2.getMasterKeyId(), null);
PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(), data, out);
Assert.assertTrue("decryption with cached passphrase must succeed", result.success());
Assert.assertArrayEquals("decrypted ciphertext with cached passphrase should equal plaintext",
out.toByteArray(), plaintext.getBytes());
Assert.assertNull("signature should be empty", result.getSignatureResult());
}
}
@Test
public void testMultiAsymmetricSignEncryptDecryptVerify() {
String plaintext = "dies ist ein plaintext ☭" + TestingUtils.genPassphrase(true);
byte[] ciphertext;
{ // encrypt data with a given passphrase
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(plaintext.getBytes());
PgpSignEncryptOperation op = new PgpSignEncryptOperation(RuntimeEnvironment.application,
new ProviderHelper(RuntimeEnvironment.application), null);
InputData data = new InputData(in, in.available());
PgpSignEncryptInputParcel b = new PgpSignEncryptInputParcel();
b.setEncryptionMasterKeyIds(new long[] {
mStaticRing1.getMasterKeyId(),
mStaticRing2.getMasterKeyId()
});
b.setSignatureMasterKeyId(mStaticRing1.getMasterKeyId());
b.setSignatureSubKeyId(KeyringTestingHelper.getSubkeyId(mStaticRing1, 1));
b.setSymmetricEncryptionAlgorithm(PGPEncryptedData.AES_128);
PgpSignEncryptResult result = op.execute(b, new CryptoInputParcel(mKeyPhrase1), data, out);
Assert.assertTrue("encryption must succeed", result.success());
ciphertext = out.toByteArray();
}
{ // decryption with passphrase cached should succeed for the first key
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
InputData data = new InputData(in, in.available());
PgpDecryptVerify op = operationWithFakePassphraseCache(
mKeyPhrase1, mStaticRing1.getMasterKeyId(), null);
PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(), data, out);
Assert.assertTrue("decryption with cached passphrase must succeed for the first key", result.success());
Assert.assertArrayEquals("decrypted ciphertext with cached passphrase should equal plaintext",
out.toByteArray(), plaintext.getBytes());
Assert.assertEquals("signature should be verified and certified",
OpenPgpSignatureResult.SIGNATURE_SUCCESS_CERTIFIED, result.getSignatureResult().getStatus());
OpenPgpMetadata metadata = result.getDecryptMetadata();
Assert.assertEquals("filesize must be correct",
out.toByteArray().length, metadata.getOriginalSize());
}
{ // decryption with passphrase cached should succeed for the other key if first is gone
// delete first key from database
new ProviderHelper(RuntimeEnvironment.application).getContentResolver().delete(
KeyRingData.buildPublicKeyRingUri(mStaticRing1.getMasterKeyId()), null, null
);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
InputData data = new InputData(in, in.available());
PgpDecryptVerify op = operationWithFakePassphraseCache(
mKeyPhrase2, mStaticRing2.getMasterKeyId(), null);
PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(), data, out);
Assert.assertTrue("decryption with cached passphrase must succeed", result.success());
Assert.assertArrayEquals("decrypted ciphertext with cached passphrase should equal plaintext",
out.toByteArray(), plaintext.getBytes());
Assert.assertEquals("signature key should be missing",
OpenPgpSignatureResult.SIGNATURE_KEY_MISSING,
result.getSignatureResult().getStatus());
}
}
@Test
public void testForeignEncoding() throws Exception {
String plaintext = "ウィキペディア";
byte[] plaindata = plaintext.getBytes("iso-2022-jp");
{ // some quick sanity checks
Assert.assertEquals(plaintext, new String(plaindata, "iso-2022-jp"));
Assert.assertNotEquals(plaintext, new String(plaindata, "utf-8"));
}
byte[] ciphertext;
{ // encrypt data with a given passphrase
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(plaindata);
PgpSignEncryptOperation op = new PgpSignEncryptOperation(RuntimeEnvironment.application,
new ProviderHelper(RuntimeEnvironment.application), null);
InputData data = new InputData(in, in.available());
PgpSignEncryptInputParcel b = new PgpSignEncryptInputParcel();
b.setEncryptionMasterKeyIds(new long[] { mStaticRing1.getMasterKeyId() });
b.setSymmetricEncryptionAlgorithm(PGPEncryptedData.AES_128);
// this only works with ascii armored output!
b.setEnableAsciiArmorOutput(true);
b.setCharset("iso-2022-jp");
PgpSignEncryptResult result = op.execute(b, new CryptoInputParcel(), data, out);
Assert.assertTrue("encryption must succeed", result.success());
ciphertext = out.toByteArray();
}
{ // decryption with provided passphrase should yield the same result
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
InputData data = new InputData(in, in.available());
PgpDecryptVerify op = operationWithFakePassphraseCache(null, null, null);
PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(mKeyPhrase1), data, out);
Assert.assertTrue("decryption with provided passphrase must succeed", result.success());
Assert.assertArrayEquals("decrypted ciphertext should equal plaintext bytes",
out.toByteArray(), plaindata);
Assert.assertEquals("charset should be read correctly",
"iso-2022-jp", result.getCharset());
Assert.assertEquals("decrypted ciphertext should equal plaintext",
new String(out.toByteArray(), result.getCharset()), plaintext);
Assert.assertNull("signature be empty", result.getSignatureResult());
}
}
private PgpDecryptVerify operationWithFakePassphraseCache(
final Passphrase passphrase, final Long checkMasterKeyId, final Long checkSubKeyId) {
return new PgpDecryptVerify(RuntimeEnvironment.application,
new ProviderHelper(RuntimeEnvironment.application), null) {
@Override
public Passphrase getCachedPassphrase(long masterKeyId, long subKeyId)
throws NoSecretKeyException {
if (checkMasterKeyId != null) {
Assert.assertEquals("requested passphrase should be for expected master key id",
(long) checkMasterKeyId, masterKeyId);
}
if (checkSubKeyId != null) {
Assert.assertEquals("requested passphrase should be for expected sub key id",
(long) checkSubKeyId, subKeyId);
}
if (passphrase == null) {
return null;
}
return passphrase;
}
};
}
}

View File

@@ -0,0 +1,767 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
* 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.pgp;
import org.junit.BeforeClass;
import org.junit.runner.RunWith;
import org.junit.Assert;
import org.junit.Test;
import org.junit.Before;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLog;
import org.spongycastle.bcpg.BCPGInputStream;
import org.spongycastle.bcpg.HashAlgorithmTags;
import org.spongycastle.bcpg.Packet;
import org.spongycastle.bcpg.PacketTags;
import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags;
import org.spongycastle.bcpg.UserIDPacket;
import org.spongycastle.bcpg.sig.KeyFlags;
import org.spongycastle.jce.provider.BouncyCastleProvider;
import org.spongycastle.openpgp.PGPPrivateKey;
import org.spongycastle.openpgp.PGPPublicKey;
import org.spongycastle.openpgp.PGPSecretKey;
import org.spongycastle.openpgp.PGPSecretKeyRing;
import org.spongycastle.openpgp.PGPSignature;
import org.spongycastle.openpgp.PGPSignatureGenerator;
import org.spongycastle.openpgp.PGPSignatureSubpacketGenerator;
import org.spongycastle.openpgp.PGPUtil;
import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor;
import org.spongycastle.openpgp.operator.PBESecretKeyEncryptor;
import org.spongycastle.openpgp.operator.PGPContentSignerBuilder;
import org.spongycastle.openpgp.operator.PGPDigestCalculator;
import org.spongycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
import org.spongycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder;
import org.spongycastle.util.Strings;
import org.sufficientlysecure.keychain.BuildConfig;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.support.KeyringTestingHelper;
import org.sufficientlysecure.keychain.support.KeyringTestingHelper.RawPacket;
import org.sufficientlysecure.keychain.util.Passphrase;
import java.io.ByteArrayInputStream;
import java.security.Security;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
/** Tests for the UncachedKeyring.canonicalize method.
*
* This is a complex and crypto-relevant method, which takes care of sanitizing keyrings.
* Test cases are made for all its assertions.
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
public class UncachedKeyringCanonicalizeTest {
static UncachedKeyRing staticRing;
static int totalPackets;
UncachedKeyRing ring;
ArrayList<RawPacket> onlyA = new ArrayList<RawPacket>();
ArrayList<RawPacket> onlyB = new ArrayList<RawPacket>();
OperationResult.OperationLog log = new OperationResult.OperationLog();
PGPSignatureSubpacketGenerator subHashedPacketsGen;
PGPSecretKey secretKey;
@BeforeClass
public static void setUpOnce() throws Exception {
Security.insertProviderAt(new BouncyCastleProvider(), 1);
ShadowLog.stream = System.out;
SaveKeyringParcel parcel = new SaveKeyringParcel();
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
Algorithm.RSA, 1024, null, KeyFlags.CERTIFY_OTHER, 0L));
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
Algorithm.RSA, 1024, null, KeyFlags.SIGN_DATA, 0L));
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
Algorithm.RSA, 1024, null, KeyFlags.ENCRYPT_COMMS, 0L));
parcel.mAddUserIds.add("twi");
parcel.mAddUserIds.add("pink");
{
WrappedUserAttribute uat = WrappedUserAttribute.fromSubpacket(100,
"sunshine, sunshine, ladybugs awake~".getBytes());
parcel.mAddUserAttribute.add(uat);
}
// passphrase is tested in PgpKeyOperationTest, just use empty here
parcel.mNewUnlock = new ChangeUnlockParcel(new Passphrase());
PgpKeyOperation op = new PgpKeyOperation(null);
PgpEditKeyResult result = op.createSecretKeyRing(parcel);
Assert.assertTrue("initial test key creation must succeed", result.success());
staticRing = result.getRing();
Assert.assertNotNull("initial test key creation must succeed", staticRing);
staticRing = staticRing.canonicalize(new OperationLog(), 0).getUncachedKeyRing();
// just for later reference
totalPackets = 11;
// we sleep here for a second, to make sure all new certificates have different timestamps
Thread.sleep(1000);
}
@Before public void setUp() throws Exception {
// show Log.x messages in system.out
ShadowLog.stream = System.out;
ring = staticRing;
subHashedPacketsGen = new PGPSignatureSubpacketGenerator();
secretKey = new PGPSecretKeyRing(ring.getEncoded(), new JcaKeyFingerprintCalculator())
.getSecretKey();
}
/** Make sure the assumptions made about the generated ring packet structure are valid. */
@Test public void testGeneratedRingStructure() throws Exception {
Iterator<RawPacket> it = KeyringTestingHelper.parseKeyring(ring.getEncoded());
Assert.assertEquals("packet #0 should be secret key",
PacketTags.SECRET_KEY, it.next().tag);
Assert.assertEquals("packet #1 should be user id",
PacketTags.USER_ID, it.next().tag);
Assert.assertEquals("packet #2 should be signature",
PacketTags.SIGNATURE, it.next().tag);
Assert.assertEquals("packet #3 should be user id",
PacketTags.USER_ID, it.next().tag);
Assert.assertEquals("packet #4 should be signature",
PacketTags.SIGNATURE, it.next().tag);
Assert.assertEquals("packet #5 should be user id",
PacketTags.USER_ATTRIBUTE, it.next().tag);
Assert.assertEquals("packet #6 should be signature",
PacketTags.SIGNATURE, it.next().tag);
Assert.assertEquals("packet #7 should be secret subkey",
PacketTags.SECRET_SUBKEY, it.next().tag);
Assert.assertEquals("packet #8 should be signature",
PacketTags.SIGNATURE, it.next().tag);
Assert.assertEquals("packet #9 should be secret subkey",
PacketTags.SECRET_SUBKEY, it.next().tag);
Assert.assertEquals("packet #10 should be signature",
PacketTags.SIGNATURE, it.next().tag);
Assert.assertFalse("exactly 11 packets total", it.hasNext());
Assert.assertArrayEquals("created keyring should be constant through canonicalization",
ring.getEncoded(), ring.canonicalize(log, 0).getEncoded());
}
@Test public void testUidSignature() throws Exception {
UncachedPublicKey masterKey = ring.getPublicKey();
final WrappedSignature sig = masterKey.getSignaturesForRawId(Strings.toUTF8ByteArray("twi")).next();
byte[] raw = sig.getEncoded();
// destroy the signature
raw[raw.length - 5] += 1;
final WrappedSignature brokenSig = WrappedSignature.fromBytes(raw);
{ // bad certificates get stripped
UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, brokenSig.getEncoded(), 3);
CanonicalizedKeyRing canonicalized = modified.canonicalize(log, 0);
Assert.assertTrue("canonicalized keyring with invalid extra sig must be same as original one",
!KeyringTestingHelper.diffKeyrings(
ring.getEncoded(), canonicalized.getEncoded(), onlyA, onlyB));
}
// remove user id certificate for one user
final UncachedKeyRing base = KeyringTestingHelper.removePacket(ring, 2);
{ // user id without certificate should be removed
CanonicalizedKeyRing modified = base.canonicalize(log, 0);
Assert.assertTrue("canonicalized keyring must differ", KeyringTestingHelper.diffKeyrings(
ring.getEncoded(), modified.getEncoded(), onlyA, onlyB));
Assert.assertEquals("two packets should be stripped after canonicalization", 2, onlyA.size());
Assert.assertEquals("no new packets after canonicalization", 0, onlyB.size());
Packet p = new BCPGInputStream(new ByteArrayInputStream(onlyA.get(0).buf)).readPacket();
Assert.assertTrue("first stripped packet must be user id", p instanceof UserIDPacket);
Assert.assertEquals("missing user id must be the expected one",
"twi", ((UserIDPacket) p).getID());
Assert.assertArrayEquals("second stripped packet must be signature we removed",
sig.getEncoded(), onlyA.get(1).buf);
}
{ // add error to signature
UncachedKeyRing modified = KeyringTestingHelper.injectPacket(base, brokenSig.getEncoded(), 3);
CanonicalizedKeyRing canonicalized = modified.canonicalize(log, 0);
Assert.assertTrue("canonicalized keyring must differ", KeyringTestingHelper.diffKeyrings(
ring.getEncoded(), canonicalized.getEncoded(), onlyA, onlyB));
Assert.assertEquals("two packets should be missing after canonicalization", 2, onlyA.size());
Assert.assertEquals("no new packets after canonicalization", 0, onlyB.size());
Packet p = new BCPGInputStream(new ByteArrayInputStream(onlyA.get(0).buf)).readPacket();
Assert.assertTrue("first stripped packet must be user id", p instanceof UserIDPacket);
Assert.assertEquals("missing user id must be the expected one",
"twi", ((UserIDPacket) p).getID());
Assert.assertArrayEquals("second stripped packet must be signature we removed",
sig.getEncoded(), onlyA.get(1).buf);
}
}
@Test public void testUidDestroy() throws Exception {
// signature for "twi"
ring = KeyringTestingHelper.removePacket(ring, 2);
// signature for "pink"
ring = KeyringTestingHelper.removePacket(ring, 3);
// canonicalization should fail, because there are no valid uids left
CanonicalizedKeyRing canonicalized = ring.canonicalize(log, 0);
Assert.assertNull("canonicalization of keyring with no valid uids should fail", canonicalized);
}
@Test public void testRevocationRedundant() throws Exception {
PGPSignature revocation = forgeSignature(
secretKey, PGPSignature.KEY_REVOCATION, subHashedPacketsGen, secretKey.getPublicKey());
UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, revocation.getEncoded(), 1);
// try to add the same packet again, it should be rejected in all positions
injectEverywhere(modified, revocation.getEncoded());
// an older (but different!) revocation should be rejected as well
subHashedPacketsGen.setSignatureCreationTime(false, new Date(new Date().getTime() -1000*1000));
revocation = forgeSignature(
secretKey, PGPSignature.KEY_REVOCATION, subHashedPacketsGen, secretKey.getPublicKey());
injectEverywhere(modified, revocation.getEncoded());
}
@Test public void testUidRedundant() throws Exception {
// an older uid certificate should be rejected
subHashedPacketsGen.setSignatureCreationTime(false, new Date(new Date().getTime() -1000*1000));
PGPSignature revocation = forgeSignature(
secretKey, PGPSignature.DEFAULT_CERTIFICATION, subHashedPacketsGen, "twi", secretKey.getPublicKey());
injectEverywhere(ring, revocation.getEncoded());
}
@Test public void testUidRevocationOutdated() throws Exception {
// an older uid revocation cert should be rejected
subHashedPacketsGen.setSignatureCreationTime(false, new Date(new Date().getTime() -1000*1000));
PGPSignature revocation = forgeSignature(
secretKey, PGPSignature.CERTIFICATION_REVOCATION, subHashedPacketsGen, "twi", secretKey.getPublicKey());
injectEverywhere(ring, revocation.getEncoded());
}
@Test public void testUidRevocationRedundant() throws Exception {
PGPSignature revocation = forgeSignature(
secretKey, PGPSignature.CERTIFICATION_REVOCATION, subHashedPacketsGen, "twi", secretKey.getPublicKey());
// add that revocation to the base, and check if the redundant one will be rejected as well
UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, revocation.getEncoded(), 2);
injectEverywhere(modified, revocation.getEncoded());
// an older (but different!) uid revocation should be rejected as well
subHashedPacketsGen.setSignatureCreationTime(false, new Date(new Date().getTime() -1000*1000));
revocation = forgeSignature(
secretKey, PGPSignature.CERTIFICATION_REVOCATION, subHashedPacketsGen, "twi", secretKey.getPublicKey());
injectEverywhere(modified, revocation.getEncoded());
}
@Test public void testDuplicateUid() throws Exception {
// get subkey packets
Iterator<RawPacket> it = KeyringTestingHelper.parseKeyring(ring.getEncoded());
RawPacket uidPacket = KeyringTestingHelper.getNth(it, 3);
RawPacket uidSig = it.next();
// inject at a second position
UncachedKeyRing modified = ring;
modified = KeyringTestingHelper.injectPacket(modified, uidPacket.buf, 5);
modified = KeyringTestingHelper.injectPacket(modified, uidSig.buf, 6);
// canonicalize, and check if we lose the bad signature
OperationLog log = new OperationLog();
CanonicalizedKeyRing canonicalized = modified.canonicalize(log, 0);
Assert.assertNotNull("canonicalization with duplicate user id should succeed", canonicalized);
Assert.assertTrue("log should contain uid_dup event", log.containsType(LogType.MSG_KC_UID_DUP));
/* TODO actually test ths, and fix behavior
Assert.assertTrue("duplicate user id packets should be gone after canonicalization",
KeyringTestingHelper.diffKeyrings(modified.getEncoded(), canonicalized.getEncoded(),
onlyA, onlyB)
);
Assert.assertEquals("canonicalized keyring should have lost the two duplicate packets",
2, onlyA.size());
Assert.assertTrue("canonicalized keyring should still contain the user id",
canonicalized.getUnorderedUserIds().contains(new UserIDPacket(uidPacket.buf).getID()));
*/
}
@Test public void testSignatureBroken() throws Exception {
injectEverytype(secretKey, ring, subHashedPacketsGen, true);
}
@Test public void testForeignSignature() throws Exception {
SaveKeyringParcel parcel = new SaveKeyringParcel();
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
Algorithm.RSA, 1024, null, KeyFlags.CERTIFY_OTHER, 0L));
parcel.mAddUserIds.add("trix");
PgpKeyOperation op = new PgpKeyOperation(null);
OperationResult.OperationLog log = new OperationResult.OperationLog();
UncachedKeyRing foreign = op.createSecretKeyRing(parcel).getRing();
Assert.assertNotNull("initial test key creation must succeed", foreign);
PGPSecretKey foreignSecretKey =
new PGPSecretKeyRing(foreign.getEncoded(), new JcaKeyFingerprintCalculator())
.getSecretKey();
injectEverytype(foreignSecretKey, ring, subHashedPacketsGen);
}
@Test public void testSignatureFuture() throws Exception {
// generate future timestamp (we allow up to one day future timestamps)
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_YEAR, 2);
subHashedPacketsGen.setSignatureCreationTime(false, cal.getTime());
injectEverytype(secretKey, ring, subHashedPacketsGen);
}
@Test public void testSignatureLocal() throws Exception {
// make key local only
subHashedPacketsGen.setExportable(false, false);
injectEverytype(secretKey, ring, subHashedPacketsGen);
}
@Test public void testSubkeyDestroy() throws Exception {
// signature for second key (first subkey)
UncachedKeyRing modified = KeyringTestingHelper.removePacket(ring, 8);
// canonicalization should fail, because there are no valid uids left
CanonicalizedKeyRing canonicalized = modified.canonicalize(log, 0);
Assert.assertTrue("keyring with missing subkey binding sig should differ from intact one after canonicalization",
KeyringTestingHelper.diffKeyrings(ring.getEncoded(), canonicalized.getEncoded(),
onlyA, onlyB)
);
Assert.assertEquals("canonicalized keyring should have two extra packets", 2, onlyA.size());
Assert.assertEquals("canonicalized keyring should have no extra packets", 0, onlyB.size());
Assert.assertEquals("first missing packet should be the subkey",
PacketTags.SECRET_SUBKEY, onlyA.get(0).tag);
Assert.assertEquals("second missing packet should be subkey's signature",
PacketTags.SIGNATURE, onlyA.get(1).tag);
Assert.assertEquals("second missing packet should be next to subkey",
onlyA.get(0).position + 1, onlyA.get(1).position);
}
@Test public void testSubkeyBindingNoPKB() throws Exception {
UncachedPublicKey pKey = KeyringTestingHelper.getNth(ring.getPublicKeys(), 1);
PGPSignature sig;
subHashedPacketsGen.setKeyFlags(false, KeyFlags.SIGN_DATA);
{
// forge a (newer) signature, which has the sign flag but no primary key binding sig
PGPSignatureSubpacketGenerator unhashedSubs = new PGPSignatureSubpacketGenerator();
// just add any random signature, because why not
unhashedSubs.setEmbeddedSignature(false, forgeSignature(
secretKey, PGPSignature.POSITIVE_CERTIFICATION, subHashedPacketsGen,
secretKey.getPublicKey()
)
);
sig = forgeSignature(
secretKey, PGPSignature.SUBKEY_BINDING, subHashedPacketsGen, unhashedSubs,
secretKey.getPublicKey(), pKey.getPublicKey());
// inject in the right position
UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, sig.getEncoded(), 8);
// canonicalize, and check if we lose the bad signature
CanonicalizedKeyRing canonicalized = modified.canonicalize(log, 0);
Assert.assertFalse("subkey binding signature should be gone after canonicalization",
KeyringTestingHelper.diffKeyrings(ring.getEncoded(), canonicalized.getEncoded(),
onlyA, onlyB)
);
}
{ // now try one with a /bad/ primary key binding signature
PGPSignatureSubpacketGenerator unhashedSubs = new PGPSignatureSubpacketGenerator();
// this one is signed by the primary key itself, not the subkey - but it IS primary binding
unhashedSubs.setEmbeddedSignature(false, forgeSignature(
secretKey, PGPSignature.PRIMARYKEY_BINDING, subHashedPacketsGen,
secretKey.getPublicKey(), pKey.getPublicKey()
)
);
sig = forgeSignature(
secretKey, PGPSignature.SUBKEY_BINDING, subHashedPacketsGen, unhashedSubs,
secretKey.getPublicKey(), pKey.getPublicKey());
// inject in the right position
UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, sig.getEncoded(), 8);
// canonicalize, and check if we lose the bad signature
CanonicalizedKeyRing canonicalized = modified.canonicalize(log, 0);
Assert.assertFalse("subkey binding signature should be gone after canonicalization",
KeyringTestingHelper.diffKeyrings(ring.getEncoded(), canonicalized.getEncoded(),
onlyA, onlyB)
);
}
}
@Test public void testSubkeyBindingRedundant() throws Exception {
UncachedPublicKey pKey = KeyringTestingHelper.getNth(ring.getPublicKeys(), 2);
subHashedPacketsGen.setKeyFlags(false, KeyFlags.ENCRYPT_COMMS);
PGPSignature sig2 = forgeSignature(
secretKey, PGPSignature.SUBKEY_BINDING, subHashedPacketsGen,
secretKey.getPublicKey(), pKey.getPublicKey());
subHashedPacketsGen.setSignatureCreationTime(false, new Date(new Date().getTime() -1000*1000));
PGPSignature sig1 = forgeSignature(
secretKey, PGPSignature.SUBKEY_REVOCATION, subHashedPacketsGen,
secretKey.getPublicKey(), pKey.getPublicKey());
subHashedPacketsGen = new PGPSignatureSubpacketGenerator();
subHashedPacketsGen.setSignatureCreationTime(false, new Date(new Date().getTime() -100*1000));
PGPSignature sig3 = forgeSignature(
secretKey, PGPSignature.SUBKEY_BINDING, subHashedPacketsGen,
secretKey.getPublicKey(), pKey.getPublicKey());
UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, sig1.getEncoded(), 10);
modified = KeyringTestingHelper.injectPacket(modified, sig2.getEncoded(), 11);
modified = KeyringTestingHelper.injectPacket(modified, sig1.getEncoded(), 12);
modified = KeyringTestingHelper.injectPacket(modified, sig3.getEncoded(), 13);
// canonicalize, and check if we lose the bad signature
CanonicalizedKeyRing canonicalized = modified.canonicalize(log, 0);
Assert.assertTrue("subkey binding signature should be gone after canonicalization",
KeyringTestingHelper.diffKeyrings(modified.getEncoded(), canonicalized.getEncoded(),
onlyA, onlyB)
);
Assert.assertEquals("canonicalized keyring should have lost two packets", 3, onlyA.size());
Assert.assertEquals("canonicalized keyring should have no extra packets", 0, onlyB.size());
Assert.assertEquals("first missing packet should be the subkey",
PacketTags.SIGNATURE, onlyA.get(0).tag);
Assert.assertEquals("second missing packet should be a signature",
PacketTags.SIGNATURE, onlyA.get(1).tag);
Assert.assertEquals("second missing packet should be a signature",
PacketTags.SIGNATURE, onlyA.get(2).tag);
}
@Test
public void testDuplicateSubkey() throws Exception {
{ // duplicate subkey
// get subkey packets
Iterator<RawPacket> it = KeyringTestingHelper.parseKeyring(ring.getEncoded());
RawPacket subKey = KeyringTestingHelper.getNth(it, 7);
RawPacket subSig = it.next();
// inject at a second position
UncachedKeyRing modified = ring;
modified = KeyringTestingHelper.injectPacket(modified, subKey.buf, 9);
modified = KeyringTestingHelper.injectPacket(modified, subSig.buf, 10);
// canonicalize, and check if we lose the bad signature
OperationLog log = new OperationLog();
CanonicalizedKeyRing canonicalized = modified.canonicalize(log, 0);
Assert.assertNull("canonicalization with duplicate subkey should fail", canonicalized);
Assert.assertTrue("log should contain dup_key event", log.containsType(LogType.MSG_KC_ERROR_DUP_KEY));
}
{ // duplicate subkey, which is the same as the master key
// We actually encountered one of these in the wild:
// https://www.sparkasse-holstein.de/firmenkunden/electronic_banking/secure-e-mail/pdf/Spk_Holstein_PGP_Domain-Zertifikat.asc
CanonicalizedSecretKeyRing canonicalized = (CanonicalizedSecretKeyRing) ring.canonicalize(log, 0);
CanonicalizedSecretKey masterSecretKey = canonicalized.getSecretKey();
masterSecretKey.unlock(new Passphrase());
PGPPublicKey masterPublicKey = masterSecretKey.getPublicKey();
CryptoInputParcel cryptoInput = new CryptoInputParcel();
PGPSignature cert = PgpKeyOperation.generateSubkeyBindingSignature(
PgpKeyOperation.getSignatureGenerator(masterSecretKey.getSecretKey(), cryptoInput),
cryptoInput.getSignatureTime(),
masterPublicKey, masterSecretKey.getPrivateKey(), masterSecretKey.getPrivateKey(),
masterPublicKey, masterSecretKey.getKeyUsage(), 0);
PGPPublicKey subPubKey = PGPPublicKey.addSubkeyBindingCertification(masterPublicKey, cert);
PGPSecretKey sKey;
{
// Build key encrypter and decrypter based on passphrase
PGPDigestCalculator encryptorHashCalc = new JcaPGPDigestCalculatorProviderBuilder()
.build().get(HashAlgorithmTags.SHA256);
PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder(
SymmetricKeyAlgorithmTags.AES_256, encryptorHashCalc, 10)
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build("".toCharArray());
// NOTE: only SHA1 is supported for key checksum calculations.
PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder()
.build().get(HashAlgorithmTags.SHA1);
sKey = new PGPSecretKey(masterSecretKey.getPrivateKey(), subPubKey, sha1Calc, false, keyEncryptor);
}
UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, sKey.getEncoded(), 7);
// canonicalize, and check if we lose the bad signature
OperationLog log = new OperationLog();
CanonicalizedKeyRing result = modified.canonicalize(log, 0);
Assert.assertNull("canonicalization with duplicate subkey (from master) should fail", result);
Assert.assertTrue("log should contain dup_key event", log.containsType(LogType.MSG_KC_ERROR_DUP_KEY));
}
}
private static final int[] sigtypes_direct = new int[] {
PGPSignature.KEY_REVOCATION,
PGPSignature.DIRECT_KEY,
};
private static final int[] sigtypes_uid = new int[] {
PGPSignature.DEFAULT_CERTIFICATION,
PGPSignature.NO_CERTIFICATION,
PGPSignature.CASUAL_CERTIFICATION,
PGPSignature.POSITIVE_CERTIFICATION,
PGPSignature.CERTIFICATION_REVOCATION,
};
private static final int[] sigtypes_subkey = new int[] {
PGPSignature.SUBKEY_BINDING,
PGPSignature.PRIMARYKEY_BINDING,
PGPSignature.SUBKEY_REVOCATION,
};
private static void injectEverytype(PGPSecretKey secretKey,
UncachedKeyRing ring,
PGPSignatureSubpacketGenerator subHashedPacketsGen)
throws Exception {
injectEverytype(secretKey, ring, subHashedPacketsGen, false);
}
private static void injectEverytype(PGPSecretKey secretKey,
UncachedKeyRing ring,
PGPSignatureSubpacketGenerator subHashedPacketsGen,
boolean breakSig)
throws Exception {
for (int sigtype : sigtypes_direct) {
PGPSignature sig = forgeSignature(
secretKey, sigtype, subHashedPacketsGen, secretKey.getPublicKey());
byte[] encoded = sig.getEncoded();
if (breakSig) {
encoded[encoded.length-10] += 1;
}
injectEverywhere(ring, encoded);
}
for (int sigtype : sigtypes_uid) {
PGPSignature sig = forgeSignature(
secretKey, sigtype, subHashedPacketsGen, "twi", secretKey.getPublicKey());
byte[] encoded = sig.getEncoded();
if (breakSig) {
encoded[encoded.length-10] += 1;
}
injectEverywhere(ring, encoded);
}
for (int sigtype : sigtypes_subkey) {
PGPSignature sig = forgeSignature(
secretKey, sigtype, subHashedPacketsGen,
secretKey.getPublicKey(), secretKey.getPublicKey());
byte[] encoded = sig.getEncoded();
if (breakSig) {
encoded[encoded.length-10] += 1;
}
injectEverywhere(ring, encoded);
}
}
private static void injectEverywhere(UncachedKeyRing ring, byte[] packet) throws Exception {
OperationResult.OperationLog log = new OperationResult.OperationLog();
byte[] encodedRing = ring.getEncoded();
for(int i = 0; i < totalPackets; i++) {
byte[] brokenEncoded = KeyringTestingHelper.injectPacket(encodedRing, packet, i);
try {
UncachedKeyRing brokenRing = UncachedKeyRing.decodeFromData(brokenEncoded);
CanonicalizedKeyRing canonicalized = brokenRing.canonicalize(log, 0);
if (canonicalized == null) {
System.out.println("ok, canonicalization failed.");
continue;
}
Assert.assertArrayEquals("injected bad signature must be gone after canonicalization",
ring.getEncoded(), canonicalized.getEncoded());
} catch (Exception e) {
System.out.println("ok, rejected with: " + e.getMessage());
}
}
}
private static PGPSignature forgeSignature(PGPSecretKey key, int type,
PGPSignatureSubpacketGenerator subpackets,
PGPPublicKey publicKey)
throws Exception {
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build("".toCharArray());
PGPPrivateKey privateKey = key.extractPrivateKey(keyDecryptor);
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
publicKey.getAlgorithm(), PGPUtil.SHA1)
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
sGen.setHashedSubpackets(subpackets.generate());
sGen.init(type, privateKey);
return sGen.generateCertification(publicKey);
}
private static PGPSignature forgeSignature(PGPSecretKey key, int type,
PGPSignatureSubpacketGenerator subpackets,
String userId, PGPPublicKey publicKey)
throws Exception {
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build("".toCharArray());
PGPPrivateKey privateKey = key.extractPrivateKey(keyDecryptor);
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
publicKey.getAlgorithm(), PGPUtil.SHA1)
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
sGen.setHashedSubpackets(subpackets.generate());
sGen.init(type, privateKey);
return sGen.generateCertification(userId, publicKey);
}
private static PGPSignature forgeSignature(PGPSecretKey key, int type,
PGPSignatureSubpacketGenerator subpackets,
PGPPublicKey publicKey, PGPPublicKey signedKey)
throws Exception {
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build("".toCharArray());
PGPPrivateKey privateKey = key.extractPrivateKey(keyDecryptor);
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
publicKey.getAlgorithm(), PGPUtil.SHA1)
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
sGen.setHashedSubpackets(subpackets.generate());
sGen.init(type, privateKey);
return sGen.generateCertification(publicKey, signedKey);
}
private static PGPSignature forgeSignature(PGPSecretKey key, int type,
PGPSignatureSubpacketGenerator hashedSubs,
PGPSignatureSubpacketGenerator unhashedSubs,
PGPPublicKey publicKey, PGPPublicKey signedKey)
throws Exception {
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build("".toCharArray());
PGPPrivateKey privateKey = key.extractPrivateKey(keyDecryptor);
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
publicKey.getAlgorithm(), PGPUtil.SHA1)
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
sGen.setHashedSubpackets(hashedSubs.generate());
sGen.setUnhashedSubpackets(unhashedSubs.generate());
sGen.init(type, privateKey);
return sGen.generateCertification(publicKey, signedKey);
}
}

View File

@@ -0,0 +1,496 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
* 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.pgp;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLog;
import org.spongycastle.bcpg.BCPGInputStream;
import org.spongycastle.bcpg.PacketTags;
import org.spongycastle.bcpg.S2K;
import org.spongycastle.bcpg.SecretKeyPacket;
import org.spongycastle.bcpg.sig.KeyFlags;
import org.spongycastle.jce.provider.BouncyCastleProvider;
import org.spongycastle.util.Strings;
import org.sufficientlysecure.keychain.BuildConfig;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.pgp.PgpCertifyOperation.PgpCertifyResult;
import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction;
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.support.KeyringTestingHelper;
import org.sufficientlysecure.keychain.support.KeyringTestingHelper.RawPacket;
import org.sufficientlysecure.keychain.util.Passphrase;
import org.sufficientlysecure.keychain.util.ProgressScaler;
import java.io.ByteArrayInputStream;
import java.security.Security;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.Random;
/** Tests for the UncachedKeyring.merge method.
*
* This is another complex, crypto-related method. It merges information from one keyring into
* another, keeping information from the base (ie, called object) keyring in case of conflicts.
* The types of keys may be Public or Secret and can be mixed, For mixed types the result type
* will be the same as the base keyring.
*
* Test cases:
* - Merging keyrings with different masterKeyIds should fail
* - Merging a key with itself should be a no-operation
* - Merging a key with an extra revocation certificate, it should have that certificate
* - Merging a key with an extra user id, it should have that extra user id and its certificates
* - Merging a key with an extra user id certificate, it should have that certificate
* - Merging a key with an extra subkey, it should have that subkey
* - Merging a key with an extra subkey certificate, it should have that certificate
* - All of the above operations should work regardless of the key types. This means in particular
* that for new subkeys, an equivalent subkey of the proper type must be generated.
* - In case of two secret keys with the same id but different S2K, the key of the base keyring
* should be preferred (TODO or should it?)
*
* Note that the merge operation does not care about certificate validity, a bad certificate or
* packet will be copied regardless. Filtering out bad packets is done with canonicalization.
*
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
public class UncachedKeyringMergeTest {
static UncachedKeyRing staticRingA, staticRingB;
UncachedKeyRing ringA, ringB;
ArrayList<RawPacket> onlyA = new ArrayList<RawPacket>();
ArrayList<RawPacket> onlyB = new ArrayList<RawPacket>();
OperationResult.OperationLog log = new OperationResult.OperationLog();
PgpKeyOperation op;
SaveKeyringParcel parcel;
@BeforeClass
public static void setUpOnce() throws Exception {
Security.insertProviderAt(new BouncyCastleProvider(), 1);
ShadowLog.stream = System.out;
{
SaveKeyringParcel parcel = new SaveKeyringParcel();
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
Algorithm.RSA, 1024, null, KeyFlags.CERTIFY_OTHER, 0L));
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
Algorithm.RSA, 1024, null, KeyFlags.SIGN_DATA, 0L));
parcel.mAddUserIds.add("twi");
parcel.mAddUserIds.add("pink");
{
WrappedUserAttribute uat = WrappedUserAttribute.fromSubpacket(100,
"sunshine, sunshine, ladybugs awake~".getBytes());
parcel.mAddUserAttribute.add(uat);
}
// passphrase is tested in PgpKeyOperationTest, just use empty here
parcel.mNewUnlock = new ChangeUnlockParcel(new Passphrase());
PgpKeyOperation op = new PgpKeyOperation(null);
OperationResult.OperationLog log = new OperationResult.OperationLog();
PgpEditKeyResult result = op.createSecretKeyRing(parcel);
staticRingA = result.getRing();
staticRingA = staticRingA.canonicalize(new OperationLog(), 0).getUncachedKeyRing();
}
{
SaveKeyringParcel parcel = new SaveKeyringParcel();
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
Algorithm.RSA, 1024, null, KeyFlags.CERTIFY_OTHER, 0L));
parcel.mAddUserIds.add("shy");
// passphrase is tested in PgpKeyOperationTest, just use empty here
parcel.mNewUnlock = new ChangeUnlockParcel(new Passphrase());
PgpKeyOperation op = new PgpKeyOperation(null);
OperationResult.OperationLog log = new OperationResult.OperationLog();
PgpEditKeyResult result = op.createSecretKeyRing(parcel);
staticRingB = result.getRing();
staticRingB = staticRingB.canonicalize(new OperationLog(), 0).getUncachedKeyRing();
}
Assert.assertNotNull("initial test key creation must succeed", staticRingA);
Assert.assertNotNull("initial test key creation must succeed", staticRingB);
// we sleep here for a second, to make sure all new certificates have different timestamps
Thread.sleep(1000);
}
@Before
public void setUp() throws Exception {
// show Log.x messages in system.out
ShadowLog.stream = System.out;
ringA = staticRingA;
ringB = staticRingB;
// setting up some parameters just to reduce code duplication
op = new PgpKeyOperation(new ProgressScaler(null, 0, 100, 100));
// set this up, gonna need it more than once
parcel = new SaveKeyringParcel();
parcel.mMasterKeyId = ringA.getMasterKeyId();
parcel.mFingerprint = ringA.getFingerprint();
}
public void testSelfNoOp() throws Exception {
UncachedKeyRing merged = mergeWithChecks(ringA, ringA, null);
Assert.assertArrayEquals("keyring merged with itself must be identical",
ringA.getEncoded(), merged.getEncoded()
);
}
@Test
public void testDifferentMasterKeyIds() throws Exception {
Assert.assertNotEquals("generated key ids must be different",
ringA.getMasterKeyId(), ringB.getMasterKeyId());
Assert.assertNull("merging keys with differing key ids must fail",
ringA.merge(ringB, log, 0));
Assert.assertNull("merging keys with differing key ids must fail",
ringB.merge(ringA, log, 0));
}
@Test
public void testAddedUserId() throws Exception {
UncachedKeyRing modifiedA, modifiedB; {
CanonicalizedSecretKeyRing secretRing =
new CanonicalizedSecretKeyRing(ringA.getEncoded(), false, 0);
parcel.reset();
parcel.mAddUserIds.add("flim");
modifiedA = op.modifySecretKeyRing(secretRing, new CryptoInputParcel(new Passphrase()), parcel).getRing();
parcel.reset();
parcel.mAddUserIds.add("flam");
modifiedB = op.modifySecretKeyRing(secretRing, new CryptoInputParcel(new Passphrase()), parcel).getRing();
}
{ // merge A into base
UncachedKeyRing merged = mergeWithChecks(ringA, modifiedA);
Assert.assertEquals("merged keyring must have lost no packets", 0, onlyA.size());
Assert.assertEquals("merged keyring must have gained two packets", 2, onlyB.size());
Assert.assertTrue("merged keyring must contain new user id",
merged.getPublicKey().getUnorderedUserIds().contains("flim"));
}
{ // merge A into B
UncachedKeyRing merged = mergeWithChecks(modifiedA, modifiedB, ringA);
Assert.assertEquals("merged keyring must have lost no packets", 0, onlyA.size());
Assert.assertEquals("merged keyring must have gained four packets", 4, onlyB.size());
Assert.assertTrue("merged keyring must contain first new user id",
merged.getPublicKey().getUnorderedUserIds().contains("flim"));
Assert.assertTrue("merged keyring must contain second new user id",
merged.getPublicKey().getUnorderedUserIds().contains("flam"));
}
}
@Test
public void testAddedSubkeyId() throws Exception {
UncachedKeyRing modifiedA, modifiedB;
long subKeyIdA, subKeyIdB;
{
CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ringA.getEncoded(), false, 0);
parcel.reset();
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
Algorithm.RSA, 1024, null, KeyFlags.SIGN_DATA, 0L));
modifiedA = op.modifySecretKeyRing(secretRing, new CryptoInputParcel(new Passphrase()), parcel).getRing();
modifiedB = op.modifySecretKeyRing(secretRing, new CryptoInputParcel(new Passphrase()), parcel).getRing();
subKeyIdA = KeyringTestingHelper.getSubkeyId(modifiedA, 2);
subKeyIdB = KeyringTestingHelper.getSubkeyId(modifiedB, 2);
}
{
UncachedKeyRing merged = mergeWithChecks(ringA, modifiedA);
Assert.assertEquals("merged keyring must have lost no packets", 0, onlyA.size());
Assert.assertEquals("merged keyring must have gained two packets", 2, onlyB.size());
long mergedKeyId = KeyringTestingHelper.getSubkeyId(merged, 2);
Assert.assertEquals("merged keyring must contain the new subkey", subKeyIdA, mergedKeyId);
}
{
UncachedKeyRing merged = mergeWithChecks(modifiedA, modifiedB, ringA);
Assert.assertEquals("merged keyring must have lost no packets", 0, onlyA.size());
Assert.assertEquals("merged keyring must have gained four packets", 4, onlyB.size());
Iterator<UncachedPublicKey> it = merged.getPublicKeys();
it.next(); it.next();
Assert.assertEquals("merged keyring must contain the new subkey",
subKeyIdA, it.next().getKeyId());
Assert.assertEquals("merged keyring must contain both new subkeys",
subKeyIdB, it.next().getKeyId());
}
}
@Test
public void testAddedKeySignature() throws Exception {
final UncachedKeyRing modified; {
parcel.reset();
parcel.mRevokeSubKeys.add(KeyringTestingHelper.getSubkeyId(ringA, 1));
CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(
ringA.getEncoded(), false, 0);
modified = op.modifySecretKeyRing(secretRing, new CryptoInputParcel(new Passphrase()), parcel).getRing();
}
{
UncachedKeyRing merged = ringA.merge(modified, log, 0);
Assert.assertNotNull("merge must succeed", merged);
Assert.assertFalse(
"merging keyring with extra signatures into its base should yield that same keyring",
KeyringTestingHelper.diffKeyrings(merged.getEncoded(), modified.getEncoded(), onlyA, onlyB)
);
}
}
@Test
public void testAddedUserIdSignature() throws Exception {
final UncachedKeyRing pubRing = ringA.extractPublicKeyRing();
final UncachedKeyRing modified; {
CanonicalizedPublicKeyRing publicRing = new CanonicalizedPublicKeyRing(
pubRing.getEncoded(), 0);
CanonicalizedSecretKey secretKey = new CanonicalizedSecretKeyRing(
ringB.getEncoded(), false, 0).getSecretKey();
secretKey.unlock(new Passphrase());
PgpCertifyOperation op = new PgpCertifyOperation();
CertifyAction action = new CertifyAction(pubRing.getMasterKeyId(), publicRing.getPublicKey().getUnorderedUserIds());
// sign all user ids
PgpCertifyResult result = op.certify(secretKey, publicRing, new OperationLog(), 0, action, null, new Date());
Assert.assertTrue("certification must succeed", result.success());
Assert.assertNotNull("certification must yield result", result.getCertifiedRing());
modified = result.getCertifiedRing();
}
{
UncachedKeyRing merged = ringA.merge(modified, log, 0);
Assert.assertNotNull("merge must succeed", merged);
Assert.assertArrayEquals("foreign signatures should not be merged into secret key",
ringA.getEncoded(), merged.getEncoded()
);
}
{
byte[] sig = KeyringTestingHelper.getNth(
modified.getPublicKey().getSignaturesForRawId(Strings.toUTF8ByteArray("twi")), 1).getEncoded();
// inject the (foreign!) signature into subkey signature position
UncachedKeyRing moreModified = KeyringTestingHelper.injectPacket(modified, sig, 1);
UncachedKeyRing merged = ringA.merge(moreModified, log, 0);
Assert.assertNotNull("merge must succeed", merged);
Assert.assertArrayEquals("foreign signatures should not be merged into secret key",
ringA.getEncoded(), merged.getEncoded()
);
merged = pubRing.merge(moreModified, log, 0);
Assert.assertNotNull("merge must succeed", merged);
Assert.assertTrue(
"merged keyring should contain new signature",
KeyringTestingHelper.diffKeyrings(pubRing.getEncoded(), merged.getEncoded(), onlyA, onlyB)
);
Assert.assertEquals("merged keyring should be missing no packets", 0, onlyA.size());
Assert.assertEquals("merged keyring should contain exactly two more packets", 2, onlyB.size());
Assert.assertEquals("first added packet should be a signature",
PacketTags.SIGNATURE, onlyB.get(0).tag);
Assert.assertEquals("first added packet should be in the position we injected it at",
1, onlyB.get(0).position);
Assert.assertEquals("second added packet should be a signature",
PacketTags.SIGNATURE, onlyB.get(1).tag);
}
{
UncachedKeyRing merged = pubRing.merge(modified, log, 0);
Assert.assertNotNull("merge must succeed", merged);
Assert.assertFalse(
"merging keyring with extra signatures into its base should yield that same keyring",
KeyringTestingHelper.diffKeyrings(merged.getEncoded(), modified.getEncoded(), onlyA, onlyB)
);
}
}
@Test
public void testAddedUserAttributeSignature() throws Exception {
final UncachedKeyRing modified; {
parcel.reset();
Random r = new Random();
int type = r.nextInt(110)+1;
byte[] data = new byte[r.nextInt(2000)];
new Random().nextBytes(data);
WrappedUserAttribute uat = WrappedUserAttribute.fromSubpacket(type, data);
parcel.mAddUserAttribute.add(uat);
CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(
ringA.getEncoded(), false, 0);
modified = op.modifySecretKeyRing(secretRing, new CryptoInputParcel(new Passphrase()), parcel).getRing();
}
{
UncachedKeyRing merged = ringA.merge(modified, log, 0);
Assert.assertNotNull("merge must succeed", merged);
Assert.assertFalse(
"merging keyring with extra user attribute into its base should yield that same keyring",
KeyringTestingHelper.diffKeyrings(merged.getEncoded(), modified.getEncoded(), onlyA, onlyB)
);
}
}
private UncachedKeyRing mergeWithChecks(UncachedKeyRing a, UncachedKeyRing b)
throws Exception {
return mergeWithChecks(a, b, a);
}
private UncachedKeyRing mergeWithChecks(UncachedKeyRing a, UncachedKeyRing b,
UncachedKeyRing base)
throws Exception {
Assert.assertTrue("merging keyring must be secret type", a.isSecret());
Assert.assertTrue("merged keyring must be secret type", b.isSecret());
final UncachedKeyRing resultA;
UncachedKeyRing resultB;
{ // sec + sec
resultA = a.merge(b, log, 0);
Assert.assertNotNull("merge must succeed as sec(a)+sec(b)", resultA);
resultB = b.merge(a, log, 0);
Assert.assertNotNull("merge must succeed as sec(b)+sec(a)", resultB);
// check commutativity, if requested
Assert.assertFalse("result of merge must be commutative",
KeyringTestingHelper.diffKeyrings(
resultA.getEncoded(), resultB.getEncoded(), onlyA, onlyB)
);
}
final UncachedKeyRing pubA = a.extractPublicKeyRing();
final UncachedKeyRing pubB = b.extractPublicKeyRing();
{ // sec + pub
// this one is special, because GNU_DUMMY keys might be generated!
resultB = a.merge(pubB, log, 0);
Assert.assertNotNull("merge must succeed as sec(a)+pub(b)", resultA);
// these MAY diff
KeyringTestingHelper.diffKeyrings(resultA.getEncoded(), resultB.getEncoded(),
onlyA, onlyB);
Assert.assertEquals("sec(a)+pub(b): results must have equal number of packets",
onlyA.size(), onlyB.size());
for (int i = 0; i < onlyA.size(); i++) {
Assert.assertEquals("sec(a)+pub(c): old packet must be secret subkey",
PacketTags.SECRET_SUBKEY, onlyA.get(i).tag);
Assert.assertEquals("sec(a)+pub(c): new packet must be dummy secret subkey",
PacketTags.SECRET_SUBKEY, onlyB.get(i).tag);
SecretKeyPacket pA = (SecretKeyPacket) new BCPGInputStream(new ByteArrayInputStream(onlyA.get(i).buf)).readPacket();
SecretKeyPacket pB = (SecretKeyPacket) new BCPGInputStream(new ByteArrayInputStream(onlyB.get(i).buf)).readPacket();
Assert.assertArrayEquals("sec(a)+pub(c): both packets must have equal pubkey parts",
pA.getPublicKeyPacket().getEncoded(), pB.getPublicKeyPacket().getEncoded()
);
Assert.assertEquals("sec(a)+pub(c): new packet should have GNU_DUMMY S2K type",
S2K.GNU_DUMMY_S2K, pB.getS2K().getType());
Assert.assertEquals("sec(a)+pub(c): new packet should have GNU_DUMMY protection mode 0x1",
0x1, pB.getS2K().getProtectionMode());
Assert.assertEquals("sec(a)+pub(c): new packet secret key data should have length zero",
0, pB.getSecretKeyData().length);
Assert.assertNull("sec(a)+pub(c): new packet should have no iv data", pB.getIV());
}
}
{ // pub + sec, and pub + pub
final UncachedKeyRing pubResult = resultA.extractPublicKeyRing();
resultB = pubA.merge(b, log, 0);
Assert.assertNotNull("merge must succeed as pub(a)+sec(b)", resultA);
Assert.assertFalse("result of pub(a)+sec(b) must be same as pub(sec(a)+sec(b))",
KeyringTestingHelper.diffKeyrings(
pubResult.getEncoded(), resultB.getEncoded(), onlyA, onlyB)
);
resultB = pubA.merge(pubB, log, 0);
Assert.assertNotNull("merge must succeed as pub(a)+pub(b)", resultA);
Assert.assertFalse("result of pub(a)+pub(b) must be same as pub(sec(a)+sec(b))",
KeyringTestingHelper.diffKeyrings(
pubResult.getEncoded(), resultB.getEncoded(), onlyA, onlyB)
);
}
if (base != null) {
// set up onlyA and onlyB to be a diff to the base
Assert.assertTrue("merged keyring must differ from base",
KeyringTestingHelper.diffKeyrings(
base.getEncoded(), resultA.getEncoded(), onlyA, onlyB)
);
}
return resultA;
}
}

View File

@@ -0,0 +1,160 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
* 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.pgp;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLog;
import org.spongycastle.bcpg.sig.KeyFlags;
import org.sufficientlysecure.keychain.BuildConfig;
import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing.IteratorWithIOThrow;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.util.Passphrase;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.Random;
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
public class UncachedKeyringTest {
static UncachedKeyRing staticRing, staticPubRing;
UncachedKeyRing ring, pubRing;
@BeforeClass
public static void setUpOnce() throws Exception {
ShadowLog.stream = System.out;
SaveKeyringParcel parcel = new SaveKeyringParcel();
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
Algorithm.RSA, 1024, null, KeyFlags.CERTIFY_OTHER, 0L));
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
Algorithm.RSA, 1024, null, KeyFlags.SIGN_DATA, 0L));
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
Algorithm.RSA, 1024, null, KeyFlags.ENCRYPT_COMMS, 0L));
parcel.mAddUserIds.add("twi");
parcel.mAddUserIds.add("pink");
{
Random r = new Random();
int type = r.nextInt(110)+1;
byte[] data = new byte[r.nextInt(2000)];
new Random().nextBytes(data);
WrappedUserAttribute uat = WrappedUserAttribute.fromSubpacket(type, data);
parcel.mAddUserAttribute.add(uat);
}
// passphrase is tested in PgpKeyOperationTest, just use empty here
parcel.mNewUnlock = new ChangeUnlockParcel(new Passphrase());
PgpKeyOperation op = new PgpKeyOperation(null);
PgpEditKeyResult result = op.createSecretKeyRing(parcel);
staticRing = result.getRing();
staticPubRing = staticRing.extractPublicKeyRing();
Assert.assertNotNull("initial test key creation must succeed", staticRing);
// we sleep here for a second, to make sure all new certificates have different timestamps
Thread.sleep(1000);
}
@Before
public void setUp() throws Exception {
// show Log.x messages in system.out
ShadowLog.stream = System.out;
ring = staticRing;
pubRing = staticPubRing;
}
@Test(expected = UnsupportedOperationException.class)
public void testPublicKeyItRemove() throws Exception {
Iterator<UncachedPublicKey> it = ring.getPublicKeys();
it.remove();
}
@Test(expected = PgpGeneralException.class)
public void testDecodeFromEmpty() throws Exception {
UncachedKeyRing.decodeFromData(new byte[0]);
}
@Test
public void testArmorIdentity() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ring.encodeArmored(out, "OpenKeychain");
Assert.assertArrayEquals("armor encoded and decoded ring should be identical to original",
ring.getEncoded(),
UncachedKeyRing.decodeFromData(out.toByteArray()).getEncoded());
}
@Test(expected = PgpGeneralException.class)
public void testDecodeEncodeMulti() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
// encode secret and public ring in here
ring.encodeArmored(out, "OpenKeychain");
pubRing.encodeArmored(out, "OpenKeychain");
IteratorWithIOThrow<UncachedKeyRing> it =
UncachedKeyRing.fromStream(new ByteArrayInputStream(out.toByteArray()));
Assert.assertTrue("there should be two rings in the stream", it.hasNext());
Assert.assertArrayEquals("first ring should be the first we put in",
ring.getEncoded(), it.next().getEncoded());
Assert.assertTrue("there should be two rings in the stream", it.hasNext());
Assert.assertArrayEquals("second ring should be the second we put in",
pubRing.getEncoded(), it.next().getEncoded());
Assert.assertFalse("there should be two rings in the stream", it.hasNext());
// this should fail with PgpGeneralException, since it expects exactly one ring
UncachedKeyRing.decodeFromData(out.toByteArray());
}
@Test(expected = RuntimeException.class)
public void testPublicExtractPublic() throws Exception {
// can't do this, either!
pubRing.extractPublicKeyRing();
}
@Test(expected = IOException.class)
public void testBrokenVersionCert() throws Throwable {
// this is a test for one of the patches we use on top of stock bouncycastle, which
// returns an IOException rather than a RuntimeException in case of a bad certificate
// version byte
readRingFromResource("/test-keys/broken_cert_version.asc");
}
UncachedKeyRing readRingFromResource(String name) throws Throwable {
return UncachedKeyRing.fromStream(UncachedKeyringTest.class.getResourceAsStream(name)).next();
}
}