mirror of
https://github.com/moparisthebest/open-keychain
synced 2024-11-30 12:32:17 -05:00
Merge pull request #711 from open-keychain/external-test
External test + working SaveKeyringParcel
This commit is contained in:
commit
d8f941fa70
4
.gitmodules
vendored
4
.gitmodules
vendored
@ -31,6 +31,6 @@
|
|||||||
[submodule "extern/minidns"]
|
[submodule "extern/minidns"]
|
||||||
path = extern/minidns
|
path = extern/minidns
|
||||||
url = https://github.com/open-keychain/minidns.git
|
url = https://github.com/open-keychain/minidns.git
|
||||||
[submodule "OpenKeychain/src/test/resources/extern/OpenPGP-Haskell"]
|
[submodule "OpenKeychain-Test/src/test/resources/extern/OpenPGP-Haskell"]
|
||||||
path = OpenKeychain/src/test/resources/extern/OpenPGP-Haskell
|
path = OpenKeychain-Test/src/test/resources/extern/OpenPGP-Haskell
|
||||||
url = https://github.com/singpolyma/OpenPGP-Haskell.git
|
url = https://github.com/singpolyma/OpenPGP-Haskell.git
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
language: java
|
language: java
|
||||||
jdk: oraclejdk7
|
jdk: openjdk7
|
||||||
before_install:
|
before_install:
|
||||||
# Install base Android SDK
|
# Install base Android SDK
|
||||||
- sudo apt-get update -qq
|
- sudo apt-get update -qq
|
||||||
@ -12,6 +12,9 @@ before_install:
|
|||||||
# Install required Android components.
|
# Install required Android components.
|
||||||
#- echo "y" | android update sdk -a --filter build-tools-19.1.0,android-19,platform-tools,extra-android-support,extra-android-m2repository --no-ui --force
|
#- echo "y" | android update sdk -a --filter build-tools-19.1.0,android-19,platform-tools,extra-android-support,extra-android-m2repository --no-ui --force
|
||||||
- ( sleep 5 && while [ 1 ]; do sleep 1; echo y; done ) | android update sdk --no-ui --all --force --filter build-tools-19.1.0,android-19,platform-tools,extra-android-support,extra-android-m2repository
|
- ( sleep 5 && while [ 1 ]; do sleep 1; echo y; done ) | android update sdk --no-ui --all --force --filter build-tools-19.1.0,android-19,platform-tools,extra-android-support,extra-android-m2repository
|
||||||
|
- ./install-custom-gradle-test-plugin.sh
|
||||||
install: echo "Installation done"
|
install: echo "Installation done"
|
||||||
script: gradle assemble -S -q
|
script:
|
||||||
|
- gradle assemble -S -q
|
||||||
|
- gradle --info OpenKeychain-Test:testDebug
|
||||||
|
|
||||||
|
80
OpenKeychain-Test/build.gradle
Normal file
80
OpenKeychain-Test/build.gradle
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
apply plugin: 'java'
|
||||||
|
apply plugin: 'android-test'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testCompile 'junit:junit:4.11'
|
||||||
|
testCompile 'com.google.android:android:4.1.1.4'
|
||||||
|
testCompile('com.squareup:fest-android:1.0.+') { exclude module: 'support-v4' }
|
||||||
|
testCompile ('org.robolectric:robolectric:2.3') {
|
||||||
|
exclude module: 'classworlds'
|
||||||
|
exclude module: 'maven-artifact'
|
||||||
|
exclude module: 'maven-artifact-manager'
|
||||||
|
exclude module: 'maven-error-diagnostics'
|
||||||
|
exclude module: 'maven-model'
|
||||||
|
exclude module: 'maven-plugin-registry'
|
||||||
|
exclude module: 'maven-profile'
|
||||||
|
exclude module: 'maven-project'
|
||||||
|
exclude module: 'maven-settings'
|
||||||
|
exclude module: 'nekohtml'
|
||||||
|
exclude module: 'plexus-container-default'
|
||||||
|
exclude module: 'plexus-interpolation'
|
||||||
|
exclude module: 'plexus-utils'
|
||||||
|
exclude module: 'support-v4' // crazy but my android studio don't like this dependency and to fix it remove .idea and re import project
|
||||||
|
exclude module: 'wagon-file'
|
||||||
|
exclude module: 'wagon-http-lightweight'
|
||||||
|
exclude module: 'wagon-http-shared'
|
||||||
|
exclude module: 'wagon-provider-api'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
projectUnderTest ':OpenKeychain'
|
||||||
|
}
|
||||||
|
|
||||||
|
// new workaround to force add custom output dirs for android studio
|
||||||
|
task addTest {
|
||||||
|
def file = file(project.name + ".iml")
|
||||||
|
doLast {
|
||||||
|
try {
|
||||||
|
def parsedXml = (new XmlParser()).parse(file)
|
||||||
|
def node = parsedXml.component[1]
|
||||||
|
def outputNode = parsedXml.component[1].output[0]
|
||||||
|
def outputTestNode = parsedXml.component[1].'output-test'[0]
|
||||||
|
def rewrite = false
|
||||||
|
|
||||||
|
new Node(node, 'sourceFolder', ['url': 'file://$MODULE_DIR$/' + "${it}", 'isTestSource': "true"])
|
||||||
|
|
||||||
|
if(outputNode == null) {
|
||||||
|
new Node(node, 'output', ['url': 'file://$MODULE_DIR$/build/resources/testDebug'])
|
||||||
|
} else {
|
||||||
|
if(outputNode.attributes['url'] != 'file://$MODULE_DIR$/build/resources/testDebug') {
|
||||||
|
outputNode.attributes = ['url': 'file://$MODULE_DIR$/build/resources/testDebug']
|
||||||
|
rewrite = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(outputTestNode == null) {
|
||||||
|
new Node(node, 'output-test', ['url': 'file://$MODULE_DIR$/build/test-classes/debug'])
|
||||||
|
} else {
|
||||||
|
if(outputTestNode.attributes['url'] != 'file://$MODULE_DIR$/build/test-classes/debug') {
|
||||||
|
outputTestNode.attributes = ['url': 'file://$MODULE_DIR$/build/test-classes/debug']
|
||||||
|
rewrite = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(rewrite) {
|
||||||
|
def writer = new StringWriter()
|
||||||
|
new XmlNodePrinter(new PrintWriter(writer)).print(parsedXml)
|
||||||
|
file.text = writer.toString()
|
||||||
|
}
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
// iml not found, common on command line only builds
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// always do the addtest on prebuild
|
||||||
|
gradle.projectsEvaluated {
|
||||||
|
testDebugClasses.dependsOn(addTest)
|
||||||
|
}
|
@ -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,331 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) Art O Cathain, Vincent Breitmoser
|
||||||
|
*
|
||||||
|
* 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.NullProgressable;
|
||||||
|
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||||
|
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||||
|
import org.sufficientlysecure.keychain.service.OperationResults;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
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);
|
||||||
|
|
||||||
|
OperationResults.SaveKeyringResult saveKeyringResult = providerHelper.savePublicKeyRing(ring, new NullProgressable());
|
||||||
|
|
||||||
|
boolean saveSuccess = saveKeyringResult.success();
|
||||||
|
|
||||||
|
// Now re-retrieve the saved key. Should not throw an exception.
|
||||||
|
providerHelper.getWrappedPublicKeyRing(masterKeyId);
|
||||||
|
|
||||||
|
// A different ID should still fail
|
||||||
|
retrieveKeyAndExpectNotFound(providerHelper, masterKeyId - 1);
|
||||||
|
|
||||||
|
return saveSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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 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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void retrieveKeyAndExpectNotFound(ProviderHelper providerHelper, long masterKeyId) {
|
||||||
|
try {
|
||||||
|
providerHelper.getWrappedPublicKeyRing(masterKeyId);
|
||||||
|
throw new AssertionError("Was expecting the previous call to fail!");
|
||||||
|
} catch (ProviderHelper.NotFoundException expectedException) {
|
||||||
|
// good
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,4 +1,20 @@
|
|||||||
package org.sufficientlysecure.keychain.testsupport;
|
package org.sufficientlysecure.keychain.support;
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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.WrappedPublicKeyRing;
|
||||||
|
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by art on 21/06/14.
|
||||||
|
*/
|
||||||
|
class ProviderHelperStub extends ProviderHelper {
|
||||||
|
public ProviderHelperStub(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WrappedPublicKeyRing getWrappedPublicKeyRing(Uri id) throws NotFoundException {
|
||||||
|
byte[] data = TestDataUtil.readFully(getClass().getResourceAsStream("/public-key-for-sample.blob"));
|
||||||
|
return new WrappedPublicKeyRing(data, false, 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,297 @@
|
|||||||
|
/*
|
||||||
|
* 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.BCPGKey;
|
||||||
|
import org.spongycastle.bcpg.PublicKeyPacket;
|
||||||
|
import org.spongycastle.bcpg.SignatureSubpacket;
|
||||||
|
import org.spongycastle.openpgp.PGPException;
|
||||||
|
import org.spongycastle.openpgp.PGPPublicKey;
|
||||||
|
import org.spongycastle.openpgp.PGPSignature;
|
||||||
|
import org.spongycastle.openpgp.PGPSignatureSubpacketVector;
|
||||||
|
import org.spongycastle.openpgp.PGPUserAttributeSubpacketVector;
|
||||||
|
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||||
|
import org.sufficientlysecure.keychain.pgp.UncachedPublicKey;
|
||||||
|
import org.sufficientlysecure.keychain.service.OperationResultParcel;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by art on 28/06/14.
|
||||||
|
*/
|
||||||
|
public class UncachedKeyringTestingHelper {
|
||||||
|
|
||||||
|
public static boolean compareRing(UncachedKeyRing keyRing1, UncachedKeyRing keyRing2) {
|
||||||
|
OperationResultParcel.OperationLog operationLog = new OperationResultParcel.OperationLog();
|
||||||
|
UncachedKeyRing canonicalized = keyRing1.canonicalize(operationLog, 0);
|
||||||
|
|
||||||
|
if (canonicalized == null) {
|
||||||
|
throw new AssertionError("Canonicalization failed; messages: [" + operationLog.toList() + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
return TestDataUtil.iterEquals(canonicalized.getPublicKeys(), keyRing2.getPublicKeys(), new
|
||||||
|
TestDataUtil.EqualityChecker<UncachedPublicKey>() {
|
||||||
|
@Override
|
||||||
|
public boolean areEquals(UncachedPublicKey lhs, UncachedPublicKey rhs) {
|
||||||
|
return comparePublicKey(lhs, rhs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean comparePublicKey(UncachedPublicKey key1, UncachedPublicKey key2) {
|
||||||
|
boolean equal = true;
|
||||||
|
|
||||||
|
if (key1.canAuthenticate() != key2.canAuthenticate()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (key1.canCertify() != key2.canCertify()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (key1.canEncrypt() != key2.canEncrypt()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (key1.canSign() != key2.canSign()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (key1.getAlgorithm() != key2.getAlgorithm()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (key1.getBitStrength() != key2.getBitStrength()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!TestDataUtil.equals(key1.getCreationTime(), key2.getCreationTime())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!TestDataUtil.equals(key1.getExpiryTime(), key2.getExpiryTime())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Arrays.equals(key1.getFingerprint(), key2.getFingerprint())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (key1.getKeyId() != key2.getKeyId()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (key1.getKeyUsage() != key2.getKeyUsage()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!TestDataUtil.equals(key1.getPrimaryUserId(), key2.getPrimaryUserId())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ooops, getPublicKey is due to disappear. But then how to compare?
|
||||||
|
if (!keysAreEqual(key1.getPublicKey(), key2.getPublicKey())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return equal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean keysAreEqual(PGPPublicKey a, PGPPublicKey b) {
|
||||||
|
|
||||||
|
if (a.getAlgorithm() != b.getAlgorithm()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.getBitStrength() != b.getBitStrength()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TestDataUtil.equals(a.getCreationTime(), b.getCreationTime())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Arrays.equals(a.getFingerprint(), b.getFingerprint())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.getKeyID() != b.getKeyID()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pubKeyPacketsAreEqual(a.getPublicKeyPacket(), b.getPublicKeyPacket())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.getVersion() != b.getVersion()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.getValidDays() != b.getValidDays()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.getValidSeconds() != b.getValidSeconds()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Arrays.equals(a.getTrustData(), b.getTrustData())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TestDataUtil.iterEquals(a.getUserIDs(), b.getUserIDs())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TestDataUtil.iterEquals(a.getUserAttributes(), b.getUserAttributes(),
|
||||||
|
new TestDataUtil.EqualityChecker<PGPUserAttributeSubpacketVector>() {
|
||||||
|
public boolean areEquals(PGPUserAttributeSubpacketVector lhs, PGPUserAttributeSubpacketVector rhs) {
|
||||||
|
// For once, BC defines equals, so we use it implicitly.
|
||||||
|
return TestDataUtil.equals(lhs, rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!TestDataUtil.iterEquals(a.getSignatures(), b.getSignatures(),
|
||||||
|
new TestDataUtil.EqualityChecker<PGPSignature>() {
|
||||||
|
public boolean areEquals(PGPSignature lhs, PGPSignature rhs) {
|
||||||
|
return signaturesAreEqual(lhs, rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean signaturesAreEqual(PGPSignature a, PGPSignature b) {
|
||||||
|
|
||||||
|
if (a.getVersion() != b.getVersion()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.getKeyAlgorithm() != b.getKeyAlgorithm()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.getHashAlgorithm() != b.getHashAlgorithm()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.getSignatureType() != b.getSignatureType()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!Arrays.equals(a.getSignature(), b.getSignature())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (PGPException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.getKeyID() != b.getKeyID()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TestDataUtil.equals(a.getCreationTime(), b.getCreationTime())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Arrays.equals(a.getSignatureTrailer(), b.getSignatureTrailer())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subPacketVectorsAreEqual(a.getHashedSubPackets(), b.getHashedSubPackets())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subPacketVectorsAreEqual(a.getUnhashedSubPackets(), b.getUnhashedSubPackets())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean subPacketVectorsAreEqual(PGPSignatureSubpacketVector aHashedSubPackets, PGPSignatureSubpacketVector bHashedSubPackets) {
|
||||||
|
for (int i = 0; i < Byte.MAX_VALUE; i++) {
|
||||||
|
if (!TestDataUtil.iterEquals(Arrays.asList(aHashedSubPackets.getSubpackets(i)).iterator(),
|
||||||
|
Arrays.asList(bHashedSubPackets.getSubpackets(i)).iterator(),
|
||||||
|
new TestDataUtil.EqualityChecker<SignatureSubpacket>() {
|
||||||
|
@Override
|
||||||
|
public boolean areEquals(SignatureSubpacket lhs, SignatureSubpacket rhs) {
|
||||||
|
return signatureSubpacketsAreEqual(lhs, rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean signatureSubpacketsAreEqual(SignatureSubpacket lhs, SignatureSubpacket rhs) {
|
||||||
|
if (lhs.getType() != rhs.getType()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Arrays.equals(lhs.getData(), rhs.getData())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean pubKeyPacketsAreEqual(PublicKeyPacket a, PublicKeyPacket b) {
|
||||||
|
|
||||||
|
if (a.getAlgorithm() != b.getAlgorithm()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bcpgKeysAreEqual(a.getKey(), b.getKey())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TestDataUtil.equals(a.getTime(), b.getTime())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.getValidDays() != b.getValidDays()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.getVersion() != b.getVersion()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean bcpgKeysAreEqual(BCPGKey a, BCPGKey b) {
|
||||||
|
|
||||||
|
if (!TestDataUtil.equals(a.getFormat(), b.getFormat())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Arrays.equals(a.getEncoded(), b.getEncoded())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void doTestCanonicalize(UncachedKeyRing inputKeyRing, UncachedKeyRing expectedKeyRing) {
|
||||||
|
if (!compareRing(inputKeyRing, expectedKeyRing)) {
|
||||||
|
throw new AssertionError("Expected [" + inputKeyRing + "] to match [" + expectedKeyRing + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,3 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* 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 tests;
|
package tests;
|
||||||
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
@ -5,7 +22,7 @@ import org.junit.Test;
|
|||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.robolectric.*;
|
import org.robolectric.*;
|
||||||
import org.openintents.openpgp.OpenPgpSignatureResult;
|
import org.openintents.openpgp.OpenPgpSignatureResult;
|
||||||
import org.sufficientlysecure.keychain.testsupport.PgpVerifyTestingHelper;
|
import org.sufficientlysecure.keychain.support.PgpVerifyTestingHelper;
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(RobolectricTestRunner.class)
|
||||||
@org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19
|
@org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19
|
@ -0,0 +1,783 @@
|
|||||||
|
package org.sufficientlysecure.keychain.tests;
|
||||||
|
|
||||||
|
import junit.framework.AssertionFailedError;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.robolectric.*;
|
||||||
|
import org.robolectric.shadows.ShadowLog;
|
||||||
|
import org.spongycastle.bcpg.BCPGInputStream;
|
||||||
|
import org.spongycastle.bcpg.Packet;
|
||||||
|
import org.spongycastle.bcpg.PacketTags;
|
||||||
|
import org.spongycastle.bcpg.SecretSubkeyPacket;
|
||||||
|
import org.spongycastle.bcpg.SignaturePacket;
|
||||||
|
import org.spongycastle.bcpg.UserIDPacket;
|
||||||
|
import org.spongycastle.bcpg.sig.KeyFlags;
|
||||||
|
import org.spongycastle.openpgp.PGPSignature;
|
||||||
|
import org.sufficientlysecure.keychain.Constants;
|
||||||
|
import org.sufficientlysecure.keychain.Constants.choice.algorithm;
|
||||||
|
import org.sufficientlysecure.keychain.pgp.PgpKeyOperation;
|
||||||
|
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||||
|
import org.sufficientlysecure.keychain.pgp.UncachedPublicKey;
|
||||||
|
import org.sufficientlysecure.keychain.pgp.WrappedSecretKeyRing;
|
||||||
|
import org.sufficientlysecure.keychain.pgp.WrappedSignature;
|
||||||
|
import org.sufficientlysecure.keychain.service.OperationResultParcel;
|
||||||
|
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
|
||||||
|
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyAdd;
|
||||||
|
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyChange;
|
||||||
|
import org.sufficientlysecure.keychain.support.KeyringBuilder;
|
||||||
|
import org.sufficientlysecure.keychain.support.KeyringTestingHelper;
|
||||||
|
import org.sufficientlysecure.keychain.support.KeyringTestingHelper.RawPacket;
|
||||||
|
import org.sufficientlysecure.keychain.support.TestDataUtil;
|
||||||
|
import org.sufficientlysecure.keychain.util.ProgressScaler;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner.class)
|
||||||
|
@org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19
|
||||||
|
public class PgpKeyOperationTest {
|
||||||
|
|
||||||
|
static UncachedKeyRing staticRing;
|
||||||
|
static String passphrase;
|
||||||
|
|
||||||
|
UncachedKeyRing ring;
|
||||||
|
PgpKeyOperation op;
|
||||||
|
SaveKeyringParcel parcel;
|
||||||
|
ArrayList<RawPacket> onlyA = new ArrayList<RawPacket>();
|
||||||
|
ArrayList<RawPacket> onlyB = new ArrayList<RawPacket>();
|
||||||
|
|
||||||
|
@BeforeClass public static void setUpOnce() throws Exception {
|
||||||
|
ShadowLog.stream = System.out;
|
||||||
|
|
||||||
|
{
|
||||||
|
String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789!@#$%^&*()-_=";
|
||||||
|
Random r = new Random();
|
||||||
|
StringBuilder passbuilder = new StringBuilder();
|
||||||
|
// 20% chance for an empty passphrase
|
||||||
|
for(int i = 0, j = r.nextInt(10) > 2 ? r.nextInt(20) : 0; i < j; i++) {
|
||||||
|
passbuilder.append(chars.charAt(r.nextInt(chars.length())));
|
||||||
|
}
|
||||||
|
passphrase = passbuilder.toString();
|
||||||
|
System.out.println("Passphrase is '" + passphrase + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveKeyringParcel parcel = new SaveKeyringParcel();
|
||||||
|
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
|
||||||
|
Constants.choice.algorithm.rsa, 1024, KeyFlags.CERTIFY_OTHER, null));
|
||||||
|
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
|
||||||
|
Constants.choice.algorithm.rsa, 1024, KeyFlags.SIGN_DATA, null));
|
||||||
|
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
|
||||||
|
Constants.choice.algorithm.rsa, 1024, KeyFlags.ENCRYPT_COMMS, null));
|
||||||
|
|
||||||
|
parcel.mAddUserIds.add("twi");
|
||||||
|
parcel.mAddUserIds.add("pink");
|
||||||
|
parcel.mNewPassphrase = passphrase;
|
||||||
|
PgpKeyOperation op = new PgpKeyOperation(null);
|
||||||
|
|
||||||
|
OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
|
||||||
|
staticRing = op.createSecretKeyRing(parcel, log, 0);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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 = ring.getMasterKeyId();
|
||||||
|
parcel.mFingerprint = ring.getFingerprint();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAlgorithmChoice() {
|
||||||
|
|
||||||
|
OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
|
||||||
|
|
||||||
|
{
|
||||||
|
parcel.reset();
|
||||||
|
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
|
||||||
|
Constants.choice.algorithm.rsa, new Random().nextInt(256)+255, KeyFlags.CERTIFY_OTHER, null));
|
||||||
|
parcel.mAddUserIds.add("shy");
|
||||||
|
parcel.mNewPassphrase = passphrase;
|
||||||
|
|
||||||
|
UncachedKeyRing ring = op.createSecretKeyRing(parcel, log, 0);
|
||||||
|
|
||||||
|
Assert.assertNull("creating ring with < 512 bytes keysize should fail", ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
parcel.reset();
|
||||||
|
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
|
||||||
|
Constants.choice.algorithm.elgamal, 1024, KeyFlags.CERTIFY_OTHER, null));
|
||||||
|
parcel.mAddUserIds.add("shy");
|
||||||
|
parcel.mNewPassphrase = passphrase;
|
||||||
|
|
||||||
|
UncachedKeyRing ring = op.createSecretKeyRing(parcel, log, 0);
|
||||||
|
|
||||||
|
Assert.assertNull("creating ring with ElGamal master key should fail", ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
parcel.reset();
|
||||||
|
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
|
||||||
|
12345, 1024, KeyFlags.CERTIFY_OTHER, null));
|
||||||
|
parcel.mAddUserIds.add("shy");
|
||||||
|
parcel.mNewPassphrase = passphrase;
|
||||||
|
|
||||||
|
UncachedKeyRing ring = op.createSecretKeyRing(parcel, log, 0);
|
||||||
|
Assert.assertNull("creating ring with bad algorithm choice should fail", ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
parcel.reset();
|
||||||
|
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
|
||||||
|
Constants.choice.algorithm.rsa, 1024, KeyFlags.SIGN_DATA, null));
|
||||||
|
parcel.mAddUserIds.add("shy");
|
||||||
|
parcel.mNewPassphrase = passphrase;
|
||||||
|
|
||||||
|
UncachedKeyRing ring = op.createSecretKeyRing(parcel, log, 0);
|
||||||
|
Assert.assertNull("creating ring with non-certifying master key should fail", ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
parcel.reset();
|
||||||
|
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
|
||||||
|
Constants.choice.algorithm.rsa, 1024, KeyFlags.CERTIFY_OTHER, null));
|
||||||
|
parcel.mNewPassphrase = passphrase;
|
||||||
|
|
||||||
|
UncachedKeyRing ring = op.createSecretKeyRing(parcel, log, 0);
|
||||||
|
Assert.assertNull("creating ring without user ids should fail", ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
parcel.reset();
|
||||||
|
parcel.mAddUserIds.add("shy");
|
||||||
|
parcel.mNewPassphrase = passphrase;
|
||||||
|
|
||||||
|
UncachedKeyRing ring = op.createSecretKeyRing(parcel, log, 0);
|
||||||
|
Assert.assertNull("creating ring without subkeys should fail", ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// this is a special case since the flags are in user id certificates rather than
|
||||||
|
// subkey binding certificates
|
||||||
|
public void testMasterFlags() throws Exception {
|
||||||
|
SaveKeyringParcel parcel = new SaveKeyringParcel();
|
||||||
|
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
|
||||||
|
Constants.choice.algorithm.rsa, 1024, KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA, null));
|
||||||
|
parcel.mAddUserIds.add("luna");
|
||||||
|
OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
|
||||||
|
ring = op.createSecretKeyRing(parcel, log, 0);
|
||||||
|
|
||||||
|
Assert.assertEquals("the keyring should contain only the master key",
|
||||||
|
1, ring.getAvailableSubkeys().size());
|
||||||
|
Assert.assertEquals("first (master) key must have both flags",
|
||||||
|
KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA, ring.getPublicKey().getKeyUsage());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreatedKey() throws Exception {
|
||||||
|
|
||||||
|
// an empty modification should change nothing. this also ensures the keyring
|
||||||
|
// is constant through canonicalization.
|
||||||
|
// applyModificationWithChecks(parcel, ring, onlyA, onlyB);
|
||||||
|
|
||||||
|
Assert.assertNotNull("key creation failed", ring);
|
||||||
|
|
||||||
|
Assert.assertNull("primary user id must be empty",
|
||||||
|
ring.getPublicKey().getPrimaryUserId());
|
||||||
|
|
||||||
|
Assert.assertEquals("number of user ids must be two",
|
||||||
|
2, ring.getPublicKey().getUnorderedUserIds().size());
|
||||||
|
|
||||||
|
Assert.assertEquals("number of subkeys must be three",
|
||||||
|
3, ring.getAvailableSubkeys().size());
|
||||||
|
|
||||||
|
Assert.assertTrue("key ring should have been created in the last 120 seconds",
|
||||||
|
ring.getPublicKey().getCreationTime().after(new Date(new Date().getTime()-1000*120)));
|
||||||
|
|
||||||
|
Assert.assertNull("key ring should not expire",
|
||||||
|
ring.getPublicKey().getExpiryTime());
|
||||||
|
|
||||||
|
Iterator<UncachedPublicKey> it = ring.getPublicKeys();
|
||||||
|
|
||||||
|
Assert.assertEquals("first (master) key can certify",
|
||||||
|
KeyFlags.CERTIFY_OTHER, it.next().getKeyUsage());
|
||||||
|
|
||||||
|
UncachedPublicKey signingKey = it.next();
|
||||||
|
Assert.assertEquals("second key can sign",
|
||||||
|
KeyFlags.SIGN_DATA, signingKey.getKeyUsage());
|
||||||
|
ArrayList<WrappedSignature> sigs = signingKey.getSignatures().next().getEmbeddedSignatures();
|
||||||
|
Assert.assertEquals("signing key signature should have one embedded signature",
|
||||||
|
1, sigs.size());
|
||||||
|
Assert.assertEquals("embedded signature should be of primary key binding type",
|
||||||
|
PGPSignature.PRIMARYKEY_BINDING, sigs.get(0).getSignatureType());
|
||||||
|
Assert.assertEquals("primary key binding signature issuer should be signing subkey",
|
||||||
|
signingKey.getKeyId(), sigs.get(0).getKeyId());
|
||||||
|
|
||||||
|
Assert.assertEquals("third key can encrypt",
|
||||||
|
KeyFlags.ENCRYPT_COMMS, it.next().getKeyUsage());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBadKeyModification() throws Exception {
|
||||||
|
|
||||||
|
{
|
||||||
|
SaveKeyringParcel parcel = new SaveKeyringParcel();
|
||||||
|
// off by one
|
||||||
|
parcel.mMasterKeyId = ring.getMasterKeyId() -1;
|
||||||
|
parcel.mFingerprint = ring.getFingerprint();
|
||||||
|
|
||||||
|
WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0);
|
||||||
|
OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
|
||||||
|
UncachedKeyRing modified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0);
|
||||||
|
|
||||||
|
Assert.assertNull("keyring modification with bad master key id should fail", modified);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
SaveKeyringParcel parcel = new SaveKeyringParcel();
|
||||||
|
// off by one
|
||||||
|
parcel.mMasterKeyId = null;
|
||||||
|
parcel.mFingerprint = ring.getFingerprint();
|
||||||
|
|
||||||
|
WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0);
|
||||||
|
OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
|
||||||
|
UncachedKeyRing modified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0);
|
||||||
|
|
||||||
|
Assert.assertNull("keyring modification with null master key id should fail", modified);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
SaveKeyringParcel parcel = new SaveKeyringParcel();
|
||||||
|
parcel.mMasterKeyId = ring.getMasterKeyId();
|
||||||
|
parcel.mFingerprint = ring.getFingerprint();
|
||||||
|
// some byte, off by one
|
||||||
|
parcel.mFingerprint[5] += 1;
|
||||||
|
|
||||||
|
WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0);
|
||||||
|
OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
|
||||||
|
UncachedKeyRing modified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0);
|
||||||
|
|
||||||
|
Assert.assertNull("keyring modification with bad fingerprint should fail", modified);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
SaveKeyringParcel parcel = new SaveKeyringParcel();
|
||||||
|
parcel.mMasterKeyId = ring.getMasterKeyId();
|
||||||
|
parcel.mFingerprint = null;
|
||||||
|
|
||||||
|
WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0);
|
||||||
|
OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
|
||||||
|
UncachedKeyRing modified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0);
|
||||||
|
|
||||||
|
Assert.assertNull("keyring modification with null fingerprint should fail", modified);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0);
|
||||||
|
OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
|
||||||
|
UncachedKeyRing modified = op.modifySecretKeyRing(secretRing, parcel, "bad passphrase", log, 0);
|
||||||
|
|
||||||
|
Assert.assertNull("keyring modification with bad passphrase should fail", modified);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSubkeyAdd() throws Exception {
|
||||||
|
|
||||||
|
long expiry = new Date().getTime() / 1000 + 159;
|
||||||
|
int flags = KeyFlags.SIGN_DATA;
|
||||||
|
int bits = 1024 + new Random().nextInt(8);
|
||||||
|
parcel.mAddSubKeys.add(new SubkeyAdd(algorithm.rsa, bits, flags, expiry));
|
||||||
|
|
||||||
|
UncachedKeyRing modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB);
|
||||||
|
|
||||||
|
Assert.assertEquals("no extra packets in original", 0, onlyA.size());
|
||||||
|
Assert.assertEquals("exactly two extra packets in modified", 2, onlyB.size());
|
||||||
|
|
||||||
|
Packet p;
|
||||||
|
|
||||||
|
p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
|
||||||
|
Assert.assertTrue("first new packet must be secret subkey", p instanceof SecretSubkeyPacket);
|
||||||
|
|
||||||
|
p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(1).buf)).readPacket();
|
||||||
|
Assert.assertTrue("second new packet must be signature", p instanceof SignaturePacket);
|
||||||
|
Assert.assertEquals("signature type must be subkey binding certificate",
|
||||||
|
PGPSignature.SUBKEY_BINDING, ((SignaturePacket) p).getSignatureType());
|
||||||
|
Assert.assertEquals("signature must have been created by master key",
|
||||||
|
ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
|
||||||
|
|
||||||
|
// get new key from ring. it should be the last one (add a check to make sure?)
|
||||||
|
UncachedPublicKey newKey = null;
|
||||||
|
{
|
||||||
|
Iterator<UncachedPublicKey> it = modified.getPublicKeys();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
newKey = it.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.assertNotNull("new key is not null", newKey);
|
||||||
|
Assert.assertNotNull("added key must have an expiry date",
|
||||||
|
newKey.getExpiryTime());
|
||||||
|
Assert.assertEquals("added key must have expected expiry date",
|
||||||
|
expiry, newKey.getExpiryTime().getTime()/1000);
|
||||||
|
Assert.assertEquals("added key must have expected flags",
|
||||||
|
flags, newKey.getKeyUsage());
|
||||||
|
Assert.assertEquals("added key must have expected bitsize",
|
||||||
|
bits, newKey.getBitStrength());
|
||||||
|
|
||||||
|
{ // bad keysize should fail
|
||||||
|
parcel.reset();
|
||||||
|
parcel.mAddSubKeys.add(new SubkeyAdd(algorithm.rsa, 77, KeyFlags.SIGN_DATA, null));
|
||||||
|
|
||||||
|
WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0);
|
||||||
|
OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
|
||||||
|
modified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0);
|
||||||
|
|
||||||
|
Assert.assertNull("creating a subkey with keysize < 512 should fail", modified);
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // a past expiry should fail
|
||||||
|
parcel.reset();
|
||||||
|
parcel.mAddSubKeys.add(new SubkeyAdd(algorithm.rsa, 1024, KeyFlags.SIGN_DATA,
|
||||||
|
new Date().getTime()/1000-10));
|
||||||
|
|
||||||
|
WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0);
|
||||||
|
OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
|
||||||
|
modified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0);
|
||||||
|
|
||||||
|
Assert.assertNull("creating subkey with past expiry date should fail", modified);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSubkeyModify() throws Exception {
|
||||||
|
|
||||||
|
long expiry = new Date().getTime()/1000 + 1024;
|
||||||
|
long keyId;
|
||||||
|
{
|
||||||
|
Iterator<UncachedPublicKey> it = ring.getPublicKeys();
|
||||||
|
it.next();
|
||||||
|
keyId = it.next().getKeyId();
|
||||||
|
}
|
||||||
|
|
||||||
|
UncachedKeyRing modified = ring;
|
||||||
|
{
|
||||||
|
parcel.mChangeSubKeys.add(new SubkeyChange(keyId, null, expiry));
|
||||||
|
modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB);
|
||||||
|
|
||||||
|
Assert.assertEquals("one extra packet in original", 1, onlyA.size());
|
||||||
|
Assert.assertEquals("one extra packet in modified", 1, onlyB.size());
|
||||||
|
|
||||||
|
Assert.assertEquals("old packet must be signature",
|
||||||
|
PacketTags.SIGNATURE, onlyA.get(0).tag);
|
||||||
|
|
||||||
|
Packet p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
|
||||||
|
Assert.assertTrue("first new packet must be signature", p instanceof SignaturePacket);
|
||||||
|
Assert.assertEquals("signature type must be subkey binding certificate",
|
||||||
|
PGPSignature.SUBKEY_BINDING, ((SignaturePacket) p).getSignatureType());
|
||||||
|
Assert.assertEquals("signature must have been created by master key",
|
||||||
|
ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
|
||||||
|
|
||||||
|
Assert.assertNotNull("modified key must have an expiry date",
|
||||||
|
modified.getPublicKey(keyId).getExpiryTime());
|
||||||
|
Assert.assertEquals("modified key must have expected expiry date",
|
||||||
|
expiry, modified.getPublicKey(keyId).getExpiryTime().getTime()/1000);
|
||||||
|
Assert.assertEquals("modified key must have same flags as before",
|
||||||
|
ring.getPublicKey(keyId).getKeyUsage(), modified.getPublicKey(keyId).getKeyUsage());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
int flags = KeyFlags.SIGN_DATA | KeyFlags.ENCRYPT_COMMS;
|
||||||
|
parcel.reset();
|
||||||
|
parcel.mChangeSubKeys.add(new SubkeyChange(keyId, flags, null));
|
||||||
|
modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB);
|
||||||
|
|
||||||
|
Assert.assertEquals("old packet must be signature",
|
||||||
|
PacketTags.SIGNATURE, onlyA.get(0).tag);
|
||||||
|
|
||||||
|
Packet p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
|
||||||
|
Assert.assertTrue("first new packet must be signature", p instanceof SignaturePacket);
|
||||||
|
Assert.assertEquals("signature type must be subkey binding certificate",
|
||||||
|
PGPSignature.SUBKEY_BINDING, ((SignaturePacket) p).getSignatureType());
|
||||||
|
Assert.assertEquals("signature must have been created by master key",
|
||||||
|
ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
|
||||||
|
|
||||||
|
Assert.assertEquals("modified key must have expected flags",
|
||||||
|
flags, modified.getPublicKey(keyId).getKeyUsage());
|
||||||
|
Assert.assertNotNull("key must retain its expiry",
|
||||||
|
modified.getPublicKey(keyId).getExpiryTime());
|
||||||
|
Assert.assertEquals("key expiry must be unchanged",
|
||||||
|
expiry, modified.getPublicKey(keyId).getExpiryTime().getTime()/1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // a past expiry should fail
|
||||||
|
parcel.reset();
|
||||||
|
parcel.mChangeSubKeys.add(new SubkeyChange(keyId, null, new Date().getTime()/1000-10));
|
||||||
|
|
||||||
|
WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0);
|
||||||
|
OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
|
||||||
|
modified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0);
|
||||||
|
|
||||||
|
Assert.assertNull("setting subkey expiry to a past date should fail", modified);
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // modifying nonexistent keyring should fail
|
||||||
|
parcel.reset();
|
||||||
|
parcel.mChangeSubKeys.add(new SubkeyChange(123, null, null));
|
||||||
|
|
||||||
|
WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0);
|
||||||
|
OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
|
||||||
|
modified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0);
|
||||||
|
|
||||||
|
Assert.assertNull("modifying non-existent subkey should fail", modified);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSubkeyRevoke() throws Exception {
|
||||||
|
|
||||||
|
long keyId;
|
||||||
|
{
|
||||||
|
Iterator<UncachedPublicKey> it = ring.getPublicKeys();
|
||||||
|
it.next();
|
||||||
|
keyId = it.next().getKeyId();
|
||||||
|
}
|
||||||
|
|
||||||
|
int flags = ring.getPublicKey(keyId).getKeyUsage();
|
||||||
|
|
||||||
|
UncachedKeyRing modified;
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
parcel.reset();
|
||||||
|
parcel.mRevokeSubKeys.add(123L);
|
||||||
|
|
||||||
|
WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0);
|
||||||
|
OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
|
||||||
|
UncachedKeyRing otherModified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0);
|
||||||
|
|
||||||
|
Assert.assertNull("revoking a nonexistent subkey should fail", otherModified);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // revoked second subkey
|
||||||
|
|
||||||
|
parcel.reset();
|
||||||
|
parcel.mRevokeSubKeys.add(keyId);
|
||||||
|
|
||||||
|
modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB);
|
||||||
|
|
||||||
|
Assert.assertEquals("no extra packets in original", 0, onlyA.size());
|
||||||
|
Assert.assertEquals("exactly one extra packet in modified", 1, onlyB.size());
|
||||||
|
|
||||||
|
Packet p;
|
||||||
|
|
||||||
|
p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
|
||||||
|
Assert.assertTrue("first new packet must be secret subkey", p instanceof SignaturePacket);
|
||||||
|
Assert.assertEquals("signature type must be subkey binding certificate",
|
||||||
|
PGPSignature.SUBKEY_REVOCATION, ((SignaturePacket) p).getSignatureType());
|
||||||
|
Assert.assertEquals("signature must have been created by master key",
|
||||||
|
ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
|
||||||
|
|
||||||
|
Assert.assertTrue("subkey must actually be revoked",
|
||||||
|
modified.getPublicKey(keyId).isRevoked());
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // re-add second subkey
|
||||||
|
|
||||||
|
parcel.reset();
|
||||||
|
parcel.mChangeSubKeys.add(new SubkeyChange(keyId, null, null));
|
||||||
|
|
||||||
|
modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB);
|
||||||
|
|
||||||
|
Assert.assertEquals("exactly two outdated packets in original", 2, onlyA.size());
|
||||||
|
Assert.assertEquals("exactly one extra packet in modified", 1, onlyB.size());
|
||||||
|
|
||||||
|
Packet p;
|
||||||
|
|
||||||
|
p = new BCPGInputStream(new ByteArrayInputStream(onlyA.get(0).buf)).readPacket();
|
||||||
|
Assert.assertTrue("first outdated packet must be signature", p instanceof SignaturePacket);
|
||||||
|
Assert.assertEquals("first outdated signature type must be subkey binding certification",
|
||||||
|
PGPSignature.SUBKEY_BINDING, ((SignaturePacket) p).getSignatureType());
|
||||||
|
Assert.assertEquals("first outdated signature must have been created by master key",
|
||||||
|
ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
|
||||||
|
|
||||||
|
p = new BCPGInputStream(new ByteArrayInputStream(onlyA.get(1).buf)).readPacket();
|
||||||
|
Assert.assertTrue("second outdated packet must be signature", p instanceof SignaturePacket);
|
||||||
|
Assert.assertEquals("second outdated signature type must be subkey revocation",
|
||||||
|
PGPSignature.SUBKEY_REVOCATION, ((SignaturePacket) p).getSignatureType());
|
||||||
|
Assert.assertEquals("second outdated signature must have been created by master key",
|
||||||
|
ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
|
||||||
|
|
||||||
|
p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
|
||||||
|
Assert.assertTrue("new packet must be signature ", p instanceof SignaturePacket);
|
||||||
|
Assert.assertEquals("new signature type must be subkey binding certification",
|
||||||
|
PGPSignature.SUBKEY_BINDING, ((SignaturePacket) p).getSignatureType());
|
||||||
|
Assert.assertEquals("signature must have been created by master key",
|
||||||
|
ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
|
||||||
|
|
||||||
|
Assert.assertFalse("subkey must no longer be revoked",
|
||||||
|
modified.getPublicKey(keyId).isRevoked());
|
||||||
|
Assert.assertEquals("subkey must have the same usage flags as before",
|
||||||
|
flags, modified.getPublicKey(keyId).getKeyUsage());
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUserIdRevoke() throws Exception {
|
||||||
|
|
||||||
|
UncachedKeyRing modified;
|
||||||
|
String uid = ring.getPublicKey().getUnorderedUserIds().get(1);
|
||||||
|
|
||||||
|
{ // revoke second user id
|
||||||
|
|
||||||
|
parcel.mRevokeUserIds.add(uid);
|
||||||
|
|
||||||
|
modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB);
|
||||||
|
|
||||||
|
Assert.assertEquals("no extra packets in original", 0, onlyA.size());
|
||||||
|
Assert.assertEquals("exactly one extra packet in modified", 1, onlyB.size());
|
||||||
|
|
||||||
|
Packet p;
|
||||||
|
|
||||||
|
p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
|
||||||
|
Assert.assertTrue("first new packet must be secret subkey", p instanceof SignaturePacket);
|
||||||
|
Assert.assertEquals("signature type must be subkey binding certificate",
|
||||||
|
PGPSignature.CERTIFICATION_REVOCATION, ((SignaturePacket) p).getSignatureType());
|
||||||
|
Assert.assertEquals("signature must have been created by master key",
|
||||||
|
ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // re-add second user id
|
||||||
|
|
||||||
|
parcel.reset();
|
||||||
|
parcel.mChangePrimaryUserId = uid;
|
||||||
|
|
||||||
|
WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(modified.getEncoded(), false, 0);
|
||||||
|
OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
|
||||||
|
UncachedKeyRing otherModified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0);
|
||||||
|
|
||||||
|
Assert.assertNull("setting primary user id to a revoked user id should fail", otherModified);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // re-add second user id
|
||||||
|
|
||||||
|
parcel.reset();
|
||||||
|
parcel.mAddUserIds.add(uid);
|
||||||
|
|
||||||
|
applyModificationWithChecks(parcel, modified, onlyA, onlyB);
|
||||||
|
|
||||||
|
Assert.assertEquals("exactly two outdated packets in original", 2, onlyA.size());
|
||||||
|
Assert.assertEquals("exactly one extra packet in modified", 1, onlyB.size());
|
||||||
|
|
||||||
|
Packet p;
|
||||||
|
|
||||||
|
p = new BCPGInputStream(new ByteArrayInputStream(onlyA.get(0).buf)).readPacket();
|
||||||
|
Assert.assertTrue("first outdated packet must be signature", p instanceof SignaturePacket);
|
||||||
|
Assert.assertEquals("first outdated signature type must be positive certification",
|
||||||
|
PGPSignature.POSITIVE_CERTIFICATION, ((SignaturePacket) p).getSignatureType());
|
||||||
|
Assert.assertEquals("first outdated signature must have been created by master key",
|
||||||
|
ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
|
||||||
|
|
||||||
|
p = new BCPGInputStream(new ByteArrayInputStream(onlyA.get(1).buf)).readPacket();
|
||||||
|
Assert.assertTrue("second outdated packet must be signature", p instanceof SignaturePacket);
|
||||||
|
Assert.assertEquals("second outdated signature type must be certificate revocation",
|
||||||
|
PGPSignature.CERTIFICATION_REVOCATION, ((SignaturePacket) p).getSignatureType());
|
||||||
|
Assert.assertEquals("second outdated signature must have been created by master key",
|
||||||
|
ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
|
||||||
|
|
||||||
|
p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
|
||||||
|
Assert.assertTrue("new packet must be signature ", p instanceof SignaturePacket);
|
||||||
|
Assert.assertEquals("new signature type must be positive certification",
|
||||||
|
PGPSignature.POSITIVE_CERTIFICATION, ((SignaturePacket) p).getSignatureType());
|
||||||
|
Assert.assertEquals("signature must have been created by master key",
|
||||||
|
ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUserIdAdd() throws Exception {
|
||||||
|
|
||||||
|
parcel.mAddUserIds.add("rainbow");
|
||||||
|
|
||||||
|
UncachedKeyRing modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB);
|
||||||
|
|
||||||
|
Assert.assertTrue("keyring must contain added user id",
|
||||||
|
modified.getPublicKey().getUnorderedUserIds().contains("rainbow"));
|
||||||
|
|
||||||
|
Assert.assertEquals("no extra packets in original", 0, onlyA.size());
|
||||||
|
Assert.assertEquals("exactly two extra packets in modified", 2, onlyB.size());
|
||||||
|
|
||||||
|
Assert.assertTrue("keyring must contain added user id",
|
||||||
|
modified.getPublicKey().getUnorderedUserIds().contains("rainbow"));
|
||||||
|
|
||||||
|
Packet p;
|
||||||
|
|
||||||
|
p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
|
||||||
|
Assert.assertTrue("first new packet must be user id", p instanceof UserIDPacket);
|
||||||
|
Assert.assertEquals("user id packet must match added user id",
|
||||||
|
"rainbow", ((UserIDPacket) p).getID());
|
||||||
|
|
||||||
|
p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(1).buf)).readPacket();
|
||||||
|
Assert.assertTrue("second new packet must be signature", p instanceof SignaturePacket);
|
||||||
|
Assert.assertEquals("signature type must be positive certification",
|
||||||
|
PGPSignature.POSITIVE_CERTIFICATION, ((SignaturePacket) p).getSignatureType());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUserIdPrimary() throws Exception {
|
||||||
|
|
||||||
|
UncachedKeyRing modified = ring;
|
||||||
|
String uid = ring.getPublicKey().getUnorderedUserIds().get(1);
|
||||||
|
|
||||||
|
{ // first part, add new user id which is also primary
|
||||||
|
parcel.mAddUserIds.add("jack");
|
||||||
|
parcel.mChangePrimaryUserId = "jack";
|
||||||
|
|
||||||
|
modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB);
|
||||||
|
|
||||||
|
Assert.assertEquals("primary user id must be the one added",
|
||||||
|
"jack", modified.getPublicKey().getPrimaryUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // second part, change primary to a different one
|
||||||
|
parcel.reset();
|
||||||
|
parcel.mChangePrimaryUserId = uid;
|
||||||
|
|
||||||
|
modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB);
|
||||||
|
|
||||||
|
Assert.assertEquals("old keyring must have two outdated certificates", 2, onlyA.size());
|
||||||
|
Assert.assertEquals("new keyring must have two new packets", 2, onlyB.size());
|
||||||
|
|
||||||
|
Assert.assertEquals("primary user id must be the one changed to",
|
||||||
|
"pink", modified.getPublicKey().getPrimaryUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // third part, change primary to a non-existent one
|
||||||
|
parcel.reset();
|
||||||
|
//noinspection SpellCheckingInspection
|
||||||
|
parcel.mChangePrimaryUserId = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
|
|
||||||
|
WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0);
|
||||||
|
OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
|
||||||
|
modified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0);
|
||||||
|
|
||||||
|
Assert.assertNull("changing primary user id to a non-existent one should fail", modified);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for revoked primary user id already done in revoke test
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static UncachedKeyRing applyModificationWithChecks(SaveKeyringParcel parcel,
|
||||||
|
UncachedKeyRing ring,
|
||||||
|
ArrayList<RawPacket> onlyA,
|
||||||
|
ArrayList<RawPacket> onlyB) {
|
||||||
|
return applyModificationWithChecks(parcel, ring, onlyA, onlyB, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// applies a parcel modification while running some integrity checks
|
||||||
|
private static UncachedKeyRing applyModificationWithChecks(SaveKeyringParcel parcel,
|
||||||
|
UncachedKeyRing ring,
|
||||||
|
ArrayList<RawPacket> onlyA,
|
||||||
|
ArrayList<RawPacket> onlyB,
|
||||||
|
boolean canonicalize,
|
||||||
|
boolean constantCanonicalize) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
Assert.assertTrue("modified keyring must be secret", ring.isSecret());
|
||||||
|
WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0);
|
||||||
|
|
||||||
|
PgpKeyOperation op = new PgpKeyOperation(null);
|
||||||
|
OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
|
||||||
|
UncachedKeyRing rawModified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0);
|
||||||
|
Assert.assertNotNull("key modification failed", rawModified);
|
||||||
|
|
||||||
|
if (!canonicalize) {
|
||||||
|
Assert.assertTrue("keyring must differ from original", KeyringTestingHelper.diffKeyrings(
|
||||||
|
ring.getEncoded(), rawModified.getEncoded(), onlyA, onlyB));
|
||||||
|
return rawModified;
|
||||||
|
}
|
||||||
|
|
||||||
|
UncachedKeyRing modified = rawModified.canonicalize(log, 0);
|
||||||
|
if (constantCanonicalize) {
|
||||||
|
Assert.assertTrue("key must be constant through canonicalization",
|
||||||
|
!KeyringTestingHelper.diffKeyrings(
|
||||||
|
modified.getEncoded(), rawModified.getEncoded(), onlyA, onlyB)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Assert.assertTrue("keyring must differ from original", KeyringTestingHelper.diffKeyrings(
|
||||||
|
ring.getEncoded(), modified.getEncoded(), onlyA, onlyB));
|
||||||
|
return modified;
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssertionFailedError("error during encoding!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVerifySuccess() throws Exception {
|
||||||
|
|
||||||
|
UncachedKeyRing expectedKeyRing = KeyringBuilder.correctRing();
|
||||||
|
UncachedKeyRing inputKeyRing = KeyringBuilder.ringWithExtraIncorrectSignature();
|
||||||
|
|
||||||
|
OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
|
||||||
|
UncachedKeyRing canonicalizedRing = inputKeyRing.canonicalize(log, 0);
|
||||||
|
|
||||||
|
if (canonicalizedRing == null) {
|
||||||
|
throw new AssertionError("Canonicalization failed; messages: [" + log + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList onlyA = new ArrayList<RawPacket>();
|
||||||
|
ArrayList onlyB = new ArrayList<RawPacket>();
|
||||||
|
//noinspection unchecked
|
||||||
|
Assert.assertTrue("keyrings differ", !KeyringTestingHelper.diffKeyrings(
|
||||||
|
expectedKeyRing.getEncoded(), expectedKeyRing.getEncoded(), onlyA, onlyB));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Just testing my own test code. Should really be using a library for this.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testConcat() throws Exception {
|
||||||
|
byte[] actual = TestDataUtil.concatAll(new byte[]{1}, new byte[]{2,-2}, new byte[]{5},new byte[]{3});
|
||||||
|
byte[] expected = new byte[]{1,2,-2,5,3};
|
||||||
|
Assert.assertEquals(java.util.Arrays.toString(expected), java.util.Arrays.toString(actual));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -1,3 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* 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 tests;
|
package tests;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@ -9,9 +26,7 @@ import org.junit.Assert;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.robolectric.*;
|
import org.robolectric.*;
|
||||||
import org.openintents.openpgp.OpenPgpSignatureResult;
|
import org.sufficientlysecure.keychain.support.KeyringTestingHelper;
|
||||||
import org.sufficientlysecure.keychain.testsupport.KeyringTestingHelper;
|
|
||||||
import org.sufficientlysecure.keychain.testsupport.PgpVerifyTestingHelper;
|
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(RobolectricTestRunner.class)
|
||||||
@org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19
|
@org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19
|
||||||
@ -24,7 +39,7 @@ public class ProviderHelperKeyringTest {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
// @Test
|
||||||
public void testSavePublicKeyringRsa() throws Exception {
|
public void testSavePublicKeyringRsa() throws Exception {
|
||||||
Assert.assertTrue(new KeyringTestingHelper(Robolectric.application).addKeyring(prependResourcePath(Arrays.asList(
|
Assert.assertTrue(new KeyringTestingHelper(Robolectric.application).addKeyring(prependResourcePath(Arrays.asList(
|
||||||
"000001-006.public_key",
|
"000001-006.public_key",
|
||||||
@ -45,7 +60,7 @@ public class ProviderHelperKeyringTest {
|
|||||||
))));
|
))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
// @Test
|
||||||
public void testSavePublicKeyringDsa() throws Exception {
|
public void testSavePublicKeyringDsa() throws Exception {
|
||||||
Assert.assertTrue(new KeyringTestingHelper(Robolectric.application).addKeyring(prependResourcePath(Arrays.asList(
|
Assert.assertTrue(new KeyringTestingHelper(Robolectric.application).addKeyring(prependResourcePath(Arrays.asList(
|
||||||
"000016-006.public_key",
|
"000016-006.public_key",
|
||||||
@ -62,7 +77,7 @@ public class ProviderHelperKeyringTest {
|
|||||||
))));
|
))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
// @Test
|
||||||
public void testSavePublicKeyringDsa2() throws Exception {
|
public void testSavePublicKeyringDsa2() throws Exception {
|
||||||
Assert.assertTrue(new KeyringTestingHelper(Robolectric.application).addKeyring(prependResourcePath(Arrays.asList(
|
Assert.assertTrue(new KeyringTestingHelper(Robolectric.application).addKeyring(prependResourcePath(Arrays.asList(
|
||||||
"000027-006.public_key",
|
"000027-006.public_key",
|
@ -0,0 +1,106 @@
|
|||||||
|
package org.sufficientlysecure.keychain.tests;
|
||||||
|
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.robolectric.RobolectricTestRunner;
|
||||||
|
import org.robolectric.shadows.ShadowLog;
|
||||||
|
import org.spongycastle.bcpg.BCPGInputStream;
|
||||||
|
import org.spongycastle.bcpg.Packet;
|
||||||
|
import org.spongycastle.bcpg.PacketTags;
|
||||||
|
import org.spongycastle.bcpg.UserIDPacket;
|
||||||
|
import org.spongycastle.bcpg.sig.KeyFlags;
|
||||||
|
import org.sufficientlysecure.keychain.Constants;
|
||||||
|
import org.sufficientlysecure.keychain.pgp.PgpKeyOperation;
|
||||||
|
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||||
|
import org.sufficientlysecure.keychain.pgp.UncachedPublicKey;
|
||||||
|
import org.sufficientlysecure.keychain.pgp.WrappedSignature;
|
||||||
|
import org.sufficientlysecure.keychain.service.OperationResultParcel;
|
||||||
|
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
|
||||||
|
import org.sufficientlysecure.keychain.support.KeyringTestingHelper;
|
||||||
|
import org.sufficientlysecure.keychain.support.KeyringTestingHelper.RawPacket;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner.class)
|
||||||
|
@org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19
|
||||||
|
public class UncachedKeyringTest {
|
||||||
|
|
||||||
|
static UncachedKeyRing staticRing;
|
||||||
|
UncachedKeyRing ring;
|
||||||
|
ArrayList<RawPacket> onlyA = new ArrayList<RawPacket>();
|
||||||
|
ArrayList<RawPacket> onlyB = new ArrayList<RawPacket>();
|
||||||
|
OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setUpOnce() throws Exception {
|
||||||
|
ShadowLog.stream = System.out;
|
||||||
|
|
||||||
|
SaveKeyringParcel parcel = new SaveKeyringParcel();
|
||||||
|
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
|
||||||
|
Constants.choice.algorithm.rsa, 1024, KeyFlags.CERTIFY_OTHER, null));
|
||||||
|
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
|
||||||
|
Constants.choice.algorithm.rsa, 1024, KeyFlags.SIGN_DATA, null));
|
||||||
|
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
|
||||||
|
Constants.choice.algorithm.rsa, 1024, KeyFlags.ENCRYPT_COMMS, null));
|
||||||
|
|
||||||
|
parcel.mAddUserIds.add("twi");
|
||||||
|
parcel.mAddUserIds.add("pink");
|
||||||
|
// passphrase is tested in PgpKeyOperationTest, just use empty here
|
||||||
|
parcel.mNewPassphrase = "";
|
||||||
|
PgpKeyOperation op = new PgpKeyOperation(null);
|
||||||
|
|
||||||
|
OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
|
||||||
|
staticRing = op.createSecretKeyRing(parcel, log, 0);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void testGeneratedRingStructure() throws Exception {
|
||||||
|
|
||||||
|
Iterator<RawPacket> it = KeyringTestingHelper.parseKeyring(ring.getEncoded());
|
||||||
|
|
||||||
|
Assert.assertEquals("packet #1 should be secret key",
|
||||||
|
PacketTags.SECRET_KEY, it.next().tag);
|
||||||
|
|
||||||
|
Assert.assertEquals("packet #2 should be secret key",
|
||||||
|
PacketTags.USER_ID, it.next().tag);
|
||||||
|
Assert.assertEquals("packet #3 should be secret key",
|
||||||
|
PacketTags.SIGNATURE, it.next().tag);
|
||||||
|
|
||||||
|
Assert.assertEquals("packet #4 should be secret key",
|
||||||
|
PacketTags.USER_ID, it.next().tag);
|
||||||
|
Assert.assertEquals("packet #5 should be secret key",
|
||||||
|
PacketTags.SIGNATURE, it.next().tag);
|
||||||
|
|
||||||
|
Assert.assertEquals("packet #6 should be secret key",
|
||||||
|
PacketTags.SECRET_SUBKEY, it.next().tag);
|
||||||
|
Assert.assertEquals("packet #7 should be secret key",
|
||||||
|
PacketTags.SIGNATURE, it.next().tag);
|
||||||
|
|
||||||
|
Assert.assertEquals("packet #8 should be secret key",
|
||||||
|
PacketTags.SECRET_SUBKEY, it.next().tag);
|
||||||
|
Assert.assertEquals("packet #9 should be secret key",
|
||||||
|
PacketTags.SIGNATURE, it.next().tag);
|
||||||
|
|
||||||
|
Assert.assertFalse("exactly 9 packets total", it.hasNext());
|
||||||
|
|
||||||
|
Assert.assertArrayEquals("created keyring should be constant through canonicalization",
|
||||||
|
ring.getEncoded(), ring.canonicalize(log, 0).getEncoded());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Binary file not shown.
@ -21,13 +21,6 @@ dependencies {
|
|||||||
compile project(':extern:minidns')
|
compile project(':extern:minidns')
|
||||||
compile project(':extern:KeybaseLib:Lib')
|
compile project(':extern:KeybaseLib:Lib')
|
||||||
|
|
||||||
|
|
||||||
// Unit tests are run with Robolectric
|
|
||||||
testCompile 'junit:junit:4.11'
|
|
||||||
testCompile 'org.robolectric:robolectric:2.3'
|
|
||||||
testCompile 'com.squareup:fest-android:1.0.8'
|
|
||||||
testCompile 'com.google.android:android:4.1.1.4'
|
|
||||||
// compile dependencies are automatically also included in testCompile
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -86,7 +79,7 @@ android {
|
|||||||
|
|
||||||
// NOTE: This disables Lint!
|
// NOTE: This disables Lint!
|
||||||
tasks.whenTaskAdded { task ->
|
tasks.whenTaskAdded { task ->
|
||||||
if (task.name.equals("lint")) {
|
if (task.name.contains("lint")) {
|
||||||
task.enabled = false
|
task.enabled = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,10 +65,8 @@ import java.security.NoSuchProviderException;
|
|||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.security.SignatureException;
|
import java.security.SignatureException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is the single place where ALL operations that actually modify a PGP public or secret
|
* This class is the single place where ALL operations that actually modify a PGP public or secret
|
||||||
@ -104,11 +102,12 @@ public class PgpKeyOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Creates new secret key. */
|
/** Creates new secret key. */
|
||||||
private PGPKeyPair createKey(int algorithmChoice, int keySize) throws PgpGeneralMsgIdException {
|
private PGPKeyPair createKey(int algorithmChoice, int keySize, OperationLog log, int indent) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (keySize < 512) {
|
if (keySize < 512) {
|
||||||
throw new PgpGeneralMsgIdException(R.string.error_key_size_minimum512bit);
|
log.add(LogLevel.ERROR, LogType.MSG_CR_ERROR_KEYSIZE_512, indent);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
int algorithm;
|
int algorithm;
|
||||||
@ -143,7 +142,8 @@ public class PgpKeyOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
throw new PgpGeneralMsgIdException(R.string.error_unknown_algorithm_choice);
|
log.add(LogLevel.ERROR, LogType.MSG_CR_ERROR_UNKNOWN_ALGO, indent);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,7 +157,9 @@ public class PgpKeyOperation {
|
|||||||
} catch(InvalidAlgorithmParameterException e) {
|
} catch(InvalidAlgorithmParameterException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
} catch(PGPException e) {
|
} catch(PGPException e) {
|
||||||
throw new PgpGeneralMsgIdException(R.string.msg_mf_error_pgp, e);
|
Log.e(Constants.TAG, "internal pgp error", e);
|
||||||
|
log.add(LogLevel.ERROR, LogType.MSG_CR_ERROR_INTERNAL_PGP, indent);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,20 +168,36 @@ public class PgpKeyOperation {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
log.add(LogLevel.ERROR, LogType.MSG_MF_ERROR_KEYID, indent);
|
log.add(LogLevel.START, LogType.MSG_CR, indent);
|
||||||
indent += 1;
|
indent += 1;
|
||||||
updateProgress(R.string.progress_building_key, 0, 100);
|
updateProgress(R.string.progress_building_key, 0, 100);
|
||||||
|
|
||||||
if (saveParcel.addSubKeys == null || saveParcel.addSubKeys.isEmpty()) {
|
if (saveParcel.mAddSubKeys.isEmpty()) {
|
||||||
log.add(LogLevel.ERROR, LogType.MSG_CR_ERROR_NO_MASTER, indent);
|
log.add(LogLevel.ERROR, LogType.MSG_CR_ERROR_NO_MASTER, indent);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
SubkeyAdd add = saveParcel.addSubKeys.remove(0);
|
if (saveParcel.mAddUserIds.isEmpty()) {
|
||||||
PGPKeyPair keyPair = createKey(add.mAlgorithm, add.mKeysize);
|
log.add(LogLevel.ERROR, LogType.MSG_CR_ERROR_NO_USER_ID, indent);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
SubkeyAdd add = saveParcel.mAddSubKeys.remove(0);
|
||||||
|
if ((add.mFlags & KeyFlags.CERTIFY_OTHER) != KeyFlags.CERTIFY_OTHER) {
|
||||||
|
log.add(LogLevel.ERROR, LogType.MSG_CR_ERROR_NO_CERTIFY, indent);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (add.mAlgorithm == Constants.choice.algorithm.elgamal) {
|
if (add.mAlgorithm == Constants.choice.algorithm.elgamal) {
|
||||||
throw new PgpGeneralMsgIdException(R.string.error_master_key_must_not_be_el_gamal);
|
log.add(LogLevel.ERROR, LogType.MSG_CR_ERROR_MASTER_ELGAMAL, indent);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
PGPKeyPair keyPair = createKey(add.mAlgorithm, add.mKeysize, log, indent);
|
||||||
|
|
||||||
|
// return null if this failed (an error will already have been logged by createKey)
|
||||||
|
if (keyPair == null) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// define hashing and signing algos
|
// define hashing and signing algos
|
||||||
@ -195,17 +213,15 @@ public class PgpKeyOperation {
|
|||||||
PGPSecretKeyRing sKR = new PGPSecretKeyRing(
|
PGPSecretKeyRing sKR = new PGPSecretKeyRing(
|
||||||
masterSecretKey.getEncoded(), new JcaKeyFingerprintCalculator());
|
masterSecretKey.getEncoded(), new JcaKeyFingerprintCalculator());
|
||||||
|
|
||||||
return internal(sKR, masterSecretKey, saveParcel, "", log, indent);
|
return internal(sKR, masterSecretKey, add.mFlags, saveParcel, "", log, indent);
|
||||||
|
|
||||||
} catch (PGPException e) {
|
} catch (PGPException e) {
|
||||||
|
log.add(LogLevel.ERROR, LogType.MSG_CR_ERROR_INTERNAL_PGP, indent);
|
||||||
Log.e(Constants.TAG, "pgp error encoding key", e);
|
Log.e(Constants.TAG, "pgp error encoding key", e);
|
||||||
return null;
|
return null;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(Constants.TAG, "io error encoding key", e);
|
Log.e(Constants.TAG, "io error encoding key", e);
|
||||||
return null;
|
return null;
|
||||||
} catch (PgpGeneralMsgIdException e) {
|
|
||||||
Log.e(Constants.TAG, "pgp msg id error", e);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -257,11 +273,16 @@ public class PgpKeyOperation {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return internal(sKR, masterSecretKey, saveParcel, passphrase, log, indent);
|
// read masterKeyFlags, and use the same as before.
|
||||||
|
// since this is the master key, this contains at least CERTIFY_OTHER
|
||||||
|
int masterKeyFlags = readKeyFlags(masterSecretKey.getPublicKey()) | KeyFlags.CERTIFY_OTHER;
|
||||||
|
|
||||||
|
return internal(sKR, masterSecretKey, masterKeyFlags, saveParcel, passphrase, log, indent);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private UncachedKeyRing internal(PGPSecretKeyRing sKR, PGPSecretKey masterSecretKey,
|
private UncachedKeyRing internal(PGPSecretKeyRing sKR, PGPSecretKey masterSecretKey,
|
||||||
|
int masterKeyFlags,
|
||||||
SaveKeyringParcel saveParcel, String passphrase,
|
SaveKeyringParcel saveParcel, String passphrase,
|
||||||
OperationLog log, int indent) {
|
OperationLog log, int indent) {
|
||||||
|
|
||||||
@ -289,7 +310,7 @@ public class PgpKeyOperation {
|
|||||||
PGPPublicKey modifiedPublicKey = masterPublicKey;
|
PGPPublicKey modifiedPublicKey = masterPublicKey;
|
||||||
|
|
||||||
// 2a. Add certificates for new user ids
|
// 2a. Add certificates for new user ids
|
||||||
for (String userId : saveParcel.addUserIds) {
|
for (String userId : saveParcel.mAddUserIds) {
|
||||||
log.add(LogLevel.INFO, LogType.MSG_MF_UID_ADD, indent);
|
log.add(LogLevel.INFO, LogType.MSG_MF_UID_ADD, indent);
|
||||||
|
|
||||||
// this operation supersedes all previous binding and revocation certificates,
|
// this operation supersedes all previous binding and revocation certificates,
|
||||||
@ -298,9 +319,10 @@ public class PgpKeyOperation {
|
|||||||
Iterator<PGPSignature> it = modifiedPublicKey.getSignaturesForID(userId);
|
Iterator<PGPSignature> it = modifiedPublicKey.getSignaturesForID(userId);
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
for (PGPSignature cert : new IterableIterator<PGPSignature>(it)) {
|
for (PGPSignature cert : new IterableIterator<PGPSignature>(it)) {
|
||||||
// if it's not a self cert, never mind
|
|
||||||
if (cert.getKeyID() != masterPublicKey.getKeyID()) {
|
if (cert.getKeyID() != masterPublicKey.getKeyID()) {
|
||||||
continue;
|
// foreign certificate?! error error error
|
||||||
|
log.add(LogLevel.ERROR, LogType.MSG_MF_ERROR_INTEGRITY, indent);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
if (cert.getSignatureType() == PGPSignature.CERTIFICATION_REVOCATION
|
if (cert.getSignatureType() == PGPSignature.CERTIFICATION_REVOCATION
|
||||||
|| cert.getSignatureType() == PGPSignature.NO_CERTIFICATION
|
|| cert.getSignatureType() == PGPSignature.NO_CERTIFICATION
|
||||||
@ -314,27 +336,31 @@ public class PgpKeyOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if it's supposed to be primary, we can do that here as well
|
// if it's supposed to be primary, we can do that here as well
|
||||||
boolean isPrimary = saveParcel.changePrimaryUserId != null
|
boolean isPrimary = saveParcel.mChangePrimaryUserId != null
|
||||||
&& userId.equals(saveParcel.changePrimaryUserId);
|
&& userId.equals(saveParcel.mChangePrimaryUserId);
|
||||||
// generate and add new certificate
|
// generate and add new certificate
|
||||||
PGPSignature cert = generateUserIdSignature(masterPrivateKey,
|
PGPSignature cert = generateUserIdSignature(masterPrivateKey,
|
||||||
masterPublicKey, userId, isPrimary);
|
masterPublicKey, userId, isPrimary, masterKeyFlags);
|
||||||
modifiedPublicKey = PGPPublicKey.addCertification(masterPublicKey, userId, cert);
|
modifiedPublicKey = PGPPublicKey.addCertification(modifiedPublicKey, userId, cert);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2b. Add revocations for revoked user ids
|
// 2b. Add revocations for revoked user ids
|
||||||
for (String userId : saveParcel.revokeUserIds) {
|
for (String userId : saveParcel.mRevokeUserIds) {
|
||||||
log.add(LogLevel.INFO, LogType.MSG_MF_UID_REVOKE, indent);
|
log.add(LogLevel.INFO, LogType.MSG_MF_UID_REVOKE, indent);
|
||||||
// a duplicate revocatin will be removed during canonicalization, so no need to
|
// a duplicate revocation will be removed during canonicalization, so no need to
|
||||||
// take care of that here.
|
// take care of that here.
|
||||||
PGPSignature cert = generateRevocationSignature(masterPrivateKey,
|
PGPSignature cert = generateRevocationSignature(masterPrivateKey,
|
||||||
masterPublicKey, userId);
|
masterPublicKey, userId);
|
||||||
modifiedPublicKey = PGPPublicKey.addCertification(masterPublicKey, userId, cert);
|
modifiedPublicKey = PGPPublicKey.addCertification(modifiedPublicKey, userId, cert);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. If primary user id changed, generate new certificates for both old and new
|
// 3. If primary user id changed, generate new certificates for both old and new
|
||||||
if (saveParcel.changePrimaryUserId != null) {
|
if (saveParcel.mChangePrimaryUserId != null) {
|
||||||
|
|
||||||
|
// keep track if we actually changed one
|
||||||
|
boolean ok = false;
|
||||||
log.add(LogLevel.INFO, LogType.MSG_MF_UID_PRIMARY, indent);
|
log.add(LogLevel.INFO, LogType.MSG_MF_UID_PRIMARY, indent);
|
||||||
|
indent += 1;
|
||||||
|
|
||||||
// we work on the modifiedPublicKey here, to respect new or newly revoked uids
|
// we work on the modifiedPublicKey here, to respect new or newly revoked uids
|
||||||
// noinspection unchecked
|
// noinspection unchecked
|
||||||
@ -343,10 +369,11 @@ public class PgpKeyOperation {
|
|||||||
PGPSignature currentCert = null;
|
PGPSignature currentCert = null;
|
||||||
// noinspection unchecked
|
// noinspection unchecked
|
||||||
for (PGPSignature cert : new IterableIterator<PGPSignature>(
|
for (PGPSignature cert : new IterableIterator<PGPSignature>(
|
||||||
masterPublicKey.getSignaturesForID(userId))) {
|
modifiedPublicKey.getSignaturesForID(userId))) {
|
||||||
// if it's not a self cert, never mind
|
|
||||||
if (cert.getKeyID() != masterPublicKey.getKeyID()) {
|
if (cert.getKeyID() != masterPublicKey.getKeyID()) {
|
||||||
continue;
|
// foreign certificate?! error error error
|
||||||
|
log.add(LogLevel.ERROR, LogType.MSG_MF_ERROR_INTEGRITY, indent);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
// we know from canonicalization that if there is any revocation here, it
|
// we know from canonicalization that if there is any revocation here, it
|
||||||
// is valid and not superseded by a newer certification.
|
// is valid and not superseded by a newer certification.
|
||||||
@ -373,7 +400,7 @@ public class PgpKeyOperation {
|
|||||||
// we definitely should not update certifications of revoked keys, so just leave it.
|
// we definitely should not update certifications of revoked keys, so just leave it.
|
||||||
if (isRevoked) {
|
if (isRevoked) {
|
||||||
// revoked user ids cannot be primary!
|
// revoked user ids cannot be primary!
|
||||||
if (userId.equals(saveParcel.changePrimaryUserId)) {
|
if (userId.equals(saveParcel.mChangePrimaryUserId)) {
|
||||||
log.add(LogLevel.ERROR, LogType.MSG_MF_ERROR_REVOKED_PRIMARY, indent);
|
log.add(LogLevel.ERROR, LogType.MSG_MF_ERROR_REVOKED_PRIMARY, indent);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -383,14 +410,16 @@ public class PgpKeyOperation {
|
|||||||
// if this is~ the/a primary user id
|
// if this is~ the/a primary user id
|
||||||
if (currentCert.hasSubpackets() && currentCert.getHashedSubPackets().isPrimaryUserID()) {
|
if (currentCert.hasSubpackets() && currentCert.getHashedSubPackets().isPrimaryUserID()) {
|
||||||
// if it's the one we want, just leave it as is
|
// if it's the one we want, just leave it as is
|
||||||
if (userId.equals(saveParcel.changePrimaryUserId)) {
|
if (userId.equals(saveParcel.mChangePrimaryUserId)) {
|
||||||
|
ok = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// otherwise, generate new non-primary certification
|
// otherwise, generate new non-primary certification
|
||||||
|
log.add(LogLevel.DEBUG, LogType.MSG_MF_PRIMARY_REPLACE_OLD, indent);
|
||||||
modifiedPublicKey = PGPPublicKey.removeCertification(
|
modifiedPublicKey = PGPPublicKey.removeCertification(
|
||||||
modifiedPublicKey, userId, currentCert);
|
modifiedPublicKey, userId, currentCert);
|
||||||
PGPSignature newCert = generateUserIdSignature(
|
PGPSignature newCert = generateUserIdSignature(
|
||||||
masterPrivateKey, masterPublicKey, userId, false);
|
masterPrivateKey, masterPublicKey, userId, false, masterKeyFlags);
|
||||||
modifiedPublicKey = PGPPublicKey.addCertification(
|
modifiedPublicKey = PGPPublicKey.addCertification(
|
||||||
modifiedPublicKey, userId, newCert);
|
modifiedPublicKey, userId, newCert);
|
||||||
continue;
|
continue;
|
||||||
@ -399,20 +428,28 @@ public class PgpKeyOperation {
|
|||||||
// if we are here, this is not currently a primary user id
|
// if we are here, this is not currently a primary user id
|
||||||
|
|
||||||
// if it should be
|
// if it should be
|
||||||
if (userId.equals(saveParcel.changePrimaryUserId)) {
|
if (userId.equals(saveParcel.mChangePrimaryUserId)) {
|
||||||
// add shiny new primary user id certificate
|
// add shiny new primary user id certificate
|
||||||
|
log.add(LogLevel.DEBUG, LogType.MSG_MF_PRIMARY_NEW, indent);
|
||||||
modifiedPublicKey = PGPPublicKey.removeCertification(
|
modifiedPublicKey = PGPPublicKey.removeCertification(
|
||||||
modifiedPublicKey, userId, currentCert);
|
modifiedPublicKey, userId, currentCert);
|
||||||
PGPSignature newCert = generateUserIdSignature(
|
PGPSignature newCert = generateUserIdSignature(
|
||||||
masterPrivateKey, masterPublicKey, userId, true);
|
masterPrivateKey, masterPublicKey, userId, true, masterKeyFlags);
|
||||||
modifiedPublicKey = PGPPublicKey.addCertification(
|
modifiedPublicKey = PGPPublicKey.addCertification(
|
||||||
modifiedPublicKey, userId, newCert);
|
modifiedPublicKey, userId, newCert);
|
||||||
|
ok = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// user id is not primary and is not supposed to be - nothing to do here.
|
// user id is not primary and is not supposed to be - nothing to do here.
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
indent -= 1;
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
log.add(LogLevel.ERROR, LogType.MSG_MF_ERROR_NOEXIST_PRIMARY, indent);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the secret key ring
|
// Update the secret key ring
|
||||||
@ -423,9 +460,16 @@ public class PgpKeyOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4a. For each subkey change, generate new subkey binding certificate
|
// 4a. For each subkey change, generate new subkey binding certificate
|
||||||
for (SaveKeyringParcel.SubkeyChange change : saveParcel.changeSubKeys) {
|
for (SaveKeyringParcel.SubkeyChange change : saveParcel.mChangeSubKeys) {
|
||||||
log.add(LogLevel.INFO, LogType.MSG_MF_SUBKEY_CHANGE,
|
log.add(LogLevel.INFO, LogType.MSG_MF_SUBKEY_CHANGE,
|
||||||
indent, PgpKeyHelper.convertKeyIdToHex(change.mKeyId));
|
indent, PgpKeyHelper.convertKeyIdToHex(change.mKeyId));
|
||||||
|
|
||||||
|
// TODO allow changes in master key? this implies generating new user id certs...
|
||||||
|
if (change.mKeyId == masterPublicKey.getKeyID()) {
|
||||||
|
Log.e(Constants.TAG, "changing the master key not supported");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
PGPSecretKey sKey = sKR.getSecretKey(change.mKeyId);
|
PGPSecretKey sKey = sKR.getSecretKey(change.mKeyId);
|
||||||
if (sKey == null) {
|
if (sKey == null) {
|
||||||
log.add(LogLevel.ERROR, LogType.MSG_MF_SUBKEY_MISSING,
|
log.add(LogLevel.ERROR, LogType.MSG_MF_SUBKEY_MISSING,
|
||||||
@ -434,22 +478,45 @@ public class PgpKeyOperation {
|
|||||||
}
|
}
|
||||||
PGPPublicKey pKey = sKey.getPublicKey();
|
PGPPublicKey pKey = sKey.getPublicKey();
|
||||||
|
|
||||||
if (change.mExpiry != null && new Date(change.mExpiry).before(new Date())) {
|
// expiry must not be in the past
|
||||||
|
if (change.mExpiry != null && new Date(change.mExpiry*1000).before(new Date())) {
|
||||||
log.add(LogLevel.ERROR, LogType.MSG_MF_SUBKEY_PAST_EXPIRY,
|
log.add(LogLevel.ERROR, LogType.MSG_MF_SUBKEY_PAST_EXPIRY,
|
||||||
indent + 1, PgpKeyHelper.convertKeyIdToHex(change.mKeyId));
|
indent + 1, PgpKeyHelper.convertKeyIdToHex(change.mKeyId));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate and add new signature. we can be sloppy here and just leave the old one,
|
// keep old flags, or replace with new ones
|
||||||
// it will be removed during canonicalization
|
int flags = change.mFlags == null ? readKeyFlags(pKey) : change.mFlags;
|
||||||
|
long expiry;
|
||||||
|
if (change.mExpiry == null) {
|
||||||
|
long valid = pKey.getValidSeconds();
|
||||||
|
expiry = valid == 0
|
||||||
|
? 0
|
||||||
|
: pKey.getCreationTime().getTime() / 1000 + pKey.getValidSeconds();
|
||||||
|
} else {
|
||||||
|
expiry = change.mExpiry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// drop all old signatures, they will be superseded by the new one
|
||||||
|
//noinspection unchecked
|
||||||
|
for (PGPSignature sig : new IterableIterator<PGPSignature>(pKey.getSignatures())) {
|
||||||
|
// special case: if there is a revocation, don't use expiry from before
|
||||||
|
if (change.mExpiry == null
|
||||||
|
&& sig.getSignatureType() == PGPSignature.SUBKEY_REVOCATION) {
|
||||||
|
expiry = 0;
|
||||||
|
}
|
||||||
|
pKey = PGPPublicKey.removeCertification(pKey, sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate and add new signature
|
||||||
PGPSignature sig = generateSubkeyBindingSignature(masterPublicKey, masterPrivateKey,
|
PGPSignature sig = generateSubkeyBindingSignature(masterPublicKey, masterPrivateKey,
|
||||||
sKey, pKey, change.mFlags, change.mExpiry, passphrase);
|
sKey, pKey, flags, expiry, passphrase);
|
||||||
pKey = PGPPublicKey.addCertification(pKey, sig);
|
pKey = PGPPublicKey.addCertification(pKey, sig);
|
||||||
sKR = PGPSecretKeyRing.insertSecretKey(sKR, PGPSecretKey.replacePublicKey(sKey, pKey));
|
sKR = PGPSecretKeyRing.insertSecretKey(sKR, PGPSecretKey.replacePublicKey(sKey, pKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4b. For each subkey revocation, generate new subkey revocation certificate
|
// 4b. For each subkey revocation, generate new subkey revocation certificate
|
||||||
for (long revocation : saveParcel.revokeSubKeys) {
|
for (long revocation : saveParcel.mRevokeSubKeys) {
|
||||||
log.add(LogLevel.INFO, LogType.MSG_MF_SUBKEY_REVOKE,
|
log.add(LogLevel.INFO, LogType.MSG_MF_SUBKEY_REVOKE,
|
||||||
indent, PgpKeyHelper.convertKeyIdToHex(revocation));
|
indent, PgpKeyHelper.convertKeyIdToHex(revocation));
|
||||||
PGPSecretKey sKey = sKR.getSecretKey(revocation);
|
PGPSecretKey sKey = sKR.getSecretKey(revocation);
|
||||||
@ -468,10 +535,9 @@ public class PgpKeyOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. Generate and add new subkeys
|
// 5. Generate and add new subkeys
|
||||||
for (SaveKeyringParcel.SubkeyAdd add : saveParcel.addSubKeys) {
|
for (SaveKeyringParcel.SubkeyAdd add : saveParcel.mAddSubKeys) {
|
||||||
try {
|
|
||||||
|
|
||||||
if (add.mExpiry != null && new Date(add.mExpiry).before(new Date())) {
|
if (add.mExpiry != null && new Date(add.mExpiry*1000).before(new Date())) {
|
||||||
log.add(LogLevel.ERROR, LogType.MSG_MF_SUBKEY_PAST_EXPIRY, indent +1);
|
log.add(LogLevel.ERROR, LogType.MSG_MF_SUBKEY_PAST_EXPIRY, indent +1);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -479,13 +545,16 @@ public class PgpKeyOperation {
|
|||||||
log.add(LogLevel.INFO, LogType.MSG_MF_SUBKEY_NEW, indent);
|
log.add(LogLevel.INFO, LogType.MSG_MF_SUBKEY_NEW, indent);
|
||||||
|
|
||||||
// generate a new secret key (privkey only for now)
|
// generate a new secret key (privkey only for now)
|
||||||
PGPKeyPair keyPair = createKey(add.mAlgorithm, add.mKeysize);
|
PGPKeyPair keyPair = createKey(add.mAlgorithm, add.mKeysize, log, indent);
|
||||||
|
if(keyPair == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// add subkey binding signature (making this a sub rather than master key)
|
// add subkey binding signature (making this a sub rather than master key)
|
||||||
PGPPublicKey pKey = keyPair.getPublicKey();
|
PGPPublicKey pKey = keyPair.getPublicKey();
|
||||||
PGPSignature cert = generateSubkeyBindingSignature(
|
PGPSignature cert = generateSubkeyBindingSignature(
|
||||||
masterPublicKey, masterPrivateKey, keyPair.getPrivateKey(), pKey,
|
masterPublicKey, masterPrivateKey, keyPair.getPrivateKey(), pKey,
|
||||||
add.mFlags, add.mExpiry);
|
add.mFlags, add.mExpiry == null ? 0 : add.mExpiry);
|
||||||
pKey = PGPPublicKey.addSubkeyBindingCertification(pKey, cert);
|
pKey = PGPPublicKey.addSubkeyBindingCertification(pKey, cert);
|
||||||
|
|
||||||
PGPSecretKey sKey; {
|
PGPSecretKey sKey; {
|
||||||
@ -507,13 +576,10 @@ public class PgpKeyOperation {
|
|||||||
|
|
||||||
sKR = PGPSecretKeyRing.insertSecretKey(sKR, sKey);
|
sKR = PGPSecretKeyRing.insertSecretKey(sKR, sKey);
|
||||||
|
|
||||||
} catch (PgpGeneralMsgIdException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. If requested, change passphrase
|
// 6. If requested, change passphrase
|
||||||
if (saveParcel.newPassphrase != null) {
|
if (saveParcel.mNewPassphrase != null) {
|
||||||
log.add(LogLevel.INFO, LogType.MSG_MF_PASSPHRASE, indent);
|
log.add(LogLevel.INFO, LogType.MSG_MF_PASSPHRASE, indent);
|
||||||
PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build()
|
PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build()
|
||||||
.get(HashAlgorithmTags.SHA1);
|
.get(HashAlgorithmTags.SHA1);
|
||||||
@ -523,7 +589,7 @@ public class PgpKeyOperation {
|
|||||||
PBESecretKeyEncryptor keyEncryptorNew = new JcePBESecretKeyEncryptorBuilder(
|
PBESecretKeyEncryptor keyEncryptorNew = new JcePBESecretKeyEncryptorBuilder(
|
||||||
PGPEncryptedData.CAST5, sha1Calc)
|
PGPEncryptedData.CAST5, sha1Calc)
|
||||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
|
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
|
||||||
saveParcel.newPassphrase.toCharArray());
|
saveParcel.mNewPassphrase.toCharArray());
|
||||||
|
|
||||||
sKR = PGPSecretKeyRing.copyWithNewPassword(sKR, keyDecryptor, keyEncryptorNew);
|
sKR = PGPSecretKeyRing.copyWithNewPassword(sKR, keyDecryptor, keyEncryptorNew);
|
||||||
}
|
}
|
||||||
@ -546,7 +612,7 @@ public class PgpKeyOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static PGPSignature generateUserIdSignature(
|
private static PGPSignature generateUserIdSignature(
|
||||||
PGPPrivateKey masterPrivateKey, PGPPublicKey pKey, String userId, boolean primary)
|
PGPPrivateKey masterPrivateKey, PGPPublicKey pKey, String userId, boolean primary, int flags)
|
||||||
throws IOException, PGPException, SignatureException {
|
throws IOException, PGPException, SignatureException {
|
||||||
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
|
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
|
||||||
pKey.getAlgorithm(), PGPUtil.SHA1)
|
pKey.getAlgorithm(), PGPUtil.SHA1)
|
||||||
@ -558,6 +624,7 @@ public class PgpKeyOperation {
|
|||||||
subHashedPacketsGen.setPreferredHashAlgorithms(true, PREFERRED_HASH_ALGORITHMS);
|
subHashedPacketsGen.setPreferredHashAlgorithms(true, PREFERRED_HASH_ALGORITHMS);
|
||||||
subHashedPacketsGen.setPreferredCompressionAlgorithms(true, PREFERRED_COMPRESSION_ALGORITHMS);
|
subHashedPacketsGen.setPreferredCompressionAlgorithms(true, PREFERRED_COMPRESSION_ALGORITHMS);
|
||||||
subHashedPacketsGen.setPrimaryUserID(false, primary);
|
subHashedPacketsGen.setPrimaryUserID(false, primary);
|
||||||
|
subHashedPacketsGen.setKeyFlags(false, flags);
|
||||||
sGen.setHashedSubpackets(subHashedPacketsGen.generate());
|
sGen.setHashedSubpackets(subHashedPacketsGen.generate());
|
||||||
sGen.init(PGPSignature.POSITIVE_CERTIFICATION, masterPrivateKey);
|
sGen.init(PGPSignature.POSITIVE_CERTIFICATION, masterPrivateKey);
|
||||||
return sGen.generateCertification(userId, pKey);
|
return sGen.generateCertification(userId, pKey);
|
||||||
@ -599,7 +666,7 @@ public class PgpKeyOperation {
|
|||||||
|
|
||||||
private static PGPSignature generateSubkeyBindingSignature(
|
private static PGPSignature generateSubkeyBindingSignature(
|
||||||
PGPPublicKey masterPublicKey, PGPPrivateKey masterPrivateKey,
|
PGPPublicKey masterPublicKey, PGPPrivateKey masterPrivateKey,
|
||||||
PGPSecretKey sKey, PGPPublicKey pKey, int flags, Long expiry, String passphrase)
|
PGPSecretKey sKey, PGPPublicKey pKey, int flags, long expiry, String passphrase)
|
||||||
throws IOException, PGPException, SignatureException {
|
throws IOException, PGPException, SignatureException {
|
||||||
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder()
|
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder()
|
||||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
|
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
|
||||||
@ -611,7 +678,7 @@ public class PgpKeyOperation {
|
|||||||
|
|
||||||
private static PGPSignature generateSubkeyBindingSignature(
|
private static PGPSignature generateSubkeyBindingSignature(
|
||||||
PGPPublicKey masterPublicKey, PGPPrivateKey masterPrivateKey,
|
PGPPublicKey masterPublicKey, PGPPrivateKey masterPrivateKey,
|
||||||
PGPPrivateKey subPrivateKey, PGPPublicKey pKey, int flags, Long expiry)
|
PGPPrivateKey subPrivateKey, PGPPublicKey pKey, int flags, long expiry)
|
||||||
throws IOException, PGPException, SignatureException {
|
throws IOException, PGPException, SignatureException {
|
||||||
|
|
||||||
// date for signing
|
// date for signing
|
||||||
@ -640,17 +707,9 @@ public class PgpKeyOperation {
|
|||||||
hashedPacketsGen.setKeyFlags(false, flags);
|
hashedPacketsGen.setKeyFlags(false, flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expiry != null) {
|
if (expiry > 0) {
|
||||||
Calendar creationDate = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
|
long creationTime = pKey.getCreationTime().getTime() / 1000;
|
||||||
creationDate.setTime(pKey.getCreationTime());
|
hashedPacketsGen.setKeyExpirationTime(false, expiry - creationTime);
|
||||||
|
|
||||||
// (Just making sure there's no programming error here, this MUST have been checked above!)
|
|
||||||
if (new Date(expiry).before(todayDate)) {
|
|
||||||
throw new RuntimeException("Bad subkey creation date, this is a bug!");
|
|
||||||
}
|
|
||||||
hashedPacketsGen.setKeyExpirationTime(false, expiry - creationDate.getTimeInMillis());
|
|
||||||
} else {
|
|
||||||
hashedPacketsGen.setKeyExpirationTime(false, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
|
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
|
||||||
@ -665,4 +724,22 @@ public class PgpKeyOperation {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns all flags valid for this key.
|
||||||
|
*
|
||||||
|
* This method does not do any validity checks on the signature, so it should not be used on
|
||||||
|
* a non-canonicalized key!
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private static int readKeyFlags(PGPPublicKey key) {
|
||||||
|
int flags = 0;
|
||||||
|
//noinspection unchecked
|
||||||
|
for(PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) {
|
||||||
|
if (!sig.hasSubpackets()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
flags |= sig.getHashedSubPackets().getKeyFlags();
|
||||||
|
}
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -81,6 +81,10 @@ public class UncachedKeyRing {
|
|||||||
return new UncachedPublicKey(mRing.getPublicKey());
|
return new UncachedPublicKey(mRing.getPublicKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UncachedPublicKey getPublicKey(long keyId) {
|
||||||
|
return new UncachedPublicKey(mRing.getPublicKey(keyId));
|
||||||
|
}
|
||||||
|
|
||||||
public Iterator<UncachedPublicKey> getPublicKeys() {
|
public Iterator<UncachedPublicKey> getPublicKeys() {
|
||||||
final Iterator<PGPPublicKey> it = mRing.getPublicKeys();
|
final Iterator<PGPPublicKey> it = mRing.getPublicKeys();
|
||||||
return new Iterator<UncachedPublicKey>() {
|
return new Iterator<UncachedPublicKey>() {
|
||||||
@ -558,7 +562,7 @@ public class UncachedKeyRing {
|
|||||||
// make sure the certificate checks out
|
// make sure the certificate checks out
|
||||||
try {
|
try {
|
||||||
cert.init(masterKey);
|
cert.init(masterKey);
|
||||||
if (!cert.verifySignature(key)) {
|
if (!cert.verifySignature(masterKey, key)) {
|
||||||
log.add(LogLevel.WARN, LogType.MSG_KC_SUB_REVOKE_BAD, indent);
|
log.add(LogLevel.WARN, LogType.MSG_KC_SUB_REVOKE_BAD, indent);
|
||||||
badCerts += 1;
|
badCerts += 1;
|
||||||
continue;
|
continue;
|
||||||
|
@ -9,6 +9,7 @@ import org.spongycastle.openpgp.PGPSignatureSubpacketVector;
|
|||||||
import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
|
import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
|
||||||
import org.sufficientlysecure.keychain.Constants;
|
import org.sufficientlysecure.keychain.Constants;
|
||||||
import org.sufficientlysecure.keychain.util.IterableIterator;
|
import org.sufficientlysecure.keychain.util.IterableIterator;
|
||||||
|
import org.sufficientlysecure.keychain.util.Log;
|
||||||
|
|
||||||
import java.security.SignatureException;
|
import java.security.SignatureException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -44,14 +45,19 @@ public class UncachedPublicKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Date getExpiryTime() {
|
public Date getExpiryTime() {
|
||||||
Date creationDate = getCreationTime();
|
long seconds = mPublicKey.getValidSeconds();
|
||||||
if (mPublicKey.getValidDays() == 0) {
|
if (seconds > Integer.MAX_VALUE) {
|
||||||
|
Log.e(Constants.TAG, "error, expiry time too large");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (seconds == 0) {
|
||||||
// no expiry
|
// no expiry
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
Date creationDate = getCreationTime();
|
||||||
Calendar calendar = GregorianCalendar.getInstance();
|
Calendar calendar = GregorianCalendar.getInstance();
|
||||||
calendar.setTime(creationDate);
|
calendar.setTime(creationDate);
|
||||||
calendar.add(Calendar.DATE, mPublicKey.getValidDays());
|
calendar.add(Calendar.SECOND, (int) seconds);
|
||||||
|
|
||||||
return calendar.getTime();
|
return calendar.getTime();
|
||||||
}
|
}
|
||||||
@ -77,26 +83,65 @@ public class UncachedPublicKey {
|
|||||||
return mPublicKey.getBitStrength();
|
return mPublicKey.getBitStrength();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the primary user id, as indicated by the public key's self certificates.
|
||||||
|
*
|
||||||
|
* This is an expensive operation, since potentially a lot of certificates (and revocations)
|
||||||
|
* have to be checked, and even then the result is NOT guaranteed to be constant through a
|
||||||
|
* canonicalization operation.
|
||||||
|
*
|
||||||
|
* Returns null if there is no primary user id (as indicated by certificates)
|
||||||
|
*
|
||||||
|
*/
|
||||||
public String getPrimaryUserId() {
|
public String getPrimaryUserId() {
|
||||||
|
String found = null;
|
||||||
|
PGPSignature foundSig = null;
|
||||||
for (String userId : new IterableIterator<String>(mPublicKey.getUserIDs())) {
|
for (String userId : new IterableIterator<String>(mPublicKey.getUserIDs())) {
|
||||||
|
PGPSignature revocation = null;
|
||||||
|
|
||||||
for (PGPSignature sig : new IterableIterator<PGPSignature>(mPublicKey.getSignaturesForID(userId))) {
|
for (PGPSignature sig : new IterableIterator<PGPSignature>(mPublicKey.getSignaturesForID(userId))) {
|
||||||
if (sig.getHashedSubPackets() != null
|
|
||||||
&& sig.getHashedSubPackets().hasSubpacket(SignatureSubpacketTags.PRIMARY_USER_ID)) {
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
// if this is a revocation, this is not the user id
|
||||||
|
if (sig.getSignatureType() == PGPSignature.CERTIFICATION_REVOCATION) {
|
||||||
|
// make sure it's actually valid
|
||||||
|
sig.init(new JcaPGPContentVerifierBuilderProvider().setProvider(
|
||||||
|
Constants.BOUNCY_CASTLE_PROVIDER_NAME), mPublicKey);
|
||||||
|
if (!sig.verifyCertification(userId, mPublicKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (found != null && found.equals(userId)) {
|
||||||
|
found = null;
|
||||||
|
}
|
||||||
|
revocation = sig;
|
||||||
|
// this revocation may still be overridden by a newer cert
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sig.getHashedSubPackets() != null && sig.getHashedSubPackets().isPrimaryUserID()) {
|
||||||
|
if (foundSig != null && sig.getCreationTime().before(foundSig.getCreationTime())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// ignore if there is a newer revocation for this user id
|
||||||
|
if (revocation != null && sig.getCreationTime().before(revocation.getCreationTime())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// make sure it's actually valid
|
// make sure it's actually valid
|
||||||
sig.init(new JcaPGPContentVerifierBuilderProvider().setProvider(
|
sig.init(new JcaPGPContentVerifierBuilderProvider().setProvider(
|
||||||
Constants.BOUNCY_CASTLE_PROVIDER_NAME), mPublicKey);
|
Constants.BOUNCY_CASTLE_PROVIDER_NAME), mPublicKey);
|
||||||
if (sig.verifyCertification(userId, mPublicKey)) {
|
if (sig.verifyCertification(userId, mPublicKey)) {
|
||||||
return userId;
|
found = userId;
|
||||||
|
foundSig = sig;
|
||||||
|
// this one can't be relevant anymore at this point
|
||||||
|
revocation = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// nothing bad happens, the key is just not considered the primary key id
|
// nothing bad happens, the key is just not considered the primary key id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
return found;
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ArrayList<String> getUnorderedUserIds() {
|
public ArrayList<String> getUnorderedUserIds() {
|
||||||
@ -186,6 +231,21 @@ public class UncachedPublicKey {
|
|||||||
return mPublicKey;
|
return mPublicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Iterator<WrappedSignature> getSignatures() {
|
||||||
|
final Iterator<PGPSignature> it = mPublicKey.getSignatures();
|
||||||
|
return new Iterator<WrappedSignature>() {
|
||||||
|
public void remove() {
|
||||||
|
it.remove();
|
||||||
|
}
|
||||||
|
public WrappedSignature next() {
|
||||||
|
return new WrappedSignature(it.next());
|
||||||
|
}
|
||||||
|
public boolean hasNext() {
|
||||||
|
return it.hasNext();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public Iterator<WrappedSignature> getSignaturesForId(String userId) {
|
public Iterator<WrappedSignature> getSignaturesForId(String userId) {
|
||||||
final Iterator<PGPSignature> it = mPublicKey.getSignaturesForID(userId);
|
final Iterator<PGPSignature> it = mPublicKey.getSignaturesForID(userId);
|
||||||
return new Iterator<WrappedSignature>() {
|
return new Iterator<WrappedSignature>() {
|
||||||
|
@ -101,8 +101,8 @@ public abstract class WrappedKeyRing extends KeyRing {
|
|||||||
|
|
||||||
abstract public IterableIterator<WrappedPublicKey> publicKeyIterator();
|
abstract public IterableIterator<WrappedPublicKey> publicKeyIterator();
|
||||||
|
|
||||||
public UncachedKeyRing getUncached() {
|
public byte[] getEncoded() throws IOException {
|
||||||
return new UncachedKeyRing(getRing());
|
return getRing().getEncoded();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import org.sufficientlysecure.keychain.util.Log;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.SignatureException;
|
import java.security.SignatureException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
/** OpenKeychain wrapper around PGPSignature objects.
|
/** OpenKeychain wrapper around PGPSignature objects.
|
||||||
@ -55,6 +56,31 @@ public class WrappedSignature {
|
|||||||
return mSig.getCreationTime();
|
return mSig.getCreationTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ArrayList<WrappedSignature> getEmbeddedSignatures() {
|
||||||
|
ArrayList<WrappedSignature> sigs = new ArrayList<WrappedSignature>();
|
||||||
|
if (!mSig.hasSubpackets()) {
|
||||||
|
return sigs;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
PGPSignatureList list;
|
||||||
|
list = mSig.getHashedSubPackets().getEmbeddedSignatures();
|
||||||
|
for(int i = 0; i < list.size(); i++) {
|
||||||
|
sigs.add(new WrappedSignature(list.get(i)));
|
||||||
|
}
|
||||||
|
list = mSig.getUnhashedSubPackets().getEmbeddedSignatures();
|
||||||
|
for(int i = 0; i < list.size(); i++) {
|
||||||
|
sigs.add(new WrappedSignature(list.get(i)));
|
||||||
|
}
|
||||||
|
} catch (PGPException e) {
|
||||||
|
// no matter
|
||||||
|
Log.e(Constants.TAG, "exception reading embedded signatures", e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// no matter
|
||||||
|
Log.e(Constants.TAG, "exception reading embedded signatures", e);
|
||||||
|
}
|
||||||
|
return sigs;
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] getEncoded() throws IOException {
|
public byte[] getEncoded() throws IOException {
|
||||||
return mSig.getEncoded();
|
return mSig.getEncoded();
|
||||||
}
|
}
|
||||||
|
@ -450,8 +450,7 @@ public class ProviderHelper {
|
|||||||
if (cert.verifySignature(masterKey, userId)) {
|
if (cert.verifySignature(masterKey, userId)) {
|
||||||
item.trustedCerts.add(cert);
|
item.trustedCerts.add(cert);
|
||||||
log(LogLevel.INFO, LogType.MSG_IP_UID_CERT_GOOD,
|
log(LogLevel.INFO, LogType.MSG_IP_UID_CERT_GOOD,
|
||||||
PgpKeyHelper.convertKeyIdToHexShort(trustedKey.getKeyId()),
|
PgpKeyHelper.convertKeyIdToHexShort(trustedKey.getKeyId())
|
||||||
trustedKey.getPrimaryUserId()
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
log(LogLevel.WARN, LogType.MSG_IP_UID_CERT_BAD);
|
log(LogLevel.WARN, LogType.MSG_IP_UID_CERT_BAD);
|
||||||
@ -670,7 +669,7 @@ public class ProviderHelper {
|
|||||||
|
|
||||||
// If there is an old keyring, merge it
|
// If there is an old keyring, merge it
|
||||||
try {
|
try {
|
||||||
UncachedKeyRing oldPublicRing = getWrappedPublicKeyRing(masterKeyId).getUncached();
|
UncachedKeyRing oldPublicRing = getWrappedPublicKeyRing(masterKeyId).getUncachedKeyRing();
|
||||||
|
|
||||||
// Merge data from new public ring into the old one
|
// Merge data from new public ring into the old one
|
||||||
publicRing = oldPublicRing.merge(publicRing, mLog, mIndent);
|
publicRing = oldPublicRing.merge(publicRing, mLog, mIndent);
|
||||||
@ -706,7 +705,7 @@ public class ProviderHelper {
|
|||||||
// If there is a secret key, merge new data (if any) and save the key for later
|
// If there is a secret key, merge new data (if any) and save the key for later
|
||||||
UncachedKeyRing secretRing;
|
UncachedKeyRing secretRing;
|
||||||
try {
|
try {
|
||||||
secretRing = getWrappedSecretKeyRing(publicRing.getMasterKeyId()).getUncached();
|
secretRing = getWrappedSecretKeyRing(publicRing.getMasterKeyId()).getUncachedKeyRing();
|
||||||
|
|
||||||
// Merge data from new public ring into secret one
|
// Merge data from new public ring into secret one
|
||||||
secretRing = secretRing.merge(publicRing, mLog, mIndent);
|
secretRing = secretRing.merge(publicRing, mLog, mIndent);
|
||||||
@ -754,7 +753,7 @@ public class ProviderHelper {
|
|||||||
|
|
||||||
// If there is an old secret key, merge it.
|
// If there is an old secret key, merge it.
|
||||||
try {
|
try {
|
||||||
UncachedKeyRing oldSecretRing = getWrappedSecretKeyRing(masterKeyId).getUncached();
|
UncachedKeyRing oldSecretRing = getWrappedSecretKeyRing(masterKeyId).getUncachedKeyRing();
|
||||||
|
|
||||||
// Merge data from new secret ring into old one
|
// Merge data from new secret ring into old one
|
||||||
secretRing = oldSecretRing.merge(secretRing, mLog, mIndent);
|
secretRing = oldSecretRing.merge(secretRing, mLog, mIndent);
|
||||||
@ -791,7 +790,7 @@ public class ProviderHelper {
|
|||||||
// Merge new data into public keyring as well, if there is any
|
// Merge new data into public keyring as well, if there is any
|
||||||
UncachedKeyRing publicRing;
|
UncachedKeyRing publicRing;
|
||||||
try {
|
try {
|
||||||
UncachedKeyRing oldPublicRing = getWrappedPublicKeyRing(masterKeyId).getUncached();
|
UncachedKeyRing oldPublicRing = getWrappedPublicKeyRing(masterKeyId).getUncachedKeyRing();
|
||||||
|
|
||||||
// Merge data from new public ring into secret one
|
// Merge data from new public ring into secret one
|
||||||
publicRing = oldPublicRing.merge(secretRing, mLog, mIndent);
|
publicRing = oldPublicRing.merge(secretRing, mLog, mIndent);
|
||||||
|
@ -350,9 +350,9 @@ public class KeychainIntentService extends IntentService
|
|||||||
providerHelper.saveSecretKeyRing(ring, new ProgressScaler(this, 10, 95, 100));
|
providerHelper.saveSecretKeyRing(ring, new ProgressScaler(this, 10, 95, 100));
|
||||||
|
|
||||||
// cache new passphrase
|
// cache new passphrase
|
||||||
if (saveParcel.newPassphrase != null) {
|
if (saveParcel.mNewPassphrase != null) {
|
||||||
PassphraseCacheService.addCachedPassphrase(this, ring.getMasterKeyId(),
|
PassphraseCacheService.addCachedPassphrase(this, ring.getMasterKeyId(),
|
||||||
saveParcel.newPassphrase, ring.getPublicKey().getPrimaryUserId());
|
saveParcel.mNewPassphrase, ring.getPublicKey().getPrimaryUserId());
|
||||||
}
|
}
|
||||||
} catch (ProviderHelper.NotFoundException e) {
|
} catch (ProviderHelper.NotFoundException e) {
|
||||||
sendErrorToHandler(e);
|
sendErrorToHandler(e);
|
||||||
|
@ -9,6 +9,7 @@ import org.sufficientlysecure.keychain.util.IterableIterator;
|
|||||||
import org.sufficientlysecure.keychain.util.Log;
|
import org.sufficientlysecure.keychain.util.Log;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -70,9 +71,6 @@ public class OperationResultParcel implements Parcelable {
|
|||||||
mParameters = parameters;
|
mParameters = parameters;
|
||||||
mIndent = indent;
|
mIndent = indent;
|
||||||
}
|
}
|
||||||
public LogEntryParcel(LogLevel level, LogType type, Object... parameters) {
|
|
||||||
this(level, type, 0, parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LogEntryParcel(Parcel source) {
|
public LogEntryParcel(Parcel source) {
|
||||||
mLevel = LogLevel.values()[source.readInt()];
|
mLevel = LogLevel.values()[source.readInt()];
|
||||||
@ -104,6 +102,15 @@ public class OperationResultParcel implements Parcelable {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "LogEntryParcel{" +
|
||||||
|
"mLevel=" + mLevel +
|
||||||
|
", mType=" + mType +
|
||||||
|
", mParameters=" + Arrays.toString(mParameters) +
|
||||||
|
", mIndent=" + mIndent +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** This is an enum of all possible log events.
|
/** This is an enum of all possible log events.
|
||||||
@ -237,7 +244,14 @@ public class OperationResultParcel implements Parcelable {
|
|||||||
MSG_MG_FOUND_NEW (R.string.msg_mg_found_new),
|
MSG_MG_FOUND_NEW (R.string.msg_mg_found_new),
|
||||||
|
|
||||||
// secret key create
|
// secret key create
|
||||||
MSG_CR_ERROR_NO_MASTER (R.string.msg_mr),
|
MSG_CR (R.string.msg_cr),
|
||||||
|
MSG_CR_ERROR_NO_MASTER (R.string.msg_cr_error_no_master),
|
||||||
|
MSG_CR_ERROR_NO_USER_ID (R.string.msg_cr_error_no_user_id),
|
||||||
|
MSG_CR_ERROR_NO_CERTIFY (R.string.msg_cr_error_no_certify),
|
||||||
|
MSG_CR_ERROR_KEYSIZE_512 (R.string.msg_cr_error_keysize_512),
|
||||||
|
MSG_CR_ERROR_UNKNOWN_ALGO (R.string.msg_cr_error_unknown_algo),
|
||||||
|
MSG_CR_ERROR_INTERNAL_PGP (R.string.msg_cr_error_internal_pgp),
|
||||||
|
MSG_CR_ERROR_MASTER_ELGAMAL (R.string.msg_cr_error_master_elgamal),
|
||||||
|
|
||||||
// secret key modify
|
// secret key modify
|
||||||
MSG_MF (R.string.msg_mr),
|
MSG_MF (R.string.msg_mr),
|
||||||
@ -245,10 +259,13 @@ public class OperationResultParcel implements Parcelable {
|
|||||||
MSG_MF_ERROR_FINGERPRINT (R.string.msg_mf_error_fingerprint),
|
MSG_MF_ERROR_FINGERPRINT (R.string.msg_mf_error_fingerprint),
|
||||||
MSG_MF_ERROR_KEYID (R.string.msg_mf_error_keyid),
|
MSG_MF_ERROR_KEYID (R.string.msg_mf_error_keyid),
|
||||||
MSG_MF_ERROR_INTEGRITY (R.string.msg_mf_error_integrity),
|
MSG_MF_ERROR_INTEGRITY (R.string.msg_mf_error_integrity),
|
||||||
|
MSG_MF_ERROR_NOEXIST_PRIMARY (R.string.msg_mf_error_noexist_primary),
|
||||||
MSG_MF_ERROR_REVOKED_PRIMARY (R.string.msg_mf_error_revoked_primary),
|
MSG_MF_ERROR_REVOKED_PRIMARY (R.string.msg_mf_error_revoked_primary),
|
||||||
MSG_MF_ERROR_PGP (R.string.msg_mf_error_pgp),
|
MSG_MF_ERROR_PGP (R.string.msg_mf_error_pgp),
|
||||||
MSG_MF_ERROR_SIG (R.string.msg_mf_error_sig),
|
MSG_MF_ERROR_SIG (R.string.msg_mf_error_sig),
|
||||||
MSG_MF_PASSPHRASE (R.string.msg_mf_passphrase),
|
MSG_MF_PASSPHRASE (R.string.msg_mf_passphrase),
|
||||||
|
MSG_MF_PRIMARY_REPLACE_OLD (R.string.msg_mf_primary_replace_old),
|
||||||
|
MSG_MF_PRIMARY_NEW (R.string.msg_mf_primary_new),
|
||||||
MSG_MF_SUBKEY_CHANGE (R.string.msg_mf_subkey_change),
|
MSG_MF_SUBKEY_CHANGE (R.string.msg_mf_subkey_change),
|
||||||
MSG_MF_SUBKEY_MISSING (R.string.msg_mf_subkey_missing),
|
MSG_MF_SUBKEY_MISSING (R.string.msg_mf_subkey_missing),
|
||||||
MSG_MF_SUBKEY_NEW_ID (R.string.msg_mf_subkey_new_id),
|
MSG_MF_SUBKEY_NEW_ID (R.string.msg_mf_subkey_new_id),
|
||||||
@ -314,6 +331,7 @@ public class OperationResultParcel implements Parcelable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void add(LogLevel level, LogType type, int indent) {
|
public void add(LogLevel level, LogType type, int indent) {
|
||||||
|
Log.d(Constants.TAG, type.toString());
|
||||||
mParcels.add(new OperationResultParcel.LogEntryParcel(level, type, indent, (Object[]) null));
|
mParcels.add(new OperationResultParcel.LogEntryParcel(level, type, indent, (Object[]) null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,23 +27,19 @@ public class SaveKeyringParcel implements Parcelable {
|
|||||||
// the key fingerprint, for safety. MUST be null for a new key.
|
// the key fingerprint, for safety. MUST be null for a new key.
|
||||||
public byte[] mFingerprint;
|
public byte[] mFingerprint;
|
||||||
|
|
||||||
public String newPassphrase;
|
public String mNewPassphrase;
|
||||||
|
|
||||||
public ArrayList<String> addUserIds;
|
public ArrayList<String> mAddUserIds;
|
||||||
public ArrayList<SubkeyAdd> addSubKeys;
|
public ArrayList<SubkeyAdd> mAddSubKeys;
|
||||||
|
|
||||||
public ArrayList<SubkeyChange> changeSubKeys;
|
public ArrayList<SubkeyChange> mChangeSubKeys;
|
||||||
public String changePrimaryUserId;
|
public String mChangePrimaryUserId;
|
||||||
|
|
||||||
public ArrayList<String> revokeUserIds;
|
public ArrayList<String> mRevokeUserIds;
|
||||||
public ArrayList<Long> revokeSubKeys;
|
public ArrayList<Long> mRevokeSubKeys;
|
||||||
|
|
||||||
public SaveKeyringParcel() {
|
public SaveKeyringParcel() {
|
||||||
addUserIds = new ArrayList<String>();
|
reset();
|
||||||
addSubKeys = new ArrayList<SubkeyAdd>();
|
|
||||||
changeSubKeys = new ArrayList<SubkeyChange>();
|
|
||||||
revokeUserIds = new ArrayList<String>();
|
|
||||||
revokeSubKeys = new ArrayList<Long>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public SaveKeyringParcel(long masterKeyId, byte[] fingerprint) {
|
public SaveKeyringParcel(long masterKeyId, byte[] fingerprint) {
|
||||||
@ -52,6 +48,16 @@ public class SaveKeyringParcel implements Parcelable {
|
|||||||
mFingerprint = fingerprint;
|
mFingerprint = fingerprint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
mNewPassphrase = null;
|
||||||
|
mAddUserIds = new ArrayList<String>();
|
||||||
|
mAddSubKeys = new ArrayList<SubkeyAdd>();
|
||||||
|
mChangePrimaryUserId = null;
|
||||||
|
mChangeSubKeys = new ArrayList<SubkeyChange>();
|
||||||
|
mRevokeUserIds = new ArrayList<String>();
|
||||||
|
mRevokeSubKeys = new ArrayList<Long>();
|
||||||
|
}
|
||||||
|
|
||||||
// performance gain for using Parcelable here would probably be negligible,
|
// performance gain for using Parcelable here would probably be negligible,
|
||||||
// use Serializable instead.
|
// use Serializable instead.
|
||||||
public static class SubkeyAdd implements Serializable {
|
public static class SubkeyAdd implements Serializable {
|
||||||
@ -70,6 +76,7 @@ public class SaveKeyringParcel implements Parcelable {
|
|||||||
public static class SubkeyChange implements Serializable {
|
public static class SubkeyChange implements Serializable {
|
||||||
public long mKeyId;
|
public long mKeyId;
|
||||||
public Integer mFlags;
|
public Integer mFlags;
|
||||||
|
// this is a long unix timestamp, in seconds (NOT MILLISECONDS!)
|
||||||
public Long mExpiry;
|
public Long mExpiry;
|
||||||
public SubkeyChange(long keyId, Integer flags, Long expiry) {
|
public SubkeyChange(long keyId, Integer flags, Long expiry) {
|
||||||
mKeyId = keyId;
|
mKeyId = keyId;
|
||||||
@ -82,16 +89,16 @@ public class SaveKeyringParcel implements Parcelable {
|
|||||||
mMasterKeyId = source.readInt() != 0 ? source.readLong() : null;
|
mMasterKeyId = source.readInt() != 0 ? source.readLong() : null;
|
||||||
mFingerprint = source.createByteArray();
|
mFingerprint = source.createByteArray();
|
||||||
|
|
||||||
newPassphrase = source.readString();
|
mNewPassphrase = source.readString();
|
||||||
|
|
||||||
addUserIds = source.createStringArrayList();
|
mAddUserIds = source.createStringArrayList();
|
||||||
addSubKeys = (ArrayList<SubkeyAdd>) source.readSerializable();
|
mAddSubKeys = (ArrayList<SubkeyAdd>) source.readSerializable();
|
||||||
|
|
||||||
changeSubKeys = (ArrayList<SubkeyChange>) source.readSerializable();
|
mChangeSubKeys = (ArrayList<SubkeyChange>) source.readSerializable();
|
||||||
changePrimaryUserId = source.readString();
|
mChangePrimaryUserId = source.readString();
|
||||||
|
|
||||||
revokeUserIds = source.createStringArrayList();
|
mRevokeUserIds = source.createStringArrayList();
|
||||||
revokeSubKeys = (ArrayList<Long>) source.readSerializable();
|
mRevokeSubKeys = (ArrayList<Long>) source.readSerializable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -102,16 +109,16 @@ public class SaveKeyringParcel implements Parcelable {
|
|||||||
}
|
}
|
||||||
destination.writeByteArray(mFingerprint);
|
destination.writeByteArray(mFingerprint);
|
||||||
|
|
||||||
destination.writeString(newPassphrase);
|
destination.writeString(mNewPassphrase);
|
||||||
|
|
||||||
destination.writeStringList(addUserIds);
|
destination.writeStringList(mAddUserIds);
|
||||||
destination.writeSerializable(addSubKeys);
|
destination.writeSerializable(mAddSubKeys);
|
||||||
|
|
||||||
destination.writeSerializable(changeSubKeys);
|
destination.writeSerializable(mChangeSubKeys);
|
||||||
destination.writeString(changePrimaryUserId);
|
destination.writeString(mChangePrimaryUserId);
|
||||||
|
|
||||||
destination.writeStringList(revokeUserIds);
|
destination.writeStringList(mRevokeUserIds);
|
||||||
destination.writeSerializable(revokeSubKeys);
|
destination.writeSerializable(mRevokeSubKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final Creator<SaveKeyringParcel> CREATOR = new Creator<SaveKeyringParcel>() {
|
public static final Creator<SaveKeyringParcel> CREATOR = new Creator<SaveKeyringParcel>() {
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
package org.sufficientlysecure.keychain.testsupport;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import org.sufficientlysecure.keychain.pgp.NullProgressable;
|
|
||||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
|
||||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
|
||||||
import org.sufficientlysecure.keychain.service.OperationResults;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper for tests of the Keyring import in ProviderHelper.
|
|
||||||
*/
|
|
||||||
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);
|
|
||||||
|
|
||||||
OperationResults.SaveKeyringResult saveKeyringResult = providerHelper.savePublicKeyRing(ring, new NullProgressable());
|
|
||||||
|
|
||||||
boolean saveSuccess = saveKeyringResult.success();
|
|
||||||
|
|
||||||
// Now re-retrieve the saved key. Should not throw an exception.
|
|
||||||
providerHelper.getWrappedPublicKeyRing(masterKeyId);
|
|
||||||
|
|
||||||
// A different ID should still fail
|
|
||||||
retrieveKeyAndExpectNotFound(providerHelper, masterKeyId - 1);
|
|
||||||
|
|
||||||
return saveSuccess;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void retrieveKeyAndExpectNotFound(ProviderHelper providerHelper, long masterKeyId) {
|
|
||||||
try {
|
|
||||||
providerHelper.getWrappedPublicKeyRing(masterKeyId);
|
|
||||||
throw new AssertionError("Was expecting the previous call to fail!");
|
|
||||||
} catch (ProviderHelper.NotFoundException expectedException) {
|
|
||||||
// good
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
package org.sufficientlysecure.keychain.testsupport;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import org.sufficientlysecure.keychain.pgp.WrappedPublicKeyRing;
|
|
||||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by art on 21/06/14.
|
|
||||||
*/
|
|
||||||
class ProviderHelperStub extends ProviderHelper {
|
|
||||||
public ProviderHelperStub(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public WrappedPublicKeyRing getWrappedPublicKeyRing(Uri id) throws NotFoundException {
|
|
||||||
byte[] data = TestDataUtil.readFully(getClass().getResourceAsStream("/public-key-for-sample.blob"));
|
|
||||||
return new WrappedPublicKeyRing(data, false, 0);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
package org.sufficientlysecure.keychain.testsupport;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void appendToOutput(InputStream input, ByteArrayOutputStream 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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
/**
|
|
||||||
* Test support classes.
|
|
||||||
* This is only in main code because of gradle-Android Studio-robolectric issues. Having
|
|
||||||
* classes in main code means IDE autocomplete, class detection, etc., all works.
|
|
||||||
* TODO Move into test package when possible
|
|
||||||
*/
|
|
||||||
package org.sufficientlysecure.keychain.testsupport;
|
|
@ -556,7 +556,7 @@ public class EditKeyActivityOld extends ActionBarActivity implements EditorListe
|
|||||||
saveParams.deletedKeys = mKeysView.getDeletedKeys();
|
saveParams.deletedKeys = mKeysView.getDeletedKeys();
|
||||||
saveParams.keysExpiryDates = getKeysExpiryDates(mKeysView);
|
saveParams.keysExpiryDates = getKeysExpiryDates(mKeysView);
|
||||||
saveParams.keysUsages = getKeysUsages(mKeysView);
|
saveParams.keysUsages = getKeysUsages(mKeysView);
|
||||||
saveParams.newPassphrase = mNewPassphrase;
|
saveParams.mNewPassphrase = mNewPassphrase;
|
||||||
saveParams.oldPassphrase = mCurrentPassphrase;
|
saveParams.oldPassphrase = mCurrentPassphrase;
|
||||||
saveParams.newKeys = toPrimitiveArray(mKeysView.getNewKeysArray());
|
saveParams.newKeys = toPrimitiveArray(mKeysView.getNewKeysArray());
|
||||||
saveParams.keys = getKeys(mKeysView);
|
saveParams.keys = getKeys(mKeysView);
|
||||||
|
@ -228,7 +228,7 @@ public class EditKeyFragment extends LoaderFragment implements
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
mSubkeysAddedAdapter = new SubkeysAddedAdapter(getActivity(), mSaveKeyringParcel.addSubKeys);
|
mSubkeysAddedAdapter = new SubkeysAddedAdapter(getActivity(), mSaveKeyringParcel.mAddSubKeys);
|
||||||
mSubkeysAddedList.setAdapter(mSubkeysAddedAdapter);
|
mSubkeysAddedList.setAdapter(mSubkeysAddedAdapter);
|
||||||
|
|
||||||
// Prepare the loaders. Either re-connect with an existing ones,
|
// Prepare the loaders. Either re-connect with an existing ones,
|
||||||
@ -298,7 +298,7 @@ public class EditKeyFragment extends LoaderFragment implements
|
|||||||
Bundle data = message.getData();
|
Bundle data = message.getData();
|
||||||
|
|
||||||
// cache new returned passphrase!
|
// cache new returned passphrase!
|
||||||
mSaveKeyringParcel.newPassphrase = data
|
mSaveKeyringParcel.mNewPassphrase = data
|
||||||
.getString(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE);
|
.getString(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -320,19 +320,19 @@ public class EditKeyFragment extends LoaderFragment implements
|
|||||||
switch (message.what) {
|
switch (message.what) {
|
||||||
case EditUserIdDialogFragment.MESSAGE_CHANGE_PRIMARY_USER_ID:
|
case EditUserIdDialogFragment.MESSAGE_CHANGE_PRIMARY_USER_ID:
|
||||||
// toggle
|
// toggle
|
||||||
if (mSaveKeyringParcel.changePrimaryUserId != null
|
if (mSaveKeyringParcel.mChangePrimaryUserId != null
|
||||||
&& mSaveKeyringParcel.changePrimaryUserId.equals(userId)) {
|
&& mSaveKeyringParcel.mChangePrimaryUserId.equals(userId)) {
|
||||||
mSaveKeyringParcel.changePrimaryUserId = null;
|
mSaveKeyringParcel.mChangePrimaryUserId = null;
|
||||||
} else {
|
} else {
|
||||||
mSaveKeyringParcel.changePrimaryUserId = userId;
|
mSaveKeyringParcel.mChangePrimaryUserId = userId;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case EditUserIdDialogFragment.MESSAGE_REVOKE:
|
case EditUserIdDialogFragment.MESSAGE_REVOKE:
|
||||||
// toggle
|
// toggle
|
||||||
if (mSaveKeyringParcel.revokeUserIds.contains(userId)) {
|
if (mSaveKeyringParcel.mRevokeUserIds.contains(userId)) {
|
||||||
mSaveKeyringParcel.revokeUserIds.remove(userId);
|
mSaveKeyringParcel.mRevokeUserIds.remove(userId);
|
||||||
} else {
|
} else {
|
||||||
mSaveKeyringParcel.revokeUserIds.add(userId);
|
mSaveKeyringParcel.mRevokeUserIds.add(userId);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -363,10 +363,10 @@ public class EditKeyFragment extends LoaderFragment implements
|
|||||||
break;
|
break;
|
||||||
case EditSubkeyDialogFragment.MESSAGE_REVOKE:
|
case EditSubkeyDialogFragment.MESSAGE_REVOKE:
|
||||||
// toggle
|
// toggle
|
||||||
if (mSaveKeyringParcel.revokeSubKeys.contains(keyId)) {
|
if (mSaveKeyringParcel.mRevokeSubKeys.contains(keyId)) {
|
||||||
mSaveKeyringParcel.revokeSubKeys.remove(keyId);
|
mSaveKeyringParcel.mRevokeSubKeys.remove(keyId);
|
||||||
} else {
|
} else {
|
||||||
mSaveKeyringParcel.revokeSubKeys.add(keyId);
|
mSaveKeyringParcel.mRevokeSubKeys.add(keyId);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -451,9 +451,9 @@ public class EditKeyFragment extends LoaderFragment implements
|
|||||||
|
|
||||||
private void save(String passphrase) {
|
private void save(String passphrase) {
|
||||||
Log.d(Constants.TAG, "add userids to parcel: " + mUserIdsAddedAdapter.getDataAsStringList());
|
Log.d(Constants.TAG, "add userids to parcel: " + mUserIdsAddedAdapter.getDataAsStringList());
|
||||||
Log.d(Constants.TAG, "mSaveKeyringParcel.newPassphrase: " + mSaveKeyringParcel.newPassphrase);
|
Log.d(Constants.TAG, "mSaveKeyringParcel.mNewPassphrase: " + mSaveKeyringParcel.mNewPassphrase);
|
||||||
|
|
||||||
mSaveKeyringParcel.addUserIds = mUserIdsAddedAdapter.getDataAsStringList();
|
mSaveKeyringParcel.mAddUserIds = mUserIdsAddedAdapter.getDataAsStringList();
|
||||||
|
|
||||||
// Message is received after importing is done in KeychainIntentService
|
// Message is received after importing is done in KeychainIntentService
|
||||||
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(
|
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(
|
||||||
|
@ -32,8 +32,6 @@ import org.sufficientlysecure.keychain.Constants;
|
|||||||
import org.sufficientlysecure.keychain.Constants.choice.algorithm;
|
import org.sufficientlysecure.keychain.Constants.choice.algorithm;
|
||||||
import org.sufficientlysecure.keychain.R;
|
import org.sufficientlysecure.keychain.R;
|
||||||
import org.sufficientlysecure.keychain.helper.ExportHelper;
|
import org.sufficientlysecure.keychain.helper.ExportHelper;
|
||||||
import org.sufficientlysecure.keychain.helper.OtherHelper;
|
|
||||||
import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry;
|
|
||||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
|
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
|
||||||
import org.sufficientlysecure.keychain.service.KeychainIntentService;
|
import org.sufficientlysecure.keychain.service.KeychainIntentService;
|
||||||
@ -43,7 +41,6 @@ import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyAdd;
|
|||||||
import org.sufficientlysecure.keychain.util.Log;
|
import org.sufficientlysecure.keychain.util.Log;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
public class KeyListActivity extends DrawerActivity {
|
public class KeyListActivity extends DrawerActivity {
|
||||||
|
|
||||||
@ -153,10 +150,10 @@ public class KeyListActivity extends DrawerActivity {
|
|||||||
Bundle data = new Bundle();
|
Bundle data = new Bundle();
|
||||||
|
|
||||||
SaveKeyringParcel parcel = new SaveKeyringParcel();
|
SaveKeyringParcel parcel = new SaveKeyringParcel();
|
||||||
parcel.addSubKeys.add(new SubkeyAdd(algorithm.rsa, 1024, KeyFlags.CERTIFY_OTHER, null));
|
parcel.mAddSubKeys.add(new SubkeyAdd(algorithm.rsa, 1024, KeyFlags.CERTIFY_OTHER, null));
|
||||||
parcel.addSubKeys.add(new SubkeyAdd(algorithm.rsa, 1024, KeyFlags.SIGN_DATA, null));
|
parcel.mAddSubKeys.add(new SubkeyAdd(algorithm.rsa, 1024, KeyFlags.SIGN_DATA, null));
|
||||||
parcel.addUserIds.add("swagerinho");
|
parcel.mAddUserIds.add("swagerinho");
|
||||||
parcel.newPassphrase = "swag";
|
parcel.mNewPassphrase = "swag";
|
||||||
|
|
||||||
// get selected key entries
|
// get selected key entries
|
||||||
data.putParcelable(KeychainIntentService.SAVE_KEYRING_PARCEL, parcel);
|
data.putParcelable(KeychainIntentService.SAVE_KEYRING_PARCEL, parcel);
|
||||||
|
@ -328,7 +328,7 @@ public class WizardActivity extends ActionBarActivity {
|
|||||||
&& isEditTextNotEmpty(this, passphraseEdit)) {
|
&& isEditTextNotEmpty(this, passphraseEdit)) {
|
||||||
|
|
||||||
// SaveKeyringParcel newKey = new SaveKeyringParcel();
|
// SaveKeyringParcel newKey = new SaveKeyringParcel();
|
||||||
// newKey.addUserIds.add(nameEdit.getText().toString() + " <"
|
// newKey.mAddUserIds.add(nameEdit.getText().toString() + " <"
|
||||||
// + emailEdit.getText().toString() + ">");
|
// + emailEdit.getText().toString() + ">");
|
||||||
|
|
||||||
|
|
||||||
|
@ -143,7 +143,7 @@ public class SubkeysAdapter extends CursorAdapter {
|
|||||||
|
|
||||||
// for edit key
|
// for edit key
|
||||||
if (mSaveKeyringParcel != null) {
|
if (mSaveKeyringParcel != null) {
|
||||||
boolean revokeThisSubkey = (mSaveKeyringParcel.revokeSubKeys.contains(keyId));
|
boolean revokeThisSubkey = (mSaveKeyringParcel.mRevokeSubKeys.contains(keyId));
|
||||||
|
|
||||||
if (revokeThisSubkey) {
|
if (revokeThisSubkey) {
|
||||||
if (!isRevoked) {
|
if (!isRevoked) {
|
||||||
|
@ -131,10 +131,10 @@ public class UserIdsAdapter extends CursorAdapter implements AdapterView.OnItemC
|
|||||||
|
|
||||||
// for edit key
|
// for edit key
|
||||||
if (mSaveKeyringParcel != null) {
|
if (mSaveKeyringParcel != null) {
|
||||||
boolean changeAnyPrimaryUserId = (mSaveKeyringParcel.changePrimaryUserId != null);
|
boolean changeAnyPrimaryUserId = (mSaveKeyringParcel.mChangePrimaryUserId != null);
|
||||||
boolean changeThisPrimaryUserId = (mSaveKeyringParcel.changePrimaryUserId != null
|
boolean changeThisPrimaryUserId = (mSaveKeyringParcel.mChangePrimaryUserId != null
|
||||||
&& mSaveKeyringParcel.changePrimaryUserId.equals(userId));
|
&& mSaveKeyringParcel.mChangePrimaryUserId.equals(userId));
|
||||||
boolean revokeThisUserId = (mSaveKeyringParcel.revokeUserIds.contains(userId));
|
boolean revokeThisUserId = (mSaveKeyringParcel.mRevokeUserIds.contains(userId));
|
||||||
|
|
||||||
// only if primary user id will be changed
|
// only if primary user id will be changed
|
||||||
// (this is not triggered if the user id is currently the primary one)
|
// (this is not triggered if the user id is currently the primary one)
|
||||||
|
@ -39,15 +39,21 @@ public class ProgressScaler implements Progressable {
|
|||||||
* Set progress of ProgressDialog by sending message to handler on UI thread
|
* Set progress of ProgressDialog by sending message to handler on UI thread
|
||||||
*/
|
*/
|
||||||
public void setProgress(String message, int progress, int max) {
|
public void setProgress(String message, int progress, int max) {
|
||||||
|
if (mWrapped != null) {
|
||||||
mWrapped.setProgress(message, mFrom + progress * (mTo - mFrom) / max, mMax);
|
mWrapped.setProgress(message, mFrom + progress * (mTo - mFrom) / max, mMax);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setProgress(int resourceId, int progress, int max) {
|
public void setProgress(int resourceId, int progress, int max) {
|
||||||
|
if (mWrapped != null) {
|
||||||
mWrapped.setProgress(resourceId, progress, mMax);
|
mWrapped.setProgress(resourceId, progress, mMax);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setProgress(int progress, int max) {
|
public void setProgress(int progress, int max) {
|
||||||
|
if (mWrapped != null) {
|
||||||
mWrapped.setProgress(progress, max);
|
mWrapped.setProgress(progress, max);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -510,7 +510,7 @@
|
|||||||
<string name="cert_casual">casual</string>
|
<string name="cert_casual">casual</string>
|
||||||
<string name="cert_positive">positive</string>
|
<string name="cert_positive">positive</string>
|
||||||
<string name="cert_revoke">revoked</string>
|
<string name="cert_revoke">revoked</string>
|
||||||
<string name="cert_verify_ok">ok</string>
|
<string name="cert_verify_ok">OK</string>
|
||||||
<string name="cert_verify_failed">failed!</string>
|
<string name="cert_verify_failed">failed!</string>
|
||||||
<string name="cert_verify_error">error!</string>
|
<string name="cert_verify_error">error!</string>
|
||||||
<string name="cert_verify_unavailable">key unavailable</string>
|
<string name="cert_verify_unavailable">key unavailable</string>
|
||||||
@ -555,7 +555,7 @@
|
|||||||
<string name="msg_ip_reinsert_secret">Re-inserting secret key</string>
|
<string name="msg_ip_reinsert_secret">Re-inserting secret key</string>
|
||||||
<string name="msg_ip_uid_cert_bad">Encountered bad certificate!</string>
|
<string name="msg_ip_uid_cert_bad">Encountered bad certificate!</string>
|
||||||
<string name="msg_ip_uid_cert_error">Error processing certificate!</string>
|
<string name="msg_ip_uid_cert_error">Error processing certificate!</string>
|
||||||
<string name="msg_ip_uid_cert_good">User id is certified by %1$s (%2$s)</string>
|
<string name="msg_ip_uid_cert_good">User id is certified by %1$s</string>
|
||||||
<plurals name="msg_ip_uid_certs_unknown">
|
<plurals name="msg_ip_uid_certs_unknown">
|
||||||
<item quantity="one">Ignoring one certificate issued by an unknown public key</item>
|
<item quantity="one">Ignoring one certificate issued by an unknown public key</item>
|
||||||
<item quantity="other">Ignoring %s certificates issued by unknown public keys</item>
|
<item quantity="other">Ignoring %s certificates issued by unknown public keys</item>
|
||||||
@ -637,16 +637,29 @@
|
|||||||
<string name="msg_mg_new_subkey">Adding new subkey %s</string>
|
<string name="msg_mg_new_subkey">Adding new subkey %s</string>
|
||||||
<string name="msg_mg_found_new">Found %s new certificates in keyring</string>
|
<string name="msg_mg_found_new">Found %s new certificates in keyring</string>
|
||||||
|
|
||||||
|
<!-- createSecretKeyRing -->
|
||||||
|
<string name="msg_cr">Generating new master key</string>
|
||||||
|
<string name="msg_cr_error_no_master">No master key options specified!</string>
|
||||||
|
<string name="msg_cr_error_no_user_id">Keyrings must be created with at least one user id!</string>
|
||||||
|
<string name="msg_cr_error_no_certify">Master key must have certify flag!</string>
|
||||||
|
<string name="msg_cr_error_keysize_512">Key size must be greater or equal 512!</string>
|
||||||
|
<string name="msg_cr_error_internal_pgp">Internal PGP error!</string>
|
||||||
|
<string name="msg_cr_error_unknown_algo">Bad algorithm choice!</string>
|
||||||
|
<string name="msg_cr_error_master_elgamal">Master key must not be of type ElGamal!</string>
|
||||||
|
|
||||||
<!-- modifySecretKeyRing -->
|
<!-- modifySecretKeyRing -->
|
||||||
<string name="msg_mr">Modifying keyring %s</string>
|
<string name="msg_mr">Modifying keyring %s</string>
|
||||||
<string name="msg_mf_error_encode">Encoding exception!</string>
|
<string name="msg_mf_error_encode">Encoding exception!</string>
|
||||||
<string name="msg_mf_error_fingerprint">Actual key fingerprint does not match the expected one!</string>
|
<string name="msg_mf_error_fingerprint">Actual key fingerprint does not match the expected one!</string>
|
||||||
<string name="msg_mf_error_keyid">No key ID. This is an internal error, please file a bug report!</string>
|
<string name="msg_mf_error_keyid">No key ID. This is an internal error, please file a bug report!</string>
|
||||||
<string name="msg_mf_error_integrity">Internal error, integrity check failed!</string>
|
<string name="msg_mf_error_integrity">Internal error, integrity check failed!</string>
|
||||||
|
<string name="msg_mf_error_noexist_primary">Bad primary user id specified!</string>
|
||||||
<string name="msg_mf_error_revoked_primary">Revoked user ids cannot be primary!</string>
|
<string name="msg_mf_error_revoked_primary">Revoked user ids cannot be primary!</string>
|
||||||
<string name="msg_mf_error_pgp">PGP internal exception!</string>
|
<string name="msg_mf_error_pgp">PGP internal exception!</string>
|
||||||
<string name="msg_mf_error_sig">Signature exception!</string>
|
<string name="msg_mf_error_sig">Signature exception!</string>
|
||||||
<string name="msg_mf_passphrase">Changing passphrase</string>
|
<string name="msg_mf_passphrase">Changing passphrase</string>
|
||||||
|
<string name="msg_mf_primary_replace_old">Replacing certificate of previous primary user id</string>
|
||||||
|
<string name="msg_mf_primary_new">Generating new certificate for new primary user id</string>
|
||||||
<string name="msg_mf_subkey_change">Modifying subkey %s</string>
|
<string name="msg_mf_subkey_change">Modifying subkey %s</string>
|
||||||
<string name="msg_mf_subkey_missing">Tried to operate on missing subkey %s!</string>
|
<string name="msg_mf_subkey_missing">Tried to operate on missing subkey %s!</string>
|
||||||
<string name="msg_mf_subkey_new">Generating new %1$s bit %2$s subkey</string>
|
<string name="msg_mf_subkey_new">Generating new %1$s bit %2$s subkey</string>
|
||||||
|
10
build.gradle
10
build.gradle
@ -1,12 +1,16 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
// need this for com.novoda:gradle-android-test-plugin:0.9.9-SNAPSHOT below (0.9.3 in repos doesn't work!)
|
||||||
|
// run ./install-custom-gradle-test-plugin.sh to pull the thing into the local repository
|
||||||
|
mavenLocal()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// NOTE: Always use fixed version codes not dynamic ones, e.g. 0.7.3 instead of 0.7.+, see README for more information
|
// NOTE: Always use fixed version codes not dynamic ones, e.g. 0.7.3 instead of 0.7.+, see README for more information
|
||||||
classpath 'com.android.tools.build:gradle:0.12.0'
|
classpath 'com.android.tools.build:gradle:0.12.0'
|
||||||
classpath 'org.robolectric:robolectric-gradle-plugin:0.11.0'
|
classpath 'org.robolectric:robolectric-gradle-plugin:0.11.0'
|
||||||
|
classpath 'com.novoda:gradle-android-test-plugin:0.9.9-SNAPSHOT'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,3 +23,9 @@ allprojects {
|
|||||||
task wrapper(type: Wrapper) {
|
task wrapper(type: Wrapper) {
|
||||||
gradleVersion = '1.12'
|
gradleVersion = '1.12'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
tasks.withType(Test) {
|
||||||
|
maxParallelForks = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
15
install-custom-gradle-test-plugin.sh
Executable file
15
install-custom-gradle-test-plugin.sh
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
mkdir temp
|
||||||
|
cd temp
|
||||||
|
|
||||||
|
git clone https://github.com/nenick/gradle-android-test-plugin.git
|
||||||
|
cd gradle-android-test-plugin
|
||||||
|
|
||||||
|
echo "rootProject.name = 'gradle-android-test-plugin-parent'" > settings.gradle
|
||||||
|
echo "include ':gradle-android-test-plugin'" >> settings.gradle
|
||||||
|
|
||||||
|
./gradlew :gradle-android-test-plugin:install
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
cd ..
|
@ -1,4 +1,5 @@
|
|||||||
include ':OpenKeychain'
|
include ':OpenKeychain'
|
||||||
|
include ':OpenKeychain-Test'
|
||||||
include ':extern:openpgp-api-lib'
|
include ':extern:openpgp-api-lib'
|
||||||
include ':extern:openkeychain-api-lib'
|
include ':extern:openkeychain-api-lib'
|
||||||
include ':extern:html-textview'
|
include ':extern:html-textview'
|
||||||
|
Loading…
Reference in New Issue
Block a user