Merge remote-tracking branch 'origin/master' into v/multi-decrypt
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'witness'
|
||||
apply plugin: 'jacoco'
|
||||
apply plugin: 'com.github.kt3k.coveralls'
|
||||
|
||||
dependencies {
|
||||
// NOTE: Always use fixed version codes not dynamic ones, e.g. 0.7.3 instead of 0.7.+, see README for more information
|
||||
@@ -10,8 +12,15 @@ dependencies {
|
||||
compile 'com.android.support:appcompat-v7:22.1.1'
|
||||
compile 'com.android.support:recyclerview-v7:22.1.0'
|
||||
compile 'com.android.support:cardview-v7:22.1.0'
|
||||
|
||||
// Unit tests in the local JVM with Robolectric
|
||||
// https://developer.android.com/training/testing/unit-testing/local-unit-tests.html
|
||||
// https://github.com/nenick/AndroidStudioAndRobolectric
|
||||
// http://www.vogella.com/tutorials/Robolectric/article.html
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'org.robolectric:robolectric:3.0-rc3'
|
||||
|
||||
// UI testing libs
|
||||
// UI testing with Espresso
|
||||
androidTestCompile 'com.android.support.test:runner:0.2'
|
||||
androidTestCompile 'com.android.support.test:rules:0.2'
|
||||
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.1'
|
||||
@@ -98,7 +107,9 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 22
|
||||
|
||||
versionCode 32300
|
||||
versionName "3.2.3"
|
||||
applicationId "org.sufficientlysecure.keychain"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -111,6 +122,26 @@ android {
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
|
||||
// Reference them in the java files with e.g. BuildConfig.ACCOUNT_TYPE.
|
||||
buildConfigField "String", "ACCOUNT_TYPE", "\"org.sufficientlysecure.keychain.account\""
|
||||
|
||||
// Reference them in .xml files.
|
||||
resValue "string", "account_type", "org.sufficientlysecure.keychain.account"
|
||||
}
|
||||
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
|
||||
// Reference them in the java files with e.g. BuildConfig.ACCOUNT_TYPE.
|
||||
buildConfigField "String", "ACCOUNT_TYPE", "\"org.sufficientlysecure.keychain.debug.account\""
|
||||
|
||||
// Reference them in .xml files.
|
||||
resValue "string", "account_type", "org.sufficientlysecure.keychain.debug.account"
|
||||
|
||||
// Disabled: only works for androidTest not test!
|
||||
// Enable code coverage (Jacoco)
|
||||
//testCoverageEnabled true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +194,37 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
task jacocoTestReport(type:JacocoReport) {
|
||||
group = "Reporting"
|
||||
description = "Generate Jacoco coverage reports"
|
||||
|
||||
classDirectories = fileTree(
|
||||
dir: "${buildDir}/intermediates/classes/debug",
|
||||
excludes: ['**/R.class',
|
||||
'**/R$*.class',
|
||||
'**/*$ViewInjector*.*',
|
||||
'**/BuildConfig.*',
|
||||
'**/Manifest*.*']
|
||||
)
|
||||
|
||||
sourceDirectories = files("${buildDir.parent}/src/main/java")
|
||||
additionalSourceDirs = files([
|
||||
"${buildDir}/generated/source/buildConfig/debug",
|
||||
"${buildDir}/generated/source/r/debug"
|
||||
])
|
||||
executionData = files("${buildDir}/jacoco/testDebug.exec")
|
||||
|
||||
reports {
|
||||
xml.enabled = true
|
||||
html.enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
// Fix for: No report file available: [/home/travis/build/open-keychain/open-keychain/OpenKeychain/build/reports/cobertura/coverage.xml, /home/travis/build/open-keychain/open-keychain/OpenKeychain/build/reports/jacoco/test/jacocoTestReport.xml]
|
||||
coveralls {
|
||||
jacocoReportPath 'build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml'
|
||||
}
|
||||
|
||||
// NOTE: This disables Lint!
|
||||
tasks.whenTaskAdded { task ->
|
||||
if (task.name.contains('lint')) {
|
||||
|
||||
BIN
OpenKeychain/src/debug/res/drawable-hdpi/ic_launcher.png
Normal file
BIN
OpenKeychain/src/debug/res/drawable-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
BIN
OpenKeychain/src/debug/res/drawable-mdpi/ic_launcher.png
Normal file
BIN
OpenKeychain/src/debug/res/drawable-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
BIN
OpenKeychain/src/debug/res/drawable-xhdpi/ic_launcher.png
Normal file
BIN
OpenKeychain/src/debug/res/drawable-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
BIN
OpenKeychain/src/debug/res/drawable-xxhdpi/ic_launcher.png
Normal file
BIN
OpenKeychain/src/debug/res/drawable-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
OpenKeychain/src/debug/res/drawable-xxxhdpi/ic_launcher.png
Normal file
BIN
OpenKeychain/src/debug/res/drawable-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -2,9 +2,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.sufficientlysecure.keychain"
|
||||
android:installLocation="auto"
|
||||
android:versionCode="32300"
|
||||
android:versionName="3.2.3">
|
||||
android:installLocation="auto">
|
||||
|
||||
<!--
|
||||
General remarks
|
||||
@@ -50,9 +48,9 @@
|
||||
android:name="android.hardware.screen.portrait"
|
||||
android:required="false" />
|
||||
|
||||
<permission android:name="org.sufficientlysecure.keychain.WRITE_TEMPORARY_STORAGE" />
|
||||
<permission android:name="${applicationId}.WRITE_TEMPORARY_STORAGE" />
|
||||
|
||||
<uses-permission android:name="org.sufficientlysecure.keychain.WRITE_TEMPORARY_STORAGE" />
|
||||
<uses-permission android:name="${applicationId}.WRITE_TEMPORARY_STORAGE" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
@@ -724,13 +722,10 @@
|
||||
<service
|
||||
android:name=".service.KeychainService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".service.CloudImportService"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name=".provider.KeychainProvider"
|
||||
android:authorities="org.sufficientlysecure.keychain.provider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Internal classes of the remote APIs (not exported) -->
|
||||
@@ -808,9 +803,9 @@
|
||||
<!-- Storage Provider for temporary decrypted files -->
|
||||
<provider
|
||||
android:name=".provider.TemporaryStorageProvider"
|
||||
android:authorities="org.sufficientlysecure.keychain.tempstorage"
|
||||
android:authorities="${applicationId}.tempstorage"
|
||||
android:exported="true"
|
||||
android:writePermission="org.sufficientlysecure.keychain.WRITE_TEMPORARY_STORAGE" />
|
||||
android:writePermission="${applicationId}.WRITE_TEMPORARY_STORAGE" />
|
||||
|
||||
</application>
|
||||
|
||||
|
||||
@@ -31,14 +31,17 @@ public final class Constants {
|
||||
public static final boolean DEBUG_LOG_DB_QUERIES = false;
|
||||
public static final boolean DEBUG_SYNC_REMOVE_CONTACTS = false;
|
||||
|
||||
public static final String TAG = "Keychain";
|
||||
public static final String TAG = DEBUG ? "Keychain D" : "Keychain";
|
||||
|
||||
public static final String PACKAGE_NAME = "org.sufficientlysecure.keychain";
|
||||
|
||||
public static final String ACCOUNT_NAME = "OpenKeychain";
|
||||
public static final String ACCOUNT_TYPE = PACKAGE_NAME + ".account";
|
||||
public static final String ACCOUNT_NAME = DEBUG ? "OpenKeychain D" : "OpenKeychain";
|
||||
public static final String ACCOUNT_TYPE = BuildConfig.ACCOUNT_TYPE;
|
||||
public static final String CUSTOM_CONTACT_DATA_MIME_TYPE = "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.key";
|
||||
|
||||
public static final String PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".provider";
|
||||
public static final String TEMPSTORAGE_AUTHORITY = BuildConfig.APPLICATION_ID + ".tempstorage";
|
||||
|
||||
// as defined in http://tools.ietf.org/html/rfc3156, section 7
|
||||
public static final String NFC_MIME = "application/pgp-keys";
|
||||
|
||||
|
||||
@@ -128,8 +128,6 @@ public class KeychainApplication extends Application {
|
||||
|
||||
/**
|
||||
* Add OpenKeychain account to Android to link contacts with keys
|
||||
*
|
||||
* @param context
|
||||
*/
|
||||
public static void setupAccountAsNeeded(Context context) {
|
||||
try {
|
||||
@@ -165,7 +163,7 @@ public class KeychainApplication extends Application {
|
||||
int edgeDrawableId = context.getResources().getIdentifier("overscroll_edge", "drawable", "android");
|
||||
Drawable androidEdge = context.getResources().getDrawable(edgeDrawableId);
|
||||
androidEdge.setColorFilter(brandColor, PorterDuff.Mode.SRC_IN);
|
||||
} catch (Resources.NotFoundException e) {
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ public class KeychainContract {
|
||||
String PACKAGE_NAME = "package_name"; // foreign key to api_apps.package_name
|
||||
}
|
||||
|
||||
public static final String CONTENT_AUTHORITY = Constants.PACKAGE_NAME + ".provider";
|
||||
public static final String CONTENT_AUTHORITY = Constants.PROVIDER_AUTHORITY;
|
||||
|
||||
private static final Uri BASE_CONTENT_URI_INTERNAL = Uri
|
||||
.parse("content://" + CONTENT_AUTHORITY);
|
||||
|
||||
@@ -45,7 +45,8 @@ public class TemporaryStorageProvider extends ContentProvider {
|
||||
private static final String COLUMN_ID = "id";
|
||||
private static final String COLUMN_NAME = "name";
|
||||
private static final String COLUMN_TIME = "time";
|
||||
private static final Uri BASE_URI = Uri.parse("content://org.sufficientlysecure.keychain.tempstorage/");
|
||||
public static final String CONTENT_AUTHORITY = Constants.TEMPSTORAGE_AUTHORITY;
|
||||
private static final Uri BASE_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
|
||||
private static final int DB_VERSION = 2;
|
||||
|
||||
private static File cacheDir;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accountType="org.sufficientlysecure.keychain.account"
|
||||
android:accountType="@string/account_type"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:contentAuthority="com.android.contacts"
|
||||
android:accountType="org.sufficientlysecure.keychain.account"
|
||||
android:accountType="@string/account_type"
|
||||
android:supportsUploading="false"
|
||||
android:userVisible="true"
|
||||
android:allowParallelSyncs="false"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.sufficientlysecure.keychain;
|
||||
|
||||
/**
|
||||
* Temporary workaround for https://github.com/robolectric/robolectric/issues/1747
|
||||
*/
|
||||
public final class WorkaroundBuildConfig {
|
||||
public static final boolean DEBUG = BuildConfig.DEBUG;
|
||||
// Workaround: Use real packageName not applicationId
|
||||
public static final String APPLICATION_ID = "org.sufficientlysecure.keychain";
|
||||
public static final String BUILD_TYPE = BuildConfig.BUILD_TYPE;
|
||||
public static final String FLAVOR = BuildConfig.FLAVOR;
|
||||
public static final int VERSION_CODE = BuildConfig.VERSION_CODE;
|
||||
public static final String VERSION_NAME = BuildConfig.VERSION_NAME;
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* 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.RobolectricGradleTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadows.ShadowLog;
|
||||
import org.spongycastle.bcpg.sig.KeyFlags;
|
||||
import org.spongycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.sufficientlysecure.keychain.BuildConfig;
|
||||
import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
|
||||
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.pgp.CanonicalizedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
|
||||
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.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.util.Passphrase;
|
||||
import org.sufficientlysecure.keychain.util.ProgressScaler;
|
||||
import org.sufficientlysecure.keychain.util.TestingUtils;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.security.Security;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Random;
|
||||
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
|
||||
public class CertifyOperationTest {
|
||||
|
||||
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;
|
||||
|
||||
Random random = new Random();
|
||||
|
||||
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");
|
||||
byte[] uatdata = new byte[random.nextInt(150)+10];
|
||||
random.nextBytes(uatdata);
|
||||
parcel.mAddUserAttribute.add(
|
||||
WrappedUserAttribute.fromSubpacket(random.nextInt(100)+1, uatdata));
|
||||
|
||||
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() throws Exception {
|
||||
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.savePublicKeyRing(mStaticRing2.extractPublicKeyRing(), new ProgressScaler());
|
||||
|
||||
// ok NOW log verbosely!
|
||||
ShadowLog.stream = System.out;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelfCertifyFlag() throws Exception {
|
||||
|
||||
CanonicalizedPublicKeyRing ring = new ProviderHelper(RuntimeEnvironment.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 testCertifyId() throws Exception {
|
||||
CertifyOperation op = new CertifyOperation(RuntimeEnvironment.application,
|
||||
new ProviderHelper(RuntimeEnvironment.application), null, null);
|
||||
|
||||
{
|
||||
CanonicalizedPublicKeyRing ring = new ProviderHelper(RuntimeEnvironment.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(),
|
||||
mStaticRing2.getPublicKey().getUnorderedUserIds()));
|
||||
CertifyResult result = op.execute(actions, new CryptoInputParcel(mKeyPhrase1));
|
||||
|
||||
Assert.assertTrue("certification must succeed", result.success());
|
||||
|
||||
{
|
||||
CanonicalizedPublicKeyRing ring = new ProviderHelper(RuntimeEnvironment.application)
|
||||
.getCanonicalizedPublicKeyRing(mStaticRing2.getMasterKeyId());
|
||||
Assert.assertEquals("new key must be verified now",
|
||||
Certs.VERIFIED_SECRET, ring.getVerified());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCertifyAttribute() throws Exception {
|
||||
CertifyOperation op = new CertifyOperation(RuntimeEnvironment.application,
|
||||
new ProviderHelper(RuntimeEnvironment.application), null, null);
|
||||
|
||||
{
|
||||
CanonicalizedPublicKeyRing ring = new ProviderHelper(RuntimeEnvironment.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(), null,
|
||||
mStaticRing2.getPublicKey().getUnorderedUserAttributes()));
|
||||
CertifyResult result = op.execute(actions, new CryptoInputParcel(mKeyPhrase1));
|
||||
|
||||
Assert.assertTrue("certification must succeed", result.success());
|
||||
|
||||
{
|
||||
CanonicalizedPublicKeyRing ring = new ProviderHelper(RuntimeEnvironment.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 = new CertifyOperation(RuntimeEnvironment.application,
|
||||
new ProviderHelper(RuntimeEnvironment.application), null, null);
|
||||
|
||||
CertifyActionsParcel actions = new CertifyActionsParcel(mStaticRing1.getMasterKeyId());
|
||||
actions.add(new CertifyAction(mStaticRing1.getMasterKeyId(),
|
||||
mStaticRing2.getPublicKey().getUnorderedUserIds()));
|
||||
|
||||
CertifyResult result = op.execute(actions, new CryptoInputParcel(mKeyPhrase1));
|
||||
|
||||
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 = new CertifyOperation(RuntimeEnvironment.application,
|
||||
new ProviderHelper(RuntimeEnvironment.application), null, null);
|
||||
|
||||
{
|
||||
CertifyActionsParcel actions = new CertifyActionsParcel(mStaticRing1.getMasterKeyId());
|
||||
ArrayList<String> uids = new ArrayList<String>();
|
||||
uids.add("nonexistent");
|
||||
actions.add(new CertifyAction(1234L, uids));
|
||||
|
||||
CertifyResult result = op.execute(actions, new CryptoInputParcel(mKeyPhrase1));
|
||||
|
||||
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(),
|
||||
mStaticRing2.getPublicKey().getUnorderedUserIds()));
|
||||
|
||||
CertifyResult result = op.execute(actions, new CryptoInputParcel(mKeyPhrase1));
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
* 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.RobolectricGradleTestRunner;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadows.ShadowLog;
|
||||
import org.spongycastle.bcpg.sig.KeyFlags;
|
||||
import org.spongycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.sufficientlysecure.keychain.BuildConfig;
|
||||
import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
|
||||
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.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.Iterator;
|
||||
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
|
||||
public class ExportTest {
|
||||
|
||||
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("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, new Passphrase("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(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 testExportAll() throws Exception {
|
||||
ImportExportOperation op = new ImportExportOperation(RuntimeEnvironment.application,
|
||||
new ProviderHelper(RuntimeEnvironment.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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* 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.RobolectricGradleTestRunner;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
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.util.encoders.Hex;
|
||||
import org.sufficientlysecure.keychain.BuildConfig;
|
||||
import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
|
||||
import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.PromoteKeyResult;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
|
||||
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.support.KeyringTestingHelper;
|
||||
import org.sufficientlysecure.keychain.util.Passphrase;
|
||||
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(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
|
||||
public class PromoteKeyOperationTest {
|
||||
|
||||
static UncachedKeyRing mStaticRing;
|
||||
static Passphrase 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(RuntimeEnvironment.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(RuntimeEnvironment.application,
|
||||
new ProviderHelper(RuntimeEnvironment.application), null, null);
|
||||
|
||||
PromoteKeyResult result = op.execute(mStaticRing.getMasterKeyId(), null, null);
|
||||
|
||||
Assert.assertTrue("promotion must succeed", result.success());
|
||||
|
||||
{
|
||||
CachedPublicKeyRing ring = new ProviderHelper(RuntimeEnvironment.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 gnu dummy",
|
||||
SecretKeyType.GNU_DUMMY, ring.getSecretKeyType(keyId));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPromoteDivert() throws Exception {
|
||||
PromoteKeyOperation op = new PromoteKeyOperation(RuntimeEnvironment.application,
|
||||
new ProviderHelper(RuntimeEnvironment.application), null, null);
|
||||
|
||||
byte[] aid = Hex.decode("D2760001240102000000012345670000");
|
||||
|
||||
PromoteKeyResult result = op.execute(mStaticRing.getMasterKeyId(), aid, null);
|
||||
|
||||
Assert.assertTrue("promotion must succeed", result.success());
|
||||
|
||||
{
|
||||
CanonicalizedSecretKeyRing ring = new ProviderHelper(RuntimeEnvironment.application)
|
||||
.getCanonicalizedSecretKeyRing(mStaticRing.getMasterKeyId());
|
||||
|
||||
for (CanonicalizedSecretKey key : ring.secretKeyIterator()) {
|
||||
Assert.assertEquals("all subkeys must be divert-to-card",
|
||||
SecretKeyType.DIVERT_TO_CARD, key.getSecretKeyType());
|
||||
Assert.assertArrayEquals("all subkeys must have correct iv",
|
||||
aid, key.getIv());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPromoteDivertSpecific() throws Exception {
|
||||
PromoteKeyOperation op = new PromoteKeyOperation(RuntimeEnvironment.application,
|
||||
new ProviderHelper(RuntimeEnvironment.application), null, null);
|
||||
|
||||
byte[] aid = Hex.decode("D2760001240102000000012345670000");
|
||||
|
||||
// only promote the first, rest stays dummy
|
||||
long keyId = KeyringTestingHelper.getSubkeyId(mStaticRing, 1);
|
||||
|
||||
PromoteKeyResult result = op.execute(mStaticRing.getMasterKeyId(), aid, new long[] {
|
||||
keyId
|
||||
});
|
||||
|
||||
Assert.assertTrue("promotion must succeed", result.success());
|
||||
|
||||
{
|
||||
CanonicalizedSecretKeyRing ring = new ProviderHelper(RuntimeEnvironment.application)
|
||||
.getCanonicalizedSecretKeyRing(mStaticRing.getMasterKeyId());
|
||||
|
||||
for (CanonicalizedSecretKey key : ring.secretKeyIterator()) {
|
||||
if (key.getKeyId() == keyId) {
|
||||
Assert.assertEquals("subkey must be divert-to-card",
|
||||
SecretKeyType.DIVERT_TO_CARD, key.getSecretKeyType());
|
||||
Assert.assertArrayEquals("subkey must have correct iv",
|
||||
aid, key.getIv());
|
||||
} else {
|
||||
Assert.assertEquals("some subkeys must be gnu dummy",
|
||||
SecretKeyType.GNU_DUMMY, key.getSecretKeyType());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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;
|
||||
import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
|
||||
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = WorkaroundBuildConfig.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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
/*
|
||||
* 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.WorkaroundBuildConfig;
|
||||
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 = WorkaroundBuildConfig.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());
|
||||
|
||||
CryptoInputParcel cryptoInput = result.getCachedCryptoInputParcel();
|
||||
Assert.assertEquals("cached session keys must be empty",
|
||||
0, cryptoInput.getCryptoData().size());
|
||||
|
||||
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());
|
||||
|
||||
CryptoInputParcel cryptoInput = result.getCachedCryptoInputParcel();
|
||||
Assert.assertEquals("must have one cached session key",
|
||||
1, cryptoInput.getCryptoData().size());
|
||||
|
||||
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);
|
||||
|
||||
CryptoInputParcel cryptoInput = result.getCachedCryptoInputParcel();
|
||||
Assert.assertEquals("must have one cached session key",
|
||||
1, cryptoInput.getCryptoData().size());
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,768 @@
|
||||
/*
|
||||
* 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.WorkaroundBuildConfig;
|
||||
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 = WorkaroundBuildConfig.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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
/*
|
||||
* 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.WorkaroundBuildConfig;
|
||||
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 = WorkaroundBuildConfig.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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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.WorkaroundBuildConfig;
|
||||
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 = WorkaroundBuildConfig.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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright (C) Art O Cathain
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.*;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.sufficientlysecure.keychain.BuildConfig;
|
||||
import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
|
||||
import org.sufficientlysecure.keychain.support.KeyringTestingHelper;
|
||||
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
|
||||
public class ProviderHelperKeyringTest {
|
||||
|
||||
@Test
|
||||
public void testSavePublicKeyring() throws Exception {
|
||||
Assert.assertTrue(new KeyringTestingHelper(RuntimeEnvironment.application).addKeyring(Collections.singleton(
|
||||
"/public-key-for-sample.blob"
|
||||
)));
|
||||
}
|
||||
|
||||
// @Test
|
||||
public void testSavePublicKeyringRsa() throws Exception {
|
||||
Assert.assertTrue(new KeyringTestingHelper(RuntimeEnvironment.application).addKeyring(prependResourcePath(Arrays.asList(
|
||||
"000001-006.public_key",
|
||||
"000002-013.user_id",
|
||||
"000003-002.sig",
|
||||
"000004-012.ring_trust",
|
||||
"000005-002.sig",
|
||||
"000006-012.ring_trust",
|
||||
"000007-002.sig",
|
||||
"000008-012.ring_trust",
|
||||
"000009-002.sig",
|
||||
"000010-012.ring_trust",
|
||||
"000011-002.sig",
|
||||
"000012-012.ring_trust",
|
||||
"000013-014.public_subkey",
|
||||
"000014-002.sig",
|
||||
"000015-012.ring_trust"
|
||||
))));
|
||||
}
|
||||
|
||||
// @Test
|
||||
public void testSavePublicKeyringDsa() throws Exception {
|
||||
Assert.assertTrue(new KeyringTestingHelper(RuntimeEnvironment.application).addKeyring(prependResourcePath(Arrays.asList(
|
||||
"000016-006.public_key",
|
||||
"000017-002.sig",
|
||||
"000018-012.ring_trust",
|
||||
"000019-013.user_id",
|
||||
"000020-002.sig",
|
||||
"000021-012.ring_trust",
|
||||
"000022-002.sig",
|
||||
"000023-012.ring_trust",
|
||||
"000024-014.public_subkey",
|
||||
"000025-002.sig",
|
||||
"000026-012.ring_trust"
|
||||
))));
|
||||
}
|
||||
|
||||
// @Test
|
||||
public void testSavePublicKeyringDsa2() throws Exception {
|
||||
Assert.assertTrue(new KeyringTestingHelper(RuntimeEnvironment.application).addKeyring(prependResourcePath(Arrays.asList(
|
||||
"000027-006.public_key",
|
||||
"000028-002.sig",
|
||||
"000029-012.ring_trust",
|
||||
"000030-013.user_id",
|
||||
"000031-002.sig",
|
||||
"000032-012.ring_trust",
|
||||
"000033-002.sig",
|
||||
"000034-012.ring_trust"
|
||||
))));
|
||||
}
|
||||
|
||||
private static Collection<String> prependResourcePath(Collection<String> files) {
|
||||
Collection<String> prependedFiles = new ArrayList<String>();
|
||||
for (String file: files) {
|
||||
prependedFiles.add("/OpenPGP-Haskell/tests/data/" + file);
|
||||
}
|
||||
return prependedFiles;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* 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.provider;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricGradleTestRunner;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadows.ShadowLog;
|
||||
import org.spongycastle.bcpg.sig.KeyFlags;
|
||||
import org.spongycastle.util.encoders.Hex;
|
||||
import org.sufficientlysecure.keychain.BuildConfig;
|
||||
import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
|
||||
import org.sufficientlysecure.keychain.util.IterableIterator;
|
||||
import org.sufficientlysecure.keychain.util.ProgressScaler;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
|
||||
public class ProviderHelperSaveTest {
|
||||
|
||||
ProviderHelper mProviderHelper = new ProviderHelper(RuntimeEnvironment.application);
|
||||
|
||||
@BeforeClass
|
||||
public static void setUpOnce() throws Exception {
|
||||
ShadowLog.stream = System.out;
|
||||
}
|
||||
|
||||
@Test public void testImportCooperPair() throws Exception {
|
||||
|
||||
// insert two keys with same long key id, make sure the second one gets rejected either way!
|
||||
UncachedKeyRing first =
|
||||
readRingFromResource("/test-keys/cooperpair/9E669861368BCA0BE42DAF7DDDA252EBB8EBE1AF.asc");
|
||||
UncachedKeyRing second =
|
||||
readRingFromResource("/test-keys/cooperpair/A55120427374F3F7AA5F1166DDA252EBB8EBE1AF.asc");
|
||||
|
||||
SaveKeyringResult result;
|
||||
|
||||
// insert both keys, second should fail
|
||||
result = new ProviderHelper(RuntimeEnvironment.application).savePublicKeyRing(first);
|
||||
Assert.assertTrue("first keyring import should succeed", result.success());
|
||||
result = new ProviderHelper(RuntimeEnvironment.application).savePublicKeyRing(second);
|
||||
Assert.assertFalse("second keyring import should fail", result.success());
|
||||
|
||||
new KeychainDatabase(RuntimeEnvironment.application).clearDatabase();
|
||||
|
||||
// and the other way around
|
||||
result = new ProviderHelper(RuntimeEnvironment.application).savePublicKeyRing(second);
|
||||
Assert.assertTrue("first keyring import should succeed", result.success());
|
||||
result = new ProviderHelper(RuntimeEnvironment.application).savePublicKeyRing(first);
|
||||
Assert.assertFalse("second keyring import should fail", result.success());
|
||||
|
||||
}
|
||||
|
||||
@Test public void testImportSymantec() throws Exception {
|
||||
|
||||
// symantec pgp desktop exports secret keys without self certificates. we don't support
|
||||
// those on their own, but if they are imported together with their public key (or if
|
||||
// the public key is already known), the self certs info will be merged in as a special
|
||||
// case.
|
||||
UncachedKeyRing seckey =
|
||||
readRingFromResource("/test-keys/symantec_secret.asc");
|
||||
UncachedKeyRing pubkey =
|
||||
readRingFromResource("/test-keys/symantec_public.asc");
|
||||
|
||||
SaveKeyringResult result;
|
||||
|
||||
// insert secret, this should fail because of missing self-cert
|
||||
result = new ProviderHelper(RuntimeEnvironment.application).saveSecretKeyRing(seckey, new ProgressScaler());
|
||||
Assert.assertFalse("secret keyring import before pubring import should fail", result.success());
|
||||
|
||||
// insert pubkey, then seckey - both should succeed
|
||||
result = new ProviderHelper(RuntimeEnvironment.application).savePublicKeyRing(pubkey);
|
||||
Assert.assertTrue("public keyring import should succeed", result.success());
|
||||
result = new ProviderHelper(RuntimeEnvironment.application).saveSecretKeyRing(seckey, new ProgressScaler());
|
||||
Assert.assertTrue("secret keyring import after pubring import should succeed", result.success());
|
||||
|
||||
}
|
||||
|
||||
@Test public void testImportNoFlagKey() throws Exception {
|
||||
|
||||
UncachedKeyRing pub = readRingFromResource("/test-keys/mailvelope_07_no_key_flags.asc");
|
||||
long keyId = pub.getMasterKeyId();
|
||||
Assert.assertEquals("key flags should be zero",
|
||||
0, (long) pub.canonicalize(new OperationLog(), 0).getPublicKey().getKeyUsage());
|
||||
|
||||
mProviderHelper.savePublicKeyRing(pub);
|
||||
|
||||
CachedPublicKeyRing cachedRing = mProviderHelper.getCachedPublicKeyRing(keyId);
|
||||
CanonicalizedPublicKeyRing pubRing = mProviderHelper.getCanonicalizedPublicKeyRing(keyId);
|
||||
|
||||
Assert.assertEquals("master key should be encryption key", keyId, pubRing.getEncryptId());
|
||||
Assert.assertEquals("master key should be encryption key (cached)", keyId, cachedRing.getEncryptId());
|
||||
|
||||
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());
|
||||
|
||||
}
|
||||
|
||||
@Test public void testImportDivertToCard() throws Exception {
|
||||
|
||||
UncachedKeyRing sec = readRingFromResource("/test-keys/divert_to_card_sec.asc");
|
||||
long keyId = sec.getMasterKeyId();
|
||||
|
||||
SaveKeyringResult result;
|
||||
|
||||
result = mProviderHelper.saveSecretKeyRing(sec, new ProgressScaler());
|
||||
Assert.assertTrue("import of secret keyring should succeed", result.success());
|
||||
|
||||
// make sure both the CanonicalizedSecretKeyRing as well as the CachedPublicKeyRing correctly
|
||||
// indicate the secret key type
|
||||
CachedPublicKeyRing cachedRing = mProviderHelper.getCachedPublicKeyRing(keyId);
|
||||
CanonicalizedSecretKeyRing secRing = mProviderHelper.getCanonicalizedSecretKeyRing(keyId);
|
||||
|
||||
Iterator<CanonicalizedSecretKey> it = secRing.secretKeyIterator().iterator();
|
||||
|
||||
{ // first subkey
|
||||
Assert.assertTrue("keyring should have 3 subkeys (1)", it.hasNext());
|
||||
CanonicalizedSecretKey key = it.next();
|
||||
Assert.assertEquals("first subkey should be of type sign+certify",
|
||||
KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA, (int) key.getKeyUsage());
|
||||
Assert.assertEquals("first subkey should be divert-to-card",
|
||||
SecretKeyType.DIVERT_TO_CARD, key.getSecretKeyType());
|
||||
Assert.assertTrue("canCertify() should be true", key.canCertify());
|
||||
Assert.assertTrue("canSign() should be true", key.canSign());
|
||||
|
||||
// cached
|
||||
Assert.assertEquals("all subkeys from CachedPublicKeyRing should be divert-to-key",
|
||||
SecretKeyType.DIVERT_TO_CARD, cachedRing.getSecretKeyType(key.getKeyId()));
|
||||
}
|
||||
|
||||
{ // second subkey
|
||||
Assert.assertTrue("keyring should have 3 subkeys (2)", it.hasNext());
|
||||
CanonicalizedSecretKey key = it.next();
|
||||
Assert.assertEquals("second subkey should be of type authenticate",
|
||||
KeyFlags.AUTHENTICATION, (int) key.getKeyUsage());
|
||||
Assert.assertEquals("second subkey should be divert-to-card",
|
||||
SecretKeyType.DIVERT_TO_CARD, key.getSecretKeyType());
|
||||
Assert.assertTrue("canAuthenticate() should be true", key.canAuthenticate());
|
||||
|
||||
// cached
|
||||
Assert.assertEquals("all subkeys from CachedPublicKeyRing should be divert-to-key",
|
||||
SecretKeyType.DIVERT_TO_CARD, cachedRing.getSecretKeyType(key.getKeyId()));
|
||||
}
|
||||
|
||||
{ // third subkey
|
||||
Assert.assertTrue("keyring should have 3 subkeys (3)", it.hasNext());
|
||||
CanonicalizedSecretKey key = it.next();
|
||||
Assert.assertEquals("first subkey should be of type encrypt (both types)",
|
||||
KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, (int) key.getKeyUsage());
|
||||
Assert.assertEquals("third subkey should be divert-to-card",
|
||||
SecretKeyType.DIVERT_TO_CARD, key.getSecretKeyType());
|
||||
Assert.assertTrue("canEncrypt() should be true", key.canEncrypt());
|
||||
|
||||
// cached
|
||||
Assert.assertEquals("all subkeys from CachedPublicKeyRing should be divert-to-key",
|
||||
SecretKeyType.DIVERT_TO_CARD, cachedRing.getSecretKeyType(key.getKeyId()));
|
||||
}
|
||||
|
||||
Assert.assertFalse("keyring should have 3 subkeys (4)", it.hasNext());
|
||||
|
||||
}
|
||||
|
||||
@Test public void testImportBadEncodedUserId() throws Exception {
|
||||
|
||||
UncachedKeyRing key = readRingFromResource("/test-keys/bad_user_id_encoding.asc");
|
||||
long keyId = key.getMasterKeyId();
|
||||
|
||||
SaveKeyringResult result;
|
||||
|
||||
result = mProviderHelper.savePublicKeyRing(key, new ProgressScaler());
|
||||
Assert.assertTrue("import of keyring should succeed", result.success());
|
||||
|
||||
CanonicalizedPublicKeyRing ring = mProviderHelper.getCanonicalizedPublicKeyRing(keyId);
|
||||
boolean found = false;
|
||||
byte[] badUserId = Hex.decode("436c61757320467261656e6b656c203c436c6175732e4672e46e6b656c4068616c696661782e727774682d61616368656e2e64653e");
|
||||
for (byte[] rawUserId : new IterableIterator<byte[]>(
|
||||
ring.getUnorderedRawUserIds().iterator())) {
|
||||
if (Arrays.equals(rawUserId, badUserId)) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.assertTrue("import of the badly encoded user id should succeed", found);
|
||||
}
|
||||
|
||||
@Test
|
||||
/** Tests a master key which may sign, but is stripped. In this case, if there is a different
|
||||
* subkey available which can sign, that one should be selected.
|
||||
*/
|
||||
public void testImportStrippedFlags() throws Exception {
|
||||
|
||||
UncachedKeyRing key = readRingFromResource("/test-keys/stripped_flags.asc");
|
||||
long masterKeyId = key.getMasterKeyId();
|
||||
|
||||
SaveKeyringResult result;
|
||||
|
||||
result = mProviderHelper.saveSecretKeyRing(key, new ProgressScaler());
|
||||
Assert.assertTrue("import of keyring should succeed", result.success());
|
||||
|
||||
long signId;
|
||||
{
|
||||
CanonicalizedSecretKeyRing ring = mProviderHelper.getCanonicalizedSecretKeyRing(masterKeyId);
|
||||
Assert.assertTrue("master key should have sign flag", ring.getPublicKey().canSign());
|
||||
Assert.assertTrue("master key should have encrypt flag", ring.getPublicKey().canEncrypt());
|
||||
|
||||
signId = ring.getSecretSignId();
|
||||
Assert.assertNotEquals("encrypt id should not be 0", 0, signId);
|
||||
Assert.assertNotEquals("encrypt key should be different from master key", masterKeyId, signId);
|
||||
}
|
||||
|
||||
{
|
||||
CachedPublicKeyRing ring = mProviderHelper.getCachedPublicKeyRing(masterKeyId);
|
||||
Assert.assertEquals("signing key should be same id cached as uncached", signId, ring.getSecretSignId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UncachedKeyRing readRingFromResource(String name) throws Exception {
|
||||
return UncachedKeyRing.fromStream(ProviderHelperSaveTest.class.getResourceAsStream(name)).next();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Copyright (C) Art O Cathain
|
||||
*
|
||||
* 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.support;
|
||||
|
||||
import org.spongycastle.bcpg.CompressionAlgorithmTags;
|
||||
import org.spongycastle.bcpg.ContainedPacket;
|
||||
import org.spongycastle.bcpg.HashAlgorithmTags;
|
||||
import org.spongycastle.bcpg.MPInteger;
|
||||
import org.spongycastle.bcpg.PublicKeyAlgorithmTags;
|
||||
import org.spongycastle.bcpg.PublicKeyPacket;
|
||||
import org.spongycastle.bcpg.PublicSubkeyPacket;
|
||||
import org.spongycastle.bcpg.RSAPublicBCPGKey;
|
||||
import org.spongycastle.bcpg.SignaturePacket;
|
||||
import org.spongycastle.bcpg.SignatureSubpacket;
|
||||
import org.spongycastle.bcpg.SignatureSubpacketInputStream;
|
||||
import org.spongycastle.bcpg.SignatureSubpacketTags;
|
||||
import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags;
|
||||
import org.spongycastle.bcpg.UserIDPacket;
|
||||
import org.spongycastle.bcpg.sig.Features;
|
||||
import org.spongycastle.bcpg.sig.IssuerKeyID;
|
||||
import org.spongycastle.bcpg.sig.KeyExpirationTime;
|
||||
import org.spongycastle.bcpg.sig.KeyFlags;
|
||||
import org.spongycastle.bcpg.sig.PreferredAlgorithms;
|
||||
import org.spongycastle.bcpg.sig.SignatureCreationTime;
|
||||
import org.spongycastle.openpgp.PGPSignature;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Helps create correct and incorrect keyrings for tests.
|
||||
*
|
||||
* The original "correct" keyring was generated by GnuPG.
|
||||
*/
|
||||
public class KeyringBuilder {
|
||||
|
||||
|
||||
private static final BigInteger PUBLIC_KEY_MODULUS = new BigInteger(
|
||||
"cbab78d90d5f2cc0c54dd3c3953005a1e6b521f1ffa5465a102648bf7b91ec72" +
|
||||
"f9c180759301587878caeb73332156209f81ca5b3b94309d96110f6972cfc56a" +
|
||||
"37fd6279f61d71f19b8f64b288e338299dce133520f5b9b4253e6f4ba31ca36a" +
|
||||
"fd87c2081b15f0b283e9350e370e181a23d31379101f17a23ae9192250db6540" +
|
||||
"2e9cab2a275bc5867563227b197c8b136c832a94325b680e144ed864fb00b9b8" +
|
||||
"b07e13f37b40d5ac27dae63cd6a470a7b40fa3c7479b5b43e634850cc680b177" +
|
||||
"8dd6b1b51856f36c3520f258f104db2f96b31a53dd74f708ccfcefccbe420a90" +
|
||||
"1c37f1f477a6a4b15f5ecbbfd93311a647bcc3f5f81c59dfe7252e3cd3be6e27"
|
||||
, 16
|
||||
);
|
||||
|
||||
private static final BigInteger PUBLIC_SUBKEY_MODULUS = new BigInteger(
|
||||
"e8e2e2a33102649f19f8a07486fb076a1406ca888d72ae05d28f0ef372b5408e" +
|
||||
"45132c69f6e5cb6a79bb8aed84634196731393a82d53e0ddd42f28f92cc15850" +
|
||||
"8ce3b7ca1a9830502745aee774f86987993df984781f47c4a2910f95cf4c950c" +
|
||||
"c4c6cccdc134ad408a0c5418b5e360c9781a8434d366053ea6338b975fae88f9" +
|
||||
"383a10a90e7b2caa9ddb95708aa9d8a90246e29b04dbd6136613085c9a287315" +
|
||||
"c6e9c7ff4012defc1713875e3ff6073333a1c93d7cd75ebeaaf16b8b853d96ba" +
|
||||
"7003258779e8d2f70f1bc0bcd3ef91d7a9ccd8e225579b2d6fcae32799b0a6c0" +
|
||||
"e7305fc65dc4edc849c6130a0d669c90c193b1e746c812510f9d600a208be4a5"
|
||||
, 16
|
||||
);
|
||||
|
||||
private static final Date SIGNATURE_DATE = new Date(1404566755000L);
|
||||
|
||||
private static final BigInteger EXPONENT = BigInteger.valueOf(0x010001);
|
||||
|
||||
private static final String USER_ID_STRING = "OpenKeychain User (NOT A REAL KEY) <openkeychain@example.com>";
|
||||
|
||||
public static final BigInteger CORRECT_SIGNATURE = new BigInteger(
|
||||
"b065c071d3439d5610eb22e5b4df9e42ed78b8c94f487389e4fc98e8a75a043f" +
|
||||
"14bf57d591811e8e7db2d31967022d2ee64372829183ec51d0e20c42d7a1e519" +
|
||||
"e9fa22cd9db90f0fd7094fd093b78be2c0db62022193517404d749152c71edc6" +
|
||||
"fd48af3416038d8842608ecddebbb11c5823a4321d2029b8993cb017fa8e5ad7" +
|
||||
"8a9a618672d0217c4b34002f1a4a7625a514b6a86475e573cb87c64d7069658e" +
|
||||
"627f2617874007a28d525e0f87d93ca7b15ad10dbdf10251e542afb8f9b16cbf" +
|
||||
"7bebdb5fe7e867325a44e59cad0991cb239b1c859882e2ebb041b80e5cdc3b40" +
|
||||
"ed259a8a27d63869754c0881ccdcb50f0564fecdc6966be4a4b87a3507a9d9be"
|
||||
, 16
|
||||
);
|
||||
public static final BigInteger CORRECT_SUBKEY_SIGNATURE = new BigInteger(
|
||||
"9c40543e646cfa6d3d1863d91a4e8f1421d0616ddb3187505df75fbbb6c59dd5" +
|
||||
"3136b866f246a0320e793cb142c55c8e0e521d1e8d9ab864650f10690f5f1429" +
|
||||
"2eb8402a3b1f82c01079d12f5c57c43fce524a530e6f49f6f87d984e26db67a2" +
|
||||
"d469386dac87553c50147ebb6c2edd9248325405f737b815253beedaaba4f5c9" +
|
||||
"3acd5d07fe6522ceda1027932d849e3ec4d316422cd43ea6e506f643936ab0be" +
|
||||
"8246e546bb90d9a83613185047566864ffe894946477e939725171e0e15710b2" +
|
||||
"089f78752a9cb572f5907323f1b62f14cb07671aeb02e6d7178f185467624ec5" +
|
||||
"74e4a73c439a12edba200a4832106767366a1e6f63da0a42d593fa3914deee2b"
|
||||
, 16
|
||||
);
|
||||
public static final BigInteger KEY_ID = BigInteger.valueOf(0x15130BCF071AE6BFL);
|
||||
|
||||
public static UncachedKeyRing correctRing() {
|
||||
return convertToKeyring(correctKeyringPackets());
|
||||
}
|
||||
|
||||
public static UncachedKeyRing ringWithExtraIncorrectSignature() {
|
||||
List<ContainedPacket> packets = correctKeyringPackets();
|
||||
SignaturePacket incorrectSignaturePacket = createSignaturePacket(CORRECT_SIGNATURE.subtract(BigInteger.ONE));
|
||||
packets.add(2, incorrectSignaturePacket);
|
||||
return convertToKeyring(packets);
|
||||
}
|
||||
|
||||
private static UncachedKeyRing convertToKeyring(List<ContainedPacket> packets) {
|
||||
try {
|
||||
return UncachedKeyRing.decodeFromData(TestDataUtil.concatAll(packets));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<ContainedPacket> correctKeyringPackets() {
|
||||
PublicKeyPacket publicKey = createPgpPublicKey(PUBLIC_KEY_MODULUS);
|
||||
UserIDPacket userId = createUserId(USER_ID_STRING);
|
||||
SignaturePacket signaturePacket = createSignaturePacket(CORRECT_SIGNATURE);
|
||||
PublicKeyPacket subKey = createPgpPublicSubKey(PUBLIC_SUBKEY_MODULUS);
|
||||
SignaturePacket subKeySignaturePacket = createSubkeySignaturePacket();
|
||||
|
||||
return new ArrayList<ContainedPacket>(Arrays.asList(
|
||||
publicKey,
|
||||
userId,
|
||||
signaturePacket,
|
||||
subKey,
|
||||
subKeySignaturePacket
|
||||
));
|
||||
}
|
||||
|
||||
private static SignaturePacket createSignaturePacket(BigInteger signature) {
|
||||
MPInteger[] signatureArray = new MPInteger[]{
|
||||
new MPInteger(signature)
|
||||
};
|
||||
|
||||
int signatureType = PGPSignature.POSITIVE_CERTIFICATION;
|
||||
int keyAlgorithm = SignaturePacket.RSA_GENERAL;
|
||||
int hashAlgorithm = HashAlgorithmTags.SHA1;
|
||||
|
||||
SignatureSubpacket[] hashedData = new SignatureSubpacket[]{
|
||||
new SignatureCreationTime(false, SIGNATURE_DATE),
|
||||
new KeyFlags(false, KeyFlags.CERTIFY_OTHER + KeyFlags.SIGN_DATA),
|
||||
new KeyExpirationTime(false, TimeUnit.DAYS.toSeconds(2)),
|
||||
new PreferredAlgorithms(SignatureSubpacketTags.PREFERRED_SYM_ALGS, false, new int[]{
|
||||
SymmetricKeyAlgorithmTags.AES_256,
|
||||
SymmetricKeyAlgorithmTags.AES_192,
|
||||
SymmetricKeyAlgorithmTags.AES_128,
|
||||
SymmetricKeyAlgorithmTags.CAST5,
|
||||
SymmetricKeyAlgorithmTags.TRIPLE_DES
|
||||
}),
|
||||
new PreferredAlgorithms(SignatureSubpacketTags.PREFERRED_HASH_ALGS, false, new int[]{
|
||||
HashAlgorithmTags.SHA256,
|
||||
HashAlgorithmTags.SHA1,
|
||||
HashAlgorithmTags.SHA384,
|
||||
HashAlgorithmTags.SHA512,
|
||||
HashAlgorithmTags.SHA224
|
||||
}),
|
||||
new PreferredAlgorithms(SignatureSubpacketTags.PREFERRED_COMP_ALGS, false, new int[]{
|
||||
CompressionAlgorithmTags.ZLIB,
|
||||
CompressionAlgorithmTags.BZIP2,
|
||||
CompressionAlgorithmTags.ZIP
|
||||
}),
|
||||
new Features(false, Features.FEATURE_MODIFICATION_DETECTION),
|
||||
createPreferencesSignatureSubpacket()
|
||||
};
|
||||
SignatureSubpacket[] unhashedData = new SignatureSubpacket[]{
|
||||
new IssuerKeyID(false, KEY_ID.toByteArray())
|
||||
};
|
||||
byte[] fingerPrint = new BigInteger("522c", 16).toByteArray();
|
||||
|
||||
return new SignaturePacket(signatureType,
|
||||
KEY_ID.longValue(),
|
||||
keyAlgorithm,
|
||||
hashAlgorithm,
|
||||
hashedData,
|
||||
unhashedData,
|
||||
fingerPrint,
|
||||
signatureArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* There is no Preferences subpacket in BouncyCastle, so we have
|
||||
* to create one manually.
|
||||
*/
|
||||
private static SignatureSubpacket createPreferencesSignatureSubpacket() {
|
||||
SignatureSubpacket prefs;
|
||||
try {
|
||||
prefs = new SignatureSubpacketInputStream(new ByteArrayInputStream(
|
||||
new byte[]{2, SignatureSubpacketTags.KEY_SERVER_PREFS, (byte) 0x80})
|
||||
).readPacket();
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
return prefs;
|
||||
}
|
||||
|
||||
private static SignaturePacket createSubkeySignaturePacket() {
|
||||
int signatureType = PGPSignature.SUBKEY_BINDING;
|
||||
int keyAlgorithm = SignaturePacket.RSA_GENERAL;
|
||||
int hashAlgorithm = HashAlgorithmTags.SHA1;
|
||||
|
||||
SignatureSubpacket[] hashedData = new SignatureSubpacket[]{
|
||||
new SignatureCreationTime(false, SIGNATURE_DATE),
|
||||
new KeyFlags(false, KeyFlags.ENCRYPT_COMMS + KeyFlags.ENCRYPT_STORAGE),
|
||||
new KeyExpirationTime(false, TimeUnit.DAYS.toSeconds(2)),
|
||||
};
|
||||
SignatureSubpacket[] unhashedData = new SignatureSubpacket[]{
|
||||
new IssuerKeyID(false, KEY_ID.toByteArray())
|
||||
};
|
||||
byte[] fingerPrint = new BigInteger("234a", 16).toByteArray();
|
||||
MPInteger[] signature = new MPInteger[]{
|
||||
new MPInteger(CORRECT_SUBKEY_SIGNATURE)
|
||||
};
|
||||
return new SignaturePacket(signatureType,
|
||||
KEY_ID.longValue(),
|
||||
keyAlgorithm,
|
||||
hashAlgorithm,
|
||||
hashedData,
|
||||
unhashedData,
|
||||
fingerPrint,
|
||||
signature);
|
||||
}
|
||||
|
||||
private static PublicKeyPacket createPgpPublicKey(BigInteger modulus) {
|
||||
return new PublicKeyPacket(PublicKeyAlgorithmTags.RSA_GENERAL, SIGNATURE_DATE, new RSAPublicBCPGKey(modulus, EXPONENT));
|
||||
}
|
||||
|
||||
private static PublicKeyPacket createPgpPublicSubKey(BigInteger modulus) {
|
||||
return new PublicSubkeyPacket(PublicKeyAlgorithmTags.RSA_GENERAL, SIGNATURE_DATE, new RSAPublicBCPGKey(modulus, EXPONENT));
|
||||
}
|
||||
|
||||
private static UserIDPacket createUserId(String userId) {
|
||||
return new UserIDPacket(userId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
* Copyright (C) Art O Cathain
|
||||
* 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.support;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.spongycastle.util.Arrays;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
|
||||
import org.sufficientlysecure.keychain.util.ProgressScaler;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/** Helper methods for keyring tests. */
|
||||
public class KeyringTestingHelper {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public KeyringTestingHelper(Context robolectricContext) {
|
||||
this.context = robolectricContext;
|
||||
}
|
||||
|
||||
public boolean addKeyring(Collection<String> blobFiles) throws Exception {
|
||||
|
||||
ProviderHelper providerHelper = new ProviderHelper(context);
|
||||
|
||||
byte[] data = TestDataUtil.readAllFully(blobFiles);
|
||||
UncachedKeyRing ring = UncachedKeyRing.decodeFromData(data);
|
||||
long masterKeyId = ring.getMasterKeyId();
|
||||
|
||||
// Should throw an exception; key is not yet saved
|
||||
retrieveKeyAndExpectNotFound(providerHelper, masterKeyId);
|
||||
|
||||
SaveKeyringResult saveKeyringResult = providerHelper.savePublicKeyRing(ring, new ProgressScaler());
|
||||
|
||||
boolean saveSuccess = saveKeyringResult.success();
|
||||
|
||||
// Now re-retrieve the saved key. Should not throw an exception.
|
||||
providerHelper.getCanonicalizedPublicKeyRing(masterKeyId);
|
||||
|
||||
// A different ID should still fail
|
||||
retrieveKeyAndExpectNotFound(providerHelper, masterKeyId - 1);
|
||||
|
||||
return saveSuccess;
|
||||
}
|
||||
|
||||
public static UncachedKeyRing removePacket(UncachedKeyRing ring, int position)
|
||||
throws IOException, PgpGeneralException {
|
||||
return UncachedKeyRing.decodeFromData(removePacket(ring.getEncoded(), position));
|
||||
}
|
||||
|
||||
public static byte[] removePacket(byte[] ring, int position) throws IOException {
|
||||
Iterator<RawPacket> it = parseKeyring(ring);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(ring.length);
|
||||
|
||||
int i = 0;
|
||||
while(it.hasNext()) {
|
||||
// at the right position, skip the packet
|
||||
if(i++ == position) {
|
||||
it.next();
|
||||
continue;
|
||||
}
|
||||
// write the old one
|
||||
out.write(it.next().buf);
|
||||
}
|
||||
|
||||
if (i <= position) {
|
||||
throw new IndexOutOfBoundsException("injection index did not not occur in stream!");
|
||||
}
|
||||
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
public static UncachedKeyRing injectPacket(UncachedKeyRing ring, byte[] inject, int position)
|
||||
throws IOException, PgpGeneralException {
|
||||
return UncachedKeyRing.decodeFromData(injectPacket(ring.getEncoded(), inject, position));
|
||||
}
|
||||
|
||||
public static byte[] injectPacket(byte[] ring, byte[] inject, int position) throws IOException {
|
||||
|
||||
Iterator<RawPacket> it = parseKeyring(ring);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(ring.length + inject.length);
|
||||
|
||||
int i = 0;
|
||||
while(it.hasNext()) {
|
||||
// at the right position, inject the new packet
|
||||
if(i++ == position) {
|
||||
out.write(inject);
|
||||
}
|
||||
// write the old one
|
||||
out.write(it.next().buf);
|
||||
}
|
||||
|
||||
if (i <= position) {
|
||||
throw new IndexOutOfBoundsException("injection index did not not occur in stream!");
|
||||
}
|
||||
|
||||
return out.toByteArray();
|
||||
|
||||
}
|
||||
|
||||
/** This class contains a single pgp packet, together with information about its position
|
||||
* in the keyring and its packet tag.
|
||||
*/
|
||||
public static class RawPacket {
|
||||
public int position;
|
||||
|
||||
// packet tag for convenience, this can also be read from the header
|
||||
public int tag;
|
||||
|
||||
public int headerLength, length;
|
||||
// this buf includes the header, so its length is headerLength + length!
|
||||
public byte[] buf;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return other instanceof RawPacket && Arrays.areEqual(this.buf, ((RawPacket) other).buf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(buf);
|
||||
}
|
||||
}
|
||||
|
||||
/** A comparator which compares RawPackets by their position */
|
||||
public static final Comparator<RawPacket> packetOrder = new Comparator<RawPacket>() {
|
||||
public int compare(RawPacket left, RawPacket right) {
|
||||
return Integer.compare(left.position, right.position);
|
||||
}
|
||||
};
|
||||
|
||||
/** Diff two keyrings, returning packets only present in one keyring in its associated List.
|
||||
*
|
||||
* Packets in the returned lists are annotated and ordered by their original order of appearance
|
||||
* in their origin keyrings.
|
||||
*
|
||||
* @return true if keyrings differ in at least one packet
|
||||
*/
|
||||
public static boolean diffKeyrings(byte[] ringA, byte[] ringB,
|
||||
List<RawPacket> onlyA, List<RawPacket> onlyB)
|
||||
throws IOException {
|
||||
Iterator<RawPacket> streamA = parseKeyring(ringA);
|
||||
Iterator<RawPacket> streamB = parseKeyring(ringB);
|
||||
|
||||
HashSet<RawPacket> a = new HashSet<RawPacket>(), b = new HashSet<RawPacket>();
|
||||
|
||||
RawPacket p;
|
||||
int pos = 0;
|
||||
while(true) {
|
||||
p = streamA.next();
|
||||
if (p == null) {
|
||||
break;
|
||||
}
|
||||
p.position = pos++;
|
||||
a.add(p);
|
||||
}
|
||||
pos = 0;
|
||||
while(true) {
|
||||
p = streamB.next();
|
||||
if (p == null) {
|
||||
break;
|
||||
}
|
||||
p.position = pos++;
|
||||
b.add(p);
|
||||
}
|
||||
|
||||
onlyA.clear();
|
||||
onlyB.clear();
|
||||
|
||||
onlyA.addAll(a);
|
||||
onlyA.removeAll(b);
|
||||
onlyB.addAll(b);
|
||||
onlyB.removeAll(a);
|
||||
|
||||
Collections.sort(onlyA, packetOrder);
|
||||
Collections.sort(onlyB, packetOrder);
|
||||
|
||||
return !onlyA.isEmpty() || !onlyB.isEmpty();
|
||||
}
|
||||
|
||||
/** Creates an iterator of RawPackets over a binary keyring. */
|
||||
public static Iterator<RawPacket> parseKeyring(byte[] ring) {
|
||||
|
||||
final InputStream stream = new ByteArrayInputStream(ring);
|
||||
|
||||
return new Iterator<RawPacket>() {
|
||||
RawPacket next;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
if (next == null) try {
|
||||
next = readPacket(stream);
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
return next != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RawPacket next() {
|
||||
if (!hasNext()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return next;
|
||||
} finally {
|
||||
next = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/** Read a single (raw) pgp packet from an input stream.
|
||||
*
|
||||
* Note that the RawPacket.position field is NOT set here!
|
||||
*
|
||||
* Variable length packets are not handled here. we don't use those in our test classes, and
|
||||
* otherwise rely on BouncyCastle's own unit tests to handle those correctly.
|
||||
*/
|
||||
private static RawPacket readPacket(InputStream in) throws IOException {
|
||||
|
||||
// save here. this is tag + length, max 6 bytes
|
||||
in.mark(6);
|
||||
|
||||
int hdr = in.read();
|
||||
int headerLength = 1;
|
||||
|
||||
if (hdr < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((hdr & 0x80) == 0) {
|
||||
throw new IOException("invalid header encountered");
|
||||
}
|
||||
|
||||
boolean newPacket = (hdr & 0x40) != 0;
|
||||
int tag;
|
||||
int bodyLen;
|
||||
|
||||
if (newPacket) {
|
||||
tag = hdr & 0x3f;
|
||||
|
||||
int l = in.read();
|
||||
headerLength += 1;
|
||||
|
||||
if (l < 192) {
|
||||
bodyLen = l;
|
||||
} else if (l <= 223) {
|
||||
int b = in.read();
|
||||
headerLength += 1;
|
||||
|
||||
bodyLen = ((l - 192) << 8) + (b) + 192;
|
||||
} else if (l == 255) {
|
||||
bodyLen = (in.read() << 24) | (in.read() << 16) | (in.read() << 8) | in.read();
|
||||
headerLength += 4;
|
||||
} else {
|
||||
// bodyLen = 1 << (l & 0x1f);
|
||||
throw new IOException("no support for partial bodies in test classes");
|
||||
}
|
||||
} else {
|
||||
int lengthType = hdr & 0x3;
|
||||
|
||||
tag = (hdr & 0x3f) >> 2;
|
||||
|
||||
switch (lengthType) {
|
||||
case 0:
|
||||
bodyLen = in.read();
|
||||
headerLength += 1;
|
||||
break;
|
||||
case 1:
|
||||
bodyLen = (in.read() << 8) | in.read();
|
||||
headerLength += 2;
|
||||
break;
|
||||
case 2:
|
||||
bodyLen = (in.read() << 24) | (in.read() << 16) | (in.read() << 8) | in.read();
|
||||
headerLength += 4;
|
||||
break;
|
||||
case 3:
|
||||
// bodyLen = 1 << (l & 0x1f);
|
||||
throw new IOException("no support for partial bodies in test classes");
|
||||
default:
|
||||
throw new IOException("unknown length type encountered");
|
||||
}
|
||||
}
|
||||
|
||||
in.reset();
|
||||
|
||||
// read the entire packet INCLUDING the header here
|
||||
byte[] buf = new byte[headerLength+bodyLen];
|
||||
if (in.read(buf) != headerLength+bodyLen) {
|
||||
throw new IOException("read length mismatch!");
|
||||
}
|
||||
RawPacket p = new RawPacket();
|
||||
p.tag = tag;
|
||||
p.headerLength = headerLength;
|
||||
p.length = bodyLen;
|
||||
p.buf = buf;
|
||||
return p;
|
||||
|
||||
}
|
||||
|
||||
public static <E> E getNth(Iterator<E> it, int position) {
|
||||
while(position-- > 0) {
|
||||
it.next();
|
||||
}
|
||||
return it.next();
|
||||
}
|
||||
|
||||
public static long getSubkeyId(UncachedKeyRing ring, int position) {
|
||||
return getNth(ring.getPublicKeys(), position).getKeyId();
|
||||
}
|
||||
|
||||
private void retrieveKeyAndExpectNotFound(ProviderHelper providerHelper, long masterKeyId) {
|
||||
try {
|
||||
providerHelper.getCanonicalizedPublicKeyRing(masterKeyId);
|
||||
throw new AssertionError("Was expecting the previous call to fail!");
|
||||
} catch (ProviderHelper.NotFoundException expectedException) {
|
||||
// good
|
||||
}
|
||||
}
|
||||
|
||||
public static <E> List<E> itToList(Iterator<E> it) {
|
||||
List<E> result = new ArrayList<E>();
|
||||
while(it.hasNext()) {
|
||||
result.add(it.next());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) Art O Cathain
|
||||
*
|
||||
* 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.support;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
|
||||
class ProviderHelperStub extends ProviderHelper {
|
||||
public ProviderHelperStub(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CanonicalizedPublicKeyRing getCanonicalizedPublicKeyRing(Uri id) throws NotFoundException {
|
||||
byte[] data = TestDataUtil.readFully(getClass().getResourceAsStream("/public-key-for-sample.blob"));
|
||||
return new CanonicalizedPublicKeyRing(data, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright (C) Art O Cathain
|
||||
*
|
||||
* 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.support;
|
||||
|
||||
import org.spongycastle.bcpg.ContainedPacket;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* Misc support functions. Would just use Guava / Apache Commons but
|
||||
* avoiding extra dependencies.
|
||||
*/
|
||||
public class TestDataUtil {
|
||||
public static byte[] readFully(InputStream input) {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
appendToOutput(input, output);
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
public static void appendToOutput(InputStream input, OutputStream output) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
try {
|
||||
while ((bytesRead = input.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, bytesRead);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] readAllFully(Collection<String> inputResources) {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
|
||||
for (String inputResource : inputResources) {
|
||||
appendToOutput(getResourceAsStream(inputResource), output);
|
||||
}
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
public static InputStream getResourceAsStream(String resourceName) {
|
||||
return TestDataUtil.class.getResourceAsStream(resourceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Null-safe equivalent of {@code a.equals(b)}.
|
||||
*/
|
||||
public static boolean equals(Object a, Object b) {
|
||||
return (a == null) ? (b == null) : a.equals(b);
|
||||
}
|
||||
|
||||
public static <T> boolean iterEquals(Iterator<T> a, Iterator<T> b, EqualityChecker<T> comparator) {
|
||||
while (a.hasNext()) {
|
||||
T aObject = a.next();
|
||||
if (!b.hasNext()) {
|
||||
return false;
|
||||
}
|
||||
T bObject = b.next();
|
||||
if (!comparator.areEquals(aObject, bObject)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (b.hasNext()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public static <T> boolean iterEquals(Iterator<T> a, Iterator<T> b) {
|
||||
return iterEquals(a, b, new EqualityChecker<T>() {
|
||||
@Override
|
||||
public boolean areEquals(T lhs, T rhs) {
|
||||
return TestDataUtil.equals(lhs, rhs);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static interface EqualityChecker<T> {
|
||||
public boolean areEquals(T lhs, T rhs);
|
||||
}
|
||||
|
||||
|
||||
public static byte[] concatAll(java.util.List<ContainedPacket> packets) {
|
||||
byte[][] byteArrays = new byte[packets.size()][];
|
||||
try {
|
||||
for (int i = 0; i < packets.size(); i++) {
|
||||
byteArrays[i] = packets.get(i).getEncoded();
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
|
||||
return concatAll(byteArrays);
|
||||
}
|
||||
|
||||
public static byte[] concatAll(byte[]... byteArrays) {
|
||||
if (byteArrays.length == 1) {
|
||||
return byteArrays[0];
|
||||
} else if (byteArrays.length == 2) {
|
||||
return concat(byteArrays[0], byteArrays[1]);
|
||||
} else {
|
||||
byte[] first = concat(byteArrays[0], byteArrays[1]);
|
||||
byte[][] remainingArrays = new byte[byteArrays.length - 1][];
|
||||
remainingArrays[0] = first;
|
||||
System.arraycopy(byteArrays, 2, remainingArrays, 1, byteArrays.length - 2);
|
||||
return concatAll(remainingArrays);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] concat(byte[] a, byte[] b) {
|
||||
int aLen = a.length;
|
||||
int bLen = b.length;
|
||||
byte[] c = new byte[aLen + bLen];
|
||||
System.arraycopy(a, 0, c, 0, aLen);
|
||||
System.arraycopy(b, 0, c, aLen, bLen);
|
||||
return c;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/* Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.util;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
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.util.encoders.Hex;
|
||||
import org.sufficientlysecure.keychain.BuildConfig;
|
||||
import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
|
||||
import org.sufficientlysecure.keychain.util.Iso7816TLV.Iso7816CompositeTLV;
|
||||
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
|
||||
public class Iso7816TLVTest {
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
ShadowLog.stream = System.out;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecode() throws Exception {
|
||||
|
||||
// this is an Application Related Data packet, received from my Yubikey
|
||||
String input = "6e81dd4f10d27600012401020000000000000100005f520f0073000080000000000000000000007300c00af00000ff04c000ff00ffc106010800001103c206010800001103c306010800001103c407007f7f7f030303c53c1efdb4845ca242ca6977fddb1f788094fd3b430af1114c28a08d8c5afda81191cc50ca9bf51bc99fe8e6ca03a9d4d40e7b5925cd154813df381655b2c63c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd0c5423590e5423590e5423590e9000";
|
||||
byte[] data = Hex.decode(input);
|
||||
|
||||
Iso7816TLV tlv = Iso7816TLV.readSingle(data, true);
|
||||
Assert.assertNotNull("tlv parse must succeed", tlv);
|
||||
|
||||
Assert.assertEquals("top packet must be 'application related data' tag", 0x6e, tlv.mT);
|
||||
Assert.assertEquals("length must be correct", 221, tlv.mL);
|
||||
|
||||
Assert.assertTrue("top packet must be composite", tlv instanceof Iso7816CompositeTLV);
|
||||
|
||||
Iso7816CompositeTLV ctlv = (Iso7816CompositeTLV) tlv;
|
||||
|
||||
Assert.assertEquals("top packet must have 11 sub packets", 11, ctlv.mSubs.length);
|
||||
|
||||
Assert.assertEquals("sub packet #1 must have expected tag", 0x4f, ctlv.mSubs[0].mT);
|
||||
Assert.assertEquals("sub packet #1 must have expected length", 16, ctlv.mSubs[0].mL);
|
||||
|
||||
Assert.assertEquals("sub packet #2 must have expected tag", 0x5f52, ctlv.mSubs[1].mT);
|
||||
Assert.assertEquals("sub packet #2 must have expected length", 15, ctlv.mSubs[1].mL);
|
||||
|
||||
Assert.assertEquals("sub packet #3 must have expected tag", 0x73, ctlv.mSubs[2].mT);
|
||||
Assert.assertEquals("sub packet #3 must have expected length", 0, ctlv.mSubs[2].mL);
|
||||
Assert.assertTrue("sub packet #3 muse be composite", ctlv.mSubs[2] instanceof Iso7816CompositeTLV);
|
||||
|
||||
Assert.assertEquals("sub packet #4 must have expected tag", 0xc0, ctlv.mSubs[3].mT);
|
||||
Assert.assertEquals("sub packet #4 must have expected length", 10, ctlv.mSubs[3].mL);
|
||||
|
||||
Assert.assertEquals("sub packet #5 must have expected tag", 0xc1, ctlv.mSubs[4].mT);
|
||||
Assert.assertEquals("sub packet #5 must have expected length", 6, ctlv.mSubs[4].mL);
|
||||
|
||||
Assert.assertEquals("sub packet #6 must have expected tag", 0xc2, ctlv.mSubs[5].mT);
|
||||
Assert.assertEquals("sub packet #6 must have expected length", 6, ctlv.mSubs[5].mL);
|
||||
|
||||
Assert.assertEquals("sub packet #7 must have expected tag", 0xc3, ctlv.mSubs[6].mT);
|
||||
Assert.assertEquals("sub packet #7 must have expected length", 6, ctlv.mSubs[6].mL);
|
||||
|
||||
Assert.assertEquals("sub packet #8 must have expected tag", 0xc4, ctlv.mSubs[7].mT);
|
||||
Assert.assertEquals("sub packet #8 must have expected length", 7, ctlv.mSubs[7].mL);
|
||||
|
||||
Assert.assertEquals("sub packet #9 must have expected tag", 0xc5, ctlv.mSubs[8].mT);
|
||||
Assert.assertEquals("sub packet #9 must have expected length", 60, ctlv.mSubs[8].mL);
|
||||
|
||||
{
|
||||
// this is my pubkey fingerprint
|
||||
String fingerprint = "1efdb4845ca242ca6977fddb1f788094fd3b430a";
|
||||
byte[] V1 = new byte[20];
|
||||
System.arraycopy(ctlv.mSubs[8].mV, 0, V1, 0, 20);
|
||||
Assert.assertArrayEquals("fingerprint must match", V1, Hex.decode(fingerprint));
|
||||
}
|
||||
|
||||
Assert.assertEquals("sub packet #10 must have expected tag", 0xc6, ctlv.mSubs[9].mT);
|
||||
Assert.assertEquals("sub packet #10 must have expected length", 60, ctlv.mSubs[9].mL);
|
||||
|
||||
Assert.assertEquals("sub packet #11 must have expected tag", 0xcd, ctlv.mSubs[10].mT);
|
||||
Assert.assertEquals("sub packet #11 must have expected length", 12, ctlv.mSubs[10].mL);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricGradleTestRunner;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadows.ShadowLog;
|
||||
import org.sufficientlysecure.keychain.BuildConfig;
|
||||
import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
|
||||
import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
|
||||
public class ParcelableFileCacheTest {
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
ShadowLog.stream = System.out;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInputOutput() throws Exception {
|
||||
|
||||
ParcelableFileCache<Bundle> cache = new ParcelableFileCache<Bundle>(RuntimeEnvironment.application, "test.pcl");
|
||||
|
||||
ArrayList<Bundle> list = new ArrayList<Bundle>();
|
||||
|
||||
for (int i = 0; i < 50; i++) {
|
||||
Bundle b = new Bundle();
|
||||
b.putInt("key1", i);
|
||||
b.putString("key2", Integer.toString(i));
|
||||
list.add(b);
|
||||
}
|
||||
|
||||
// write to cache file
|
||||
cache.writeCache(list.size(), list.iterator());
|
||||
|
||||
// read back
|
||||
IteratorWithSize<Bundle> it = cache.readCache();
|
||||
|
||||
Assert.assertEquals("number of entries must be correct", list.size(), it.getSize());
|
||||
|
||||
while (it.hasNext()) {
|
||||
Bundle b = it.next();
|
||||
Assert.assertEquals("input values should be equal to output values",
|
||||
b.getInt("key1"), b.getInt("key1"));
|
||||
Assert.assertEquals("input values should be equal to output values",
|
||||
b.getString("key2"), b.getString("key2"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
public class TestingUtils {
|
||||
public static Passphrase genPassphrase() {
|
||||
return genPassphrase(false);
|
||||
}
|
||||
|
||||
public static Passphrase genPassphrase(boolean noEmpty) {
|
||||
String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789!@#$%^&*()-_=";
|
||||
Random r = new Random();
|
||||
StringBuilder passbuilder = new StringBuilder();
|
||||
// 20% chance for an empty passphrase
|
||||
for(int i = 0, j = noEmpty || r.nextInt(10) > 2 ? r.nextInt(20)+1 : 0; i < j; i++) {
|
||||
passbuilder.append(chars.charAt(r.nextInt(chars.length())));
|
||||
}
|
||||
System.out.println("Generated passphrase: '" + passbuilder.toString() + "'");
|
||||
return new Passphrase(passbuilder.toString());
|
||||
}
|
||||
}
|
||||
13
OpenKeychain/src/test/resources/OpenPGP-Haskell/COPYING
Normal file
13
OpenKeychain/src/test/resources/OpenPGP-Haskell/COPYING
Normal file
@@ -0,0 +1,13 @@
|
||||
Copyright © 2011, Stephen Paul Weber <singpolyma.net>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
26
OpenKeychain/src/test/resources/OpenPGP-Haskell/README
Normal file
26
OpenKeychain/src/test/resources/OpenPGP-Haskell/README
Normal file
@@ -0,0 +1,26 @@
|
||||
These test files were copied from the OpenPGP Haskell project.
|
||||
|
||||
|
||||
|
||||
Original README
|
||||
===============
|
||||
|
||||
This is an OpenPGP library inspired by my work on OpenPGP libraries in
|
||||
Ruby <https://github.com/singpolyma/openpgp>,
|
||||
PHP <http://github.com/singpolyma/openpgp-php>,
|
||||
and Python <https://github.com/singpolyma/OpenPGP-Python>.
|
||||
|
||||
It defines types to represent OpenPGP messages as a series of packets
|
||||
and then defines instances of Data.Binary for each to facilitate
|
||||
encoding/decoding.
|
||||
|
||||
For performing cryptography, see
|
||||
<http://hackage.haskell.org/package/openpgp-crypto-api> or
|
||||
<http://hackage.haskell.org/package/openpgp-Crypto>
|
||||
|
||||
For dealing with ASCII armor, see
|
||||
<http://hackage.haskell.org/package/openpgp-asciiarmor>
|
||||
|
||||
It is intended that you use qualified imports with this library.
|
||||
|
||||
> import qualified Data.OpenPGP as OpenPGP
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
´$Test Key (RSA) <testkey@example.org>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
´$Test Key (DSA) <testkey@example.com>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
´+Test Key (DSA sign-only) <test@example.net>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
´.Test Key (RSA sign-only) <testkey@example.net>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
´$Test Key (RSA) <testkey@example.org>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
´$Test Key (DSA) <testkey@example.com>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
´+Test Key (DSA sign-only) <test@example.net>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user