From 6e7145801e54f79bad1f9e5cb8717334b5ff6c68 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 21 May 2018 12:44:27 +0200 Subject: [PATCH] Implement OpenPgpManager functions --- .../BouncyCastleIdentityStore.java | 26 ++++ .../BouncycastleOpenPgpProvider.java | 100 +++++++------- .../FileBasedBouncyCastleIdentityStore.java | 105 ++++++++++++++ .../ox/bouncycastle/BasicEncryptionTest.java | 4 +- .../BouncycastleOpenPgpProviderTest.java | 8 +- ...ileBasedBouncyCastleIdentityStoreTest.java | 86 ++++++++++++ .../ox/bouncycastle/KeyGenerationTest.java | 2 +- smack-openpgp/build.gradle | 1 + .../smackx/ox/OpenPgpManager.java | 130 ++++++++++++++++-- .../smackx/ox/OpenPgpProvider.java | 13 ++ .../smackx/ox/element/OpenPgpElement.java | 41 ------ 11 files changed, 415 insertions(+), 101 deletions(-) create mode 100644 smack-openpgp-bouncycastle/src/main/java/org/jivesoftware/smackx/ox/bouncycastle/BouncyCastleIdentityStore.java create mode 100644 smack-openpgp-bouncycastle/src/main/java/org/jivesoftware/smackx/ox/bouncycastle/FileBasedBouncyCastleIdentityStore.java create mode 100644 smack-openpgp-bouncycastle/src/test/java/org/jivesoftware/smackx/ox/bouncycastle/FileBasedBouncyCastleIdentityStoreTest.java diff --git a/smack-openpgp-bouncycastle/src/main/java/org/jivesoftware/smackx/ox/bouncycastle/BouncyCastleIdentityStore.java b/smack-openpgp-bouncycastle/src/main/java/org/jivesoftware/smackx/ox/bouncycastle/BouncyCastleIdentityStore.java new file mode 100644 index 000000000..0218ebdfc --- /dev/null +++ b/smack-openpgp-bouncycastle/src/main/java/org/jivesoftware/smackx/ox/bouncycastle/BouncyCastleIdentityStore.java @@ -0,0 +1,26 @@ +package org.jivesoftware.smackx.ox.bouncycastle; + +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.jivesoftware.smackx.ox.element.PublicKeysListElement; + +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.jxmpp.jid.BareJid; + +public interface BouncyCastleIdentityStore { + + void storePubkeyList(BareJid jid, PublicKeysListElement list) throws FileNotFoundException, IOException; + + PublicKeysListElement loadPubkeyList(BareJid jid) throws FileNotFoundException, IOException; + + void storePublicKeys(BareJid jid, PGPPublicKeyRingCollection keys); + + PGPPublicKeyRingCollection loadPublicKeys(BareJid jid); + + void storeSecretKeys(PGPSecretKeyRingCollection secretKeys); + + PGPSecretKeyRingCollection loadSecretKeys(); + +} diff --git a/smack-openpgp-bouncycastle/src/main/java/org/jivesoftware/smackx/ox/bouncycastle/BouncycastleOpenPgpProvider.java b/smack-openpgp-bouncycastle/src/main/java/org/jivesoftware/smackx/ox/bouncycastle/BouncycastleOpenPgpProvider.java index 3305a2ff5..e21ccad00 100644 --- a/smack-openpgp-bouncycastle/src/main/java/org/jivesoftware/smackx/ox/bouncycastle/BouncycastleOpenPgpProvider.java +++ b/smack-openpgp-bouncycastle/src/main/java/org/jivesoftware/smackx/ox/bouncycastle/BouncycastleOpenPgpProvider.java @@ -36,7 +36,6 @@ import org.jivesoftware.smackx.ox.element.PubkeyElement; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.BouncyGPG; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.algorithms.PublicKeySize; -import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.KeySelectionStrategy; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.KeyringConfigCallbacks; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.XmppKeySelectionStrategy; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.keyrings.InMemoryKeyring; @@ -47,6 +46,7 @@ import org.bouncycastle.openpgp.PGPKeyRingGenerator; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.encoders.Hex; import org.bouncycastle.util.io.Streams; import org.jxmpp.jid.BareJid; @@ -56,51 +56,26 @@ public class BouncycastleOpenPgpProvider implements OpenPgpProvider { private final InMemoryKeyring ourKeys; private final Long ourKeyId; private final Map theirKeys = new HashMap<>(); - private final KeySelectionStrategy keySelectionStrategy = new XmppKeySelectionStrategy(new Date()); - - @Override - public OpenPgpMessage decryptAndVerify(OpenPgpElement element, BareJid sender) throws Exception { - InMemoryKeyring decryptionConfig = KeyringConfigs.forGpgExportedKeys(KeyringConfigCallbacks.withUnprotectedKeys()); - - // Add our keys to decryption config - for (PGPPublicKeyRing p : ourKeys.getPublicKeyRings()) { - decryptionConfig.addPublicKey(p.getPublicKey().getEncoded()); - } - for (PGPSecretKeyRing s : ourKeys.getSecretKeyRings()) { - decryptionConfig.addSecretKey(s.getSecretKey().getEncoded()); - } - - // Add their keys to decryption config - for (PGPPublicKeyRing p : theirKeys.get(sender).getPublicKeyRings()) { - decryptionConfig.addPublicKey(p.getPublicKey().getEncoded()); - } - - ByteArrayInputStream encryptedIn = new ByteArrayInputStream( - element.getEncryptedBase64MessageContent().getBytes(Charset.forName("UTF-8"))); - - InputStream decrypted = BouncyGPG.decryptAndVerifyStream() - .withConfig(decryptionConfig) - .withKeySelectionStrategy(new XmppKeySelectionStrategy(new Date())) - .andValidateSomeoneSigned() - .fromEncryptedInputStream(encryptedIn); - - ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); - - Streams.pipeAll(decrypted, decryptedOut); - - return new OpenPgpMessage(null, new String(decryptedOut.toByteArray(), Charset.forName("UTF-8"))); - } public BouncycastleOpenPgpProvider(BareJid ourJid) throws IOException, PGPException, NoSuchAlgorithmException { this.ourJid = ourJid; - PGPSecretKeyRing ourKey = generateKey(ourJid, null).generateSecretKeyRing(); + PGPSecretKeyRing ourKey = generateKey(ourJid).generateSecretKeyRing(); ourKeyId = ourKey.getPublicKey().getKeyID(); ourKeys = KeyringConfigs.forGpgExportedKeys(KeyringConfigCallbacks.withUnprotectedKeys()); ourKeys.addSecretKey(ourKey.getSecretKey().getEncoded()); ourKeys.addPublicKey(ourKey.getPublicKey().getEncoded()); } - public void processRecipientsPublicKey(BareJid jid, PubkeyElement element) throws IOException, PGPException { + @Override + public PubkeyElement createPubkeyElement() throws IOException, PGPException { + PGPPublicKey pubKey = ourKeys.getPublicKeyRings().getPublicKey(ourKeyId); + PubkeyElement.PubkeyDataElement dataElement = new PubkeyElement.PubkeyDataElement( + Base64.encode(pubKey.getEncoded())); + return new PubkeyElement(dataElement, new Date()); + } + + @Override + public void processPubkeyElement(PubkeyElement element, BareJid jid) throws IOException, PGPException { byte[] decoded = Base64.decode(element.getDataElement().getB64Data()); InMemoryKeyring contactsKeyring = theirKeys.get(jid); @@ -112,13 +87,6 @@ public class BouncycastleOpenPgpProvider implements OpenPgpProvider { contactsKeyring.addPublicKey(decoded); } - public PubkeyElement createPubkeyElement() throws IOException, PGPException { - PGPPublicKey pubKey = ourKeys.getPublicKeyRings().getPublicKey(ourKeyId); - PubkeyElement.PubkeyDataElement dataElement = new PubkeyElement.PubkeyDataElement( - Base64.encode(pubKey.getEncoded())); - return new PubkeyElement(dataElement, new Date()); - } - @Override public OpenPgpElement signAndEncrypt(InputStream inputStream, Set recipients) throws Exception { @@ -170,12 +138,52 @@ public class BouncycastleOpenPgpProvider implements OpenPgpProvider { return new OpenPgpElement(base64); } - public static PGPKeyRingGenerator generateKey(BareJid owner, char[] passPhrase) throws NoSuchAlgorithmException, PGPException { + @Override + public OpenPgpMessage decryptAndVerify(OpenPgpElement element, BareJid sender) throws Exception { + InMemoryKeyring decryptionConfig = KeyringConfigs.forGpgExportedKeys(KeyringConfigCallbacks.withUnprotectedKeys()); + + // Add our keys to decryption config + for (PGPPublicKeyRing p : ourKeys.getPublicKeyRings()) { + decryptionConfig.addPublicKey(p.getPublicKey().getEncoded()); + } + for (PGPSecretKeyRing s : ourKeys.getSecretKeyRings()) { + decryptionConfig.addSecretKey(s.getSecretKey().getEncoded()); + } + + // Add their keys to decryption config + for (PGPPublicKeyRing p : theirKeys.get(sender).getPublicKeyRings()) { + decryptionConfig.addPublicKey(p.getPublicKey().getEncoded()); + } + + ByteArrayInputStream encryptedIn = new ByteArrayInputStream( + element.getEncryptedBase64MessageContent().getBytes(Charset.forName("UTF-8"))); + + InputStream decrypted = BouncyGPG.decryptAndVerifyStream() + .withConfig(decryptionConfig) + .withKeySelectionStrategy(new XmppKeySelectionStrategy(new Date())) + .andValidateSomeoneSigned() + .fromEncryptedInputStream(encryptedIn); + + ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); + + Streams.pipeAll(decrypted, decryptedOut); + + return new OpenPgpMessage(null, new String(decryptedOut.toByteArray(), Charset.forName("UTF-8"))); + } + + @Override + public String getFingerprint() throws IOException, PGPException { + return new String(Hex.encode(ourKeys.getKeyFingerPrintCalculator() + .calculateFingerprint(ourKeys.getPublicKeyRings().getPublicKey(ourKeyId) + .getPublicKeyPacket())), Charset.forName("UTF-8")).toUpperCase(); + } + + public static PGPKeyRingGenerator generateKey(BareJid owner) throws NoSuchAlgorithmException, PGPException { PGPKeyRingGenerator generator = BouncyGPG.createKeyPair() .withRSAKeys() .ofSize(PublicKeySize.RSA._2048) .forIdentity("xmpp:" + owner.toString()) - .withPassphrase(passPhrase) + .withoutPassphrase() .build(); return generator; } diff --git a/smack-openpgp-bouncycastle/src/main/java/org/jivesoftware/smackx/ox/bouncycastle/FileBasedBouncyCastleIdentityStore.java b/smack-openpgp-bouncycastle/src/main/java/org/jivesoftware/smackx/ox/bouncycastle/FileBasedBouncyCastleIdentityStore.java new file mode 100644 index 000000000..b9aec55e0 --- /dev/null +++ b/smack-openpgp-bouncycastle/src/main/java/org/jivesoftware/smackx/ox/bouncycastle/FileBasedBouncyCastleIdentityStore.java @@ -0,0 +1,105 @@ +package org.jivesoftware.smackx.ox.bouncycastle; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.text.ParseException; +import java.util.Date; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.jivesoftware.smack.util.Objects; +import org.jivesoftware.smackx.ox.element.PublicKeysListElement; + +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.jxmpp.jid.BareJid; +import org.jxmpp.util.XmppDateTime; + +public class FileBasedBouncyCastleIdentityStore implements BouncyCastleIdentityStore { + + private static final Logger LOGGER = Logger.getLogger(FileBasedBouncyCastleIdentityStore.class.getName()); + + private final File baseDirectory; + + public FileBasedBouncyCastleIdentityStore(File path) { + // Check if path is not null, not a file and directory exists + if (!Objects.requireNonNull(path).exists()) { + path.mkdirs(); + } else if (path.isFile()) { + throw new IllegalArgumentException("Path MUST point to a directory, not a file."); + } + + this.baseDirectory = path; + } + + @Override + public void storePubkeyList(BareJid jid, PublicKeysListElement list) throws IOException { + File contactsDir = contactsDir(jid); + File destination = new File(contactsDir, "pubkey_list"); + DataOutputStream dataOut = new DataOutputStream(new FileOutputStream(destination)); + + for (String key : list.getMetadata().keySet()) { + PublicKeysListElement.PubkeyMetadataElement value = list.getMetadata().get(key); + dataOut.writeUTF(value.getV4Fingerprint()); + dataOut.writeUTF(XmppDateTime.formatXEP0082Date(value.getDate())); + } + + dataOut.close(); + } + + @Override + public PublicKeysListElement loadPubkeyList(BareJid jid) throws IOException { + File contactsDir = contactsDir(jid); + File source = new File(contactsDir, "pubkey_list"); + DataInputStream dataIn = new DataInputStream(new FileInputStream(source)); + + PublicKeysListElement.Builder builder = PublicKeysListElement.builder(); + while (true) { + try { + String fingerprint = dataIn.readUTF(); + String d = dataIn.readUTF(); + Date date = XmppDateTime.parseXEP0082Date(d); + builder.addMetadata(new PublicKeysListElement.PubkeyMetadataElement(fingerprint, date)); + } catch (ParseException e) { + LOGGER.log(Level.WARNING, "Could not parse date.", e); + } catch (EOFException e) { + LOGGER.log(Level.INFO, "Reached EOF."); + break; + } + } + return builder.build(); + } + + @Override + public void storePublicKeys(BareJid jid, PGPPublicKeyRingCollection keys) { + + } + + @Override + public PGPPublicKeyRingCollection loadPublicKeys(BareJid jid) { + return null; + } + + @Override + public void storeSecretKeys(PGPSecretKeyRingCollection secretKeys) { + + } + + @Override + public PGPSecretKeyRingCollection loadSecretKeys() { + return null; + } + + private File contactsDir(BareJid contact) { + File f = new File(baseDirectory, "contacts/" + contact.toString()); + if (!f.exists()) { + f.mkdirs(); + } + return f; + } +} diff --git a/smack-openpgp-bouncycastle/src/test/java/org/jivesoftware/smackx/ox/bouncycastle/BasicEncryptionTest.java b/smack-openpgp-bouncycastle/src/test/java/org/jivesoftware/smackx/ox/bouncycastle/BasicEncryptionTest.java index 35d6b4a3d..5fc6c35b4 100644 --- a/smack-openpgp-bouncycastle/src/test/java/org/jivesoftware/smackx/ox/bouncycastle/BasicEncryptionTest.java +++ b/smack-openpgp-bouncycastle/src/test/java/org/jivesoftware/smackx/ox/bouncycastle/BasicEncryptionTest.java @@ -116,8 +116,8 @@ public class BasicEncryptionTest extends SmackTestSuite { throws IOException, PGPException, NoSuchAlgorithmException, NoSuchProviderException, SignatureException { final String alice = "alice@wonderland.lit"; final String bob = "bob@builder.tv"; - PGPKeyRingGenerator g1 = BouncycastleOpenPgpProvider.generateKey(JidCreate.bareFrom(alice), null); - PGPKeyRingGenerator g2 = BouncycastleOpenPgpProvider.generateKey(JidCreate.bareFrom(bob), null); + PGPKeyRingGenerator g1 = BouncycastleOpenPgpProvider.generateKey(JidCreate.bareFrom(alice)); + PGPKeyRingGenerator g2 = BouncycastleOpenPgpProvider.generateKey(JidCreate.bareFrom(bob)); PGPSecretKey s1 = g1.generateSecretKeyRing().getSecretKey(); PGPSecretKey s2 = g2.generateSecretKeyRing().getSecretKey(); PGPPublicKey p1 = g1.generatePublicKeyRing().getPublicKey(); diff --git a/smack-openpgp-bouncycastle/src/test/java/org/jivesoftware/smackx/ox/bouncycastle/BouncycastleOpenPgpProviderTest.java b/smack-openpgp-bouncycastle/src/test/java/org/jivesoftware/smackx/ox/bouncycastle/BouncycastleOpenPgpProviderTest.java index 05b77d433..b6d063152 100644 --- a/smack-openpgp-bouncycastle/src/test/java/org/jivesoftware/smackx/ox/bouncycastle/BouncycastleOpenPgpProviderTest.java +++ b/smack-openpgp-bouncycastle/src/test/java/org/jivesoftware/smackx/ox/bouncycastle/BouncycastleOpenPgpProviderTest.java @@ -52,9 +52,10 @@ public class BouncycastleOpenPgpProviderTest extends SmackTestSuite { // dry exchange keys PubkeyElement aliceKeys = aliceProvider.createPubkeyElement(); PubkeyElement cheshireKeys = cheshireProvider.createPubkeyElement(); - aliceProvider.processRecipientsPublicKey(cheshire, cheshireKeys); - cheshireProvider.processRecipientsPublicKey(alice, aliceKeys); + aliceProvider.processPubkeyElement(cheshireKeys, cheshire); + cheshireProvider.processPubkeyElement(aliceKeys, alice); + // Create signed and encrypted message from alice to the cheshire cat SigncryptElement signcryptElement = new SigncryptElement( Collections.singleton(cheshire), Collections.singletonList( @@ -63,10 +64,11 @@ public class BouncycastleOpenPgpProviderTest extends SmackTestSuite { signcryptElement.toInputStream(), Collections.singleton(cheshire)); + // Decrypt the message as the cheshire cat OpenPgpMessage decrypted = cheshireProvider.decryptAndVerify(encrypted, alice); OpenPgpContentElement content = decrypted.getOpenPgpContentElement(); - assertTrue(content instanceof SigncryptElement); + assertTrue(content instanceof SigncryptElement); assertXMLEqual(signcryptElement.toXML(null).toString(), content.toXML(null).toString()); } } diff --git a/smack-openpgp-bouncycastle/src/test/java/org/jivesoftware/smackx/ox/bouncycastle/FileBasedBouncyCastleIdentityStoreTest.java b/smack-openpgp-bouncycastle/src/test/java/org/jivesoftware/smackx/ox/bouncycastle/FileBasedBouncyCastleIdentityStoreTest.java new file mode 100644 index 000000000..5d2c9bd5b --- /dev/null +++ b/smack-openpgp-bouncycastle/src/test/java/org/jivesoftware/smackx/ox/bouncycastle/FileBasedBouncyCastleIdentityStoreTest.java @@ -0,0 +1,86 @@ +package org.jivesoftware.smackx.ox.bouncycastle; + +import static junit.framework.TestCase.assertEquals; + +import java.io.File; +import java.io.IOException; +import java.text.ParseException; +import java.util.Date; +import java.util.Stack; + +import org.jivesoftware.smack.test.util.SmackTestSuite; +import org.jivesoftware.smackx.ox.element.PublicKeysListElement; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.jxmpp.jid.BareJid; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.util.XmppDateTime; + +public class FileBasedBouncyCastleIdentityStoreTest extends SmackTestSuite { + + public static File storePath; + + @BeforeClass + public static void setup() { + String userHome = System.getProperty("user.home"); + if (userHome != null) { + File f = new File(userHome); + storePath = new File(f, ".config/smack-integration-test/ox_store"); + } else { + storePath = new File("int_test_ox_store"); + } + } + + @Before + public void before() { + deleteStore(); + } + + @After + public void after() { + deleteStore(); + } + + public void deleteStore() { + File[] currList; + Stack stack = new Stack<>(); + stack.push(storePath); + while (!stack.isEmpty()) { + if (stack.lastElement().isDirectory()) { + currList = stack.lastElement().listFiles(); + if (currList != null && currList.length > 0) { + for (File curr : currList) { + stack.push(curr); + } + } else { + stack.pop().delete(); + } + } else { + stack.pop().delete(); + } + } + } + + @Test + public void writeReadPublicKeysLists() throws ParseException, IOException { + BareJid jid = JidCreate.bareFrom("edward@snowden.org"); + String fp1 = "1357B01865B2503C18453D208CAC2A9678548E35"; + String fp2 = "67819B343B2AB70DED9320872C6464AF2A8E4C02"; + Date d1 = XmppDateTime.parseDate("2018-03-01T15:26:12Z"); + Date d2 = XmppDateTime.parseDate("1953-05-16T12:00:00Z"); + + PublicKeysListElement list = PublicKeysListElement.builder() + .addMetadata(new PublicKeysListElement.PubkeyMetadataElement(fp1, d1)) + .addMetadata(new PublicKeysListElement.PubkeyMetadataElement(fp2, d2)) + .build(); + + FileBasedBouncyCastleIdentityStore store = new FileBasedBouncyCastleIdentityStore(storePath); + store.storePubkeyList(jid, list); + + PublicKeysListElement retrieved = store.loadPubkeyList(jid); + assertEquals(list.getMetadata(), retrieved.getMetadata()); + } +} diff --git a/smack-openpgp-bouncycastle/src/test/java/org/jivesoftware/smackx/ox/bouncycastle/KeyGenerationTest.java b/smack-openpgp-bouncycastle/src/test/java/org/jivesoftware/smackx/ox/bouncycastle/KeyGenerationTest.java index 10e85271d..c37d3ef71 100644 --- a/smack-openpgp-bouncycastle/src/test/java/org/jivesoftware/smackx/ox/bouncycastle/KeyGenerationTest.java +++ b/smack-openpgp-bouncycastle/src/test/java/org/jivesoftware/smackx/ox/bouncycastle/KeyGenerationTest.java @@ -29,7 +29,7 @@ public class KeyGenerationTest { @Test public void createSecretKey() throws Exception { PGPSecretKey secretKey = BouncycastleOpenPgpProvider - .generateKey(JidCreate.bareFrom("alice@wonderland.lit"), null) + .generateKey(JidCreate.bareFrom("alice@wonderland.lit")) .generateSecretKeyRing() .getSecretKey(); assertEquals(PublicKeyType.RSA_GENERAL.getId(), secretKey.getPublicKey().getAlgorithm()); diff --git a/smack-openpgp/build.gradle b/smack-openpgp/build.gradle index 7085e5dcd..e20600d0f 100644 --- a/smack-openpgp/build.gradle +++ b/smack-openpgp/build.gradle @@ -5,6 +5,7 @@ Smack API for XEP-0373: OpenPGP for XMPP.""" // sourceSet.test of the core subproject dependencies { compile project(':smack-core') + compile project(':smack-extensions') testCompile project(path: ":smack-core", configuration: "testRuntime") testCompile project(path: ":smack-core", configuration: "archives") } diff --git a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpManager.java b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpManager.java index 0148195d3..0c3b23ac1 100644 --- a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpManager.java +++ b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpManager.java @@ -16,27 +16,50 @@ */ package org.jivesoftware.smackx.ox; -import java.io.InputStream; +import java.util.Date; +import java.util.List; import java.util.Map; import java.util.WeakHashMap; import org.jivesoftware.smack.Manager; +import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; +import org.jivesoftware.smackx.ox.element.PubkeyElement; +import org.jivesoftware.smackx.ox.element.PublicKeysListElement; +import org.jivesoftware.smackx.pep.PEPListener; +import org.jivesoftware.smackx.pep.PEPManager; +import org.jivesoftware.smackx.pubsub.EventElement; +import org.jivesoftware.smackx.pubsub.Item; +import org.jivesoftware.smackx.pubsub.LeafNode; +import org.jivesoftware.smackx.pubsub.PayloadItem; +import org.jivesoftware.smackx.pubsub.PubSubException; +import org.jivesoftware.smackx.pubsub.PubSubManager; import org.jxmpp.jid.BareJid; +import org.jxmpp.jid.EntityBareJid; -public final class OpenPgpManager extends Manager { +public final class OpenPgpManager extends Manager implements PEPListener { public static final String PEP_NODE_PUBLIC_KEYS = "urn:xmpp:openpgp:0:public-keys"; + public static final String PEP_NODE_PUBLIC_KEYS_NOTIFY = PEP_NODE_PUBLIC_KEYS + "+notify"; public static String PEP_NODE_PUBLIC_KEY(String id) { return PEP_NODE_PUBLIC_KEYS + ":" + id; } private static final Map INSTANCES = new WeakHashMap<>(); + private OpenPgpProvider provider; private OpenPgpManager(XMPPConnection connection) { super(connection); + + // Subscribe to public key changes + PEPManager.getInstanceFor(connection()).addPEPListener(this); + ServiceDiscoveryManager.getInstanceFor(connection()) + .addFeature(PEP_NODE_PUBLIC_KEYS_NOTIFY); } public static OpenPgpManager getInstanceFor(XMPPConnection connection) { @@ -48,17 +71,108 @@ public final class OpenPgpManager extends Manager { return manager; } - public static String bareJidToIdentity(BareJid jid) { - return "xmpp:" + jid.toString(); + public void setOpenPgpProvider(OpenPgpProvider provider) { + this.provider = provider; + } - public static OpenPgpMessage toOpenPgpMessage(InputStream is) { - return null; - } + public void publishPublicKey() throws Exception { + ensureProviderIsSet(); + PubkeyElement pubkeyElement = provider.createPubkeyElement(); + + String fingerprint = provider.getFingerprint(); + String keyNodeName = PEP_NODE_PUBLIC_KEY(fingerprint); + PubSubManager pm = PubSubManager.getInstance(connection(), connection().getUser().asBareJid()); - public void publishPublicKey() { // Check if key available at data node // If not, publish key to data node + LeafNode keyNode = pm.getOrCreateLeafNode(keyNodeName); + List items = keyNode.getItems(1); + if (items.isEmpty()) { + keyNode.publish(new PayloadItem<>(pubkeyElement)); + } + // Publish ID to metadata node + LeafNode metadataNode = pm.getOrCreateLeafNode(PEP_NODE_PUBLIC_KEYS); + List> metadataItems = metadataNode.getItems(1); + + PublicKeysListElement.Builder builder = PublicKeysListElement.builder(); + if (!metadataItems.isEmpty() && metadataItems.get(0).getPayload() != null) { + // Add old entries back to list. + PublicKeysListElement publishedList = metadataItems.get(0).getPayload(); + for (PublicKeysListElement.PubkeyMetadataElement meta : publishedList.getMetadata().values()) { + builder.addMetadata(meta); + } + } + builder.addMetadata(new PublicKeysListElement.PubkeyMetadataElement(fingerprint, new Date())); + + metadataNode.publish(new PayloadItem<>(builder.build())); + } + + public PublicKeysListElement fetchPubkeysList() + throws InterruptedException, PubSubException.NotALeafNodeException, SmackException.NoResponseException, + SmackException.NotConnectedException, XMPPException.XMPPErrorException, + PubSubException.NotAPubSubNodeException { + return fetchPubkeysList(connection().getUser().asBareJid()); + } + + public PublicKeysListElement fetchPubkeysList(BareJid jid) + throws InterruptedException, PubSubException.NotALeafNodeException, SmackException.NoResponseException, + SmackException.NotConnectedException, XMPPException.XMPPErrorException, + PubSubException.NotAPubSubNodeException + { + PubSubManager pm = PubSubManager.getInstance(connection(), jid); + + LeafNode node = pm.getLeafNode(PEP_NODE_PUBLIC_KEYS); + List> list = node.getItems(1); + + if (list.isEmpty()) { + return null; + } + + return list.get(0).getPayload(); + } + + public PubkeyElement fetchPubkey(BareJid jid, String v4_fingerprint) + throws InterruptedException, PubSubException.NotALeafNodeException, SmackException.NoResponseException, + SmackException.NotConnectedException, XMPPException.XMPPErrorException, + PubSubException.NotAPubSubNodeException { + PubSubManager pm = PubSubManager.getInstance(connection(), jid); + + LeafNode node = pm.getLeafNode(PEP_NODE_PUBLIC_KEY(v4_fingerprint)); + List> list = node.getItems(1); + + if (list.isEmpty()) { + return null; + } + + return list.get(0).getPayload(); + } + + public void depositSecretKey() { + ensureProviderIsSet(); + // Create key backup by appending serialized unencrypted secret keys. + // Encrypt the backup using a random generated password + // Publish the backup to the secret key node (whitelist protected) + // Display the backup key to the user + } + + public String getOurFingerprint() throws Exception { + ensureProviderIsSet(); + return provider.getFingerprint(); + } + + /** + * Throw an {@link IllegalStateException} if no {@link OpenPgpProvider} is set. + */ + private void ensureProviderIsSet() { + if (provider == null) { + throw new IllegalStateException("No OpenPgpProvider set!"); + } + } + + @Override + public void eventReceived(EntityBareJid from, EventElement event, Message message) { + } } diff --git a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpProvider.java b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpProvider.java index a50505549..9ce98ee0e 100644 --- a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpProvider.java +++ b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpProvider.java @@ -20,6 +20,7 @@ import java.io.InputStream; import java.util.Set; import org.jivesoftware.smackx.ox.element.OpenPgpElement; +import org.jivesoftware.smackx.ox.element.PubkeyElement; import org.jxmpp.jid.BareJid; @@ -28,4 +29,16 @@ public interface OpenPgpProvider { OpenPgpMessage decryptAndVerify(OpenPgpElement element, BareJid sender) throws Exception; OpenPgpElement signAndEncrypt(InputStream inputStream, Set recipients) throws Exception; + + PubkeyElement createPubkeyElement() throws Exception; + + void processPubkeyElement(PubkeyElement element, BareJid from) throws Exception; + + /** + * Return the OpenPGP v4-fingerprint of our key in hexadecimal upper case. + * + * @return fingerprint + * @throws Exception + */ + String getFingerprint() throws Exception; } diff --git a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/OpenPgpElement.java b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/OpenPgpElement.java index 6f154b3db..72a21c875 100644 --- a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/OpenPgpElement.java +++ b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/OpenPgpElement.java @@ -16,18 +16,8 @@ */ package org.jivesoftware.smackx.ox.element; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.util.XmlStringBuilder; -import org.jivesoftware.smack.util.stringencoder.Base64; -import org.jivesoftware.smackx.ox.OpenPgpManager; -import org.jivesoftware.smackx.ox.OpenPgpMessage; -import org.jivesoftware.smackx.ox.OpenPgpProvider; - -import org.xmlpull.v1.XmlPullParserException; public class OpenPgpElement implements ExtensionElement { @@ -37,35 +27,10 @@ public class OpenPgpElement implements ExtensionElement { // Represents the OpenPGP message, but encoded using base64. private final String base64EncodedOpenPgpMessage; - private OpenPgpMessage openPgpMessage; - - // Represents the OpenPGP message, but base64 decoded. - private byte[] openPgpMessageBytes; - - private OpenPgpContentElement openPgpContentElement; - public OpenPgpElement(String base64EncodedOpenPgpMessage) { this.base64EncodedOpenPgpMessage = base64EncodedOpenPgpMessage; } - public OpenPgpMessage getOpenPgpMessage(OpenPgpProvider provider) { - if (openPgpMessage == null) { - ensureOpenPgpMessageBytesSet(); - InputStream is = new ByteArrayInputStream(openPgpMessageBytes); - openPgpMessage = OpenPgpManager.toOpenPgpMessage(is); - } - - return openPgpMessage; - } - - public OpenPgpContentElement getContentElement(OpenPgpProvider provider) throws XmlPullParserException, IOException { - if (openPgpContentElement == null) { - openPgpContentElement = getOpenPgpMessage(provider).getOpenPgpContentElement(); - } - - return openPgpContentElement; - } - public String getEncryptedBase64MessageContent() { return base64EncodedOpenPgpMessage; } @@ -86,10 +51,4 @@ public class OpenPgpElement implements ExtensionElement { xml.rightAngleBracket().append(base64EncodedOpenPgpMessage).closeElement(this); return xml; } - - private void ensureOpenPgpMessageBytesSet() { - if (openPgpMessageBytes != null) return; - - openPgpMessageBytes = Base64.decode(base64EncodedOpenPgpMessage); - } }