diff options
Diffstat (limited to 'OpenKeychain-Test/src/test')
10 files changed, 1239 insertions, 146 deletions
diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/operations/CertifyOperationTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/operations/CertifyOperationTest.java new file mode 100644 index 000000000..c05f4a029 --- /dev/null +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/operations/CertifyOperationTest.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.operations; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowLog; +import org.spongycastle.bcpg.sig.KeyFlags; +import org.spongycastle.jce.provider.BouncyCastleProvider; +import org.sufficientlysecure.keychain.operations.results.CertifyResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; +import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; +import org.sufficientlysecure.keychain.operations.results.ExportResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerify; +import org.sufficientlysecure.keychain.pgp.PgpKeyOperation; +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing.IteratorWithIOThrow; +import org.sufficientlysecure.keychain.pgp.WrappedSignature; +import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.CertifyActionsParcel; +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.util.InputData; +import org.sufficientlysecure.keychain.util.ProgressScaler; +import org.sufficientlysecure.keychain.util.TestingUtils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.security.Security; +import java.util.Iterator; + +@RunWith(RobolectricTestRunner.class) +@org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19 +public class CertifyOperationTest { + + static String mPassphrase = TestingUtils.genPassphrase(true); + + static UncachedKeyRing mStaticRing1, mStaticRing2; + static String mKeyPhrase1 = TestingUtils.genPassphrase(true); + static String 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("derp"); + 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("ditz"); + parcel.mNewUnlock = new ChangeUnlockParcel(null, "1234"); + + 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() throws Exception { + ProviderHelper providerHelper = new ProviderHelper(Robolectric.application); + + // don't log verbosely here, we're not here to test imports + ShadowLog.stream = oldShadowStream; + + providerHelper.saveSecretKeyRing(mStaticRing1, new ProgressScaler()); + providerHelper.savePublicKeyRing(mStaticRing2.extractPublicKeyRing(), new ProgressScaler()); + + // ok NOW log verbosely! + ShadowLog.stream = System.out; + } + + @Test + public void testSelfCertifyFlag() throws Exception { + + CanonicalizedPublicKeyRing ring = new ProviderHelper(Robolectric.application) + .getCanonicalizedPublicKeyRing(mStaticRing1.getMasterKeyId()); + Assert.assertEquals("secret key must be marked self-certified in database", + // TODO this should be more correctly be VERIFIED_SELF at some point! + Certs.VERIFIED_SECRET, ring.getVerified()); + + } + + @Test + public void testCertify() throws Exception { + CertifyOperation op = operationWithFakePassphraseCache( + mStaticRing1.getMasterKeyId(), mStaticRing1.getMasterKeyId(), mKeyPhrase1); + + { + CanonicalizedPublicKeyRing ring = new ProviderHelper(Robolectric.application) + .getCanonicalizedPublicKeyRing(mStaticRing2.getMasterKeyId()); + Assert.assertEquals("public key must not be marked verified prior to certification", + Certs.UNVERIFIED, ring.getVerified()); + } + + CertifyActionsParcel actions = new CertifyActionsParcel(mStaticRing1.getMasterKeyId()); + actions.add(new CertifyAction(mStaticRing2.getMasterKeyId())); + CertifyResult result = op.certify(actions, null); + + Assert.assertTrue("certification must succeed", result.success()); + + { + CanonicalizedPublicKeyRing ring = new ProviderHelper(Robolectric.application) + .getCanonicalizedPublicKeyRing(mStaticRing2.getMasterKeyId()); + Assert.assertEquals("new key must be verified now", + Certs.VERIFIED_SECRET, ring.getVerified()); + } + + } + + @Test + public void testCertifySelf() throws Exception { + CertifyOperation op = operationWithFakePassphraseCache( + mStaticRing1.getMasterKeyId(), mStaticRing1.getMasterKeyId(), mKeyPhrase1); + + CertifyActionsParcel actions = new CertifyActionsParcel(mStaticRing1.getMasterKeyId()); + actions.add(new CertifyAction(mStaticRing1.getMasterKeyId())); + + CertifyResult result = op.certify(actions, null); + + Assert.assertFalse("certification with itself must fail!", result.success()); + Assert.assertTrue("error msg must be about self certification", + result.getLog().containsType(LogType.MSG_CRT_ERROR_SELF)); + } + + @Test + public void testCertifyNonexistent() throws Exception { + + CertifyOperation op = operationWithFakePassphraseCache(null, null, mKeyPhrase1); + + { + CertifyActionsParcel actions = new CertifyActionsParcel(mStaticRing1.getMasterKeyId()); + actions.add(new CertifyAction(1234L)); + + CertifyResult result = op.certify(actions, null); + + Assert.assertFalse("certification of nonexistent key must fail", result.success()); + Assert.assertTrue("must contain error msg about not found", + result.getLog().containsType(LogType.MSG_CRT_WARN_NOT_FOUND)); + } + + { + CertifyActionsParcel actions = new CertifyActionsParcel(1234L); + actions.add(new CertifyAction(mStaticRing1.getMasterKeyId())); + + CertifyResult result = op.certify(actions, null); + + Assert.assertFalse("certification of nonexistent key must fail", result.success()); + Assert.assertTrue("must contain error msg about not found", + result.getLog().containsType(LogType.MSG_CRT_ERROR_MASTER_NOT_FOUND)); + } + + } + + private CertifyOperation operationWithFakePassphraseCache( + final Long checkMasterKeyId, final Long checkSubKeyId, final String passphrase) { + + return new CertifyOperation(Robolectric.application, + new ProviderHelper(Robolectric.application), + null, null) { + @Override + public String 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; + } + }; + } + +} diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java new file mode 100644 index 000000000..b6fdbfc6c --- /dev/null +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.operations; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowLog; +import org.spongycastle.bcpg.sig.KeyFlags; +import org.spongycastle.jce.provider.BouncyCastleProvider; +import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; +import org.sufficientlysecure.keychain.operations.results.ExportResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.pgp.PgpKeyOperation; +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing.IteratorWithIOThrow; +import org.sufficientlysecure.keychain.pgp.WrappedSignature; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel; +import org.sufficientlysecure.keychain.util.ProgressScaler; +import org.sufficientlysecure.keychain.util.TestingUtils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.security.Security; +import java.util.Iterator; + +@RunWith(RobolectricTestRunner.class) +@org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19 +public class ExportTest { + + static String mPassphrase = TestingUtils.genPassphrase(true); + + static UncachedKeyRing mStaticRing1, mStaticRing2; + static String mKeyPhrase1 = TestingUtils.genPassphrase(true); + static String 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("snips"); + 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("snails"); + parcel.mNewUnlock = new ChangeUnlockParcel(null, "1234"); + + 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(Robolectric.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 testExportAll() throws Exception { + ImportExportOperation op = new ImportExportOperation(Robolectric.application, + new ProviderHelper(Robolectric.application), null); + + // make sure there is a local cert (so the later checks that there are none are meaningful) + Assert.assertTrue("second keyring has local certification", checkForLocal(mStaticRing2)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ExportResult result = op.exportKeyRings(new OperationLog(), null, false, out); + + Assert.assertTrue("export must be a success", result.success()); + + long masterKeyId1, masterKeyId2; + if (mStaticRing1.getMasterKeyId() < mStaticRing2.getMasterKeyId()) { + masterKeyId1 = mStaticRing1.getMasterKeyId(); + masterKeyId2 = mStaticRing2.getMasterKeyId(); + } else { + masterKeyId2 = mStaticRing1.getMasterKeyId(); + masterKeyId1 = mStaticRing2.getMasterKeyId(); + } + + IteratorWithIOThrow<UncachedKeyRing> unc = + UncachedKeyRing.fromStream(new ByteArrayInputStream(out.toByteArray())); + + { + Assert.assertTrue("export must have two keys (1/2)", unc.hasNext()); + UncachedKeyRing ring = unc.next(); + Assert.assertEquals("first exported key has correct masterkeyid", + masterKeyId1, ring.getMasterKeyId()); + Assert.assertFalse("first exported key must not be secret", ring.isSecret()); + Assert.assertFalse("there must be no local signatures in an exported keyring", + checkForLocal(ring)); + } + + { + Assert.assertTrue("export must have two keys (2/2)", unc.hasNext()); + UncachedKeyRing ring = unc.next(); + Assert.assertEquals("second exported key has correct masterkeyid", + masterKeyId2, ring.getMasterKeyId()); + Assert.assertFalse("second exported key must not be secret", ring.isSecret()); + Assert.assertFalse("there must be no local signatures in an exported keyring", + checkForLocal(ring)); + } + + out = new ByteArrayOutputStream(); + result = op.exportKeyRings(new OperationLog(), null, true, out); + + Assert.assertTrue("export must be a success", result.success()); + + unc = UncachedKeyRing.fromStream(new ByteArrayInputStream(out.toByteArray())); + + { + Assert.assertTrue("export must have four keys (1/4)", unc.hasNext()); + UncachedKeyRing ring = unc.next(); + Assert.assertEquals("1/4 exported key has correct masterkeyid", + masterKeyId1, ring.getMasterKeyId()); + Assert.assertFalse("1/4 exported key must not be public", ring.isSecret()); + Assert.assertFalse("there must be no local signatures in an exported keyring", + checkForLocal(ring)); + + Assert.assertTrue("export must have four keys (2/4)", unc.hasNext()); + ring = unc.next(); + Assert.assertEquals("2/4 exported key has correct masterkeyid", + masterKeyId1, ring.getMasterKeyId()); + Assert.assertTrue("2/4 exported key must be public", ring.isSecret()); + Assert.assertFalse("there must be no local signatures in an exported keyring", + checkForLocal(ring)); + } + + { + Assert.assertTrue("export must have four keys (3/4)", unc.hasNext()); + UncachedKeyRing ring = unc.next(); + Assert.assertEquals("3/4 exported key has correct masterkeyid", + masterKeyId2, ring.getMasterKeyId()); + Assert.assertFalse("3/4 exported key must not be public", ring.isSecret()); + Assert.assertFalse("there must be no local signatures in an exported keyring", + checkForLocal(ring)); + + Assert.assertTrue("export must have four keys (4/4)", unc.hasNext()); + ring = unc.next(); + Assert.assertEquals("4/4 exported key has correct masterkeyid", + masterKeyId2, ring.getMasterKeyId()); + Assert.assertTrue("4/4 exported key must be public", ring.isSecret()); + Assert.assertFalse("there must be no local signatures in an exported keyring", + checkForLocal(ring)); + } + + } + + /** This function checks whether or not there are any local signatures in a keyring. */ + private boolean checkForLocal(UncachedKeyRing ring) { + Iterator<WrappedSignature> sigs = ring.getPublicKey().getSignatures(); + while (sigs.hasNext()) { + if (sigs.next().isLocal()) { + return true; + } + } + return false; + } + +} diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/operations/PromoteKeyOperationTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/operations/PromoteKeyOperationTest.java new file mode 100644 index 000000000..40ade064b --- /dev/null +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/operations/PromoteKeyOperationTest.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.operations; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowLog; +import org.spongycastle.bcpg.sig.KeyFlags; +import org.spongycastle.jce.provider.BouncyCastleProvider; +import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; +import org.sufficientlysecure.keychain.operations.results.PromoteKeyResult; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; +import org.sufficientlysecure.keychain.pgp.PgpKeyOperation; +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; +import org.sufficientlysecure.keychain.pgp.UncachedPublicKey; +import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +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.ProgressScaler; +import org.sufficientlysecure.keychain.util.TestingUtils; + +import java.io.PrintStream; +import java.security.Security; +import java.util.Iterator; + +@RunWith(RobolectricTestRunner.class) +@org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19 +public class PromoteKeyOperationTest { + + static UncachedKeyRing mStaticRing; + static String mKeyPhrase1 = 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("derp"); + 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()); + + mStaticRing = result.getRing(); + } + + } + + @Before + public void setUp() throws Exception { + ProviderHelper providerHelper = new ProviderHelper(Robolectric.application); + + // don't log verbosely here, we're not here to test imports + ShadowLog.stream = oldShadowStream; + + providerHelper.savePublicKeyRing(mStaticRing.extractPublicKeyRing(), new ProgressScaler()); + + // ok NOW log verbosely! + ShadowLog.stream = System.out; + } + + @Test + public void testPromote() throws Exception { + PromoteKeyOperation op = new PromoteKeyOperation(Robolectric.application, + new ProviderHelper(Robolectric.application), null, null); + + PromoteKeyResult result = op.execute(mStaticRing.getMasterKeyId()); + + Assert.assertTrue("promotion must succeed", result.success()); + + { + CachedPublicKeyRing ring = new ProviderHelper(Robolectric.application) + .getCachedPublicKeyRing(mStaticRing.getMasterKeyId()); + Assert.assertTrue("key must have a secret now", ring.hasAnySecret()); + + Iterator<UncachedPublicKey> it = mStaticRing.getPublicKeys(); + while (it.hasNext()) { + long keyId = it.next().getKeyId(); + Assert.assertEquals("all subkeys must be divert-to-card", + SecretKeyType.GNU_DUMMY, ring.getSecretKeyType(keyId)); + } + } + + // second attempt should fail + result = op.execute(mStaticRing.getMasterKeyId()); + Assert.assertFalse("promotion of secret key must fail", result.success()); + + } + +} diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java index 9dc8f8934..d782230c7 100644 --- a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java @@ -22,25 +22,32 @@ 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.shadows.ShadowLog; import org.spongycastle.bcpg.sig.KeyFlags; import org.spongycastle.jce.provider.BouncyCastleProvider; import org.spongycastle.openpgp.PGPEncryptedData; -import org.sufficientlysecure.keychain.pgp.PgpSignEncrypt.Builder; +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.operations.results.EditKeyResult; -import org.sufficientlysecure.keychain.operations.results.SignEncryptResult; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel; +import org.sufficientlysecure.keychain.support.KeyringTestingHelper; import org.sufficientlysecure.keychain.util.InputData; import org.sufficientlysecure.keychain.util.ProgressScaler; import org.sufficientlysecure.keychain.util.TestingUtils; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.io.PrintStream; import java.security.Security; +import java.util.HashSet; @RunWith(RobolectricTestRunner.class) @org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19 @@ -52,10 +59,13 @@ public class PgpEncryptDecryptTest { static String mKeyPhrase1 = TestingUtils.genPassphrase(true); static String mKeyPhrase2 = TestingUtils.genPassphrase(true); + static PrintStream oldShadowStream; + @BeforeClass public static void setUpOnce() throws Exception { Security.insertProviderAt(new BouncyCastleProvider(), 1); - ShadowLog.stream = System.out; + oldShadowStream = ShadowLog.stream; + // ShadowLog.stream = System.out; PgpKeyOperation op = new PgpKeyOperation(null); @@ -68,9 +78,9 @@ public class PgpEncryptDecryptTest { parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd( Algorithm.ELGAMAL, 1024, null, KeyFlags.ENCRYPT_COMMS, 0L)); parcel.mAddUserIds.add("bloom"); - parcel.mNewPassphrase = mKeyPhrase1; + parcel.mNewUnlock = new ChangeUnlockParcel(mKeyPhrase1); - EditKeyResult result = op.createSecretKeyRing(parcel); + 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()); @@ -86,9 +96,9 @@ public class PgpEncryptDecryptTest { parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd( Algorithm.ELGAMAL, 1024, null, KeyFlags.ENCRYPT_COMMS, 0L)); parcel.mAddUserIds.add("belle"); - parcel.mNewPassphrase = mKeyPhrase2; + parcel.mNewUnlock = new ChangeUnlockParcel(mKeyPhrase2); - EditKeyResult result = op.createSecretKeyRing(parcel); + 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()); @@ -101,8 +111,14 @@ public class PgpEncryptDecryptTest { public void setUp() { ProviderHelper providerHelper = new ProviderHelper(Robolectric.application); + // don't log verbosely here, we're not here to test imports + ShadowLog.stream = oldShadowStream; + providerHelper.saveSecretKeyRing(mStaticRing1, new ProgressScaler()); - providerHelper.saveSecretKeyRing(mStaticRing1, new ProgressScaler()); + providerHelper.saveSecretKeyRing(mStaticRing2, new ProgressScaler()); + + // ok NOW log verbosely! + ShadowLog.stream = System.out; } @Test @@ -115,15 +131,17 @@ public class PgpEncryptDecryptTest { ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayInputStream in = new ByteArrayInputStream(plaintext.getBytes()); + PgpSignEncryptOperation op = new PgpSignEncryptOperation(Robolectric.application, + new ProviderHelper(Robolectric.application), null); + InputData data = new InputData(in, in.available()); - Builder b = new PgpSignEncrypt.Builder(Robolectric.application, - new ProviderHelper(Robolectric.application), - null, // new DummyPassphraseCache(mPassphrase, 0L) - data, out); + PgpSignEncryptInput b = new PgpSignEncryptInput(); b.setSymmetricPassphrase(mPassphrase); b.setSymmetricEncryptionAlgorithm(PGPEncryptedData.AES_128); - SignEncryptResult result = b.build().execute(); + + PgpSignEncryptResult result = op.execute(b, data, out); + Assert.assertTrue("encryption must succeed", result.success()); ciphertext = out.toByteArray(); @@ -145,6 +163,10 @@ public class PgpEncryptDecryptTest { 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 @@ -194,16 +216,15 @@ public class PgpEncryptDecryptTest { ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayInputStream in = new ByteArrayInputStream(plaintext.getBytes()); + PgpSignEncryptOperation op = new PgpSignEncryptOperation(Robolectric.application, + new ProviderHelper(Robolectric.application), null); + InputData data = new InputData(in, in.available()); - Builder b = new PgpSignEncrypt.Builder( - Robolectric.application, - new ProviderHelper(Robolectric.application), - null, // new DummyPassphraseCache(mPassphrase, 0L), - data, out); + PgpSignEncryptInput b = new PgpSignEncryptInput(); b.setEncryptionMasterKeyIds(new long[]{ mStaticRing1.getMasterKeyId() }); b.setSymmetricEncryptionAlgorithm(PGPEncryptedData.AES_128); - SignEncryptResult result = b.build().execute(); + PgpSignEncryptResult result = op.execute(b, data, out); Assert.assertTrue("encryption must succeed", result.success()); ciphertext = out.toByteArray(); @@ -215,89 +236,308 @@ public class PgpEncryptDecryptTest { ByteArrayInputStream in = new ByteArrayInputStream(ciphertext); InputData data = new InputData(in, in.available()); - PgpDecryptVerify.Builder b = new PgpDecryptVerify.Builder( - Robolectric.application, - new ProviderHelper(Robolectric.application), - null, // new DummyPassphraseCache(null, null), - data, out); + + PgpDecryptVerify.Builder b = builderWithFakePassphraseCache(data, out, null, null, null); b.setPassphrase(mKeyPhrase1); DecryptVerifyResult result = b.build().execute(); 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()); + } // TODO how to test passphrase cache? - /*{ // decryption with passphrase cached should succeed + { // decryption with passphrase cached should succeed ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayInputStream in = new ByteArrayInputStream(ciphertext); InputData data = new InputData(in, in.available()); - PassphraseCacheService.addCachedPassphrase( - Robolectric.application, mStaticRing1.getMasterKeyId(), - mStaticRing1.getMasterKeyId(), mKeyPhrase1, "dummy"); + PgpDecryptVerify.Builder b = builderWithFakePassphraseCache(data, out, + mKeyPhrase1, mStaticRing1.getMasterKeyId(), null); - PgpDecryptVerify.Builder b = new PgpDecryptVerify.Builder( - Robolectric.application, - new ProviderHelper(Robolectric.application), - null, // new DummyPassphraseCache(mKeyPhrase1, null), - data, out); DecryptVerifyResult result = b.build().execute(); 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 + { // 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.Builder b = new PgpDecryptVerify.Builder( - Robolectric.application, - new ProviderHelper(Robolectric.application), - null, // new DummyPassphraseCache(null, null), - data, out); + PgpDecryptVerify.Builder b = builderWithFakePassphraseCache(data, out, + null, mStaticRing1.getMasterKeyId(), null); DecryptVerifyResult result = b.build().execute(); 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", DecryptVerifyResult.RESULT_PENDING_ASYM_PASSPHRASE, result.getResult()); - }*/ + } } - static class DummyPassphraseCache implements PassphraseCacheInterface { + @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(Robolectric.application, + new ProviderHelper(Robolectric.application), null); - String mPassphrase; - Long mExpectedId; - public DummyPassphraseCache(String passphrase, Long expectedId) { - mPassphrase = passphrase; - mExpectedId = expectedId; + InputData data = new InputData(in, in.available()); + + PgpSignEncryptInput b = new PgpSignEncryptInput(); + b.setEncryptionMasterKeyIds(new long[] { + mStaticRing1.getMasterKeyId(), + mStaticRing2.getMasterKeyId() + }); + b.setSymmetricEncryptionAlgorithm(PGPEncryptedData.AES_128); + + PgpSignEncryptResult result = op.execute(b, data, out); + Assert.assertTrue("encryption must succeed", result.success()); + + ciphertext = out.toByteArray(); } - @Override - public String getCachedPassphrase(long masterKeyId, long subKeyId) throws NoSecretKeyException { - if (mExpectedId != null){ - Assert.assertEquals("requested passphrase must be for expected id", - (long) mExpectedId, subKeyId); - } - return mPassphrase; + { // 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.Builder b = builderWithFakePassphraseCache(data, out, + mKeyPhrase1, mStaticRing1.getMasterKeyId(), null); + + DecryptVerifyResult result = b.build().execute(); + 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()); } - @Override - public String getCachedPassphrase(long subKeyId) throws NoSecretKeyException { - if (mExpectedId != null){ - Assert.assertEquals("requested passphrase must be for expected id", - (long) mExpectedId, subKeyId); - } - return mPassphrase; + { // 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()); + + // allow only the second to decrypt + HashSet<Long> allowed = new HashSet<Long>(); + allowed.add(mStaticRing2.getMasterKeyId()); + + // provide passphrase for the second, and check that the first is never asked for! + PgpDecryptVerify.Builder b = builderWithFakePassphraseCache(data, out, + mKeyPhrase2, mStaticRing2.getMasterKeyId(), null); + b.setAllowedKeyIds(allowed); + + DecryptVerifyResult result = b.build().execute(); + 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()); } + + { // decryption with passphrase cached should succeed for the other key if first is gone + + // delete first key from database + new ProviderHelper(Robolectric.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.Builder b = builderWithFakePassphraseCache(data, out, + mKeyPhrase2, mStaticRing2.getMasterKeyId(), null); + + DecryptVerifyResult result = b.build().execute(); + 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(Robolectric.application, + new ProviderHelper(Robolectric.application), null); + + InputData data = new InputData(in, in.available()); + PgpSignEncryptInput b = new PgpSignEncryptInput(); + + b.setEncryptionMasterKeyIds(new long[] { + mStaticRing1.getMasterKeyId(), + mStaticRing2.getMasterKeyId() + }); + b.setSignatureMasterKeyId(mStaticRing1.getMasterKeyId()); + b.setSignatureSubKeyId(KeyringTestingHelper.getSubkeyId(mStaticRing1, 1)); + b.setSignaturePassphrase(mKeyPhrase1); + b.setSymmetricEncryptionAlgorithm(PGPEncryptedData.AES_128); + + PgpSignEncryptResult result = op.execute(b, 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.Builder b = builderWithFakePassphraseCache(data, out, + mKeyPhrase1, mStaticRing1.getMasterKeyId(), null); + + DecryptVerifyResult result = b.build().execute(); + 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(Robolectric.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.Builder b = builderWithFakePassphraseCache(data, out, + mKeyPhrase2, mStaticRing2.getMasterKeyId(), null); + + DecryptVerifyResult result = b.build().execute(); + 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(Robolectric.application, + new ProviderHelper(Robolectric.application), null); + + InputData data = new InputData(in, in.available()); + PgpSignEncryptInput b = new PgpSignEncryptInput(); + + 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, 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.Builder b = builderWithFakePassphraseCache(data, out, null, null, null); + b.setPassphrase(mKeyPhrase1); + DecryptVerifyResult result = b.build().execute(); + 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.Builder builderWithFakePassphraseCache ( + InputData data, OutputStream out, + final String passphrase, final Long checkMasterKeyId, final Long checkSubKeyId) { + + return new PgpDecryptVerify.Builder(Robolectric.application, + new ProviderHelper(Robolectric.application), + null, + data, out) { + public PgpDecryptVerify build() { + return new PgpDecryptVerify(this) { + @Override + public String 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; + } + }; + } + }; } } diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java index abe39b894..dd2feb825 100644 --- a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java @@ -34,15 +34,19 @@ import org.spongycastle.bcpg.S2K; import org.spongycastle.bcpg.SecretKeyPacket; import org.spongycastle.bcpg.SecretSubkeyPacket; import org.spongycastle.bcpg.SignaturePacket; +import org.spongycastle.bcpg.UserAttributePacket; +import org.spongycastle.bcpg.UserAttributeSubpacket; import org.spongycastle.bcpg.UserIDPacket; import org.spongycastle.bcpg.sig.KeyFlags; import org.spongycastle.jce.provider.BouncyCastleProvider; import org.spongycastle.openpgp.PGPSignature; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; -import org.sufficientlysecure.keychain.operations.results.EditKeyResult; +import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; 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.SaveKeyringParcel.SubkeyAdd; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyChange; import org.sufficientlysecure.keychain.support.KeyringBuilder; @@ -91,14 +95,15 @@ public class PgpKeyOperationTest { parcel.mAddUserIds.add("twi"); parcel.mAddUserIds.add("pink"); - parcel.mNewPassphrase = passphrase; + parcel.mNewUnlock = new ChangeUnlockParcel(passphrase); PgpKeyOperation op = new PgpKeyOperation(null); - EditKeyResult result = op.createSecretKeyRing(parcel); + 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()); staticRing = result.getRing(); + staticRing = staticRing.canonicalize(new OperationLog(), 0).getUncachedKeyRing(); // we sleep here for a second, to make sure all new certificates have different timestamps Thread.sleep(1000); @@ -127,7 +132,7 @@ public class PgpKeyOperationTest { parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd( Algorithm.RSA, new Random().nextInt(256)+255, null, KeyFlags.CERTIFY_OTHER, 0L)); parcel.mAddUserIds.add("shy"); - parcel.mNewPassphrase = passphrase; + parcel.mNewUnlock = new ChangeUnlockParcel(passphrase); assertFailure("creating ring with < 512 bytes keysize should fail", parcel, LogType.MSG_CR_ERROR_KEYSIZE_512); @@ -138,7 +143,7 @@ public class PgpKeyOperationTest { parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd( Algorithm.ELGAMAL, 1024, null, KeyFlags.CERTIFY_OTHER, 0L)); parcel.mAddUserIds.add("shy"); - parcel.mNewPassphrase = passphrase; + parcel.mNewUnlock = new ChangeUnlockParcel(passphrase); assertFailure("creating ring with ElGamal master key should fail", parcel, LogType.MSG_CR_ERROR_FLAGS_ELGAMAL); @@ -149,7 +154,7 @@ public class PgpKeyOperationTest { parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd( Algorithm.RSA, 1024, null, KeyFlags.CERTIFY_OTHER, null)); parcel.mAddUserIds.add("lotus"); - parcel.mNewPassphrase = passphrase; + parcel.mNewUnlock = new ChangeUnlockParcel(passphrase); assertFailure("creating master key with null expiry should fail", parcel, LogType.MSG_CR_ERROR_NULL_EXPIRY); @@ -160,7 +165,7 @@ public class PgpKeyOperationTest { parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd( Algorithm.RSA, 1024, null, KeyFlags.SIGN_DATA, 0L)); parcel.mAddUserIds.add("shy"); - parcel.mNewPassphrase = passphrase; + parcel.mNewUnlock = new ChangeUnlockParcel(passphrase); assertFailure("creating ring with non-certifying master key should fail", parcel, LogType.MSG_CR_ERROR_NO_CERTIFY); @@ -170,7 +175,7 @@ public class PgpKeyOperationTest { parcel.reset(); parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd( Algorithm.RSA, 1024, null, KeyFlags.CERTIFY_OTHER, 0L)); - parcel.mNewPassphrase = passphrase; + parcel.mNewUnlock = new ChangeUnlockParcel(passphrase); assertFailure("creating ring without user ids should fail", parcel, LogType.MSG_CR_ERROR_NO_USER_ID); @@ -179,7 +184,7 @@ public class PgpKeyOperationTest { { parcel.reset(); parcel.mAddUserIds.add("shy"); - parcel.mNewPassphrase = passphrase; + parcel.mNewUnlock = new ChangeUnlockParcel(passphrase); assertFailure("creating ring with no master key should fail", parcel, LogType.MSG_CR_ERROR_NO_MASTER); @@ -226,7 +231,7 @@ public class PgpKeyOperationTest { ring.getPublicKey().getCreationTime().after(new Date(new Date().getTime()-1000*120))); Assert.assertNull("key ring should not expire", - ring.getPublicKey().getExpiryTime()); + ring.getPublicKey().getUnsafeExpiryTimeForTesting()); Assert.assertEquals("first (master) key can certify", KeyFlags.CERTIFY_OTHER, (long) subkeys.get(0).getKeyUsage()); @@ -337,9 +342,9 @@ public class PgpKeyOperationTest { Assert.assertNotNull("new key is not null", newKey); Assert.assertNotNull("added key must have an expiry date", - newKey.getExpiryTime()); + newKey.getUnsafeExpiryTimeForTesting()); Assert.assertEquals("added key must have expected expiry date", - expiry, newKey.getExpiryTime().getTime()/1000); + expiry, newKey.getUnsafeExpiryTimeForTesting().getTime()/1000); Assert.assertEquals("added key must have expected flags", flags, (long) newKey.getKeyUsage()); Assert.assertEquals("added key must have expected bitsize", @@ -398,9 +403,9 @@ public class PgpKeyOperationTest { ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID()); Assert.assertNotNull("modified key must have an expiry date", - modified.getPublicKey(keyId).getExpiryTime()); + modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting()); Assert.assertEquals("modified key must have expected expiry date", - expiry, modified.getPublicKey(keyId).getExpiryTime().getTime()/1000); + expiry, modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting().getTime()/1000); Assert.assertEquals("modified key must have same flags as before", ring.getPublicKey(keyId).getKeyUsage(), modified.getPublicKey(keyId).getKeyUsage()); } @@ -412,9 +417,9 @@ public class PgpKeyOperationTest { modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB); Assert.assertNotNull("modified key must have an expiry date", - modified.getPublicKey(keyId).getExpiryTime()); + modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting()); Assert.assertEquals("modified key must have expected expiry date", - expiry, modified.getPublicKey(keyId).getExpiryTime().getTime()/1000); + expiry, modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting().getTime()/1000); Assert.assertEquals("modified key must have same flags as before", ring.getPublicKey(keyId).getKeyUsage(), modified.getPublicKey(keyId).getKeyUsage()); } @@ -438,9 +443,9 @@ public class PgpKeyOperationTest { Assert.assertEquals("modified key must have expected flags", flags, (long) modified.getPublicKey(keyId).getKeyUsage()); Assert.assertNotNull("key must retain its expiry", - modified.getPublicKey(keyId).getExpiryTime()); + modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting()); Assert.assertEquals("key expiry must be unchanged", - expiry, modified.getPublicKey(keyId).getExpiryTime().getTime()/1000); + expiry, modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting().getTime()/1000); } { // expiry of 0 should be "no expiry" @@ -458,7 +463,7 @@ public class PgpKeyOperationTest { Assert.assertEquals("signature must have been created by master key", ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID()); - Assert.assertNull("key must not expire anymore", modified.getPublicKey(keyId).getExpiryTime()); + Assert.assertNull("key must not expire anymore", modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting()); } { // a past expiry should fail @@ -512,9 +517,9 @@ public class PgpKeyOperationTest { PacketTags.SIGNATURE, onlyB.get(1).tag); Assert.assertNotNull("modified key must have an expiry date", - modified.getPublicKey().getExpiryTime()); + modified.getPublicKey().getUnsafeExpiryTimeForTesting()); Assert.assertEquals("modified key must have expected expiry date", - expiry, modified.getPublicKey().getExpiryTime().getTime() / 1000); + expiry, modified.getPublicKey().getUnsafeExpiryTimeForTesting().getTime() / 1000); Assert.assertEquals("modified key must have same flags as before", ring.getPublicKey().getKeyUsage(), modified.getPublicKey().getKeyUsage()); } @@ -526,9 +531,9 @@ public class PgpKeyOperationTest { modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB); Assert.assertNotNull("modified key must have an expiry date", - modified.getPublicKey(keyId).getExpiryTime()); + modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting()); Assert.assertEquals("modified key must have expected expiry date", - expiry, modified.getPublicKey(keyId).getExpiryTime().getTime()/1000); + expiry, modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting().getTime() / 1000); Assert.assertEquals("modified key must have same flags as before", ring.getPublicKey(keyId).getKeyUsage(), modified.getPublicKey(keyId).getKeyUsage()); } @@ -542,17 +547,29 @@ public class PgpKeyOperationTest { Assert.assertEquals("modified key must have expected flags", flags, (long) modified.getPublicKey(keyId).getKeyUsage()); Assert.assertNotNull("key must retain its expiry", - modified.getPublicKey(keyId).getExpiryTime()); + modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting()); Assert.assertEquals("key expiry must be unchanged", - expiry, modified.getPublicKey(keyId).getExpiryTime().getTime()/1000); + expiry, modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting().getTime()/1000); } { // expiry of 0 should be "no expiry" + + // even if there is a non-expiring user id while all others are revoked, it doesn't count! + // for this purpose we revoke one while they still have expiry times + parcel.reset(); + parcel.mRevokeUserIds.add("aloe"); + modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB); + parcel.reset(); parcel.mChangeSubKeys.add(new SubkeyChange(keyId, null, 0L)); modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB); - Assert.assertNull("key must not expire anymore", modified.getPublicKey(keyId).getExpiryTime()); + // for this check, it is relevant that we DON'T use the unsafe one! + Assert.assertNull("key must not expire anymore", + modified.canonicalize(new OperationLog(), 0).getPublicKey().getExpiryTime()); + // make sure the unsafe one behaves incorrectly as expected + Assert.assertNotNull("unsafe expiry must yield wrong result from revoked user id", + modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting()); } { // if we revoke everything, nothing is left to properly sign... @@ -604,7 +621,7 @@ public class PgpKeyOperationTest { ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID()); Assert.assertTrue("subkey must actually be revoked", - modified.getPublicKey().isRevoked()); + modified.getPublicKey().isMaybeRevoked()); } @@ -648,13 +665,14 @@ public class PgpKeyOperationTest { ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID()); Assert.assertTrue("subkey must actually be revoked", - modified.getPublicKey(keyId).isRevoked()); + modified.getPublicKey(keyId).isMaybeRevoked()); } { // re-add second subkey parcel.reset(); - parcel.mChangeSubKeys.add(new SubkeyChange(keyId, null, null)); + // re-certify the revoked subkey + parcel.mChangeSubKeys.add(new SubkeyChange(keyId, true)); modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB); @@ -685,7 +703,7 @@ public class PgpKeyOperationTest { ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID()); Assert.assertFalse("subkey must no longer be revoked", - modified.getPublicKey(keyId).isRevoked()); + modified.getPublicKey(keyId).isMaybeRevoked()); Assert.assertEquals("subkey must have the same usage flags as before", flags, (long) modified.getPublicKey(keyId).getKeyUsage()); @@ -696,7 +714,7 @@ public class PgpKeyOperationTest { public void testSubkeyStrip() throws Exception { long keyId = KeyringTestingHelper.getSubkeyId(ring, 1); - parcel.mStripSubKeys.add(keyId); + parcel.mChangeSubKeys.add(new SubkeyChange(keyId, true, null)); applyModificationWithChecks(parcel, ring, onlyA, onlyB); Assert.assertEquals("one extra packet in original", 1, onlyA.size()); @@ -722,7 +740,7 @@ public class PgpKeyOperationTest { public void testMasterStrip() throws Exception { long keyId = ring.getMasterKeyId(); - parcel.mStripSubKeys.add(keyId); + parcel.mChangeSubKeys.add(new SubkeyChange(keyId, true, null)); applyModificationWithChecks(parcel, ring, onlyA, onlyB); Assert.assertEquals("one extra packet in original", 1, onlyA.size()); @@ -741,6 +759,44 @@ public class PgpKeyOperationTest { Assert.assertEquals("new packet secret key data should have length zero", 0, ((SecretKeyPacket) p).getSecretKeyData().length); Assert.assertNull("new packet should have no iv data", ((SecretKeyPacket) p).getIV()); + } + + @Test + public void testRestrictedStrip() throws Exception { + + long keyId = KeyringTestingHelper.getSubkeyId(ring, 1); + UncachedKeyRing modified; + + { // we should be able to change the stripped/divert status of subkeys without passphrase + parcel.reset(); + parcel.mChangeSubKeys.add(new SubkeyChange(keyId, true, null)); + modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB, null); + Assert.assertEquals("one extra packet in modified", 1, onlyB.size()); + Packet p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket(); + Assert.assertEquals("new packet should have GNU_DUMMY S2K type", + S2K.GNU_DUMMY_S2K, ((SecretKeyPacket) p).getS2K().getType()); + Assert.assertEquals("new packet should have GNU_DUMMY protection mode stripped", + S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY, ((SecretKeyPacket) p).getS2K().getProtectionMode()); + } + + { // and again, changing to divert-to-card + parcel.reset(); + byte[] serial = new byte[] { + 0x6a, 0x6f, 0x6c, 0x6f, 0x73, 0x77, 0x61, 0x67, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + parcel.mChangeSubKeys.add(new SubkeyChange(keyId, false, serial)); + modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB, null); + Assert.assertEquals("one extra packet in modified", 1, onlyB.size()); + Packet p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket(); + Assert.assertEquals("new packet should have GNU_DUMMY S2K type", + S2K.GNU_DUMMY_S2K, ((SecretKeyPacket) p).getS2K().getType()); + Assert.assertEquals("new packet should have GNU_DUMMY protection mode divert-to-card", + S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD, ((SecretKeyPacket) p).getS2K().getProtectionMode()); + Assert.assertArrayEquals("new packet should have correct serial number as iv", + serial, ((SecretKeyPacket) p).getIV()); + + } } @@ -862,6 +918,70 @@ public class PgpKeyOperationTest { } @Test + public void testUserAttributeAdd() throws Exception { + + { + parcel.mAddUserAttribute.add(WrappedUserAttribute.fromData(new byte[0])); + assertModifyFailure("adding an empty user attribute should fail", ring, parcel, + LogType.MSG_MF_UAT_ERROR_EMPTY); + } + + 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); + + UncachedKeyRing modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB); + + Assert.assertEquals("no extra packets in original", 0, onlyA.size()); + Assert.assertEquals("exactly two extra packets in modified", 2, onlyB.size()); + + Assert.assertTrue("keyring must contain added user attribute", + modified.getPublicKey().getUnorderedUserAttributes().contains(uat)); + + Packet p; + + p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket(); + Assert.assertTrue("first new packet must be user attribute", p instanceof UserAttributePacket); + { + UserAttributeSubpacket[] subpackets = ((UserAttributePacket) p).getSubpackets(); + Assert.assertEquals("user attribute packet must contain one subpacket", + 1, subpackets.length); + Assert.assertEquals("user attribute subpacket type must be as specified above", + type, subpackets[0].getType()); + Assert.assertArrayEquals("user attribute subpacket data must be as specified above", + data, subpackets[0].getData()); + } + + p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(1).buf)).readPacket(); + Assert.assertTrue("second new packet must be signature", p instanceof SignaturePacket); + Assert.assertEquals("signature type must be positive certification", + PGPSignature.POSITIVE_CERTIFICATION, ((SignaturePacket) p).getSignatureType()); + + // make sure packets can be distinguished by timestamp + Thread.sleep(1000); + + // applying the same modification AGAIN should not add more certifications but drop those + // as duplicates + modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB, passphrase, true, false); + + Assert.assertEquals("duplicate modification: one extra packet in original", 1, onlyA.size()); + Assert.assertEquals("duplicate modification: one extra packet in modified", 1, onlyB.size()); + + p = new BCPGInputStream(new ByteArrayInputStream(onlyA.get(0).buf)).readPacket(); + Assert.assertTrue("lost packet must be signature", p instanceof SignaturePacket); + p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket(); + Assert.assertTrue("new packet must be signature", p instanceof SignaturePacket); + + } + + + @Test public void testUserIdPrimary() throws Exception { UncachedKeyRing modified = ring; @@ -910,8 +1030,10 @@ public class PgpKeyOperationTest { public void testPassphraseChange() throws Exception { // change passphrase to empty - parcel.mNewPassphrase = ""; - UncachedKeyRing modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB); + parcel.mNewUnlock = new ChangeUnlockParcel(""); + // note that canonicalization here necessarily strips the empty notation packet + UncachedKeyRing modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB, + passphrase); Assert.assertEquals("exactly three packets should have been modified (the secret keys)", 3, onlyB.size()); @@ -923,7 +1045,7 @@ public class PgpKeyOperationTest { // modify keyring, change to non-empty passphrase String otherPassphrase = TestingUtils.genPassphrase(true); - parcel.mNewPassphrase = otherPassphrase; + parcel.mNewUnlock = new ChangeUnlockParcel(otherPassphrase); modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB, ""); Assert.assertEquals("exactly three packets should have been modified (the secret keys)", @@ -948,7 +1070,7 @@ public class PgpKeyOperationTest { PacketTags.SECRET_SUBKEY, sKeyNoPassphrase.tag); String otherPassphrase2 = TestingUtils.genPassphrase(true); - parcel.mNewPassphrase = otherPassphrase2; + parcel.mNewUnlock = new ChangeUnlockParcel(otherPassphrase2); { // if we replace a secret key with one without passphrase modified = KeyringTestingHelper.removePacket(modified, sKeyNoPassphrase.position); @@ -957,7 +1079,7 @@ public class PgpKeyOperationTest { // we should still be able to modify it (and change its passphrase) without errors PgpKeyOperation op = new PgpKeyOperation(null); CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(modified.getEncoded(), false, 0); - EditKeyResult result = op.modifySecretKeyRing(secretRing, parcel, otherPassphrase); + PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, parcel, otherPassphrase); Assert.assertTrue("key modification must succeed", result.success()); Assert.assertFalse("log must not contain a warning", result.getLog().containsWarnings()); @@ -973,7 +1095,7 @@ public class PgpKeyOperationTest { PgpKeyOperation op = new PgpKeyOperation(null); CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(modified.getEncoded(), false, 0); - EditKeyResult result = op.modifySecretKeyRing(secretRing, parcel, otherPassphrase2); + PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, parcel, otherPassphrase2); Assert.assertTrue("key modification must succeed", result.success()); Assert.assertTrue("log must contain a failed passphrase change warning", result.getLog().containsType(LogType.MSG_MF_PASSPHRASE_FAIL)); @@ -981,6 +1103,57 @@ public class PgpKeyOperationTest { } + @Test + public void testUnlockPin() throws Exception { + + String pin = "5235125"; + + // change passphrase to a pin type + parcel.mNewUnlock = new ChangeUnlockParcel(null, pin); + UncachedKeyRing modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB); + + Assert.assertEquals("exactly three packets should have been added (the secret keys + notation packet)", + 3, onlyA.size()); + Assert.assertEquals("exactly four packets should have been added (the secret keys + notation packet)", + 4, onlyB.size()); + + RawPacket dkSig = onlyB.get(1); + Assert.assertEquals("second modified packet should be notation data", + PacketTags.SIGNATURE, dkSig.tag); + + // check that notation data contains pin + CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing( + modified.getEncoded(), false, 0); + Assert.assertEquals("secret key type should be 'pin' after this", + SecretKeyType.PIN, + secretRing.getSecretKey().getSecretKeyType()); + + // need to sleep for a sec, so the timestamp changes for notation data + Thread.sleep(1000); + + { + parcel.mNewUnlock = new ChangeUnlockParcel("phrayse", null); + applyModificationWithChecks(parcel, modified, onlyA, onlyB, pin, true, false); + + Assert.assertEquals("exactly four packets should have been removed (the secret keys + notation packet)", + 4, onlyA.size()); + Assert.assertEquals("exactly three packets should have been added (no more notation packet)", + 3, onlyB.size()); + } + + } + + @Test + public void testRestricted () throws Exception { + + CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0); + + parcel.mAddUserIds.add("discord"); + PgpKeyOperation op = new PgpKeyOperation(null); + PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, parcel, null); + Assert.assertFalse("non-restricted operations should fail without passphrase", result.success()); + } + private static UncachedKeyRing applyModificationWithChecks(SaveKeyringParcel parcel, UncachedKeyRing ring, ArrayList<RawPacket> onlyA, @@ -1011,7 +1184,7 @@ public class PgpKeyOperationTest { CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0); PgpKeyOperation op = new PgpKeyOperation(null); - EditKeyResult result = op.modifySecretKeyRing(secretRing, parcel, passphrase); + PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, parcel, passphrase); Assert.assertTrue("key modification must succeed", result.success()); UncachedKeyRing rawModified = result.getRing(); Assert.assertNotNull("key modification must not return null", rawModified); @@ -1068,7 +1241,7 @@ public class PgpKeyOperationTest { private void assertFailure(String reason, SaveKeyringParcel parcel, LogType expected) { - EditKeyResult result = op.createSecretKeyRing(parcel); + PgpEditKeyResult result = op.createSecretKeyRing(parcel); Assert.assertFalse(reason, result.success()); Assert.assertNull(reason, result.getRing()); @@ -1082,7 +1255,7 @@ public class PgpKeyOperationTest { throws Exception { CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0); - EditKeyResult result = op.modifySecretKeyRing(secretRing, parcel, passphrase); + PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, parcel, passphrase); Assert.assertFalse(reason, result.success()); Assert.assertNull(reason, result.getRing()); @@ -1096,7 +1269,7 @@ public class PgpKeyOperationTest { throws Exception { CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0); - EditKeyResult result = op.modifySecretKeyRing(secretRing, parcel, passphrase); + PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, parcel, passphrase); Assert.assertFalse(reason, result.success()); Assert.assertNull(reason, result.getRing()); @@ -1107,7 +1280,7 @@ public class PgpKeyOperationTest { private UncachedKeyRing assertCreateSuccess(String reason, SaveKeyringParcel parcel) { - EditKeyResult result = op.createSecretKeyRing(parcel); + PgpEditKeyResult result = op.createSecretKeyRing(parcel); Assert.assertTrue(reason, result.success()); Assert.assertNotNull(reason, result.getRing()); diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringCanonicalizeTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringCanonicalizeTest.java index e66026246..f9e0d52c3 100644 --- a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringCanonicalizeTest.java +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringCanonicalizeTest.java @@ -53,17 +53,19 @@ import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder; import org.spongycastle.util.Strings; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.operations.results.EditKeyResult; +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.support.KeyringTestingHelper; import org.sufficientlysecure.keychain.support.KeyringTestingHelper.RawPacket; import java.io.ByteArrayInputStream; import java.security.Security; import java.util.ArrayList; +import java.util.Calendar; import java.util.Date; import java.util.Iterator; @@ -102,17 +104,25 @@ public class UncachedKeyringCanonicalizeTest { 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.mNewPassphrase = ""; + parcel.mNewUnlock = new ChangeUnlockParcel(""); PgpKeyOperation op = new PgpKeyOperation(null); - EditKeyResult result = op.createSecretKeyRing(parcel); + 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 = 9; + totalPackets = 11; // we sleep here for a second, to make sure all new certificates have different timestamps Thread.sleep(1000); @@ -146,8 +156,8 @@ public class UncachedKeyringCanonicalizeTest { Assert.assertEquals("packet #4 should be signature", PacketTags.SIGNATURE, it.next().tag); - Assert.assertEquals("packet #5 should be secret subkey", - PacketTags.SECRET_SUBKEY, 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); @@ -156,7 +166,12 @@ public class UncachedKeyringCanonicalizeTest { Assert.assertEquals("packet #8 should be signature", PacketTags.SIGNATURE, it.next().tag); - Assert.assertFalse("exactly 9 packets total", it.hasNext()); + 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()); @@ -355,18 +370,18 @@ public class UncachedKeyringCanonicalizeTest { @Test public void testSignatureFuture() throws Exception { - // generate future - subHashedPacketsGen.setSignatureCreationTime(false, new Date(new Date().getTime() + 1000 * 1000)); + // 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 { - // generate future - subHashedPacketsGen.setSignatureCreationTime(false, new Date()); + // make key local only subHashedPacketsGen.setExportable(false, false); injectEverytype(secretKey, ring, subHashedPacketsGen); @@ -376,7 +391,7 @@ public class UncachedKeyringCanonicalizeTest { @Test public void testSubkeyDestroy() throws Exception { // signature for second key (first subkey) - UncachedKeyRing modified = KeyringTestingHelper.removePacket(ring, 6); + UncachedKeyRing modified = KeyringTestingHelper.removePacket(ring, 8); // canonicalization should fail, because there are no valid uids left CanonicalizedKeyRing canonicalized = modified.canonicalize(log, 0); @@ -420,7 +435,7 @@ public class UncachedKeyringCanonicalizeTest { secretKey.getPublicKey(), pKey.getPublicKey()); // inject in the right position - UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, sig.getEncoded(), 6); + UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, sig.getEncoded(), 8); // canonicalize, and check if we lose the bad signature CanonicalizedKeyRing canonicalized = modified.canonicalize(log, 0); @@ -445,7 +460,7 @@ public class UncachedKeyringCanonicalizeTest { secretKey.getPublicKey(), pKey.getPublicKey()); // inject in the right position - UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, sig.getEncoded(), 6); + UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, sig.getEncoded(), 8); // canonicalize, and check if we lose the bad signature CanonicalizedKeyRing canonicalized = modified.canonicalize(log, 0); @@ -477,10 +492,10 @@ public class UncachedKeyringCanonicalizeTest { secretKey, PGPSignature.SUBKEY_BINDING, subHashedPacketsGen, secretKey.getPublicKey(), pKey.getPublicKey()); - UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, sig1.getEncoded(), 8); - modified = KeyringTestingHelper.injectPacket(modified, sig2.getEncoded(), 9); - modified = KeyringTestingHelper.injectPacket(modified, sig1.getEncoded(), 10); - modified = KeyringTestingHelper.injectPacket(modified, sig3.getEncoded(), 11); + 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); @@ -508,13 +523,13 @@ public class UncachedKeyringCanonicalizeTest { // get subkey packets Iterator<RawPacket> it = KeyringTestingHelper.parseKeyring(ring.getEncoded()); - RawPacket subKey = KeyringTestingHelper.getNth(it, 5); + RawPacket subKey = KeyringTestingHelper.getNth(it, 7); RawPacket subSig = it.next(); // inject at a second position UncachedKeyRing modified = ring; - modified = KeyringTestingHelper.injectPacket(modified, subKey.buf, 7); - modified = KeyringTestingHelper.injectPacket(modified, subSig.buf, 8); + 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(); @@ -553,7 +568,7 @@ public class UncachedKeyringCanonicalizeTest { sKey = new PGPSecretKey(masterSecretKey.getPrivateKey(), subPubKey, sha1Calc, false, keyEncryptor); } - UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, sKey.getEncoded(), 5); + UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, sKey.getEncoded(), 7); // canonicalize, and check if we lose the bad signature OperationLog log = new OperationLog(); diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java index 3e43cd4b4..ccd47d0ee 100644 --- a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java @@ -33,9 +33,11 @@ import org.spongycastle.bcpg.sig.KeyFlags; import org.spongycastle.jce.provider.BouncyCastleProvider; import org.spongycastle.util.Strings; import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.operations.results.EditKeyResult; +import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel; import org.sufficientlysecure.keychain.support.KeyringTestingHelper; import org.sufficientlysecure.keychain.support.KeyringTestingHelper.RawPacket; import org.sufficientlysecure.keychain.util.ProgressScaler; @@ -44,6 +46,7 @@ import java.io.ByteArrayInputStream; import java.security.Security; import java.util.ArrayList; import java.util.Iterator; +import java.util.Random; /** Tests for the UncachedKeyring.merge method. * @@ -95,14 +98,21 @@ public class UncachedKeyringMergeTest { 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.mNewPassphrase = ""; + parcel.mNewUnlock = new ChangeUnlockParcel(""); PgpKeyOperation op = new PgpKeyOperation(null); OperationResult.OperationLog log = new OperationResult.OperationLog(); - EditKeyResult result = op.createSecretKeyRing(parcel); + PgpEditKeyResult result = op.createSecretKeyRing(parcel); staticRingA = result.getRing(); + staticRingA = staticRingA.canonicalize(new OperationLog(), 0).getUncachedKeyRing(); } { @@ -112,12 +122,13 @@ public class UncachedKeyringMergeTest { parcel.mAddUserIds.add("shy"); // passphrase is tested in PgpKeyOperationTest, just use empty here - parcel.mNewPassphrase = ""; + parcel.mNewUnlock = new ChangeUnlockParcel(""); PgpKeyOperation op = new PgpKeyOperation(null); OperationResult.OperationLog log = new OperationResult.OperationLog(); - EditKeyResult result = op.createSecretKeyRing(parcel); + PgpEditKeyResult result = op.createSecretKeyRing(parcel); staticRingB = result.getRing(); + staticRingB = staticRingB.canonicalize(new OperationLog(), 0).getUncachedKeyRing(); } Assert.assertNotNull("initial test key creation must succeed", staticRingA); @@ -335,6 +346,36 @@ public class UncachedKeyringMergeTest { } } + @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, 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); diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringTest.java index 6ed37bbbb..65395f1ab 100644 --- a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringTest.java +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringTest.java @@ -26,16 +26,18 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.shadows.ShadowLog; import org.spongycastle.bcpg.sig.KeyFlags; +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.operations.results.EditKeyResult; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm; -import org.sufficientlysecure.keychain.support.KeyringTestingHelper.RawPacket; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.util.ArrayList; +import java.io.IOException; import java.util.Iterator; +import java.util.Random; @RunWith(RobolectricTestRunner.class) @org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19 @@ -43,10 +45,6 @@ public class UncachedKeyringTest { static UncachedKeyRing staticRing, staticPubRing; UncachedKeyRing ring, pubRing; - ArrayList<RawPacket> onlyA = new ArrayList<RawPacket>(); - ArrayList<RawPacket> onlyB = new ArrayList<RawPacket>(); - PgpKeyOperation op; - SaveKeyringParcel parcel; @BeforeClass public static void setUpOnce() throws Exception { @@ -62,11 +60,20 @@ public class UncachedKeyringTest { 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.mNewPassphrase = ""; + parcel.mNewUnlock = new ChangeUnlockParcel(""); PgpKeyOperation op = new PgpKeyOperation(null); - EditKeyResult result = op.createSecretKeyRing(parcel); + PgpEditKeyResult result = op.createSecretKeyRing(parcel); staticRing = result.getRing(); staticPubRing = staticRing.extractPublicKeyRing(); @@ -114,7 +121,7 @@ public class UncachedKeyringTest { ring.encodeArmored(out, "OpenKeychain"); pubRing.encodeArmored(out, "OpenKeychain"); - Iterator<UncachedKeyRing> it = + 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", @@ -134,4 +141,16 @@ public class UncachedKeyringTest { 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(); + } + } diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/provider/ProviderHelperSaveTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/provider/ProviderHelperSaveTest.java index e4a1d62ae..171ecc377 100644 --- a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/provider/ProviderHelperSaveTest.java +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/provider/ProviderHelperSaveTest.java @@ -106,8 +106,8 @@ public class ProviderHelperSaveTest { UncachedKeyRing pub = readRingFromResource("/test-keys/mailvelope_07_no_key_flags.asc"); long keyId = pub.getMasterKeyId(); - Assert.assertNull("key flags should be null", - pub.canonicalize(new OperationLog(), 0).getPublicKey().getKeyUsage()); + Assert.assertEquals("key flags should be zero", + 0, (long) pub.canonicalize(new OperationLog(), 0).getPublicKey().getKeyUsage()); mProviderHelper.savePublicKeyRing(pub); @@ -117,7 +117,8 @@ public class ProviderHelperSaveTest { Assert.assertEquals("master key should be encryption key", keyId, pubRing.getEncryptId()); Assert.assertEquals("master key should be encryption key (cached)", keyId, cachedRing.getEncryptId()); - Assert.assertNull("canonicalized key flags should be null", pubRing.getPublicKey().getKeyUsage()); + Assert.assertEquals("canonicalized key flags should be zero", + 0, (long) pubRing.getPublicKey().getKeyUsage()); Assert.assertTrue("master key should be able to certify", pubRing.getPublicKey().canCertify()); Assert.assertTrue("master key should be allowed to sign", pubRing.getPublicKey().canSign()); Assert.assertTrue("master key should be able to encrypt", pubRing.getPublicKey().canEncrypt()); diff --git a/OpenKeychain-Test/src/test/resources/test-keys/broken_cert_version.asc b/OpenKeychain-Test/src/test/resources/test-keys/broken_cert_version.asc new file mode 100644 index 000000000..e2d2abd8e --- /dev/null +++ b/OpenKeychain-Test/src/test/resources/test-keys/broken_cert_version.asc @@ -0,0 +1,17 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFSl5DIBCADqsGJJ8RhV4Uw6a3Q48QyTMrUtvZquOTlLVaqOdEFZNL5/OBal +prft9LNkcOkIVA89Hdn715WwkmG2OJiJoQ/ZAKwal1CPGm4Q8kZIM7k57ISJL6J5 +300e7UIznc74XbG7eFNxNcjCM9wG12vW2rFwc+ogJtkBSf0IXukPwtUkRK+H5ufO +lpqS5NNZfiGbNQCrb+YsGZNRk4QTGR6WGyaIRHlcG8G00VPGNSauTqe/11MO9MoF +BvPgFeur3nefWunCQ+uDmzIEs8r94gaHu3LWbctd5w5x/o/PDfTSSiO+U8zzXrKC +4ZpEl5bk7t7jH1hYMLWyO6nn0vWTOMO1EYLBABEBAAG0GGJyb2tlbiBzaWduYXR1 +cmUgdmVyc2lvbokBOMATAQIAIgUCVKXkMgIbAwYLCQgHAwIGFQgCCQoLBBYCAwEC +HgECF4AACgkQDe00lH/2SnprLggAh64TsdHDfIhTNc1DeJLCuvuHsitAcUdEEnue +yJjodxboKNSplIwnmb5CpM3P8f736dNaW77Yd6aO4IeAy6cBlxT1tSRkJMsp+cBt +kBa3lRr+GnWZlLZs3coL2g0t5RbuyYKyQxm2qvgFJGi/7Qfty5nJOW5U1ElT3VT8 +jISNdQdDAIaBsCE+TuyW3VsP3PqnJ7x14K7VhkFuCyvYB9paLcJBnan93R0Ja0Ip +Cv1pbrNxXp0UELf0RYc2X5C1m6otZ9LKf3PmzxlEkApkb1TZUEBak2Za5p99koZT ++pg/XpZPyawi+gZeYkBAohxRGmzG/a4L+YacAZHbchfN0eG7lg== +=mxTR +-----END PGP PUBLIC KEY BLOCK----- |