mirror of
https://github.com/moparisthebest/open-keychain
synced 2024-11-27 19:22:14 -05:00
Merge remote-tracking branch 'upstream/master'
Conflicts: OpenKeychain-Test/src/test/resources/extern/OpenPGP-Haskell OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java
This commit is contained in:
commit
d63534d655
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,9 +81,14 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.WizardActivity"
|
android:name=".ui.FirstTimeActivity"
|
||||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||||
android:label="@string/title_wizard"
|
android:label="@string/app_name"
|
||||||
|
android:windowSoftInputMode="stateHidden" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.CreateKeyActivity"
|
||||||
|
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||||
|
android:label="@string/title_create_key"
|
||||||
android:windowSoftInputMode="stateHidden" />
|
android:windowSoftInputMode="stateHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.EditKeyActivityOld"
|
android:name=".ui.EditKeyActivityOld"
|
||||||
|
@ -69,6 +69,7 @@ public final class Constants {
|
|||||||
public static final String KEY_SERVERS = "keyServers";
|
public static final String KEY_SERVERS = "keyServers";
|
||||||
public static final String KEY_SERVERS_DEFAULT_VERSION = "keyServersDefaultVersion";
|
public static final String KEY_SERVERS_DEFAULT_VERSION = "keyServersDefaultVersion";
|
||||||
public static final String CONCEAL_PGP_APPLICATION = "concealPgpApplication";
|
public static final String CONCEAL_PGP_APPLICATION = "concealPgpApplication";
|
||||||
|
public static final String FIRST_TIME = "firstTime";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class Defaults {
|
public static final class Defaults {
|
||||||
|
@ -139,6 +139,16 @@ public class Preferences {
|
|||||||
editor.commit();
|
editor.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean getFirstTime() {
|
||||||
|
return mSharedPreferences.getBoolean(Constants.Pref.FIRST_TIME, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFirstTime(boolean value) {
|
||||||
|
SharedPreferences.Editor editor = mSharedPreferences.edit();
|
||||||
|
editor.putBoolean(Constants.Pref.FIRST_TIME, value);
|
||||||
|
editor.commit();
|
||||||
|
}
|
||||||
|
|
||||||
public String[] getKeyServers() {
|
public String[] getKeyServers() {
|
||||||
String rawData = mSharedPreferences.getString(Constants.Pref.KEY_SERVERS,
|
String rawData = mSharedPreferences.getString(Constants.Pref.KEY_SERVERS,
|
||||||
Constants.Defaults.KEY_SERVERS);
|
Constants.Defaults.KEY_SERVERS);
|
||||||
|
@ -250,7 +250,7 @@ public class ImportKeysListEntry implements Serializable, Parcelable {
|
|||||||
|
|
||||||
mHashCode = key.hashCode();
|
mHashCode = key.hashCode();
|
||||||
|
|
||||||
mPrimaryUserId = key.getPrimaryUserId();
|
mPrimaryUserId = key.getPrimaryUserIdWithFallback();
|
||||||
mUserIds = key.getUnorderedUserIds();
|
mUserIds = key.getUnorderedUserIds();
|
||||||
|
|
||||||
// if there was no user id flagged as primary, use the first one
|
// if there was no user id flagged as primary, use the first one
|
||||||
|
@ -22,8 +22,10 @@ public abstract class KeyRing {
|
|||||||
|
|
||||||
abstract public String getPrimaryUserId() throws PgpGeneralException;
|
abstract public String getPrimaryUserId() throws PgpGeneralException;
|
||||||
|
|
||||||
public String[] getSplitPrimaryUserId() throws PgpGeneralException {
|
abstract public String getPrimaryUserIdWithFallback() throws PgpGeneralException;
|
||||||
return splitUserId(getPrimaryUserId());
|
|
||||||
|
public String[] getSplitPrimaryUserIdWithFallback() throws PgpGeneralException {
|
||||||
|
return splitUserId(getPrimaryUserIdWithFallback());
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract public boolean isRevoked() throws PgpGeneralException;
|
abstract public boolean isRevoked() throws PgpGeneralException;
|
||||||
|
@ -258,7 +258,7 @@ public class PgpDecryptVerify {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// get subkey which has been used for this encryption packet
|
// get subkey which has been used for this encryption packet
|
||||||
secretEncryptionKey = secretKeyRing.getSubKey(encData.getKeyID());
|
secretEncryptionKey = secretKeyRing.getSecretKey(encData.getKeyID());
|
||||||
if (secretEncryptionKey == null) {
|
if (secretEncryptionKey == null) {
|
||||||
// continue with the next packet in the while loop
|
// continue with the next packet in the while loop
|
||||||
continue;
|
continue;
|
||||||
@ -393,7 +393,7 @@ public class PgpDecryptVerify {
|
|||||||
signingRing = mProviderHelper.getWrappedPublicKeyRing(
|
signingRing = mProviderHelper.getWrappedPublicKeyRing(
|
||||||
KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(sigKeyId)
|
KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(sigKeyId)
|
||||||
);
|
);
|
||||||
signingKey = signingRing.getSubkey(sigKeyId);
|
signingKey = signingRing.getPublicKey(sigKeyId);
|
||||||
signatureIndex = i;
|
signatureIndex = i;
|
||||||
} catch (ProviderHelper.NotFoundException e) {
|
} catch (ProviderHelper.NotFoundException e) {
|
||||||
Log.d(Constants.TAG, "key not found!");
|
Log.d(Constants.TAG, "key not found!");
|
||||||
@ -409,7 +409,7 @@ public class PgpDecryptVerify {
|
|||||||
signatureResultBuilder.knownKey(true);
|
signatureResultBuilder.knownKey(true);
|
||||||
signatureResultBuilder.keyId(signingRing.getMasterKeyId());
|
signatureResultBuilder.keyId(signingRing.getMasterKeyId());
|
||||||
try {
|
try {
|
||||||
signatureResultBuilder.userId(signingRing.getPrimaryUserId());
|
signatureResultBuilder.userId(signingRing.getPrimaryUserIdWithFallback());
|
||||||
} catch(PgpGeneralException e) {
|
} catch(PgpGeneralException e) {
|
||||||
Log.d(Constants.TAG, "No primary user id in key " + signingRing.getMasterKeyId());
|
Log.d(Constants.TAG, "No primary user id in key " + signingRing.getMasterKeyId());
|
||||||
}
|
}
|
||||||
@ -578,7 +578,7 @@ public class PgpDecryptVerify {
|
|||||||
signingRing = mProviderHelper.getWrappedPublicKeyRing(
|
signingRing = mProviderHelper.getWrappedPublicKeyRing(
|
||||||
KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(sigKeyId)
|
KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(sigKeyId)
|
||||||
);
|
);
|
||||||
signingKey = signingRing.getSubkey(sigKeyId);
|
signingKey = signingRing.getPublicKey(sigKeyId);
|
||||||
signatureIndex = i;
|
signatureIndex = i;
|
||||||
} catch (ProviderHelper.NotFoundException e) {
|
} catch (ProviderHelper.NotFoundException e) {
|
||||||
Log.d(Constants.TAG, "key not found!");
|
Log.d(Constants.TAG, "key not found!");
|
||||||
@ -596,7 +596,7 @@ public class PgpDecryptVerify {
|
|||||||
signatureResultBuilder.knownKey(true);
|
signatureResultBuilder.knownKey(true);
|
||||||
signatureResultBuilder.keyId(signingRing.getMasterKeyId());
|
signatureResultBuilder.keyId(signingRing.getMasterKeyId());
|
||||||
try {
|
try {
|
||||||
signatureResultBuilder.userId(signingRing.getPrimaryUserId());
|
signatureResultBuilder.userId(signingRing.getPrimaryUserIdWithFallback());
|
||||||
} catch(PgpGeneralException e) {
|
} catch(PgpGeneralException e) {
|
||||||
Log.d(Constants.TAG, "No primary user id in key " + signingRing.getMasterKeyId());
|
Log.d(Constants.TAG, "No primary user id in key " + signingRing.getMasterKeyId());
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
@ -744,8 +748,12 @@ public class UncachedKeyRing {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.add(LogLevel.DEBUG, LogType.MSG_MG_FOUND_NEW,
|
if (newCerts > 0) {
|
||||||
indent, Integer.toString(newCerts));
|
log.add(LogLevel.DEBUG, LogType.MSG_MG_FOUND_NEW, indent,
|
||||||
|
Integer.toString(newCerts));
|
||||||
|
} else {
|
||||||
|
log.add(LogLevel.DEBUG, LogType.MSG_MG_UNCHANGED, indent);
|
||||||
|
}
|
||||||
|
|
||||||
return new UncachedKeyRing(result);
|
return new UncachedKeyRing(result);
|
||||||
|
|
||||||
|
@ -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,76 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns primary user id if existing. If not, return first encountered user id.
|
||||||
|
*/
|
||||||
|
public String getPrimaryUserIdWithFallback() {
|
||||||
|
String userId = getPrimaryUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
userId = (String) mPublicKey.getUserIDs().next();
|
||||||
}
|
}
|
||||||
}
|
return userId;
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ArrayList<String> getUnorderedUserIds() {
|
public ArrayList<String> getUnorderedUserIds() {
|
||||||
@ -186,6 +242,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>() {
|
||||||
|
@ -39,8 +39,12 @@ public abstract class WrappedKeyRing extends KeyRing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getPrimaryUserId() throws PgpGeneralException {
|
public String getPrimaryUserId() throws PgpGeneralException {
|
||||||
return (String) getRing().getPublicKey().getUserIDs().next();
|
return getPublicKey().getPrimaryUserId();
|
||||||
};
|
}
|
||||||
|
|
||||||
|
public String getPrimaryUserIdWithFallback() throws PgpGeneralException {
|
||||||
|
return getPublicKey().getPrimaryUserIdWithFallback();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isRevoked() throws PgpGeneralException {
|
public boolean isRevoked() throws PgpGeneralException {
|
||||||
// Is the master key revoked?
|
// Is the master key revoked?
|
||||||
@ -101,8 +105,16 @@ public abstract class WrappedKeyRing extends KeyRing {
|
|||||||
|
|
||||||
abstract public IterableIterator<WrappedPublicKey> publicKeyIterator();
|
abstract public IterableIterator<WrappedPublicKey> publicKeyIterator();
|
||||||
|
|
||||||
public UncachedKeyRing getUncached() {
|
public WrappedPublicKey getPublicKey() {
|
||||||
return new UncachedKeyRing(getRing());
|
return new WrappedPublicKey(this, getRing().getPublicKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
public WrappedPublicKey getPublicKey(long id) {
|
||||||
|
return new WrappedPublicKey(this, getRing().getPublicKey(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getEncoded() throws IOException {
|
||||||
|
return getRing().getEncoded();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -44,14 +44,6 @@ public class WrappedPublicKeyRing extends WrappedKeyRing {
|
|||||||
getRing().encode(stream);
|
getRing().encode(stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
public WrappedPublicKey getSubkey() {
|
|
||||||
return new WrappedPublicKey(this, getRing().getPublicKey());
|
|
||||||
}
|
|
||||||
|
|
||||||
public WrappedPublicKey getSubkey(long id) {
|
|
||||||
return new WrappedPublicKey(this, getRing().getPublicKey(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Getter that returns the subkey that should be used for signing. */
|
/** Getter that returns the subkey that should be used for signing. */
|
||||||
WrappedPublicKey getEncryptionSubKey() throws PgpGeneralException {
|
WrappedPublicKey getEncryptionSubKey() throws PgpGeneralException {
|
||||||
PGPPublicKey key = getRing().getPublicKey(getEncryptId());
|
PGPPublicKey key = getRing().getPublicKey(getEncryptId());
|
||||||
|
@ -97,7 +97,7 @@ public class WrappedSecretKey extends WrappedPublicKey {
|
|||||||
signatureGenerator.init(signatureType, mPrivateKey);
|
signatureGenerator.init(signatureType, mPrivateKey);
|
||||||
|
|
||||||
PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
|
PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
|
||||||
spGen.setSignerUserID(false, mRing.getPrimaryUserId());
|
spGen.setSignerUserID(false, mRing.getPrimaryUserIdWithFallback());
|
||||||
signatureGenerator.setHashedSubpackets(spGen.generate());
|
signatureGenerator.setHashedSubpackets(spGen.generate());
|
||||||
return signatureGenerator;
|
return signatureGenerator;
|
||||||
} catch(PGPException e) {
|
} catch(PGPException e) {
|
||||||
@ -175,7 +175,7 @@ public class WrappedSecretKey extends WrappedPublicKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get the master subkey (which we certify for)
|
// get the master subkey (which we certify for)
|
||||||
PGPPublicKey publicKey = publicKeyRing.getSubkey().getPublicKey();
|
PGPPublicKey publicKey = publicKeyRing.getPublicKey().getPublicKey();
|
||||||
|
|
||||||
// fetch public key ring, add the certification and return it
|
// fetch public key ring, add the certification and return it
|
||||||
for (String userId : new IterableIterator<String>(userIds.iterator())) {
|
for (String userId : new IterableIterator<String>(userIds.iterator())) {
|
||||||
|
@ -41,11 +41,11 @@ public class WrappedSecretKeyRing extends WrappedKeyRing {
|
|||||||
return mRing;
|
return mRing;
|
||||||
}
|
}
|
||||||
|
|
||||||
public WrappedSecretKey getSubKey() {
|
public WrappedSecretKey getSecretKey() {
|
||||||
return new WrappedSecretKey(this, mRing.getSecretKey());
|
return new WrappedSecretKey(this, mRing.getSecretKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public WrappedSecretKey getSubKey(long id) {
|
public WrappedSecretKey getSecretKey(long id) {
|
||||||
return new WrappedSecretKey(this, mRing.getSecretKey(id));
|
return new WrappedSecretKey(this, mRing.getSecretKey(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,12 +56,37 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRevocation() {
|
public boolean isRevocation() {
|
||||||
return mSig.getHashedSubPackets().hasSubpacket(SignatureSubpacketTags.REVOCATION_REASON);
|
return mSig.getSignatureType() == PGPSignature.CERTIFICATION_REVOCATION;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPrimaryUserId() {
|
public boolean isPrimaryUserId() {
|
||||||
|
@ -70,6 +70,10 @@ public class CachedPublicKeyRing extends KeyRing {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getPrimaryUserIdWithFallback() throws PgpGeneralException {
|
||||||
|
return getPrimaryUserId();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isRevoked() throws PgpGeneralException {
|
public boolean isRevoked() throws PgpGeneralException {
|
||||||
try {
|
try {
|
||||||
Object data = mProviderHelper.getGenericData(mUri,
|
Object data = mProviderHelper.getGenericData(mUri,
|
||||||
|
@ -199,7 +199,7 @@ public class ProviderHelper {
|
|||||||
byte[] blob = cursor.getBlob(3);
|
byte[] blob = cursor.getBlob(3);
|
||||||
if (blob != null) {
|
if (blob != null) {
|
||||||
result.put(masterKeyId,
|
result.put(masterKeyId,
|
||||||
new WrappedPublicKeyRing(blob, hasAnySecret, verified).getSubkey());
|
new WrappedPublicKeyRing(blob, hasAnySecret, verified).getPublicKey());
|
||||||
}
|
}
|
||||||
} while (cursor.moveToNext());
|
} while (cursor.moveToNext());
|
||||||
|
|
||||||
@ -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,10 +753,10 @@ 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 = secretRing.merge(oldSecretRing, mLog, mIndent);
|
||||||
|
|
||||||
// If this is null, there is an error in the log so we can just return
|
// If this is null, there is an error in the log so we can just return
|
||||||
if (secretRing == null) {
|
if (secretRing == null) {
|
||||||
@ -791,9 +790,9 @@ 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 secret ring into public one
|
||||||
publicRing = oldPublicRing.merge(secretRing, mLog, mIndent);
|
publicRing = oldPublicRing.merge(secretRing, mLog, mIndent);
|
||||||
if (publicRing == null) {
|
if (publicRing == null) {
|
||||||
return new SaveKeyringResult(SaveKeyringResult.RESULT_ERROR, mLog);
|
return new SaveKeyringResult(SaveKeyringResult.RESULT_ERROR, mLog);
|
||||||
|
@ -44,7 +44,6 @@ import org.sufficientlysecure.keychain.pgp.PgpKeyOperation;
|
|||||||
import org.sufficientlysecure.keychain.pgp.PgpSignEncrypt;
|
import org.sufficientlysecure.keychain.pgp.PgpSignEncrypt;
|
||||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||||
import org.sufficientlysecure.keychain.pgp.WrappedPublicKey;
|
|
||||||
import org.sufficientlysecure.keychain.pgp.WrappedPublicKeyRing;
|
import org.sufficientlysecure.keychain.pgp.WrappedPublicKeyRing;
|
||||||
import org.sufficientlysecure.keychain.pgp.WrappedSecretKey;
|
import org.sufficientlysecure.keychain.pgp.WrappedSecretKey;
|
||||||
import org.sufficientlysecure.keychain.pgp.WrappedSecretKeyRing;
|
import org.sufficientlysecure.keychain.pgp.WrappedSecretKeyRing;
|
||||||
@ -350,9 +349,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().getPrimaryUserIdWithFallback());
|
||||||
}
|
}
|
||||||
} catch (ProviderHelper.NotFoundException e) {
|
} catch (ProviderHelper.NotFoundException e) {
|
||||||
sendErrorToHandler(e);
|
sendErrorToHandler(e);
|
||||||
@ -546,7 +545,7 @@ public class KeychainIntentService extends IntentService
|
|||||||
ProviderHelper providerHelper = new ProviderHelper(this);
|
ProviderHelper providerHelper = new ProviderHelper(this);
|
||||||
WrappedPublicKeyRing publicRing = providerHelper.getWrappedPublicKeyRing(pubKeyId);
|
WrappedPublicKeyRing publicRing = providerHelper.getWrappedPublicKeyRing(pubKeyId);
|
||||||
WrappedSecretKeyRing secretKeyRing = providerHelper.getWrappedSecretKeyRing(masterKeyId);
|
WrappedSecretKeyRing secretKeyRing = providerHelper.getWrappedSecretKeyRing(masterKeyId);
|
||||||
WrappedSecretKey certificationKey = secretKeyRing.getSubKey();
|
WrappedSecretKey certificationKey = secretKeyRing.getSecretKey();
|
||||||
if(!certificationKey.unlock(signaturePassphrase)) {
|
if(!certificationKey.unlock(signaturePassphrase)) {
|
||||||
throw new PgpGeneralException("Error extracting key (bad passphrase?)");
|
throw new PgpGeneralException("Error extracting key (bad passphrase?)");
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
@ -235,9 +242,17 @@ public class OperationResultParcel implements Parcelable {
|
|||||||
MSG_MG_HETEROGENEOUS (R.string.msg_mg_heterogeneous),
|
MSG_MG_HETEROGENEOUS (R.string.msg_mg_heterogeneous),
|
||||||
MSG_MG_NEW_SUBKEY (R.string.msg_mg_new_subkey),
|
MSG_MG_NEW_SUBKEY (R.string.msg_mg_new_subkey),
|
||||||
MSG_MG_FOUND_NEW (R.string.msg_mg_found_new),
|
MSG_MG_FOUND_NEW (R.string.msg_mg_found_new),
|
||||||
|
MSG_MG_UNCHANGED (R.string.msg_mg_unchanged),
|
||||||
|
|
||||||
// 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 +260,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 +332,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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,7 +191,7 @@ public class PassphraseCacheService extends Service {
|
|||||||
Log.d(Constants.TAG, "Key has no passphrase! Caches and returns empty passphrase!");
|
Log.d(Constants.TAG, "Key has no passphrase! Caches and returns empty passphrase!");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
addCachedPassphrase(this, keyId, "", key.getPrimaryUserId());
|
addCachedPassphrase(this, keyId, "", key.getPrimaryUserIdWithFallback());
|
||||||
} catch (PgpGeneralException e) {
|
} catch (PgpGeneralException e) {
|
||||||
Log.d(Constants.TAG, "PgpGeneralException occured");
|
Log.d(Constants.TAG, "PgpGeneralException occured");
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
@ -0,0 +1,187 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.sufficientlysecure.keychain.ui;
|
||||||
|
|
||||||
|
import android.app.ProgressDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.os.Messenger;
|
||||||
|
import android.support.v7.app.ActionBarActivity;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.util.Patterns;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.AutoCompleteTextView;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
|
|
||||||
|
import org.spongycastle.bcpg.sig.KeyFlags;
|
||||||
|
import org.sufficientlysecure.keychain.Constants;
|
||||||
|
import org.sufficientlysecure.keychain.R;
|
||||||
|
import org.sufficientlysecure.keychain.helper.ContactHelper;
|
||||||
|
import org.sufficientlysecure.keychain.service.KeychainIntentService;
|
||||||
|
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
|
||||||
|
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
|
||||||
|
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
|
||||||
|
public class CreateKeyActivity extends ActionBarActivity {
|
||||||
|
|
||||||
|
AutoCompleteTextView nameEdit;
|
||||||
|
AutoCompleteTextView emailEdit;
|
||||||
|
EditText passphraseEdit;
|
||||||
|
Button createButton;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
setContentView(R.layout.create_key_activity);
|
||||||
|
|
||||||
|
nameEdit = (AutoCompleteTextView) findViewById(R.id.name);
|
||||||
|
emailEdit = (AutoCompleteTextView) findViewById(R.id.email);
|
||||||
|
passphraseEdit = (EditText) findViewById(R.id.passphrase);
|
||||||
|
createButton = (Button) findViewById(R.id.create_key_button);
|
||||||
|
|
||||||
|
emailEdit.setThreshold(1); // Start working from first character
|
||||||
|
emailEdit.setAdapter(
|
||||||
|
new ArrayAdapter<String>
|
||||||
|
(this, android.R.layout.simple_spinner_dropdown_item,
|
||||||
|
ContactHelper.getPossibleUserEmails(this)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
emailEdit.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable editable) {
|
||||||
|
String email = editable.toString();
|
||||||
|
if (email.length() > 0) {
|
||||||
|
Matcher emailMatcher = Patterns.EMAIL_ADDRESS.matcher(email);
|
||||||
|
if (emailMatcher.matches()) {
|
||||||
|
emailEdit.setCompoundDrawablesWithIntrinsicBounds(0, 0,
|
||||||
|
R.drawable.uid_mail_ok, 0);
|
||||||
|
} else {
|
||||||
|
emailEdit.setCompoundDrawablesWithIntrinsicBounds(0, 0,
|
||||||
|
R.drawable.uid_mail_bad, 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// remove drawable if email is empty
|
||||||
|
emailEdit.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
nameEdit.setThreshold(1); // Start working from first character
|
||||||
|
nameEdit.setAdapter(
|
||||||
|
new ArrayAdapter<String>
|
||||||
|
(this, android.R.layout.simple_spinner_dropdown_item,
|
||||||
|
ContactHelper.getPossibleUserNames(this)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
createButton.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
createKeyCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createKeyCheck() {
|
||||||
|
if (isEditTextNotEmpty(this, nameEdit)
|
||||||
|
&& isEditTextNotEmpty(this, emailEdit)
|
||||||
|
&& isEditTextNotEmpty(this, passphraseEdit)) {
|
||||||
|
createKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createKey() {
|
||||||
|
Intent intent = new Intent(this, KeychainIntentService.class);
|
||||||
|
intent.setAction(KeychainIntentService.ACTION_SAVE_KEYRING);
|
||||||
|
|
||||||
|
// Message is received after importing is done in KeychainIntentService
|
||||||
|
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(
|
||||||
|
this,
|
||||||
|
getString(R.string.progress_importing),
|
||||||
|
ProgressDialog.STYLE_HORIZONTAL) {
|
||||||
|
public void handleMessage(Message message) {
|
||||||
|
// handle messages by standard KeychainIntentServiceHandler first
|
||||||
|
super.handleMessage(message);
|
||||||
|
|
||||||
|
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
|
||||||
|
CreateKeyActivity.this.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// fill values for this action
|
||||||
|
Bundle data = new Bundle();
|
||||||
|
|
||||||
|
SaveKeyringParcel parcel = new SaveKeyringParcel();
|
||||||
|
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Constants.choice.algorithm.rsa, 4096, KeyFlags.CERTIFY_OTHER, null));
|
||||||
|
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Constants.choice.algorithm.rsa, 4096, KeyFlags.SIGN_DATA, null));
|
||||||
|
parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Constants.choice.algorithm.rsa, 4096, KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, null));
|
||||||
|
String userId = nameEdit.getText().toString() + " <" + emailEdit.getText().toString() + ">";
|
||||||
|
parcel.mAddUserIds.add(userId);
|
||||||
|
parcel.mNewPassphrase = passphraseEdit.getText().toString();
|
||||||
|
|
||||||
|
// get selected key entries
|
||||||
|
data.putParcelable(KeychainIntentService.SAVE_KEYRING_PARCEL, parcel);
|
||||||
|
|
||||||
|
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
|
||||||
|
|
||||||
|
// Create a new Messenger for the communication back
|
||||||
|
Messenger messenger = new Messenger(saveHandler);
|
||||||
|
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
|
||||||
|
|
||||||
|
saveHandler.showProgressDialog(this);
|
||||||
|
|
||||||
|
startService(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if text of given EditText is not empty. If it is empty an error is
|
||||||
|
* set and the EditText gets the focus.
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* @param editText
|
||||||
|
* @return true if EditText is not empty
|
||||||
|
*/
|
||||||
|
private static boolean isEditTextNotEmpty(Context context, EditText editText) {
|
||||||
|
boolean output = true;
|
||||||
|
if (editText.getText().toString().length() == 0) {
|
||||||
|
editText.setError("empty!");
|
||||||
|
editText.requestFocus();
|
||||||
|
output = false;
|
||||||
|
} else {
|
||||||
|
editText.setError(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
@ -280,7 +280,7 @@ public class EditKeyActivityOld extends ActionBarActivity implements EditorListe
|
|||||||
Uri secretUri = KeyRings.buildUnifiedKeyRingUri(mDataUri);
|
Uri secretUri = KeyRings.buildUnifiedKeyRingUri(mDataUri);
|
||||||
WrappedSecretKeyRing keyRing = new ProviderHelper(this).getWrappedSecretKeyRing(secretUri);
|
WrappedSecretKeyRing keyRing = new ProviderHelper(this).getWrappedSecretKeyRing(secretUri);
|
||||||
|
|
||||||
mMasterCanSign = keyRing.getSubKey().canCertify();
|
mMasterCanSign = keyRing.getSecretKey().canCertify();
|
||||||
for (WrappedSecretKey key : keyRing.secretKeyIterator()) {
|
for (WrappedSecretKey key : keyRing.secretKeyIterator()) {
|
||||||
// Turn into uncached instance
|
// Turn into uncached instance
|
||||||
mKeys.add(key.getUncached());
|
mKeys.add(key.getUncached());
|
||||||
@ -288,7 +288,7 @@ public class EditKeyActivityOld extends ActionBarActivity implements EditorListe
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean isSet = false;
|
boolean isSet = false;
|
||||||
for (String userId : keyRing.getSubKey().getUserIds()) {
|
for (String userId : keyRing.getSecretKey().getUserIds()) {
|
||||||
Log.d(Constants.TAG, "Added userId " + userId);
|
Log.d(Constants.TAG, "Added userId " + userId);
|
||||||
if (!isSet) {
|
if (!isSet) {
|
||||||
isSet = true;
|
isSet = true;
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
@ -450,10 +450,11 @@ 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());
|
mSaveKeyringParcel.mAddUserIds = mUserIdsAddedAdapter.getDataAsStringList();
|
||||||
Log.d(Constants.TAG, "mSaveKeyringParcel.newPassphrase: " + mSaveKeyringParcel.newPassphrase);
|
|
||||||
|
|
||||||
mSaveKeyringParcel.addUserIds = mUserIdsAddedAdapter.getDataAsStringList();
|
Log.d(Constants.TAG, "mSaveKeyringParcel.mAddUserIds: " + mSaveKeyringParcel.mAddUserIds);
|
||||||
|
Log.d(Constants.TAG, "mSaveKeyringParcel.mNewPassphrase: " + mSaveKeyringParcel.mNewPassphrase);
|
||||||
|
Log.d(Constants.TAG, "mSaveKeyringParcel.mRevokeUserIds: " + mSaveKeyringParcel.mRevokeUserIds);
|
||||||
|
|
||||||
// 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(
|
||||||
|
@ -198,7 +198,7 @@ public class EncryptAsymmetricFragment extends Fragment {
|
|||||||
String[] userId;
|
String[] userId;
|
||||||
try {
|
try {
|
||||||
userId = mProviderHelper.getCachedPublicKeyRing(
|
userId = mProviderHelper.getCachedPublicKeyRing(
|
||||||
KeyRings.buildUnifiedKeyRingUri(mSecretKeyId)).getSplitPrimaryUserId();
|
KeyRings.buildUnifiedKeyRingUri(mSecretKeyId)).getSplitPrimaryUserIdWithFallback();
|
||||||
} catch (PgpGeneralException e) {
|
} catch (PgpGeneralException e) {
|
||||||
userId = null;
|
userId = null;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.sufficientlysecure.keychain.ui;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.v7.app.ActionBarActivity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.Window;
|
||||||
|
import android.widget.Button;
|
||||||
|
|
||||||
|
import org.sufficientlysecure.keychain.R;
|
||||||
|
|
||||||
|
public class FirstTimeActivity extends ActionBarActivity {
|
||||||
|
|
||||||
|
Button mCreateKey;
|
||||||
|
Button mImportKey;
|
||||||
|
Button mSkipSetup;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
|
|
||||||
|
setContentView(R.layout.first_time_activity);
|
||||||
|
|
||||||
|
mCreateKey = (Button) findViewById(R.id.first_time_create_key);
|
||||||
|
mImportKey = (Button) findViewById(R.id.first_time_import_key);
|
||||||
|
mSkipSetup = (Button) findViewById(R.id.first_time_cancel);
|
||||||
|
|
||||||
|
mSkipSetup.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
Intent intent = new Intent(FirstTimeActivity.this, KeyListActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mImportKey.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
Intent intent = new Intent(FirstTimeActivity.this, ImportKeysActivity.class);
|
||||||
|
intent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN);
|
||||||
|
startActivity(intent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mCreateKey.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
Intent intent = new Intent(FirstTimeActivity.this, CreateKeyActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -32,8 +32,7 @@ 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.helper.Preferences;
|
||||||
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 +42,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 {
|
||||||
|
|
||||||
@ -53,6 +51,15 @@ public class KeyListActivity extends DrawerActivity {
|
|||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
Preferences prefs = Preferences.getPreferences(this);
|
||||||
|
if (prefs.getFirstTime()) {
|
||||||
|
prefs.setFirstTime(false);
|
||||||
|
Intent intent = new Intent(this, FirstTimeActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
mExportHelper = new ExportHelper(this);
|
mExportHelper = new ExportHelper(this);
|
||||||
|
|
||||||
setContentView(R.layout.key_list_activity);
|
setContentView(R.layout.key_list_activity);
|
||||||
@ -69,6 +76,7 @@ public class KeyListActivity extends DrawerActivity {
|
|||||||
if (Constants.DEBUG) {
|
if (Constants.DEBUG) {
|
||||||
menu.findItem(R.id.menu_key_list_debug_read).setVisible(true);
|
menu.findItem(R.id.menu_key_list_debug_read).setVisible(true);
|
||||||
menu.findItem(R.id.menu_key_list_debug_write).setVisible(true);
|
menu.findItem(R.id.menu_key_list_debug_write).setVisible(true);
|
||||||
|
menu.findItem(R.id.menu_key_list_debug_first_time).setVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -85,10 +93,6 @@ public class KeyListActivity extends DrawerActivity {
|
|||||||
createKey();
|
createKey();
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case R.id.menu_key_list_create_expert:
|
|
||||||
createKeyExpert();
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case R.id.menu_key_list_export:
|
case R.id.menu_key_list_export:
|
||||||
mExportHelper.showExportKeysDialog(null, Constants.Path.APP_DIR_FILE, true);
|
mExportHelper.showExportKeysDialog(null, Constants.Path.APP_DIR_FILE, true);
|
||||||
return true;
|
return true;
|
||||||
@ -114,6 +118,12 @@ public class KeyListActivity extends DrawerActivity {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case R.id.menu_key_list_debug_first_time:
|
||||||
|
Intent intent = new Intent(this, FirstTimeActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
finish();
|
||||||
|
return true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
@ -125,50 +135,8 @@ public class KeyListActivity extends DrawerActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void createKey() {
|
private void createKey() {
|
||||||
Intent intent = new Intent(this, WizardActivity.class);
|
Intent intent = new Intent(this, CreateKeyActivity.class);
|
||||||
// intent.setAction(EditKeyActivity.ACTION_CREATE_KEY);
|
startActivity(intent);
|
||||||
// intent.putExtra(EditKeyActivity.EXTRA_GENERATE_DEFAULT_KEYS, true);
|
|
||||||
// intent.putExtra(EditKeyActivity.EXTRA_USER_IDS, ""); // show user id view
|
|
||||||
startActivityForResult(intent, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createKeyExpert() {
|
|
||||||
Intent intent = new Intent(this, KeychainIntentService.class);
|
|
||||||
intent.setAction(KeychainIntentService.ACTION_SAVE_KEYRING);
|
|
||||||
|
|
||||||
// Message is received after importing is done in KeychainIntentService
|
|
||||||
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(
|
|
||||||
this,
|
|
||||||
getString(R.string.progress_importing),
|
|
||||||
ProgressDialog.STYLE_HORIZONTAL) {
|
|
||||||
public void handleMessage(Message message) {
|
|
||||||
// handle messages by standard KeychainIntentServiceHandler first
|
|
||||||
super.handleMessage(message);
|
|
||||||
Bundle data = message.getData();
|
|
||||||
// OtherHelper.logDebugBundle(data, "message reply");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// fill values for this action
|
|
||||||
Bundle data = new Bundle();
|
|
||||||
|
|
||||||
SaveKeyringParcel parcel = new SaveKeyringParcel();
|
|
||||||
parcel.addSubKeys.add(new SubkeyAdd(algorithm.rsa, 1024, KeyFlags.CERTIFY_OTHER, null));
|
|
||||||
parcel.addSubKeys.add(new SubkeyAdd(algorithm.rsa, 1024, KeyFlags.SIGN_DATA, null));
|
|
||||||
parcel.addUserIds.add("swagerinho");
|
|
||||||
parcel.newPassphrase = "swag";
|
|
||||||
|
|
||||||
// get selected key entries
|
|
||||||
data.putParcelable(KeychainIntentService.SAVE_KEYRING_PARCEL, parcel);
|
|
||||||
|
|
||||||
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
|
|
||||||
|
|
||||||
// Create a new Messenger for the communication back
|
|
||||||
Messenger messenger = new Messenger(saveHandler);
|
|
||||||
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
|
|
||||||
|
|
||||||
saveHandler.showProgressDialog(this);
|
|
||||||
|
|
||||||
startService(intent);
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -149,8 +149,8 @@ public class ViewCertActivity extends ActionBarActivity
|
|||||||
providerHelper.getWrappedPublicKeyRing(sig.getKeyId());
|
providerHelper.getWrappedPublicKeyRing(sig.getKeyId());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sig.init(signerRing.getSubkey());
|
sig.init(signerRing.getPublicKey());
|
||||||
if (sig.verifySignature(signeeRing.getSubkey(), signeeUid)) {
|
if (sig.verifySignature(signeeRing.getPublicKey(), signeeUid)) {
|
||||||
mStatus.setText(R.string.cert_verify_ok);
|
mStatus.setText(R.string.cert_verify_ok);
|
||||||
mStatus.setTextColor(getResources().getColor(R.color.result_green));
|
mStatus.setTextColor(getResources().getColor(R.color.result_green));
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,466 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.sufficientlysecure.keychain.ui;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.ActivityNotFoundException;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.v4.app.Fragment;
|
|
||||||
import android.support.v4.app.FragmentManager;
|
|
||||||
import android.support.v4.app.FragmentTransaction;
|
|
||||||
import android.support.v7.app.ActionBarActivity;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.util.Patterns;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.inputmethod.InputMethodManager;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.AutoCompleteTextView;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.RadioGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.sufficientlysecure.htmltextview.HtmlTextView;
|
|
||||||
import org.sufficientlysecure.keychain.Constants;
|
|
||||||
import org.sufficientlysecure.keychain.R;
|
|
||||||
import org.sufficientlysecure.keychain.helper.ContactHelper;
|
|
||||||
import org.sufficientlysecure.keychain.util.Log;
|
|
||||||
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
|
|
||||||
|
|
||||||
public class WizardActivity extends ActionBarActivity {
|
|
||||||
|
|
||||||
private State mCurrentState;
|
|
||||||
|
|
||||||
// values for mCurrentScreen
|
|
||||||
private enum State {
|
|
||||||
START, CREATE_KEY, IMPORT_KEY, K9
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final int REQUEST_CODE_IMPORT = 0x00007703;
|
|
||||||
|
|
||||||
Button mBackButton;
|
|
||||||
Button mNextButton;
|
|
||||||
StartFragment mStartFragment;
|
|
||||||
CreateKeyFragment mCreateKeyFragment;
|
|
||||||
K9Fragment mK9Fragment;
|
|
||||||
|
|
||||||
private static final String K9_PACKAGE = "com.fsck.k9";
|
|
||||||
// private static final String K9_MARKET_INTENT_URI_BASE = "market://details?id=%s";
|
|
||||||
// private static final Intent K9_MARKET_INTENT = new Intent(Intent.ACTION_VIEW, Uri.parse(
|
|
||||||
// String.format(K9_MARKET_INTENT_URI_BASE, K9_PACKAGE)));
|
|
||||||
private static final Intent K9_MARKET_INTENT = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/k9mail/k-9/releases/tag/4.904"));
|
|
||||||
|
|
||||||
LinearLayout mProgressLayout;
|
|
||||||
View mProgressLine;
|
|
||||||
ProgressBar mProgressBar;
|
|
||||||
ImageView mProgressImage;
|
|
||||||
TextView mProgressText;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if text of given EditText is not empty. If it is empty an error is
|
|
||||||
* set and the EditText gets the focus.
|
|
||||||
*
|
|
||||||
* @param context
|
|
||||||
* @param editText
|
|
||||||
* @return true if EditText is not empty
|
|
||||||
*/
|
|
||||||
private static boolean isEditTextNotEmpty(Context context, EditText editText) {
|
|
||||||
boolean output = true;
|
|
||||||
if (editText.getText().toString().length() == 0) {
|
|
||||||
editText.setError("empty!");
|
|
||||||
editText.requestFocus();
|
|
||||||
output = false;
|
|
||||||
} else {
|
|
||||||
editText.setError(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class StartFragment extends Fragment {
|
|
||||||
public static StartFragment newInstance() {
|
|
||||||
StartFragment myFragment = new StartFragment();
|
|
||||||
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
myFragment.setArguments(args);
|
|
||||||
|
|
||||||
return myFragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
|
||||||
Bundle savedInstanceState) {
|
|
||||||
return inflater.inflate(R.layout.wizard_start_fragment,
|
|
||||||
container, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class CreateKeyFragment extends Fragment {
|
|
||||||
public static CreateKeyFragment newInstance() {
|
|
||||||
CreateKeyFragment myFragment = new CreateKeyFragment();
|
|
||||||
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
myFragment.setArguments(args);
|
|
||||||
|
|
||||||
return myFragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
|
||||||
Bundle savedInstanceState) {
|
|
||||||
View view = inflater.inflate(R.layout.wizard_create_key_fragment,
|
|
||||||
container, false);
|
|
||||||
|
|
||||||
final AutoCompleteTextView emailView = (AutoCompleteTextView) view.findViewById(R.id.email);
|
|
||||||
emailView.setThreshold(1); // Start working from first character
|
|
||||||
emailView.setAdapter(
|
|
||||||
new ArrayAdapter<String>
|
|
||||||
(getActivity(), android.R.layout.simple_spinner_dropdown_item,
|
|
||||||
ContactHelper.getPossibleUserEmails(getActivity())
|
|
||||||
)
|
|
||||||
);
|
|
||||||
emailView.addTextChangedListener(new TextWatcher() {
|
|
||||||
@Override
|
|
||||||
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterTextChanged(Editable editable) {
|
|
||||||
String email = editable.toString();
|
|
||||||
if (email.length() > 0) {
|
|
||||||
Matcher emailMatcher = Patterns.EMAIL_ADDRESS.matcher(email);
|
|
||||||
if (emailMatcher.matches()) {
|
|
||||||
emailView.setCompoundDrawablesWithIntrinsicBounds(0, 0,
|
|
||||||
R.drawable.uid_mail_ok, 0);
|
|
||||||
} else {
|
|
||||||
emailView.setCompoundDrawablesWithIntrinsicBounds(0, 0,
|
|
||||||
R.drawable.uid_mail_bad, 0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// remove drawable if email is empty
|
|
||||||
emailView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
final AutoCompleteTextView nameView = (AutoCompleteTextView) view.findViewById(R.id.name);
|
|
||||||
nameView.setThreshold(1); // Start working from first character
|
|
||||||
nameView.setAdapter(
|
|
||||||
new ArrayAdapter<String>
|
|
||||||
(getActivity(), android.R.layout.simple_spinner_dropdown_item,
|
|
||||||
ContactHelper.getPossibleUserNames(getActivity())
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class K9Fragment extends Fragment {
|
|
||||||
public static K9Fragment newInstance() {
|
|
||||||
K9Fragment myFragment = new K9Fragment();
|
|
||||||
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
myFragment.setArguments(args);
|
|
||||||
|
|
||||||
return myFragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
|
||||||
Bundle savedInstanceState) {
|
|
||||||
View v = inflater.inflate(R.layout.wizard_k9_fragment,
|
|
||||||
container, false);
|
|
||||||
|
|
||||||
HtmlTextView text = (HtmlTextView) v
|
|
||||||
.findViewById(R.id.wizard_k9_text);
|
|
||||||
text.setHtmlFromString("Install K9. It's good for you! Here is a screenhot how to enable OK in K9: (TODO)", true);
|
|
||||||
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads new fragment
|
|
||||||
*
|
|
||||||
* @param fragment
|
|
||||||
*/
|
|
||||||
private void loadFragment(Fragment fragment) {
|
|
||||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
|
||||||
FragmentTransaction fragmentTransaction = fragmentManager
|
|
||||||
.beginTransaction();
|
|
||||||
fragmentTransaction.replace(R.id.wizard_container,
|
|
||||||
fragment);
|
|
||||||
fragmentTransaction.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiate View and initialize fragments for this Activity
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
setContentView(R.layout.wizard_activity);
|
|
||||||
mBackButton = (Button) findViewById(R.id.wizard_back);
|
|
||||||
mNextButton = (Button) findViewById(R.id.wizard_next);
|
|
||||||
|
|
||||||
// progress layout
|
|
||||||
mProgressLayout = (LinearLayout) findViewById(R.id.wizard_progress);
|
|
||||||
mProgressLine = findViewById(R.id.wizard_progress_line);
|
|
||||||
mProgressBar = (ProgressBar) findViewById(R.id.wizard_progress_progressbar);
|
|
||||||
mProgressImage = (ImageView) findViewById(R.id.wizard_progress_image);
|
|
||||||
mProgressText = (TextView) findViewById(R.id.wizard_progress_text);
|
|
||||||
|
|
||||||
changeToState(State.START);
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum ProgressState {
|
|
||||||
WORKING, ENABLED, DISABLED, ERROR
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showProgress(ProgressState state, String text) {
|
|
||||||
switch (state) {
|
|
||||||
case WORKING:
|
|
||||||
mProgressBar.setVisibility(View.VISIBLE);
|
|
||||||
mProgressImage.setVisibility(View.GONE);
|
|
||||||
break;
|
|
||||||
case ENABLED:
|
|
||||||
mProgressBar.setVisibility(View.GONE);
|
|
||||||
mProgressImage.setVisibility(View.VISIBLE);
|
|
||||||
// mProgressImage.setImageDrawable(getResources().getDrawable(
|
|
||||||
// R.drawable.status_enabled));
|
|
||||||
break;
|
|
||||||
case DISABLED:
|
|
||||||
mProgressBar.setVisibility(View.GONE);
|
|
||||||
mProgressImage.setVisibility(View.VISIBLE);
|
|
||||||
// mProgressImage.setImageDrawable(getResources().getDrawable(
|
|
||||||
// R.drawable.status_disabled));
|
|
||||||
break;
|
|
||||||
case ERROR:
|
|
||||||
mProgressBar.setVisibility(View.GONE);
|
|
||||||
mProgressImage.setVisibility(View.VISIBLE);
|
|
||||||
// mProgressImage.setImageDrawable(getResources().getDrawable(
|
|
||||||
// R.drawable.status_fail));
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
mProgressText.setText(text);
|
|
||||||
|
|
||||||
mProgressLine.setVisibility(View.VISIBLE);
|
|
||||||
mProgressLayout.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void hideProgress() {
|
|
||||||
mProgressLine.setVisibility(View.GONE);
|
|
||||||
mProgressLayout.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void nextOnClick(View view) {
|
|
||||||
// close keyboard
|
|
||||||
if (getCurrentFocus() != null) {
|
|
||||||
InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
inputManager.hideSoftInputFromWindow(getCurrentFocus()
|
|
||||||
.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (mCurrentState) {
|
|
||||||
case START: {
|
|
||||||
RadioGroup radioGroup = (RadioGroup) findViewById(R.id.wizard_start_radio_group);
|
|
||||||
int selectedId = radioGroup.getCheckedRadioButtonId();
|
|
||||||
switch (selectedId) {
|
|
||||||
case R.id.wizard_start_new_key: {
|
|
||||||
changeToState(State.CREATE_KEY);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case R.id.wizard_start_import: {
|
|
||||||
changeToState(State.IMPORT_KEY);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case R.id.wizard_start_skip: {
|
|
||||||
finish();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mBackButton.setText(R.string.btn_back);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case CREATE_KEY:
|
|
||||||
EditText nameEdit = (EditText) findViewById(R.id.name);
|
|
||||||
EditText emailEdit = (EditText) findViewById(R.id.email);
|
|
||||||
EditText passphraseEdit = (EditText) findViewById(R.id.passphrase);
|
|
||||||
|
|
||||||
if (isEditTextNotEmpty(this, nameEdit)
|
|
||||||
&& isEditTextNotEmpty(this, emailEdit)
|
|
||||||
&& isEditTextNotEmpty(this, passphraseEdit)) {
|
|
||||||
|
|
||||||
// SaveKeyringParcel newKey = new SaveKeyringParcel();
|
|
||||||
// newKey.addUserIds.add(nameEdit.getText().toString() + " <"
|
|
||||||
// + emailEdit.getText().toString() + ">");
|
|
||||||
|
|
||||||
|
|
||||||
AsyncTask<String, Boolean, Boolean> generateTask = new AsyncTask<String, Boolean, Boolean>() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPreExecute() {
|
|
||||||
super.onPreExecute();
|
|
||||||
|
|
||||||
showProgress(ProgressState.WORKING, "generating key...");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Boolean doInBackground(String... params) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Boolean result) {
|
|
||||||
super.onPostExecute(result);
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
showProgress(ProgressState.ENABLED, "key generated successfully!");
|
|
||||||
|
|
||||||
changeToState(State.K9);
|
|
||||||
} else {
|
|
||||||
showProgress(ProgressState.ERROR, "error in key gen");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
generateTask.execute("");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case K9: {
|
|
||||||
RadioGroup radioGroup = (RadioGroup) findViewById(R.id.wizard_k9_radio_group);
|
|
||||||
int selectedId = radioGroup.getCheckedRadioButtonId();
|
|
||||||
switch (selectedId) {
|
|
||||||
case R.id.wizard_k9_install: {
|
|
||||||
try {
|
|
||||||
startActivity(K9_MARKET_INTENT);
|
|
||||||
} catch (ActivityNotFoundException e) {
|
|
||||||
Log.e(Constants.TAG, "Activity not found for: " + K9_MARKET_INTENT);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case R.id.wizard_k9_skip: {
|
|
||||||
finish();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finish();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
|
||||||
switch (requestCode) {
|
|
||||||
case REQUEST_CODE_IMPORT: {
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
// imported now...
|
|
||||||
changeToState(State.K9);
|
|
||||||
} else {
|
|
||||||
// back to start
|
|
||||||
changeToState(State.START);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void backOnClick(View view) {
|
|
||||||
switch (mCurrentState) {
|
|
||||||
case START:
|
|
||||||
finish();
|
|
||||||
break;
|
|
||||||
case CREATE_KEY:
|
|
||||||
changeToState(State.START);
|
|
||||||
break;
|
|
||||||
case IMPORT_KEY:
|
|
||||||
changeToState(State.START);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
changeToState(State.START);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void changeToState(State state) {
|
|
||||||
switch (state) {
|
|
||||||
case START: {
|
|
||||||
mCurrentState = State.START;
|
|
||||||
mStartFragment = StartFragment.newInstance();
|
|
||||||
loadFragment(mStartFragment);
|
|
||||||
mBackButton.setText(android.R.string.cancel);
|
|
||||||
mNextButton.setText(R.string.btn_next);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case CREATE_KEY: {
|
|
||||||
mCurrentState = State.CREATE_KEY;
|
|
||||||
mCreateKeyFragment = CreateKeyFragment.newInstance();
|
|
||||||
loadFragment(mCreateKeyFragment);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case IMPORT_KEY: {
|
|
||||||
mCurrentState = State.IMPORT_KEY;
|
|
||||||
Intent intent = new Intent(this, ImportKeysActivity.class);
|
|
||||||
intent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN);
|
|
||||||
startActivityForResult(intent, REQUEST_CODE_IMPORT);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case K9: {
|
|
||||||
mCurrentState = State.K9;
|
|
||||||
mBackButton.setEnabled(false); // don't go back to import/create key
|
|
||||||
mK9Fragment = K9Fragment.newInstance();
|
|
||||||
loadFragment(mK9Fragment);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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)
|
||||||
|
@ -152,7 +152,7 @@ public class PassphraseDialogFragment extends DialogFragment implements OnEditor
|
|||||||
// above can't be statically verified to have been set in all cases because
|
// above can't be statically verified to have been set in all cases because
|
||||||
// the catch clause doesn't return.
|
// the catch clause doesn't return.
|
||||||
try {
|
try {
|
||||||
userId = secretRing.getPrimaryUserId();
|
userId = secretRing.getPrimaryUserIdWithFallback();
|
||||||
} catch (PgpGeneralException e) {
|
} catch (PgpGeneralException e) {
|
||||||
userId = null;
|
userId = null;
|
||||||
}
|
}
|
||||||
@ -232,7 +232,7 @@ public class PassphraseDialogFragment extends DialogFragment implements OnEditor
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
PassphraseCacheService.addCachedPassphrase(activity, masterKeyId, passphrase,
|
PassphraseCacheService.addCachedPassphrase(activity, masterKeyId, passphrase,
|
||||||
secretRing.getPrimaryUserId());
|
secretRing.getPrimaryUserIdWithFallback());
|
||||||
} catch(PgpGeneralException e) {
|
} catch(PgpGeneralException e) {
|
||||||
Log.e(Constants.TAG, "adding of a passhrase failed", e);
|
Log.e(Constants.TAG, "adding of a passhrase failed", e);
|
||||||
}
|
}
|
||||||
@ -240,7 +240,7 @@ public class PassphraseDialogFragment extends DialogFragment implements OnEditor
|
|||||||
if (unlockedSecretKey.getKeyId() != masterKeyId) {
|
if (unlockedSecretKey.getKeyId() != masterKeyId) {
|
||||||
PassphraseCacheService.addCachedPassphrase(
|
PassphraseCacheService.addCachedPassphrase(
|
||||||
activity, unlockedSecretKey.getKeyId(), passphrase,
|
activity, unlockedSecretKey.getKeyId(), passphrase,
|
||||||
unlockedSecretKey.getPrimaryUserId());
|
unlockedSecretKey.getPrimaryUserIdWithFallback());
|
||||||
}
|
}
|
||||||
|
|
||||||
// also return passphrase back to activity
|
// also return passphrase back to activity
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
BIN
OpenKeychain/src/main/res/drawable/first_time_1.png
Normal file
BIN
OpenKeychain/src/main/res/drawable/first_time_1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 43 KiB |
@ -2,6 +2,7 @@
|
|||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="8dp"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -38,4 +39,16 @@
|
|||||||
android:layout_gravity="center_horizontal" />
|
android:layout_gravity="center_horizontal" />
|
||||||
|
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/create_key_button"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:text="@string/first_time_create_key"
|
||||||
|
android:background="@drawable/button_edgy"
|
||||||
|
android:drawableLeft="@drawable/ic_action_new_account" />
|
||||||
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
89
OpenKeychain/src/main/res/layout/first_time_activity.xml
Normal file
89
OpenKeychain/src/main/res/layout/first_time_activity.xml
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:paddingBottom="8dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||||
|
android:text="@string/app_name"
|
||||||
|
android:drawableLeft="@drawable/ic_launcher"
|
||||||
|
android:drawablePadding="16dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_gravity="center_horizontal" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_marginLeft="64dp"
|
||||||
|
android:layout_marginRight="64dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:layout_height="256dp"
|
||||||
|
android:src="@drawable/first_time_1" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="64dp"
|
||||||
|
android:layout_marginRight="64dp"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
android:text="@string/first_time_text1"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:gravity="center_horizontal" />
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/first_time_cancel"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:text="@string/first_time_skip"
|
||||||
|
android:background="@drawable/button_edgy" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_above="@id/first_time_cancel"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/first_time_create_key"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:text="@string/first_time_create_key"
|
||||||
|
android:background="@drawable/button_edgy"
|
||||||
|
android:drawableLeft="@drawable/ic_action_new_account" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/first_time_import_key"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:text="@string/first_time_import_key"
|
||||||
|
android:background="@drawable/button_edgy"
|
||||||
|
android:drawableLeft="@drawable/ic_action_download" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</RelativeLayout>
|
@ -1,98 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/wizard_buttons"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentBottom="true"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/wizard_back"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:onClick="backOnClick"
|
|
||||||
android:text="cancel"
|
|
||||||
style="@style/SelectableItem" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="1dip"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_marginBottom="4dip"
|
|
||||||
android:layout_marginTop="4dip"
|
|
||||||
android:background="?android:attr/listDivider" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/wizard_next"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:onClick="nextOnClick"
|
|
||||||
android:text="next"
|
|
||||||
style="@style/SelectableItem" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/wizard_progress_line"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dip"
|
|
||||||
android:layout_above="@+id/wizard_buttons"
|
|
||||||
android:layout_marginLeft="4dip"
|
|
||||||
android:layout_marginRight="4dip"
|
|
||||||
android:background="?android:attr/listDivider"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/wizard_progress"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_above="@+id/wizard_progress_line"
|
|
||||||
android:visibility="gone">
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/wizard_progress_progressbar"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/wizard_progress_image"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:src="@drawable/icon_light_refresh" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/wizard_progress_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:text="asd"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/wizard_line2"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dip"
|
|
||||||
android:layout_above="@+id/wizard_progress"
|
|
||||||
android:layout_marginLeft="4dip"
|
|
||||||
android:layout_marginRight="4dip"
|
|
||||||
android:background="?android:attr/listDivider" />
|
|
||||||
|
|
||||||
<ScrollView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_above="@+id/wizard_line2">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/wizard_container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp" />
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
@ -1,43 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical" >
|
|
||||||
|
|
||||||
<org.sufficientlysecure.htmltextview.HtmlTextView
|
|
||||||
android:id="@+id/wizard_k9_text"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingBottom="4dp"
|
|
||||||
android:text="Text..."
|
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium" />
|
|
||||||
|
|
||||||
<RadioGroup
|
|
||||||
android:id="@+id/wizard_k9_radio_group"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
|
||||||
android:checked="true"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
|
||||||
style="@style/SelectableItem"
|
|
||||||
android:text="install K9"
|
|
||||||
android:id="@+id/wizard_k9_install" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dip"
|
|
||||||
android:background="?android:attr/listDivider" />
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
|
||||||
style="@style/SelectableItem"
|
|
||||||
android:text="skip install"
|
|
||||||
android:id="@+id/wizard_k9_skip" />
|
|
||||||
</RadioGroup>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
@ -1,63 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingBottom="4dp"
|
|
||||||
android:text="Welcome to OpenKeychain"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
style="@style/SectionHeader"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_marginTop="14dp"
|
|
||||||
android:text="What you wanna do today?"
|
|
||||||
android:layout_weight="1" />
|
|
||||||
|
|
||||||
<RadioGroup
|
|
||||||
android:id="@+id/wizard_start_radio_group"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
|
||||||
android:checked="true"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
|
||||||
style="@style/SelectableItem"
|
|
||||||
android:text="new key"
|
|
||||||
android:id="@+id/wizard_start_new_key" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dip"
|
|
||||||
android:background="?android:attr/listDivider" />
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
|
||||||
style="@style/SelectableItem"
|
|
||||||
android:text="import existing key"
|
|
||||||
android:id="@+id/wizard_start_import" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dip"
|
|
||||||
android:background="?android:attr/listDivider" />
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
|
||||||
style="@style/SelectableItem"
|
|
||||||
android:text="skip wizard"
|
|
||||||
android:id="@+id/wizard_start_skip" />
|
|
||||||
</RadioGroup>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
@ -26,11 +26,6 @@
|
|||||||
app:showAsAction="never"
|
app:showAsAction="never"
|
||||||
android:title="@string/menu_create_key" />
|
android:title="@string/menu_create_key" />
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/menu_key_list_create_expert"
|
|
||||||
app:showAsAction="never"
|
|
||||||
android:title="@string/menu_create_key_expert" />
|
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_key_list_debug_read"
|
android:id="@+id/menu_key_list_debug_read"
|
||||||
app:showAsAction="never"
|
app:showAsAction="never"
|
||||||
@ -43,4 +38,10 @@
|
|||||||
android:title="Debug / DB backup"
|
android:title="Debug / DB backup"
|
||||||
android:visible="false" />
|
android:visible="false" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_key_list_debug_first_time"
|
||||||
|
app:showAsAction="never"
|
||||||
|
android:title="Debug / Show first time screen"
|
||||||
|
android:visible="false" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
<string name="title_authentication">Passphrase</string>
|
<string name="title_authentication">Passphrase</string>
|
||||||
<string name="title_create_key">Create Key</string>
|
<string name="title_create_key">Create Key</string>
|
||||||
<string name="title_edit_key">Edit Key</string>
|
<string name="title_edit_key">Edit Key</string>
|
||||||
<string name="title_wizard">Welcome to OpenKeychain</string>
|
|
||||||
<string name="title_preferences">Preferences</string>
|
<string name="title_preferences">Preferences</string>
|
||||||
<string name="title_api_registered_apps">Apps</string>
|
<string name="title_api_registered_apps">Apps</string>
|
||||||
<string name="title_key_server_preference">Keyserver Preference</string>
|
<string name="title_key_server_preference">Keyserver Preference</string>
|
||||||
@ -511,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>
|
||||||
@ -556,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,6 +636,17 @@
|
|||||||
<string name="msg_mg_heterogeneous">Tried to consolidate heterogeneous keyrings</string>
|
<string name="msg_mg_heterogeneous">Tried to consolidate heterogeneous keyrings</string>
|
||||||
<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>
|
||||||
|
<string name="msg_mg_unchanged">No new certificates</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>
|
||||||
@ -644,10 +654,13 @@
|
|||||||
<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>
|
||||||
@ -691,4 +704,10 @@
|
|||||||
<string name="info_no_manual_account_creation">Do not create OpenKeychain-Accounts manually.\nFor more information, see Help.</string>
|
<string name="info_no_manual_account_creation">Do not create OpenKeychain-Accounts manually.\nFor more information, see Help.</string>
|
||||||
<string name="contact_show_key">Show key (%s)</string>
|
<string name="contact_show_key">Show key (%s)</string>
|
||||||
|
|
||||||
|
<!-- First Time -->
|
||||||
|
<string name="first_time_text1">Take back your privacy with OpenKeychain!</string>
|
||||||
|
<string name="first_time_create_key">Create Key</string>
|
||||||
|
<string name="first_time_import_key">Import Key</string>
|
||||||
|
<string name="first_time_skip">Skip Setup</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -37,8 +37,10 @@ Expand the Tools directory and select "Android SDK Build-tools (Version 19.1)".
|
|||||||
Expand the Extras directory and install "Android Support Repository"
|
Expand the Extras directory and install "Android Support Repository"
|
||||||
Select everything for the newest SDK Platform (API-Level 19)
|
Select everything for the newest SDK Platform (API-Level 19)
|
||||||
4. Export ANDROID_HOME pointing to your Android SDK
|
4. Export ANDROID_HOME pointing to your Android SDK
|
||||||
5. Execute ``./gradlew build``
|
5. Use OpenJDK 7 instead of Oracle JDK (this is required for OpenKeychain's tests based on Bouncy Castle)
|
||||||
6. You can install the app with ``adb install -r OpenKeychain/build/outputs/apk/OpenKeychain-debug-unaligned.apk``
|
6. Execute ``./install-custom-gradle-test-plugin.sh``
|
||||||
|
7. Execute ``./gradlew build``
|
||||||
|
8. You can install the app with ``adb install -r OpenKeychain/build/outputs/apk/OpenKeychain-debug-unaligned.apk``
|
||||||
|
|
||||||
### Build API Demo with Gradle
|
### Build API Demo with Gradle
|
||||||
|
|
||||||
|
BIN
Resources/gnupg-infographic/first_time_1.png
Normal file
BIN
Resources/gnupg-infographic/first_time_1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 43 KiB |
351
Resources/gnupg-infographic/first_time_1.svg
Normal file
351
Resources/gnupg-infographic/first_time_1.svg
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
id="svg4325"
|
||||||
|
version="1.1"
|
||||||
|
inkscape:version="0.48.3.1 r9886"
|
||||||
|
sodipodi:docname="ok_start.svg">
|
||||||
|
<defs
|
||||||
|
id="defs4327" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="1.979899"
|
||||||
|
inkscape:cx="405.16912"
|
||||||
|
inkscape:cy="246.12576"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:window-width="2558"
|
||||||
|
inkscape:window-height="1419"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="19"
|
||||||
|
inkscape:window-maximized="1" />
|
||||||
|
<metadata
|
||||||
|
id="metadata4330">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-540.36218)">
|
||||||
|
<path
|
||||||
|
sodipodi:type="arc"
|
||||||
|
style="opacity:0.59999999999999998;fill:#9933cc;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||||
|
id="path4501"
|
||||||
|
sodipodi:cx="227.53687"
|
||||||
|
sodipodi:cy="246.58241"
|
||||||
|
sodipodi:rx="225.51656"
|
||||||
|
sodipodi:ry="225.51656"
|
||||||
|
d="m 453.05342,246.58241 a 225.51656,225.51656 0 1 1 -451.0331106,0 225.51656,225.51656 0 1 1 451.0331106,0 z"
|
||||||
|
transform="translate(28.463135,549.77977)" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:0.16256157;fill-rule:nonzero;stroke:none;stroke-width:1.76498926;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||||
|
id="path7021"
|
||||||
|
sodipodi:sides="25"
|
||||||
|
sodipodi:cx="237.45834"
|
||||||
|
sodipodi:cy="671.60583"
|
||||||
|
sodipodi:r1="60.973591"
|
||||||
|
sodipodi:r2="37.364418"
|
||||||
|
sodipodi:arg1="0.87061839"
|
||||||
|
sodipodi:arg2="0.99902889"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="m 276.74691,718.23402 -19.06996,-15.20673 6.23968,23.51249 -14.68908,-19.47149 0.19632,24.32554 -9.38523,-22.51278 -5.85936,23.61014 -3.49167,-24.13951 -11.54688,21.41122 2.62127,-24.24948 -16.50886,17.86696 8.56952,-22.83575 -20.43354,13.20005 13.97931,-19.98717 -23.0743,7.70373 18.51073,-15.88272 -24.26522,1.72335 21.87906,-10.7803 -23.93146,-4.3653 23.87263,-5.00052 -22.094,-10.17967 24.36621,1.09346 -18.8683,-15.35441 23.32877,7.11874 -14.45703,-19.56438 20.8255,12.69672 -9.13738,-22.54504 17.01368,17.47692 -3.24358,-24.10913 12.13283,21.15898 2.85402,-24.15834 6.48963,23.51155 8.77228,-22.6896 0.43867,24.38679 14.13936,-19.79518 -5.63987,23.72973 18.61801,-15.65696 -11.36402,21.58163 21.92682,-10.53495 -16.37414,18.07749 23.85789,-4.75101 -20.35541,13.43747 24.28988,1.33147 -23.05767,7.95313 23.19564,7.33029 -24.31112,1.96905 20.64393,12.86851 -24.03703,-4.13873 16.7951,17.59816 -22.2526,-9.98648 z"
|
||||||
|
transform="matrix(2.3346891,0,0,2.3346891,-297.98143,-788.87687)"
|
||||||
|
inkscape:transform-center-x="0.48070196"
|
||||||
|
inkscape:transform-center-y="0.080663394" />
|
||||||
|
<rect
|
||||||
|
ry="14.623538"
|
||||||
|
rx="14.623538"
|
||||||
|
y="778.16095"
|
||||||
|
x="189.31395"
|
||||||
|
height="91.847595"
|
||||||
|
width="133.37183"
|
||||||
|
id="rect3529"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.12070131;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.12070131;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||||
|
d="m 254.33815,626.00818 c -27.93766,0 -50.4195,22.48185 -50.4195,50.41951 l 0,93.04182 c 0,27.93768 22.48184,50.44562 50.4195,50.44562 l 3.3229,0 c 27.93773,0 50.4195,-22.50794 50.4195,-50.44562 l 0,-93.04182 c 0,-27.93766 -22.48177,-50.41951 -50.4195,-50.41951 l -3.3229,0 z m 1.67456,16.74544 c 20.12817,0 35.95034,15.82142 35.95034,35.47939 l 0,74.46486 c 0,19.65806 -15.82217,35.47941 -35.95034,35.47941 -20.1282,0 -35.97658,-15.82135 -35.97658,-35.47941 l 0,-74.46486 c 0,-19.65797 15.84838,-35.47939 35.97658,-35.47939 z"
|
||||||
|
id="path3531" />
|
||||||
|
<rect
|
||||||
|
ry="3.9737797"
|
||||||
|
rx="3.9737797"
|
||||||
|
y="706.40234"
|
||||||
|
x="171.49129"
|
||||||
|
height="124.14557"
|
||||||
|
width="169.01746"
|
||||||
|
id="rect3533"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.12070131;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
<path
|
||||||
|
sodipodi:nodetypes="cscc"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path3535"
|
||||||
|
d="m 199.31408,796.34234 c 12.41682,10.27683 33.179,17.03328 56.69903,17.03328 23.51804,0 44.25559,-6.75787 56.67285,-17.03328 z"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.12070131;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
<path
|
||||||
|
transform="matrix(0.90538746,0,0,0.90538746,-76.624431,405.57604)"
|
||||||
|
d="m 316.87315,407.66223 a 9.388834,9.388834 0 1 1 -18.77767,0 9.388834,9.388834 0 1 1 18.77767,0 z"
|
||||||
|
sodipodi:ry="9.388834"
|
||||||
|
sodipodi:rx="9.388834"
|
||||||
|
sodipodi:cy="407.66223"
|
||||||
|
sodipodi:cx="307.48431"
|
||||||
|
id="path3537"
|
||||||
|
style="color:#000000;fill:#4b4f2f;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.55131245;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||||
|
sodipodi:type="arc" />
|
||||||
|
<path
|
||||||
|
transform="matrix(0.90538746,0,0,0.90538746,36.007648,405.57604)"
|
||||||
|
sodipodi:type="arc"
|
||||||
|
style="color:#000000;fill:#4b4f2f;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.55131245;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||||
|
id="path3539"
|
||||||
|
sodipodi:cx="307.48431"
|
||||||
|
sodipodi:cy="407.66223"
|
||||||
|
sodipodi:rx="9.388834"
|
||||||
|
sodipodi:ry="9.388834"
|
||||||
|
d="m 316.87315,407.66223 a 9.388834,9.388834 0 1 1 -18.77767,0 9.388834,9.388834 0 1 1 18.77767,0 z" />
|
||||||
|
<g
|
||||||
|
id="g10754"
|
||||||
|
transform="matrix(0.83727181,0,0,0.83727181,829.92363,-416.95697)"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate">
|
||||||
|
<g
|
||||||
|
id="g5737-8"
|
||||||
|
transform="translate(-898.12108,631.20806)"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate">
|
||||||
|
<rect
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||||
|
id="rect5739-5"
|
||||||
|
width="29.344997"
|
||||||
|
height="96.371902"
|
||||||
|
x="153.20558"
|
||||||
|
y="918.29181"
|
||||||
|
rx="0.84849852"
|
||||||
|
ry="0.95320696" />
|
||||||
|
<path
|
||||||
|
sodipodi:nodetypes="cscc"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path5741-6"
|
||||||
|
d="m 192.11794,916.83294 c -7.43536,-12.87842 -23.7891,-17.26042 -36.66754,-9.82505 -12.87845,7.43537 -17.26039,23.78913 -9.82503,36.66755 z"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
<rect
|
||||||
|
ry="3"
|
||||||
|
rx="3"
|
||||||
|
y="1010.0776"
|
||||||
|
x="133.00574"
|
||||||
|
height="11.311689"
|
||||||
|
width="69.74469"
|
||||||
|
id="rect5743-2"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||||
|
d="m 145.00574,916.83294 c 7.43536,-12.87842 23.7891,-17.26042 36.66754,-9.82505 12.87845,7.43537 17.26039,23.78913 9.82503,36.66755 z"
|
||||||
|
id="path5745-2"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cscc" />
|
||||||
|
<rect
|
||||||
|
ry="7.1440544"
|
||||||
|
rx="7.1440544"
|
||||||
|
y="968.78937"
|
||||||
|
x="145.20558"
|
||||||
|
height="14.288109"
|
||||||
|
width="45.344997"
|
||||||
|
id="rect5747-2"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g5749-5"
|
||||||
|
transform="matrix(-1,0,0,1,-472.81628,631.20806)"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate">
|
||||||
|
<rect
|
||||||
|
ry="0.95320696"
|
||||||
|
rx="0.84849852"
|
||||||
|
y="918.29181"
|
||||||
|
x="153.20558"
|
||||||
|
height="96.371902"
|
||||||
|
width="29.344997"
|
||||||
|
id="rect5751-5"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||||
|
d="m 192.11794,916.83294 c -7.43536,-12.87842 -23.7891,-17.26042 -36.66754,-9.82505 -12.87845,7.43537 -17.26039,23.78913 -9.82503,36.66755 z"
|
||||||
|
id="path5753-0"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cscc" />
|
||||||
|
<rect
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||||
|
id="rect5755-0"
|
||||||
|
width="69.74469"
|
||||||
|
height="11.311689"
|
||||||
|
x="133.00574"
|
||||||
|
y="1010.0776"
|
||||||
|
rx="3"
|
||||||
|
ry="3" />
|
||||||
|
<path
|
||||||
|
sodipodi:nodetypes="cscc"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path5757-1"
|
||||||
|
d="m 145.00574,916.83294 c 7.43536,-12.87842 23.7891,-17.26042 36.66754,-9.82505 12.87845,7.43537 17.26039,23.78913 9.82503,36.66755 z"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
<rect
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||||
|
id="rect5759-7"
|
||||||
|
width="45.344997"
|
||||||
|
height="14.288109"
|
||||||
|
x="145.20558"
|
||||||
|
y="968.78937"
|
||||||
|
rx="7.1440544"
|
||||||
|
ry="7.1440544" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
transform="matrix(0.83727181,0,0,0.83727181,84.650265,61.513651)"
|
||||||
|
id="g5697-5"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate">
|
||||||
|
<path
|
||||||
|
sodipodi:nodetypes="cccccc"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path5699-6"
|
||||||
|
d="m 326.98547,847.70808 57.45819,0 0,-73.83379 -10,0 0,61.83379 -47.45819,0"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
<rect
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||||
|
id="rect5703-1"
|
||||||
|
width="9.4107542"
|
||||||
|
height="42.249756"
|
||||||
|
x="305.50339"
|
||||||
|
y="821.53503"
|
||||||
|
rx="4.7053771"
|
||||||
|
ry="4.7053771" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||||
|
d="m 390.56742,727.10519 c -6.09957,0 -11,4.90043 -11,11 l 0,10.15625 -10.15625,0 c -6.09957,0 -11,4.90043 -11,11 0,6.09957 4.90043,11 11,11 l 21.15625,0 c 6.09957,0 11,-4.90043 11,-11 l 0,-21.15625 c 0,-6.09957 -4.90043,-11 -11,-11 z"
|
||||||
|
id="path5705-8" />
|
||||||
|
<rect
|
||||||
|
ry="2.1935852"
|
||||||
|
rx="4.0765176"
|
||||||
|
y="771.6814"
|
||||||
|
x="369.11923"
|
||||||
|
height="12.20938"
|
||||||
|
width="21.740107"
|
||||||
|
id="rect5707-1"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
<rect
|
||||||
|
ry="2.9156427"
|
||||||
|
rx="7.0489321"
|
||||||
|
y="829.57007"
|
||||||
|
x="318.22556"
|
||||||
|
height="26.179665"
|
||||||
|
width="14.097864"
|
||||||
|
id="rect5709-1"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
<g
|
||||||
|
id="g5711-2"
|
||||||
|
transform="translate(-34.057657,-0.65450616)"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate">
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path5713-1"
|
||||||
|
d="m 404.77705,825.97291 -9.16957,15.86559 9.16957,15.84038 18.38979,0 9.19491,-15.84038 -9.19491,-15.86559 -18.38979,0 z"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path5715-3"
|
||||||
|
d="m 413.97195,836.00644 c 3.23493,0 5.85942,2.61224 5.85942,5.83206 0,3.21983 -2.62449,5.83207 -5.85942,5.83207 -3.23494,0 -5.85943,-2.61224 -5.85943,-5.83207 0,-3.21982 2.62449,-5.83206 5.85943,-5.83206 z"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
transform="matrix(-0.83727181,0,0,-0.83727181,427.34978,1471.4837)"
|
||||||
|
id="g10957"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate">
|
||||||
|
<path
|
||||||
|
sodipodi:nodetypes="cccccc"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path10959"
|
||||||
|
d="m 326.98547,847.70808 57.45819,0 0,-73.83379 -10,0 0,61.83379 -47.45819,0"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
<rect
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||||
|
id="rect10961"
|
||||||
|
width="9.4107542"
|
||||||
|
height="42.249756"
|
||||||
|
x="305.50339"
|
||||||
|
y="821.53503"
|
||||||
|
rx="4.7053771"
|
||||||
|
ry="4.7053771" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||||
|
d="m 390.56742,727.10519 c -6.09957,0 -11,4.90043 -11,11 l 0,10.15625 -10.15625,0 c -6.09957,0 -11,4.90043 -11,11 0,6.09957 4.90043,11 11,11 l 21.15625,0 c 6.09957,0 11,-4.90043 11,-11 l 0,-21.15625 c 0,-6.09957 -4.90043,-11 -11,-11 z"
|
||||||
|
id="path10963" />
|
||||||
|
<rect
|
||||||
|
ry="2.1935852"
|
||||||
|
rx="4.0765176"
|
||||||
|
y="771.6814"
|
||||||
|
x="369.11923"
|
||||||
|
height="12.20938"
|
||||||
|
width="21.740107"
|
||||||
|
id="rect10965"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
<rect
|
||||||
|
ry="2.9156427"
|
||||||
|
rx="7.0489321"
|
||||||
|
y="829.57007"
|
||||||
|
x="318.22556"
|
||||||
|
height="26.179665"
|
||||||
|
width="14.097864"
|
||||||
|
id="rect10967"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
<g
|
||||||
|
id="g10969"
|
||||||
|
transform="translate(-34.057657,-0.65450616)"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate">
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path10971"
|
||||||
|
d="m 404.77705,825.97291 -9.16957,15.86559 9.16957,15.84038 18.38979,0 9.19491,-15.84038 -9.19491,-15.86559 -18.38979,0 z"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path10973"
|
||||||
|
d="m 413.97195,836.00644 c 3.23493,0 5.85942,2.61224 5.85942,5.83206 0,3.21983 -2.62449,5.83207 -5.85942,5.83207 -3.23494,0 -5.85943,-2.61224 -5.85943,-5.83207 0,-3.21982 2.62449,-5.83206 5.85943,-5.83206 z"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<rect
|
||||||
|
ry="12.863822"
|
||||||
|
rx="12.863822"
|
||||||
|
y="836.96265"
|
||||||
|
x="214.05772"
|
||||||
|
height="25.727644"
|
||||||
|
width="83.8843"
|
||||||
|
id="rect5685-1"
|
||||||
|
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.12070131;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 24 KiB |
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