1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2025-07-14 14:41:58 +02:00

XEP-0373, XEP-0374: OpenPGP for XMPP: Instant Messaging

Fixes SMACK-826
This commit is contained in:
Paul Schaub 2018-07-29 18:52:45 +02:00
parent f3262c9d58
commit f0af00ee43
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
97 changed files with 8582 additions and 1 deletions

View file

@ -79,6 +79,7 @@ allprojects {
':smack-experimental',
':smack-omemo',
':smack-omemo-signal',
':smack-openpgp',
].collect{ project(it) }
androidBootClasspathProjects = [
':smack-android',

View file

@ -96,6 +96,8 @@ Experimental Smack Extensions and currently supported XEPs of smack-experimental
| Stable and Unique Stanza IDs | [XEP-0359](https://xmpp.org/extensions/xep-0359.html) | 0.5.0 | This specification describes unique and stable IDs for messages. |
| HTTP File Upload | [XEP-0363](https://xmpp.org/extensions/xep-0363.html) | 0.3.1 | Protocol to request permissions to upload a file to an HTTP server and get a shareable URL. |
| References | [XEP-0372](https://xmpp.org/extensions/xep-0363.html) | 0.2.0 | Add references like mentions or external data to stanzas. |
| [OpenPGP for XMPP](ox.md) | [XEP-0373](https://xmpp.org/extensions/xep-0373.html) | 0.3.2 | Utilize OpenPGP to exchange encrypted and signed content. |
| [OpenPGP for XMPP: Instant Messaging](ox-im.md) | [XEP-0374](https://xmpp.org/extensions/xep-0374.html) | 0.2.0 | OpenPGP encrypted Instant Messaging. |
| [Spoiler Messages](spoiler.md) | [XEP-0382](https://xmpp.org/extensions/xep-0382.html) | 0.2.0 | Indicate that the body of a message should be treated as a spoiler. |
| [OMEMO Multi End Message and Object Encryption](omemo.md) | [XEP-0384](https://xmpp.org/extensions/xep-0384.html) | n/a | Encrypt messages using OMEMO encryption (currently only with smack-omemo-signal -> GPLv3). |
| [Consistent Color Generation](consistent_colors.md) | [XEP-0392](https://xmpp.org/extensions/xep-0392.html) | 0.4.0 | Generate consistent colors for identifiers like usernames to provide a consistent user experience. |

View file

@ -0,0 +1,6 @@
OpenPGP for XMPP: Instant Messaging
===================================
[Back](index.md)
See the javadoc of `OpenPgpManager` and `OXInstantMessagingManager` for details.

View file

@ -0,0 +1,6 @@
OpenPGP for XMPP
================
[Back](index.md)
See the javadoc of `OpenPgpManager` for details.

View file

@ -26,4 +26,5 @@ include 'smack-core',
'smack-omemo',
'smack-omemo-signal',
'smack-omemo-signal-integration-test',
'smack-repl'
'smack-repl',
'smack-openpgp'

View file

@ -158,6 +158,9 @@ public final class FileUtils {
}
public static void deleteDirectory(File root) {
if (!root.exists()) {
return;
}
File[] currList;
Stack<File> stack = new Stack<>();
stack.push(root);
@ -176,4 +179,25 @@ public final class FileUtils {
}
}
}
/**
* Returns a {@link File} pointing to a temporary directory. On unix like systems this might be {@code /tmp}
* for example.
* If {@code suffix} is not null, the returned file points to {@code <temp>/suffix}.
*
* @param suffix optional path suffix
* @return temp directory
*/
public static File getTempDir(String suffix) {
String temp = System.getProperty("java.io.tmpdir");
if (temp == null) {
temp = "tmp";
}
if (suffix == null) {
return new File(temp);
} else {
return new File(temp, suffix);
}
}
}

View file

@ -21,5 +21,6 @@
<className>org.jivesoftware.smack.java7.Java7SmackInitializer</className>
<className>org.jivesoftware.smack.im.SmackImInitializer</className>
<className>org.jivesoftware.smackx.omemo.OmemoInitializer</className>
<className>org.jivesoftware.smackx.ox.util.OpenPgpInitializer</className>
</optionalStartupClasses>
</smack>

View file

@ -12,6 +12,7 @@ dependencies {
compile project(':smack-extensions')
compile project(':smack-experimental')
compile project(':smack-omemo')
compile project(':smack-openpgp')
compile project(':smack-debug')
compile project(path: ":smack-omemo", configuration: "testRuntime")
compile 'org.reflections:reflections:0.9.9-RC1'

View file

@ -0,0 +1,70 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil;
import org.jivesoftware.smackx.pep.PEPManager;
import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest;
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
import org.igniterealtime.smack.inttest.TestNotPossibleException;
import org.jxmpp.jid.BareJid;
public abstract class AbstractOpenPgpIntegrationTest extends AbstractSmackIntegrationTest {
protected final XMPPConnection aliceConnection;
protected final XMPPConnection bobConnection;
protected final XMPPConnection chloeConnection;
protected final BareJid alice;
protected final BareJid bob;
protected final BareJid chloe;
protected AbstractOpenPgpIntegrationTest(SmackIntegrationTestEnvironment environment)
throws XMPPException.XMPPErrorException, TestNotPossibleException, SmackException.NotConnectedException,
InterruptedException, SmackException.NoResponseException {
super(environment);
throwIfPubSubNotSupported(conOne);
throwIfPubSubNotSupported(conTwo);
throwIfPubSubNotSupported(conThree);
this.aliceConnection = conOne;
this.bobConnection = conTwo;
this.chloeConnection = conThree;
this.alice = aliceConnection.getUser().asBareJid();
this.bob = bobConnection.getUser().asBareJid();
this.chloe = chloeConnection.getUser().asBareJid();
OpenPgpPubSubUtil.deletePubkeysListNode(aliceConnection);
OpenPgpPubSubUtil.deletePubkeysListNode(bobConnection);
OpenPgpPubSubUtil.deletePubkeysListNode(chloeConnection);
}
private static void throwIfPubSubNotSupported(XMPPConnection connection)
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException, TestNotPossibleException {
if (!PEPManager.getInstanceFor(connection).isSupported()) {
throw new TestNotPossibleException("Server " + connection.getXMPPServiceDomain().toString() +
" does not support PEP.");
}
}
}

View file

@ -0,0 +1,196 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertNotNull;
import static junit.framework.TestCase.assertNull;
import static junit.framework.TestCase.assertTrue;
import java.io.File;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Arrays;
import java.util.Set;
import java.util.logging.Level;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.util.FileUtils;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.ox.callback.backup.AskForBackupCodeCallback;
import org.jivesoftware.smackx.ox.callback.backup.DisplayBackupCodeCallback;
import org.jivesoftware.smackx.ox.callback.backup.SecretKeyBackupSelectionCallback;
import org.jivesoftware.smackx.ox.crypto.PainlessOpenPgpProvider;
import org.jivesoftware.smackx.ox.exception.InvalidBackupCodeException;
import org.jivesoftware.smackx.ox.exception.MissingOpenPgpKeyException;
import org.jivesoftware.smackx.ox.exception.MissingUserIdOnKeyException;
import org.jivesoftware.smackx.ox.exception.NoBackupFoundException;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpStore;
import org.jivesoftware.smackx.ox.store.filebased.FileBasedOpenPgpStore;
import org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil;
import org.jivesoftware.smackx.pubsub.PubSubException;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.igniterealtime.smack.inttest.SmackIntegrationTest;
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
import org.igniterealtime.smack.inttest.TestNotPossibleException;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.protection.UnprotectedKeysProtector;
public class OXSecretKeyBackupIntegrationTest extends AbstractOpenPgpIntegrationTest {
private static final String sessionId = StringUtils.randomString(10);
private static final File beforePath = FileUtils.getTempDir("ox_backup_" + sessionId);
private static final File afterPath = FileUtils.getTempDir("ox_restore_" + sessionId);
private String backupCode = null;
private OpenPgpManager openPgpManager;
/**
* This integration test tests the basic secret key backup and restore functionality as described
* in XEP-0373 §5.
*
* In order to simulate two different devices, we are using two {@link FileBasedOpenPgpStore} implementations
* which point to different directories.
*
* First, Alice generates a fresh OpenPGP key pair.
*
* She then creates a backup of the key in her private PEP node.
*
* Now the {@link OpenPgpStore} implementation is replaced by another instance to simulate a different device.
*
* Then the secret key backup is restored from PubSub and the imported secret key is compared to the one in
* the original store.
*
* Afterwards the private PEP node is deleted from PubSub and the storage directories are emptied.
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html#synchro-pep">
* XEP-0373 §5: Synchronizing the Secret Key with a Private PEP Node</a>
* @param environment
* @throws XMPPException.XMPPErrorException
* @throws TestNotPossibleException
* @throws SmackException.NotConnectedException
* @throws InterruptedException
* @throws SmackException.NoResponseException
*/
public OXSecretKeyBackupIntegrationTest(SmackIntegrationTestEnvironment environment)
throws XMPPException.XMPPErrorException, TestNotPossibleException, SmackException.NotConnectedException,
InterruptedException, SmackException.NoResponseException {
super(environment);
if (!OpenPgpManager.serverSupportsSecretKeyBackups(aliceConnection)) {
throw new TestNotPossibleException("Server does not support the 'whitelist' PubSub access model.");
}
}
@AfterClass
@BeforeClass
public static void cleanStore() {
LOGGER.log(Level.INFO, "Delete store directories...");
FileUtils.deleteDirectory(afterPath);
FileUtils.deleteDirectory(beforePath);
}
@After
@Before
public void cleanUp()
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException {
OpenPgpPubSubUtil.deleteSecretKeyNode(aliceConnection);
if (openPgpManager != null) {
openPgpManager.stopMetadataListener();
}
}
@SmackIntegrationTest
public void test() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException,
NoSuchProviderException, IOException, InterruptedException, PubSubException.NotALeafNodeException,
SmackException.NoResponseException, SmackException.NotConnectedException, XMPPException.XMPPErrorException,
SmackException.NotLoggedInException, SmackException.FeatureNotSupportedException,
MissingUserIdOnKeyException, NoBackupFoundException, InvalidBackupCodeException, PGPException,
MissingOpenPgpKeyException {
OpenPgpStore beforeStore = new FileBasedOpenPgpStore(beforePath);
beforeStore.setKeyRingProtector(new UnprotectedKeysProtector());
PainlessOpenPgpProvider beforeProvider = new PainlessOpenPgpProvider(aliceConnection, beforeStore);
openPgpManager = OpenPgpManager.getInstanceFor(aliceConnection);
openPgpManager.setOpenPgpProvider(beforeProvider);
OpenPgpSelf self = openPgpManager.getOpenPgpSelf();
assertNull(self.getSigningKeyFingerprint());
OpenPgpV4Fingerprint keyFingerprint = openPgpManager.generateAndImportKeyPair(alice);
assertEquals(keyFingerprint, self.getSigningKeyFingerprint());
assertTrue(self.getSecretKeys().contains(keyFingerprint.getKeyId()));
PGPSecretKeyRing beforeSec = beforeStore.getSecretKeyRing(alice, keyFingerprint);
assertNotNull(beforeSec);
PGPPublicKeyRing beforePub = beforeStore.getPublicKeyRing(alice, keyFingerprint);
assertNotNull(beforePub);
openPgpManager.backupSecretKeyToServer(new DisplayBackupCodeCallback() {
@Override
public void displayBackupCode(String backupCode) {
OXSecretKeyBackupIntegrationTest.this.backupCode = backupCode;
}
}, new SecretKeyBackupSelectionCallback() {
@Override
public Set<OpenPgpV4Fingerprint> selectKeysToBackup(Set<OpenPgpV4Fingerprint> availableSecretKeys) {
return availableSecretKeys;
}
});
FileBasedOpenPgpStore afterStore = new FileBasedOpenPgpStore(afterPath);
afterStore.setKeyRingProtector(new UnprotectedKeysProtector());
PainlessOpenPgpProvider afterProvider = new PainlessOpenPgpProvider(aliceConnection, afterStore);
openPgpManager.setOpenPgpProvider(afterProvider);
OpenPgpV4Fingerprint fingerprint = openPgpManager.restoreSecretKeyServerBackup(new AskForBackupCodeCallback() {
@Override
public String askForBackupCode() {
return backupCode;
}
});
assertEquals(keyFingerprint, fingerprint);
assertTrue(self.getSecretKeys().contains(keyFingerprint.getKeyId()));
assertEquals(keyFingerprint, self.getSigningKeyFingerprint());
PGPSecretKeyRing afterSec = afterStore.getSecretKeyRing(alice, keyFingerprint);
assertNotNull(afterSec);
assertTrue(Arrays.equals(beforeSec.getEncoded(), afterSec.getEncoded()));
PGPPublicKeyRing afterPub = afterStore.getPublicKeyRing(alice, keyFingerprint);
assertNotNull(afterPub);
assertTrue(Arrays.equals(beforePub.getEncoded(), afterPub.getEncoded()));
}
}

View file

@ -0,0 +1,23 @@
/**
*
* Copyright 2018 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Integration Tests for Smacks support for XEP-0373: OpenPGP for XMPP.
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html">
* XEP-0373: OpenPGP for XMPP</a>
*/
package org.jivesoftware.smackx.ox;

View file

@ -0,0 +1,188 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox_im;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertTrue;
import java.io.File;
import java.util.logging.Level;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.util.FileUtils;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.ox.AbstractOpenPgpIntegrationTest;
import org.jivesoftware.smackx.ox.OpenPgpContact;
import org.jivesoftware.smackx.ox.OpenPgpManager;
import org.jivesoftware.smackx.ox.crypto.PainlessOpenPgpProvider;
import org.jivesoftware.smackx.ox.element.SigncryptElement;
import org.jivesoftware.smackx.ox.store.filebased.FileBasedOpenPgpStore;
import org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil;
import org.igniterealtime.smack.inttest.SmackIntegrationTest;
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
import org.igniterealtime.smack.inttest.TestNotPossibleException;
import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.protection.UnprotectedKeysProtector;
public class OXInstantMessagingIntegrationTest extends AbstractOpenPgpIntegrationTest {
private static final String sessionId = StringUtils.randomString(10);
private static final File aliceStorePath = FileUtils.getTempDir("basic_ox_messaging_test_alice_" + sessionId);
private static final File bobStorePath = FileUtils.getTempDir("basic_ox_messaging_test_bob_" + sessionId);
private OpenPgpV4Fingerprint aliceFingerprint = null;
private OpenPgpV4Fingerprint bobFingerprint = null;
private OpenPgpManager aliceOpenPgp;
private OpenPgpManager bobOpenPgp;
/**
* This integration test tests basic OX message exchange.
* In this scenario, Alice and Bob are strangers, as they do not have subscribed to one another.
*
* Alice (conOne) creates keys and publishes them to the server.
* Bob (conTwo) creates keys and publishes them to the server.
*
* Alice then manually fetches Bobs metadata node and all announced keys.
*
* Alice trusts Bobs keys and vice versa (even though Bob does not have copies of Alice' keys yet).
*
* She proceeds to create an OX encrypted message, which is encrypted to Bob and herself and signed by her.
*
* She sends the message.
*
* Bob receives the message, which - due to missing keys - triggers him to update Alice' keys.
*
* After the update Bob proceeds to decrypt and verify the message.
*
* After the test, the keys are deleted from local storage and from PubSub.
*
* @param environment test environment
*
* @throws XMPPException.XMPPErrorException
* @throws InterruptedException
* @throws SmackException.NotConnectedException
* @throws TestNotPossibleException if the test is not possible due to lacking server support for PEP.
* @throws SmackException.NoResponseException
*/
public OXInstantMessagingIntegrationTest(SmackIntegrationTestEnvironment environment)
throws XMPPException.XMPPErrorException, InterruptedException, SmackException.NotConnectedException,
TestNotPossibleException, SmackException.NoResponseException {
super(environment);
}
@BeforeClass
@AfterClass
public static void deleteStore() {
LOGGER.log(Level.INFO, "Deleting storage directories...");
FileUtils.deleteDirectory(aliceStorePath);
FileUtils.deleteDirectory(bobStorePath);
}
@SmackIntegrationTest
public void basicInstantMessagingTest()
throws Exception {
LOGGER.log(Level.INFO, aliceStorePath.getAbsolutePath() + " " + bobStorePath.getAbsolutePath());
final SimpleResultSyncPoint bobReceivedMessage = new SimpleResultSyncPoint();
final String body = "Writing integration tests is an annoying task, but it has to be done, so lets do it!!!";
FileBasedOpenPgpStore aliceStore = new FileBasedOpenPgpStore(aliceStorePath);
aliceStore.setKeyRingProtector(new UnprotectedKeysProtector());
FileBasedOpenPgpStore bobStore = new FileBasedOpenPgpStore(bobStorePath);
bobStore.setKeyRingProtector(new UnprotectedKeysProtector());
PainlessOpenPgpProvider aliceProvider = new PainlessOpenPgpProvider(aliceConnection, aliceStore);
PainlessOpenPgpProvider bobProvider = new PainlessOpenPgpProvider(bobConnection, bobStore);
aliceOpenPgp = OpenPgpManager.getInstanceFor(aliceConnection);
bobOpenPgp = OpenPgpManager.getInstanceFor(bobConnection);
OXInstantMessagingManager aliceInstantMessaging = OXInstantMessagingManager.getInstanceFor(aliceConnection);
OXInstantMessagingManager bobInstantMessaging = OXInstantMessagingManager.getInstanceFor(bobConnection);
bobInstantMessaging.addOxMessageListener(new OxMessageListener() {
@Override
public void newIncomingOxMessage(OpenPgpContact contact, Message originalMessage, SigncryptElement decryptedPayload, OpenPgpMetadata metadata) {
if (((Message.Body) decryptedPayload.getExtension(Message.Body.NAMESPACE)).getMessage().equals(body)) {
bobReceivedMessage.signal();
} else {
bobReceivedMessage.signalFailure();
}
}
});
aliceOpenPgp.setOpenPgpProvider(aliceProvider);
bobOpenPgp.setOpenPgpProvider(bobProvider);
aliceFingerprint = aliceOpenPgp.generateAndImportKeyPair(alice);
bobFingerprint = bobOpenPgp.generateAndImportKeyPair(bob);
aliceOpenPgp.announceSupportAndPublish();
bobOpenPgp.announceSupportAndPublish();
OpenPgpContact bobForAlice = aliceOpenPgp.getOpenPgpContact(bob.asEntityBareJidIfPossible());
OpenPgpContact aliceForBob = bobOpenPgp.getOpenPgpContact(alice.asEntityBareJidIfPossible());
bobForAlice.updateKeys(aliceConnection);
assertFalse(bobForAlice.isTrusted(bobFingerprint));
assertFalse(aliceForBob.isTrusted(aliceFingerprint));
bobForAlice.trust(bobFingerprint);
aliceForBob.trust(aliceFingerprint);
assertTrue(bobForAlice.isTrusted(bobFingerprint));
assertTrue(aliceForBob.isTrusted(aliceFingerprint));
aliceInstantMessaging.sendOxMessage(bobForAlice, body);
bobReceivedMessage.waitForResult(timeout);
}
@After
public void deleteKeyMetadata()
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException {
OpenPgpPubSubUtil.deletePubkeysListNode(aliceConnection);
OpenPgpPubSubUtil.deletePubkeysListNode(bobConnection);
if (aliceFingerprint != null) {
OpenPgpPubSubUtil.deletePublicKeyNode(aliceConnection, aliceFingerprint);
}
if (bobFingerprint != null) {
OpenPgpPubSubUtil.deletePublicKeyNode(bobConnection, bobFingerprint);
}
if (aliceOpenPgp != null) {
aliceOpenPgp.stopMetadataListener();
}
if (bobOpenPgp != null) {
bobOpenPgp.stopMetadataListener();
}
}
}

View file

@ -0,0 +1,23 @@
/**
*
* Copyright 2018 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Integration Tests for Smacks support for XEP-0374: OpenPGP for XMPP: Instant Messaging.
*
* @see <a href="https://xmpp.org/extensions/xep-0374.html">
* XEP-0374: OpenPGP for XMPP: Instant Messaging</a>
*/
package org.jivesoftware.smackx.ox_im;

View file

@ -0,0 +1,19 @@
description = """\
Smack API for XEP-0373: OpenPGP for XMPP."""
repositories {
mavenCentral()
}
// Note that the test dependencies (junit, ) are inferred from the
// sourceSet.test of the core subproject
dependencies {
compile project(':smack-core')
compile project(':smack-extensions')
compile project(':smack-experimental')
compile 'org.pgpainless:pgpainless-core:0.0.1-alpha2'
testCompile project(path: ":smack-core", configuration: "testRuntime")
testCompile project(path: ":smack-core", configuration: "archives")
}

View file

@ -0,0 +1,417 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.util.stringencoder.Base64;
import org.jivesoftware.smackx.ox.element.PubkeyElement;
import org.jivesoftware.smackx.ox.element.PublicKeysListElement;
import org.jivesoftware.smackx.ox.exception.MissingUserIdOnKeyException;
import org.jivesoftware.smackx.ox.selection_strategy.BareJidUserId;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpStore;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpTrustStore;
import org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil;
import org.jivesoftware.smackx.pubsub.LeafNode;
import org.jivesoftware.smackx.pubsub.PubSubException;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
import org.jxmpp.jid.BareJid;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.util.BCUtil;
/**
* The OpenPgpContact is sort of a specialized view on the OpenPgpStore, which gives you access to the information
* about the user. It also allows contact-specific actions like fetching the contacts keys from PubSub etc.
*/
public class OpenPgpContact {
private final Logger LOGGER;
protected final BareJid jid;
protected final OpenPgpStore store;
protected final Map<OpenPgpV4Fingerprint, Throwable> unfetchableKeys = new HashMap<>();
/**
* Create a new OpenPgpContact.
*
* @param jid {@link BareJid} of the contact.
* @param store {@link OpenPgpStore}.
*/
public OpenPgpContact(BareJid jid, OpenPgpStore store) {
this.jid = jid;
this.store = store;
LOGGER = Logger.getLogger(OpenPgpContact.class.getName() + ":" + jid.toString());
}
/**
* Return the jid of the contact.
*
* @return jid
*/
public BareJid getJid() {
return jid;
}
/**
* Return any available public keys of the user. The result might also contain outdated or invalid keys.
*
* @return any keys of the contact.
*
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
*/
public PGPPublicKeyRingCollection getAnyPublicKeys() throws IOException, PGPException {
return store.getPublicKeysOf(jid);
}
/**
* Return any announced public keys. This is the set returned by {@link #getAnyPublicKeys()} with non-announced
* keys and keys which lack a user-id with the contacts jid removed.
*
* @return announced keys of the contact
*
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
*/
public PGPPublicKeyRingCollection getAnnouncedPublicKeys() throws IOException, PGPException {
PGPPublicKeyRingCollection anyKeys = getAnyPublicKeys();
Map<OpenPgpV4Fingerprint, Date> announced = store.getAnnouncedFingerprintsOf(jid);
BareJidUserId.PubRingSelectionStrategy userIdFilter = new BareJidUserId.PubRingSelectionStrategy();
PGPPublicKeyRingCollection announcedKeysCollection = null;
for (OpenPgpV4Fingerprint announcedFingerprint : announced.keySet()) {
PGPPublicKeyRing ring = anyKeys.getPublicKeyRing(announcedFingerprint.getKeyId());
if (ring == null) continue;
ring = BCUtil.removeUnassociatedKeysFromKeyRing(ring, ring.getPublicKey(announcedFingerprint.getKeyId()));
if (!userIdFilter.accept(getJid(), ring)) {
LOGGER.log(Level.WARNING, "Ignore key " + Long.toHexString(ring.getPublicKey().getKeyID()) +
" as it lacks the user-id \"xmpp" + getJid().toString() + "\"");
continue;
}
if (announcedKeysCollection == null) {
announcedKeysCollection = new PGPPublicKeyRingCollection(Collections.singleton(ring));
} else {
announcedKeysCollection = PGPPublicKeyRingCollection.addPublicKeyRing(announcedKeysCollection, ring);
}
}
return announcedKeysCollection;
}
/**
* Return a {@link PGPPublicKeyRingCollection}, which contains all keys from {@code keys}, which are marked with the
* {@link OpenPgpTrustStore.Trust} state of {@code trust}.
*
* @param keys {@link PGPPublicKeyRingCollection}
* @param trust {@link OpenPgpTrustStore.Trust}
*
* @return all keys from {@code keys} with trust state {@code trust}.
*
* @throws IOException IO error
*/
protected PGPPublicKeyRingCollection getPublicKeysOfTrustState(PGPPublicKeyRingCollection keys,
OpenPgpTrustStore.Trust trust)
throws IOException {
if (keys == null) {
return null;
}
Set<PGPPublicKeyRing> toRemove = new HashSet<>();
Iterator<PGPPublicKeyRing> iterator = keys.iterator();
while (iterator.hasNext()) {
PGPPublicKeyRing ring = iterator.next();
OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(ring);
if (store.getTrust(getJid(), fingerprint) != trust) {
toRemove.add(ring);
}
}
for (PGPPublicKeyRing ring : toRemove) {
keys = PGPPublicKeyRingCollection.removePublicKeyRing(keys, ring);
}
if (!keys.iterator().hasNext()) {
return null;
}
return keys;
}
/**
* Return a {@link PGPPublicKeyRingCollection} which contains all public keys of the contact, which are announced,
* as well as marked as {@link OpenPgpStore.Trust#trusted}.
*
* @return announced, trusted keys.
*
* @throws IOException IO error
* @throws PGPException PGP error
*/
public PGPPublicKeyRingCollection getTrustedAnnouncedKeys()
throws IOException, PGPException {
PGPPublicKeyRingCollection announced = getAnnouncedPublicKeys();
PGPPublicKeyRingCollection trusted = getPublicKeysOfTrustState(announced, OpenPgpTrustStore.Trust.trusted);
return trusted;
}
/**
* Return a {@link Set} of {@link OpenPgpV4Fingerprint}s of all keys of the contact, which have the trust state
* {@link OpenPgpStore.Trust#trusted}.
*
* @return trusted fingerprints
*
* @throws IOException IO error
* @throws PGPException PGP error
*/
public Set<OpenPgpV4Fingerprint> getTrustedFingerprints()
throws IOException, PGPException {
return getFingerprintsOfKeysWithState(getAnyPublicKeys(), OpenPgpTrustStore.Trust.trusted);
}
/**
* Return a {@link Set} of {@link OpenPgpV4Fingerprint}s of all keys of the contact, which have the trust state
* {@link OpenPgpStore.Trust#untrusted}.
*
* @return untrusted fingerprints
*
* @throws IOException IO error
* @throws PGPException PGP error
*/
public Set<OpenPgpV4Fingerprint> getUntrustedFingerprints()
throws IOException, PGPException {
return getFingerprintsOfKeysWithState(getAnyPublicKeys(), OpenPgpTrustStore.Trust.untrusted);
}
/**
* Return a {@link Set} of {@link OpenPgpV4Fingerprint}s of all keys of the contact, which have the trust state
* {@link OpenPgpStore.Trust#undecided}.
*
* @return undecided fingerprints
*
* @throws IOException IO error
* @throws PGPException PGP error
*/
public Set<OpenPgpV4Fingerprint> getUndecidedFingerprints()
throws IOException, PGPException {
return getFingerprintsOfKeysWithState(getAnyPublicKeys(), OpenPgpTrustStore.Trust.undecided);
}
/**
* Return a {@link Set} of {@link OpenPgpV4Fingerprint}s of all keys in {@code publicKeys}, which are marked with the
* {@link OpenPgpTrustStore.Trust} of {@code trust}.
*
* @param publicKeys {@link PGPPublicKeyRingCollection} of keys which are iterated.
* @param trust {@link OpenPgpTrustStore.Trust} state.
* @return {@link Set} of fingerprints
*
* @throws IOException IO error
*/
public Set<OpenPgpV4Fingerprint> getFingerprintsOfKeysWithState(PGPPublicKeyRingCollection publicKeys,
OpenPgpTrustStore.Trust trust)
throws IOException {
PGPPublicKeyRingCollection keys = getPublicKeysOfTrustState(publicKeys, trust);
Set<OpenPgpV4Fingerprint> fingerprints = new HashSet<>();
if (keys == null) {
return fingerprints;
}
for (PGPPublicKeyRing ring : keys) {
fingerprints.add(new OpenPgpV4Fingerprint(ring));
}
return fingerprints;
}
/**
* Determine the {@link OpenPgpTrustStore.Trust} state of the key identified by the {@code fingerprint}.
*
* @param fingerprint {@link OpenPgpV4Fingerprint} of the key
* @return trust record
*
* @throws IOException IO error
*/
public OpenPgpTrustStore.Trust getTrust(OpenPgpV4Fingerprint fingerprint)
throws IOException {
return store.getTrust(getJid(), fingerprint);
}
/**
* Determine, whether the key identified by the {@code fingerprint} is marked as
* {@link OpenPgpTrustStore.Trust#trusted} or not.
*
* @param fingerprint {@link OpenPgpV4Fingerprint} of the key
* @return true, if the key is marked as trusted, false otherwise
*
* @throws IOException IO error
*/
public boolean isTrusted(OpenPgpV4Fingerprint fingerprint)
throws IOException {
return getTrust(fingerprint) == OpenPgpTrustStore.Trust.trusted;
}
/**
* Mark a key as {@link OpenPgpStore.Trust#trusted}.
*
* @param fingerprint {@link OpenPgpV4Fingerprint} of the key to mark as trusted.
*
* @throws IOException IO error
*/
public void trust(OpenPgpV4Fingerprint fingerprint)
throws IOException {
store.setTrust(getJid(), fingerprint, OpenPgpTrustStore.Trust.trusted);
}
/**
* Mark a key as {@link OpenPgpStore.Trust#untrusted}.
*
* @param fingerprint {@link OpenPgpV4Fingerprint} of the key to mark as untrusted.
*
* @throws IOException IO error
*/
public void distrust(OpenPgpV4Fingerprint fingerprint)
throws IOException {
store.setTrust(getJid(), fingerprint, OpenPgpTrustStore.Trust.untrusted);
}
/**
* Determine, whether there are keys available, for which we did not yet decided whether to trust them or not.
*
* @return more than 0 keys with trust state {@link OpenPgpTrustStore.Trust#undecided}.
*
* @throws IOException I/O error reading the keys or trust records.
* @throws PGPException PGP error reading the keys.
*/
public boolean hasUndecidedKeys()
throws IOException, PGPException {
return getUndecidedFingerprints().size() != 0;
}
/**
* Return a {@link Map} of any unfetchable keys fingerprints and the cause of them not being fetched.
*
* @return unfetchable keys
*/
public Map<OpenPgpV4Fingerprint, Throwable> getUnfetchableKeys() {
return new HashMap<>(unfetchableKeys);
}
/**
* Update the contacts keys by consulting the users PubSub nodes.
* This method fetches the users metadata node and then tries to fetch any announced keys.
*
* @param connection our {@link XMPPConnection}.
*
* @throws InterruptedException In case the thread gets interrupted.
* @throws SmackException.NotConnectedException in case the connection is not connected.
* @throws SmackException.NoResponseException in case the server doesn't respond.
* @throws XMPPException.XMPPErrorException in case of an XMPP protocol error.
* @throws PubSubException.NotALeafNodeException in case the metadata node is not a {@link LeafNode}.
* @throws PubSubException.NotAPubSubNodeException in case the metadata node is not a PubSub node.
* @throws IOException IO is brittle.
*/
public void updateKeys(XMPPConnection connection) throws InterruptedException, SmackException.NotConnectedException,
SmackException.NoResponseException, XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException,
PubSubException.NotAPubSubNodeException, IOException {
PublicKeysListElement metadata = OpenPgpPubSubUtil.fetchPubkeysList(connection, getJid());
if (metadata == null) {
return;
}
updateKeys(connection, metadata);
}
/**
* Update the contacts keys using a prefetched {@link PublicKeysListElement}.
*
* @param connection our {@link XMPPConnection}.
* @param metadata pre-fetched OX metadata node of the contact.
*
* @throws InterruptedException in case the thread gets interrupted.
* @throws SmackException.NotConnectedException in case the connection is not connected.
* @throws SmackException.NoResponseException in case the server doesn't respond.
* @throws IOException IO is dangerous.
*/
public void updateKeys(XMPPConnection connection, PublicKeysListElement metadata)
throws InterruptedException, SmackException.NotConnectedException, SmackException.NoResponseException,
IOException {
Map<OpenPgpV4Fingerprint, Date> fingerprintsAndDates = new HashMap<>();
for (OpenPgpV4Fingerprint fingerprint : metadata.getMetadata().keySet()) {
fingerprintsAndDates.put(fingerprint, metadata.getMetadata().get(fingerprint).getDate());
}
store.setAnnouncedFingerprintsOf(getJid(), fingerprintsAndDates);
Map<OpenPgpV4Fingerprint, Date> fetchDates = store.getPublicKeyFetchDates(getJid());
for (OpenPgpV4Fingerprint fingerprint : metadata.getMetadata().keySet()) {
Date fetchDate = fetchDates.get(fingerprint);
if (fetchDate != null && fingerprintsAndDates.get(fingerprint) != null && fetchDate.after(fingerprintsAndDates.get(fingerprint))) {
LOGGER.log(Level.FINE, "Skip key " + Long.toHexString(fingerprint.getKeyId()) + " as we already have the most recent version. " +
"Last announced: " + fingerprintsAndDates.get(fingerprint).toString() + " Last fetched: " + fetchDate.toString());
continue;
}
try {
PubkeyElement key = OpenPgpPubSubUtil.fetchPubkey(connection, getJid(), fingerprint);
unfetchableKeys.remove(fingerprint);
fetchDates.put(fingerprint, new Date());
if (key == null) {
LOGGER.log(Level.WARNING, "Public key " + Long.toHexString(fingerprint.getKeyId()) +
" can not be imported: Is null");
unfetchableKeys.put(fingerprint, new NullPointerException("Public key is null."));
continue;
}
PGPPublicKeyRing keyRing = new PGPPublicKeyRing(Base64.decode(key.getDataElement().getB64Data()), new BcKeyFingerprintCalculator());
store.importPublicKey(getJid(), keyRing);
} catch (PubSubException.NotAPubSubNodeException | PubSubException.NotALeafNodeException |
XMPPException.XMPPErrorException e) {
LOGGER.log(Level.WARNING, "Error fetching public key " + Long.toHexString(fingerprint.getKeyId()), e);
unfetchableKeys.put(fingerprint, e);
} catch (PGPException | IOException e) {
LOGGER.log(Level.WARNING, "Public key " + Long.toHexString(fingerprint.getKeyId()) +
" can not be imported.", e);
unfetchableKeys.put(fingerprint, e);
} catch (MissingUserIdOnKeyException e) {
LOGGER.log(Level.WARNING, "Public key " + Long.toHexString(fingerprint.getKeyId()) +
" is missing the user-id \"xmpp:" + getJid() + "\". Refuse to import it.", e);
unfetchableKeys.put(fingerprint, e);
}
}
store.setPublicKeyFetchDates(getJid(), fetchDates);
}
}

View file

@ -0,0 +1,709 @@
/**
*
* Copyright 2017 Florian Schmaus, 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox;
import static org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil.PEP_NODE_PUBLIC_KEYS;
import static org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil.PEP_NODE_PUBLIC_KEYS_NOTIFY;
import static org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil.publishPublicKey;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.Manager;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.chat2.Chat;
import org.jivesoftware.smack.chat2.ChatManager;
import org.jivesoftware.smack.chat2.IncomingChatMessageListener;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.util.Async;
import org.jivesoftware.smack.util.stringencoder.Base64;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.ox.callback.backup.AskForBackupCodeCallback;
import org.jivesoftware.smackx.ox.callback.backup.DisplayBackupCodeCallback;
import org.jivesoftware.smackx.ox.callback.backup.SecretKeyBackupSelectionCallback;
import org.jivesoftware.smackx.ox.crypto.OpenPgpProvider;
import org.jivesoftware.smackx.ox.element.CryptElement;
import org.jivesoftware.smackx.ox.element.OpenPgpContentElement;
import org.jivesoftware.smackx.ox.element.OpenPgpElement;
import org.jivesoftware.smackx.ox.element.PubkeyElement;
import org.jivesoftware.smackx.ox.element.PublicKeysListElement;
import org.jivesoftware.smackx.ox.element.SecretkeyElement;
import org.jivesoftware.smackx.ox.element.SignElement;
import org.jivesoftware.smackx.ox.element.SigncryptElement;
import org.jivesoftware.smackx.ox.exception.InvalidBackupCodeException;
import org.jivesoftware.smackx.ox.exception.MissingOpenPgpKeyException;
import org.jivesoftware.smackx.ox.exception.MissingUserIdOnKeyException;
import org.jivesoftware.smackx.ox.exception.NoBackupFoundException;
import org.jivesoftware.smackx.ox.listener.CryptElementReceivedListener;
import org.jivesoftware.smackx.ox.listener.SignElementReceivedListener;
import org.jivesoftware.smackx.ox.listener.SigncryptElementReceivedListener;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpStore;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpTrustStore;
import org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil;
import org.jivesoftware.smackx.ox.util.SecretKeyBackupHelper;
import org.jivesoftware.smackx.pep.PEPListener;
import org.jivesoftware.smackx.pep.PEPManager;
import org.jivesoftware.smackx.pubsub.EventElement;
import org.jivesoftware.smackx.pubsub.ItemsExtension;
import org.jivesoftware.smackx.pubsub.LeafNode;
import org.jivesoftware.smackx.pubsub.PayloadItem;
import org.jivesoftware.smackx.pubsub.PubSubException;
import org.jivesoftware.smackx.pubsub.PubSubFeature;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.EntityBareJid;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.collection.PGPKeyRing;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.util.BCUtil;
import org.xmlpull.v1.XmlPullParserException;
/**
* Entry point for Smacks API for OpenPGP for XMPP.
*
* <h2>Setup</h2>
*
* In order to use OpenPGP for XMPP in Smack, just follow the following procedure.<br>
* <br>
* First, acquire an instance of the {@link OpenPgpManager} for your {@link XMPPConnection} using
* {@link #getInstanceFor(XMPPConnection)}.
*
* <pre>
* {@code
* OpenPgpManager openPgpManager = OpenPgpManager.getInstanceFor(connection);
* }
* </pre>
*
* You also need an {@link OpenPgpProvider}, as well as an {@link OpenPgpStore}.
* The provider must be registered using {@link #setOpenPgpProvider(OpenPgpProvider)}.
*
* <pre>
* {@code
* OpenPgpStore store = new FileBasedOpenPgpStore(storePath);
* OpenPgpProvider provider = new PainlessOpenPgpProvider(connection, store);
* openPgpManager.setOpenPgpProvider(provider);
* }
* </pre>
*
* It is also advised to register a custom {@link SecretKeyRingProtector} using
* {@link OpenPgpStore#setKeyRingProtector(SecretKeyRingProtector)} in order to be able to handle password protected
* secret keys.<br>
* <br>
* Speaking of keys, you can now check, if you have any keys available in your {@link OpenPgpStore} by doing
* {@link #hasSecretKeysAvailable()}.<br>
* <br>
* If you do, you can now announce support for OX and publish those keys using {@link #announceSupportAndPublish()}.<br>
* <br>
* Otherwise, you can either generate fresh keys using {@link #generateAndImportKeyPair(BareJid)},
* or try to restore a secret key backup from your private PubSub node by doing
* {@link #restoreSecretKeyServerBackup(AskForBackupCodeCallback)}.<br>
* <br>
* In any case you should still do an {@link #announceSupportAndPublish()} afterwards.
* <br>
* <br>
* Contacts are represented by {@link OpenPgpContact}s in the context of OpenPGP for XMPP. You can get those by using
* {@link #getOpenPgpContact(EntityBareJid)}. The main function of {@link OpenPgpContact}s is to bundle information
* about the OpenPGP capabilities of a contact in one spot. The pendant to the {@link OpenPgpContact} is the
* {@link OpenPgpSelf}, which encapsulates your own OpenPGP identity. Both classes can be used to acquire information
* about the OpenPGP keys of a user.
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html">
* XEP-0373: OpenPGP for XMPP</a>
*/
public final class OpenPgpManager extends Manager {
private static final Logger LOGGER = Logger.getLogger(OpenPgpManager.class.getName());
/**
* Map of instances.
*/
private static final Map<XMPPConnection, OpenPgpManager> INSTANCES = new WeakHashMap<>();
/**
* {@link OpenPgpProvider} responsible for processing keys, encrypting and decrypting messages and so on.
*/
private OpenPgpProvider provider;
private final Set<SigncryptElementReceivedListener> signcryptElementReceivedListeners = new HashSet<>();
private final Set<SignElementReceivedListener> signElementReceivedListeners = new HashSet<>();
private final Set<CryptElementReceivedListener> cryptElementReceivedListeners = new HashSet<>();
/**
* Private constructor to avoid instantiation without putting the object into {@code INSTANCES}.
*
* @param connection xmpp connection.
*/
private OpenPgpManager(XMPPConnection connection) {
super(connection);
ChatManager.getInstanceFor(connection).addIncomingListener(incomingOpenPgpMessageListener);
}
/**
* Get the instance of the {@link OpenPgpManager} which belongs to the {@code connection}.
*
* @param connection xmpp connection.
* @return instance of the manager.
*/
public static OpenPgpManager getInstanceFor(XMPPConnection connection) {
OpenPgpManager manager = INSTANCES.get(connection);
if (manager == null) {
manager = new OpenPgpManager(connection);
INSTANCES.put(connection, manager);
}
return manager;
}
/**
* Return our own {@link BareJid}.
*
* @return our bareJid
*
* @throws SmackException.NotLoggedInException in case our connection is not logged in, which means our BareJid is unknown.
*/
public BareJid getJidOrThrow() throws SmackException.NotLoggedInException {
throwIfNotAuthenticated();
return connection().getUser().asEntityBareJidOrThrow();
}
/**
* Set the {@link OpenPgpProvider} which will be used to process incoming OpenPGP elements,
* as well as to execute cryptographic operations.
*
* @param provider OpenPgpProvider.
*/
public void setOpenPgpProvider(OpenPgpProvider provider) {
this.provider = provider;
}
public OpenPgpProvider getOpenPgpProvider() {
return provider;
}
/**
* Get our OpenPGP self.
*
* @return self
* @throws SmackException.NotLoggedInException if we are not logged in
*/
public OpenPgpSelf getOpenPgpSelf() throws SmackException.NotLoggedInException {
throwIfNoProviderSet();
return new OpenPgpSelf(getJidOrThrow(), provider.getStore());
}
/**
* Generate a fresh OpenPGP key pair, given we don't have one already.
* Publish the public key to the Public Key Node and update the Public Key Metadata Node with our keys fingerprint.
* Lastly register a {@link PEPListener} which listens for updates to Public Key Metadata Nodes.
*
* @throws NoSuchAlgorithmException if we are missing an algorithm to generate a fresh key pair.
* @throws NoSuchProviderException if we are missing a suitable {@link java.security.Provider}.
* @throws InterruptedException if the thread gets interrupted.
* @throws PubSubException.NotALeafNodeException if one of the PubSub nodes is not a {@link LeafNode}.
* @throws XMPPException.XMPPErrorException in case of an XMPP protocol error.
* @throws SmackException.NotConnectedException if we are not connected.
* @throws SmackException.NoResponseException if the server doesn't respond.
* @throws IOException IO is dangerous.
* @throws InvalidAlgorithmParameterException if illegal algorithm parameters are used for key generation.
* @throws SmackException.NotLoggedInException if we are not logged in.
* @throws PGPException if something goes wrong during key loading/generating
*/
public void announceSupportAndPublish()
throws NoSuchAlgorithmException, NoSuchProviderException, InterruptedException,
PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException,
SmackException.NotConnectedException, SmackException.NoResponseException, IOException,
InvalidAlgorithmParameterException, SmackException.NotLoggedInException, PGPException {
throwIfNoProviderSet();
throwIfNotAuthenticated();
OpenPgpV4Fingerprint primaryFingerprint = getOurFingerprint();
if (primaryFingerprint == null) {
primaryFingerprint = generateAndImportKeyPair(getJidOrThrow());
}
// Create <pubkey/> element
PubkeyElement pubkeyElement;
try {
pubkeyElement = createPubkeyElement(getJidOrThrow(), primaryFingerprint, new Date());
} catch (MissingOpenPgpKeyException e) {
throw new AssertionError("Cannot publish our public key, since it is missing (MUST NOT happen!)");
}
// publish it
publishPublicKey(connection(), pubkeyElement, primaryFingerprint);
// Subscribe to public key changes
PEPManager.getInstanceFor(connection()).addPEPListener(metadataListener);
ServiceDiscoveryManager.getInstanceFor(connection())
.addFeature(PEP_NODE_PUBLIC_KEYS_NOTIFY);
}
/**
* Generate a fresh OpenPGP key pair and import it.
*
* @param ourJid our {@link BareJid}.
* @return {@link OpenPgpV4Fingerprint} of the generated key.
* @throws NoSuchAlgorithmException if the JVM doesn't support one of the used algorithms.
* @throws InvalidAlgorithmParameterException if the used algorithm parameters are invalid.
* @throws NoSuchProviderException if we are missing a cryptographic provider.
* @throws PGPException PGP is brittle.
* @throws IOException IO is dangerous.
*/
public OpenPgpV4Fingerprint generateAndImportKeyPair(BareJid ourJid)
throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchProviderException,
PGPException, IOException {
throwIfNoProviderSet();
OpenPgpStore store = provider.getStore();
PGPKeyRing keys = store.generateKeyRing(ourJid);
try {
store.importSecretKey(ourJid, keys.getSecretKeys());
store.importPublicKey(ourJid, keys.getPublicKeys());
} catch (MissingUserIdOnKeyException e) {
// This should never throw, since we set our jid literally one line above this comment.
throw new AssertionError(e);
}
OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(keys.getSecretKeys());
store.setTrust(ourJid, fingerprint, OpenPgpTrustStore.Trust.trusted);
return fingerprint;
}
/**
* Return the upper-case hex encoded OpenPGP v4 fingerprint of our key pair.
*
* @return fingerprint.
* @throws SmackException.NotLoggedInException in case we are not logged in.
* @throws IOException IO is dangerous.
* @throws PGPException PGP is brittle.
*/
public OpenPgpV4Fingerprint getOurFingerprint()
throws SmackException.NotLoggedInException, IOException, PGPException {
return getOpenPgpSelf().getSigningKeyFingerprint();
}
/**
* Return an OpenPGP capable contact.
* This object can be used as an entry point to OpenPGP related API.
*
* @param jid {@link BareJid} of the contact.
* @return {@link OpenPgpContact}.
*/
public OpenPgpContact getOpenPgpContact(EntityBareJid jid) {
throwIfNoProviderSet();
return provider.getStore().getOpenPgpContact(jid);
}
/**
* Return true, if we have a secret key available, otherwise false.
*
* @return true if secret key available
*
* @throws SmackException.NotLoggedInException If we are not logged in (we need to know our jid in order to look up
* our keys in the key store.
* @throws PGPException in case the keys in the store are damaged somehow.
* @throws IOException IO is dangerous.
*/
public boolean hasSecretKeysAvailable() throws SmackException.NotLoggedInException, PGPException, IOException {
throwIfNoProviderSet();
return getOpenPgpSelf().hasSecretKeyAvailable();
}
/**
* Determine, if we can sync secret keys using private PEP nodes as described in the XEP.
* Requirements on the server side are support for PEP and support for the whitelist access model of PubSub.
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html#synchro-pep">XEP-0373 §5</a>
*
* @param connection XMPP connection
* @return true, if the server supports secret key backups, otherwise false.
* @throws XMPPException.XMPPErrorException in case of an XMPP protocol error.
* @throws SmackException.NotConnectedException if we are not connected.
* @throws InterruptedException if the thread is interrupted.
* @throws SmackException.NoResponseException if the server doesn't respond.
*/
public static boolean serverSupportsSecretKeyBackups(XMPPConnection connection)
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException {
return ServiceDiscoveryManager.getInstanceFor(connection)
.serverSupportsFeature(PubSubFeature.access_whitelist.toString());
}
/**
* Remove the metadata listener. This method is mainly used in tests.
*/
public void stopMetadataListener() {
PEPManager.getInstanceFor(connection()).removePEPListener(metadataListener);
}
/**
* Upload the encrypted secret key to a private PEP node.
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html#synchro-pep">XEP-0373 §5</a>
*
* @param displayCodeCallback callback, which will receive the backup password used to encrypt the secret key.
* @param selectKeyCallback callback, which will receive the users choice of which keys will be backed up.
* @throws InterruptedException if the thread is interrupted.
* @throws PubSubException.NotALeafNodeException if the private node is not a {@link LeafNode}.
* @throws XMPPException.XMPPErrorException in case of an XMPP protocol error.
* @throws SmackException.NotConnectedException if we are not connected.
* @throws SmackException.NoResponseException if the server doesn't respond.
* @throws SmackException.NotLoggedInException if we are not logged in.
* @throws IOException IO is dangerous.
* @throws SmackException.FeatureNotSupportedException if the server doesn't support the PubSub whitelist access model.
* @throws PGPException PGP is brittle
* @throws MissingOpenPgpKeyException in case we have no OpenPGP key pair to back up.
*/
public void backupSecretKeyToServer(DisplayBackupCodeCallback displayCodeCallback,
SecretKeyBackupSelectionCallback selectKeyCallback)
throws InterruptedException, PubSubException.NotALeafNodeException,
XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException,
SmackException.NotLoggedInException, IOException,
SmackException.FeatureNotSupportedException, PGPException, MissingOpenPgpKeyException {
throwIfNoProviderSet();
throwIfNotAuthenticated();
BareJid ownJid = connection().getUser().asBareJid();
String backupCode = SecretKeyBackupHelper.generateBackupPassword();
PGPSecretKeyRingCollection secretKeyRings = provider.getStore().getSecretKeysOf(ownJid);
Set<OpenPgpV4Fingerprint> availableKeyPairs = new HashSet<>();
for (PGPSecretKeyRing ring : secretKeyRings) {
availableKeyPairs.add(new OpenPgpV4Fingerprint(ring));
}
Set<OpenPgpV4Fingerprint> selectedKeyPairs = selectKeyCallback.selectKeysToBackup(availableKeyPairs);
SecretkeyElement secretKey = SecretKeyBackupHelper.createSecretkeyElement(provider, ownJid, selectedKeyPairs, backupCode);
OpenPgpPubSubUtil.depositSecretKey(connection(), secretKey);
displayCodeCallback.displayBackupCode(backupCode);
}
/**
* Delete the private {@link LeafNode} containing our secret key backup.
*
* @throws XMPPException.XMPPErrorException in case of an XMPP protocol error.
* @throws SmackException.NotConnectedException if we are not connected.
* @throws InterruptedException if the thread gets interrupted.
* @throws SmackException.NoResponseException if the server doesn't respond.
* @throws SmackException.NotLoggedInException if we are not logged in.
*/
public void deleteSecretKeyServerBackup()
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException, SmackException.NotLoggedInException {
throwIfNotAuthenticated();
OpenPgpPubSubUtil.deleteSecretKeyNode(connection());
}
/**
* Fetch a secret key backup from the server and try to restore a selected secret key from it.
*
* @param codeCallback callback for prompting the user to provide the secret backup code.
* @return fingerprint of the restored secret key
*
* @throws InterruptedException if the thread gets interrupted.
* @throws PubSubException.NotALeafNodeException if the private node is not a {@link LeafNode}.
* @throws XMPPException.XMPPErrorException in case of an XMPP protocol error.
* @throws SmackException.NotConnectedException if we are not connected.
* @throws SmackException.NoResponseException if the server doesn't respond.
* @throws InvalidBackupCodeException if the user-provided backup code is invalid.
* @throws SmackException.NotLoggedInException if we are not logged in
* @throws IOException IO is dangerous
* @throws MissingUserIdOnKeyException if the key that is to be imported is missing a user-id with our jid
* @throws NoBackupFoundException if no secret key backup has been found
* @throws PGPException in case the restored secret key is damaged.
*/
public OpenPgpV4Fingerprint restoreSecretKeyServerBackup(AskForBackupCodeCallback codeCallback)
throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException,
SmackException.NotConnectedException, SmackException.NoResponseException,
InvalidBackupCodeException, SmackException.NotLoggedInException, IOException, MissingUserIdOnKeyException,
NoBackupFoundException, PGPException {
throwIfNoProviderSet();
throwIfNotAuthenticated();
SecretkeyElement backup = OpenPgpPubSubUtil.fetchSecretKey(connection());
if (backup == null) {
throw new NoBackupFoundException();
}
String backupCode = codeCallback.askForBackupCode();
PGPSecretKeyRing secretKeys = SecretKeyBackupHelper.restoreSecretKeyBackup(backup, backupCode);
provider.getStore().importSecretKey(getJidOrThrow(), secretKeys);
provider.getStore().importPublicKey(getJidOrThrow(), BCUtil.publicKeyRingFromSecretKeyRing(secretKeys));
ByteArrayOutputStream buffer = new ByteArrayOutputStream(2048);
for (PGPSecretKey sk : secretKeys) {
PGPPublicKey pk = sk.getPublicKey();
if (pk != null) pk.encode(buffer);
}
PGPPublicKeyRing publicKeys = new PGPPublicKeyRing(buffer.toByteArray(), new BcKeyFingerprintCalculator());
provider.getStore().importPublicKey(getJidOrThrow(), publicKeys);
return new OpenPgpV4Fingerprint(secretKeys);
}
/*
Private stuff.
*/
/**
* {@link PEPListener} that listens for changes to the OX public keys metadata node.
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html#pubsub-notifications">XEP-0373 §4.4</a>
*/
private final PEPListener metadataListener = new PEPListener() {
@Override
public void eventReceived(final EntityBareJid from, final EventElement event, final Message message) {
if (PEP_NODE_PUBLIC_KEYS.equals(event.getEvent().getNode())) {
final BareJid contact = from.asBareJid();
LOGGER.log(Level.INFO, "Received OpenPGP metadata update from " + contact);
Async.go(new Runnable() {
@Override
public void run() {
ItemsExtension items = (ItemsExtension) event.getExtensions().get(0);
PayloadItem<?> payload = (PayloadItem) items.getItems().get(0);
PublicKeysListElement listElement = (PublicKeysListElement) payload.getPayload();
processPublicKeysListElement(from, listElement);
}
}, "ProcessOXMetadata");
}
}
};
private void processPublicKeysListElement(BareJid contact, PublicKeysListElement listElement) {
OpenPgpContact openPgpContact = getOpenPgpContact(contact.asEntityBareJidIfPossible());
try {
openPgpContact.updateKeys(connection(), listElement);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Could not update contacts keys", e);
}
}
public OpenPgpMessage decryptOpenPgpElement(OpenPgpElement element, OpenPgpContact contact) throws SmackException.NotLoggedInException, IOException, PGPException {
return provider.decryptAndOrVerify(element, getOpenPgpSelf(), contact);
}
private final IncomingChatMessageListener incomingOpenPgpMessageListener =
new IncomingChatMessageListener() {
@Override
public void newIncomingMessage(final EntityBareJid from, final Message message, Chat chat) {
Async.go(new Runnable() {
@Override
public void run() {
OpenPgpElement element = message.getExtension(OpenPgpElement.ELEMENT, OpenPgpElement.NAMESPACE);
if (element == null) {
// Message does not contain an OpenPgpElement -> discard
return;
}
OpenPgpContact contact = getOpenPgpContact(from);
OpenPgpMessage decrypted = null;
OpenPgpContentElement contentElement = null;
try {
decrypted = decryptOpenPgpElement(element, contact);
contentElement = decrypted.getOpenPgpContentElement();
} catch (PGPException e) {
LOGGER.log(Level.WARNING, "Could not decrypt incoming OpenPGP encrypted message", e);
} catch (XmlPullParserException | IOException e) {
LOGGER.log(Level.WARNING, "Invalid XML content of incoming OpenPGP encrypted message", e);
} catch (SmackException.NotLoggedInException e) {
LOGGER.log(Level.WARNING, "Cannot determine our JID, since we are not logged in.", e);
}
if (contentElement instanceof SigncryptElement) {
for (SigncryptElementReceivedListener l : signcryptElementReceivedListeners) {
l.signcryptElementReceived(contact, message, (SigncryptElement) contentElement, decrypted.getMetadata());
}
return;
}
if (contentElement instanceof SignElement) {
for (SignElementReceivedListener l : signElementReceivedListeners) {
l.signElementReceived(contact, message, (SignElement) contentElement, decrypted.getMetadata());
}
return;
}
if (contentElement instanceof CryptElement) {
for (CryptElementReceivedListener l : cryptElementReceivedListeners) {
l.cryptElementReceived(contact, message, (CryptElement) contentElement, decrypted.getMetadata());
}
return;
}
else {
throw new AssertionError("Invalid element received: " + contentElement.getClass().getName());
}
}
});
}
};
/**
* Create a {@link PubkeyElement} which contains the OpenPGP public key of {@code owner} which belongs to
* the {@link OpenPgpV4Fingerprint} {@code fingerprint}.
*
* @param owner owner of the public key
* @param fingerprint fingerprint of the key
* @param date date of creation of the element
* @return {@link PubkeyElement} containing the key
*
* @throws MissingOpenPgpKeyException if the public key notated by the fingerprint cannot be found
*/
private PubkeyElement createPubkeyElement(BareJid owner,
OpenPgpV4Fingerprint fingerprint,
Date date)
throws MissingOpenPgpKeyException, IOException, PGPException {
PGPPublicKeyRing ring = provider.getStore().getPublicKeyRing(owner, fingerprint);
if (ring != null) {
byte[] keyBytes = ring.getEncoded(true);
return createPubkeyElement(keyBytes, date);
}
throw new MissingOpenPgpKeyException(owner, fingerprint);
}
/**
* Create a {@link PubkeyElement} which contains the given {@code data} base64 encoded.
*
* @param bytes byte representation of an OpenPGP public key
* @param date date of creation of the element
* @return {@link PubkeyElement} containing the key
*/
private static PubkeyElement createPubkeyElement(byte[] bytes, Date date) {
return new PubkeyElement(new PubkeyElement.PubkeyDataElement(Base64.encode(bytes)), date);
}
/**
* Register a {@link SigncryptElementReceivedListener} on the {@link OpenPgpManager}.
* That listener will get informed whenever a {@link SigncryptElement} has been received and successfully decrypted.
*
* Note: This method is not intended for clients to listen for incoming {@link SigncryptElement}s.
* Instead its purpose is to allow easy extension of XEP-0373 for custom OpenPGP profiles such as
* OpenPGP for XMPP: Instant Messaging.
*
* @param listener listener that gets registered
*/
public void registerSigncryptReceivedListener(SigncryptElementReceivedListener listener) {
signcryptElementReceivedListeners.add(listener);
}
/**
* Unregister a prior registered {@link SigncryptElementReceivedListener}. That listener will no longer get
* informed about incoming decrypted {@link SigncryptElement}s.
*
* @param listener listener that gets unregistered
*/
void unregisterSigncryptElementReceivedListener(SigncryptElementReceivedListener listener) {
signcryptElementReceivedListeners.remove(listener);
}
/**
* Register a {@link SignElementReceivedListener} on the {@link OpenPgpManager}.
* That listener will get informed whenever a {@link SignElement} has been received and successfully verified.
*
* Note: This method is not intended for clients to listen for incoming {@link SignElement}s.
* Instead its purpose is to allow easy extension of XEP-0373 for custom OpenPGP profiles such as
* OpenPGP for XMPP: Instant Messaging.
*
* @param listener listener that gets registered
*/
void registerSignElementReceivedListener(SignElementReceivedListener listener) {
signElementReceivedListeners.add(listener);
}
/**
* Unregister a prior registered {@link SignElementReceivedListener}. That listener will no longer get
* informed about incoming decrypted {@link SignElement}s.
*
* @param listener listener that gets unregistered
*/
void unregisterSignElementReceivedListener(SignElementReceivedListener listener) {
signElementReceivedListeners.remove(listener);
}
/**
* Register a {@link CryptElementReceivedListener} on the {@link OpenPgpManager}.
* That listener will get informed whenever a {@link CryptElement} has been received and successfully decrypted.
*
* Note: This method is not intended for clients to listen for incoming {@link CryptElement}s.
* Instead its purpose is to allow easy extension of XEP-0373 for custom OpenPGP profiles such as
* OpenPGP for XMPP: Instant Messaging.
*
* @param listener listener that gets registered
*/
void registerCryptElementReceivedListener(CryptElementReceivedListener listener) {
cryptElementReceivedListeners.add(listener);
}
/**
* Unregister a prior registered {@link CryptElementReceivedListener}. That listener will no longer get
* informed about incoming decrypted {@link CryptElement}s.
*
* @param listener listener that gets unregistered
*/
void unregisterCryptElementReceivedListener(CryptElementReceivedListener listener) {
cryptElementReceivedListeners.remove(listener);
}
/**
* Throw an {@link IllegalStateException} if no {@link OpenPgpProvider} is set.
* The OpenPgpProvider is used to process information related to RFC-4880.
*/
private void throwIfNoProviderSet() {
if (provider == null) {
throw new IllegalStateException("No OpenPgpProvider set!");
}
}
/**
* Throw a {@link org.jivesoftware.smack.SmackException.NotLoggedInException} if the {@link XMPPConnection} of this
* manager is not authenticated at this point.
*
* @throws SmackException.NotLoggedInException if we are not authenticated
*/
private void throwIfNotAuthenticated() throws SmackException.NotLoggedInException {
if (!connection().isAuthenticated()) {
throw new SmackException.NotLoggedInException();
}
}
}

View file

@ -0,0 +1,145 @@
/**
*
* Copyright 2017 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox;
import java.io.IOException;
import java.nio.charset.Charset;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smackx.ox.element.CryptElement;
import org.jivesoftware.smackx.ox.element.OpenPgpContentElement;
import org.jivesoftware.smackx.ox.element.OpenPgpElement;
import org.jivesoftware.smackx.ox.element.SignElement;
import org.jivesoftware.smackx.ox.element.SigncryptElement;
import org.jivesoftware.smackx.ox.provider.OpenPgpContentElementProvider;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.xmlpull.v1.XmlPullParserException;
/**
* This class embodies a decrypted {@link OpenPgpElement}.
*/
public class OpenPgpMessage {
public enum State {
/**
* Represents a {@link SigncryptElement}.
*/
signcrypt,
/**
* Represents a {@link SignElement}.
*/
sign,
/**
* Represents a {@link CryptElement}.
*/
crypt,
;
}
private final String element;
private final State state;
private final OpenPgpMetadata metadata;
private OpenPgpContentElement openPgpContentElement;
/**
* Constructor.
*
* @param content XML representation of the decrypted {@link OpenPgpContentElement}.
* @param state {@link State} of the {@link OpenPgpContentElement}.
* @param metadata Metadata about the encryption.
*/
public OpenPgpMessage(String content, State state, OpenPgpMetadata metadata) {
this.metadata = Objects.requireNonNull(metadata);
this.state = Objects.requireNonNull(state);
this.element = Objects.requireNonNull(content);
}
/**
* Constructor.
*
* @param bytes bytes of the XML representation of the decrypted {@link OpenPgpContentElement}.
* @param state {@link State} of the {@link OpenPgpContentElement}.
* @param metadata metadata about the encryption.
*/
public OpenPgpMessage(byte[] bytes, State state, OpenPgpMetadata metadata) {
this(new String(Objects.requireNonNull(bytes), Charset.forName("UTF-8")), state, metadata);
}
/**
* Return the decrypted {@link OpenPgpContentElement} of this message.
* To determine, whether the element is a {@link SignElement}, {@link CryptElement} or {@link SigncryptElement},
* please consult {@link #getState()}.
*
* @return {@link OpenPgpContentElement}
* @throws XmlPullParserException if the parser encounters an error.
* @throws IOException if the parser encounters an error.
*/
public OpenPgpContentElement getOpenPgpContentElement() throws XmlPullParserException, IOException {
ensureOpenPgpContentElementSet();
return openPgpContentElement;
}
private void ensureOpenPgpContentElementSet() throws XmlPullParserException, IOException {
if (openPgpContentElement != null)
return;
openPgpContentElement = OpenPgpContentElementProvider.parseOpenPgpContentElement(element);
if (openPgpContentElement == null) {
return;
}
// Determine the state of the content element.
if (openPgpContentElement instanceof SigncryptElement) {
if (state != State.signcrypt) {
throw new IllegalStateException("OpenPgpContentElement was signed and encrypted, but is not a SigncryptElement.");
}
} else if (openPgpContentElement instanceof SignElement) {
if (state != State.sign) {
throw new IllegalStateException("OpenPgpContentElement was signed and unencrypted, but is not a SignElement.");
}
} else if (openPgpContentElement instanceof CryptElement) {
if (state != State.crypt) {
throw new IllegalStateException("OpenPgpContentElement was unsigned and encrypted, but is not a CryptElement.");
}
}
}
/**
* Return the state of the message. This value determines, whether the message was a {@link SignElement},
* {@link CryptElement} or {@link SigncryptElement}.
*
* @return state of the content element.
* @throws IOException if the parser encounters an error.
* @throws XmlPullParserException if the parser encounters and error.
*/
public State getState() throws IOException, XmlPullParserException {
ensureOpenPgpContentElementSet();
return state;
}
/**
* Return metadata about the encrypted message.
*
* @return metadata
*/
public OpenPgpMetadata getMetadata() {
return metadata;
}
}

View file

@ -0,0 +1,113 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox;
import java.io.IOException;
import java.util.Collections;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpStore;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.jxmpp.jid.BareJid;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.util.BCUtil;
public class OpenPgpSelf extends OpenPgpContact {
OpenPgpSelf(BareJid jid, OpenPgpStore store) {
super(jid, store);
}
/**
* Return true, if we have a usable secret key available.
* @return true if we have secret key, otherwise false.
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
*/
public boolean hasSecretKeyAvailable() throws IOException, PGPException {
return getSecretKeys() != null;
}
/**
* Return a {@link PGPSecretKeyRingCollection} which contains all of our {@link PGPSecretKeyRing}s.
* @return collection of our secret keys
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
*/
public PGPSecretKeyRingCollection getSecretKeys() throws IOException, PGPException {
return store.getSecretKeysOf(jid);
}
/**
* Return the {@link PGPSecretKeyRing} which we will use to sign our messages.
* @return signing key
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
*/
public PGPSecretKeyRing getSigningKeyRing() throws IOException, PGPException {
PGPSecretKeyRingCollection secretKeyRings = getSecretKeys();
if (secretKeyRings == null) {
return null;
}
PGPSecretKeyRing signingKeyRing = null;
for (PGPSecretKeyRing ring : secretKeyRings) {
if (signingKeyRing == null) {
signingKeyRing = ring;
continue;
}
if (ring.getPublicKey().getCreationTime().after(signingKeyRing.getPublicKey().getCreationTime())) {
signingKeyRing = ring;
}
}
return signingKeyRing;
}
/**
* Return the {@link OpenPgpV4Fingerprint} of our signing key.
* @return fingerprint of signing key
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
*/
public OpenPgpV4Fingerprint getSigningKeyFingerprint() throws IOException, PGPException {
PGPSecretKeyRing signingKeyRing = getSigningKeyRing();
return signingKeyRing != null ? new OpenPgpV4Fingerprint(signingKeyRing.getPublicKey()) : null;
}
/**
* Return a {@link PGPPublicKeyRingCollection} containing only the public keys belonging to our signing key ring.
* TODO: Add support for public keys of other devices of the owner.
*
* @return public keys
*
* @throws IOException IO is dangerous.
* @throws PGPException PGP is brittle.
*/
@Override
public PGPPublicKeyRingCollection getAnnouncedPublicKeys() throws IOException, PGPException {
PGPSecretKeyRing secretKeys = getSigningKeyRing();
PGPPublicKeyRing publicKeys = getAnyPublicKeys().getPublicKeyRing(secretKeys.getPublicKey().getKeyID());
publicKeys = BCUtil.removeUnassociatedKeysFromKeyRing(publicKeys, secretKeys.getPublicKey());
return new PGPPublicKeyRingCollection(Collections.singleton(publicKeys));
}
}

View file

@ -0,0 +1,31 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.callback;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.util.Passphrase;
public interface SecretKeyPassphraseCallback {
/**
* This method gets called in case a passphrase is needed for a secret key.
*
* @param fingerprint fingerprint of the key
* @return passphrase for the key
*/
Passphrase onPassphraseNeeded(OpenPgpV4Fingerprint fingerprint);
}

View file

@ -0,0 +1,32 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.callback.backup;
public interface AskForBackupCodeCallback {
/**
* This callback is used to ask the user to provide a backup code.
* The backup code must follow the format described in XEP-0373 §5.3
*
* TODO: Update reflink
* @see <a href="https://xmpp.org/extensions/xep-0373.html#sect-idm139662753819792">
* XEP-0373 §5.3 about the format of the backup code</a>
*
* @return backup code provided by the user.
*/
String askForBackupCode();
}

View file

@ -0,0 +1,33 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.callback.backup;
public interface DisplayBackupCodeCallback {
/**
* This method is used to provide a client access to the generated backup code.
* The client can then go ahead and display the code to the user.
* The backup code follows the format described in XEP-0373 §5.3
*
* TODO: Update reflink
* @see <a href="https://xmpp.org/extensions/xep-0373.html#sect-idm139662753819792">
* XEP-0373 §5.3 about the format of the backup code</a>
*
* @param backupCode backup code
*/
void displayBackupCode(String backupCode);
}

View file

@ -0,0 +1,38 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.callback.backup;
import java.util.Set;
import org.pgpainless.key.OpenPgpV4Fingerprint;
/**
* Callback to allow the user to decide, which locally available secret keys they want to include in a backup.
*/
public interface SecretKeyBackupSelectionCallback {
/**
* Let the user decide, which secret keys they want to backup.
*
* @param availableSecretKeys {@link Set} of {@link OpenPgpV4Fingerprint}s of locally available
* OpenPGP secret keys.
* @return {@link Set} which contains the {@link OpenPgpV4Fingerprint}s the user decided to include
* in the backup.
*/
Set<OpenPgpV4Fingerprint> selectKeysToBackup(Set<OpenPgpV4Fingerprint> availableSecretKeys);
}

View file

@ -0,0 +1,37 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.callback.backup;
import java.util.Set;
import org.pgpainless.key.OpenPgpV4Fingerprint;
/**
* Callback to let the user decide which key from a backup they want to restore.
*/
public interface SecretKeyRestoreSelectionCallback {
/**
* Let the user choose, which SecretKey they want to restore as the new primary OpenPGP signing key.
* @param availableSecretKeys {@link Set} of {@link OpenPgpV4Fingerprint}s of the keys which are contained
* in the backup.
* @return {@link OpenPgpV4Fingerprint} of the key the user wants to restore as the new primary
* signing key.
*/
OpenPgpV4Fingerprint selectSecretKeyToRestore(Set<OpenPgpV4Fingerprint> availableSecretKeys);
}

View file

@ -0,0 +1,20 @@
/**
*
* Copyright 2018 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Callback classes Secret key backups.
*/
package org.jivesoftware.smackx.ox.callback.backup;

View file

@ -0,0 +1,20 @@
/**
*
* Copyright 2018 Paul Schaub
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Callback classes for XEP-0373: OpenPGP for XMPP.
*/
package org.jivesoftware.smackx.ox.callback;

View file

@ -0,0 +1,59 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.crypto;
import org.jivesoftware.smackx.ox.element.OpenPgpElement;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
/**
* Bundle together an {@link OpenPgpElement} and {@link OpenPgpMetadata}.
*/
public class OpenPgpElementAndMetadata {
private final OpenPgpElement element;
private final OpenPgpMetadata metadata;
/**
* Constructor.
*
* @param element element
* @param metadata metadata about the elements encryption
*/
public OpenPgpElementAndMetadata(OpenPgpElement element, OpenPgpMetadata metadata) {
this.element = element;
this.metadata = metadata;
}
/**
* Return the {@link OpenPgpElement}.
*
* @return element
*/
public OpenPgpElement getElement() {
return element;
}
/**
* Return {@link OpenPgpMetadata} about the {@link OpenPgpElement}s encryption/signatures.
*
* @return metadata
*/
public OpenPgpMetadata getMetadata() {
return metadata;
}
}

View file

@ -0,0 +1,107 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.crypto;
import java.io.IOException;
import java.util.Collection;
import org.jivesoftware.smackx.ox.OpenPgpContact;
import org.jivesoftware.smackx.ox.OpenPgpMessage;
import org.jivesoftware.smackx.ox.OpenPgpSelf;
import org.jivesoftware.smackx.ox.element.CryptElement;
import org.jivesoftware.smackx.ox.element.OpenPgpContentElement;
import org.jivesoftware.smackx.ox.element.OpenPgpElement;
import org.jivesoftware.smackx.ox.element.SignElement;
import org.jivesoftware.smackx.ox.element.SigncryptElement;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpStore;
import org.bouncycastle.openpgp.PGPException;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
public interface OpenPgpProvider {
/**
* Return the {@link OpenPgpStore} instance of this provider.
* This MUST NOT return null.
*
* @return store
*/
OpenPgpStore getStore();
/**
* Sign a {@link SigncryptElement} using our signing key and encrypt it for all {@code recipients} and ourselves.
*
* @param element {@link SigncryptElement} which contains a payload which will be transmitted.
* @param self our own OpenPGP identity.
* @param recipients recipients identities.
*
* @return signed and encrypted {@link SigncryptElement} as a {@link OpenPgpElement}, along with
* {@link OpenPgpMetadata} about the encryption/signatures.
*
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
*/
OpenPgpElementAndMetadata signAndEncrypt(SigncryptElement element, OpenPgpSelf self, Collection<OpenPgpContact> recipients)
throws IOException, PGPException;
/**
* Sign a {@link SignElement} using our signing key.
* @param element {@link SignElement} which contains a payload.
* @param self our OpenPGP identity.
*
* @return signed {@link SignElement} as {@link OpenPgpElement}, along with {@link OpenPgpMetadata} about the
* signatures.
*
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
*/
OpenPgpElementAndMetadata sign(SignElement element, OpenPgpSelf self)
throws IOException, PGPException;
/**
* Encrypt a {@link CryptElement} for all {@code recipients} and ourselves.
* @param element {@link CryptElement} which contains a payload which will be transmitted.
* @param self our own OpenPGP identity.
* @param recipients recipient identities.
*
* @return encrypted {@link CryptElement} as an {@link OpenPgpElement}, along with {@link OpenPgpMetadata} about
* the encryption.
*
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
*/
OpenPgpElementAndMetadata encrypt(CryptElement element, OpenPgpSelf self, Collection<OpenPgpContact> recipients)
throws IOException, PGPException;
/**
* Decrypt and/or verify signatures on an incoming {@link OpenPgpElement}.
* If the message is encrypted, this method decrypts it. If it is (also) signed, the signature will be checked.
* The resulting {@link OpenPgpMessage} contains the original {@link OpenPgpContentElement}, as well as information
* about the encryption/signing.
*
* @param element signed and or encrypted {@link OpenPgpElement}.
* @param self our OpenPGP identity.
* @param sender OpenPGP identity of the sender.
*
* @return decrypted message as {@link OpenPgpMessage}.
*
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
*/
OpenPgpMessage decryptAndOrVerify(OpenPgpElement element, OpenPgpSelf self, OpenPgpContact sender)
throws IOException, PGPException;
}

View file

@ -0,0 +1,227 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.crypto;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.Security;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.stringencoder.Base64;
import org.jivesoftware.smackx.ox.OpenPgpContact;
import org.jivesoftware.smackx.ox.OpenPgpMessage;
import org.jivesoftware.smackx.ox.OpenPgpSelf;
import org.jivesoftware.smackx.ox.element.CryptElement;
import org.jivesoftware.smackx.ox.element.OpenPgpElement;
import org.jivesoftware.smackx.ox.element.SignElement;
import org.jivesoftware.smackx.ox.element.SigncryptElement;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpStore;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.util.io.Streams;
import org.pgpainless.PGPainless;
import org.pgpainless.decryption_verification.DecryptionStream;
import org.pgpainless.decryption_verification.MissingPublicKeyCallback;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.encryption_signing.EncryptionStream;
public class PainlessOpenPgpProvider implements OpenPgpProvider {
private static final Logger LOGGER = Logger.getLogger(PainlessOpenPgpProvider.class.getName());
static {
// Remove any BC providers and add a fresh one.
// This is done, since older Android versions ship with a crippled BC provider.
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
Security.addProvider(new BouncyCastleProvider());
}
private final XMPPConnection connection;
private final OpenPgpStore store;
public PainlessOpenPgpProvider(XMPPConnection connection, OpenPgpStore store) {
this.connection = Objects.requireNonNull(connection);
this.store = Objects.requireNonNull(store);
}
@Override
public OpenPgpStore getStore() {
return store;
}
@Override
public OpenPgpElementAndMetadata signAndEncrypt(SigncryptElement element, OpenPgpSelf self, Collection<OpenPgpContact> recipients)
throws IOException, PGPException {
InputStream plainText = element.toInputStream();
ByteArrayOutputStream cipherText = new ByteArrayOutputStream();
ArrayList<PGPPublicKeyRingCollection> recipientKeys = new ArrayList<>();
for (OpenPgpContact contact : recipients) {
PGPPublicKeyRingCollection keys = contact.getTrustedAnnouncedKeys();
if (keys != null) {
recipientKeys.add(keys);
} else {
LOGGER.log(Level.WARNING, "There are no suitable keys for contact " + contact.getJid().toString());
}
}
EncryptionStream cipherStream = PGPainless.createEncryptor().onOutputStream(cipherText)
.toRecipients(recipientKeys.toArray(new PGPPublicKeyRingCollection[] {}))
.andToSelf(self.getTrustedAnnouncedKeys())
.usingSecureAlgorithms()
.signWith(getStore().getKeyRingProtector(), self.getSigningKeyRing())
.noArmor();
Streams.pipeAll(plainText, cipherStream);
plainText.close();
cipherStream.flush();
cipherStream.close();
cipherText.close();
String base64 = Base64.encodeToString(cipherText.toByteArray());
OpenPgpElement openPgpElement = new OpenPgpElement(base64);
return new OpenPgpElementAndMetadata(openPgpElement, cipherStream.getResult());
}
@Override
public OpenPgpElementAndMetadata sign(SignElement element, OpenPgpSelf self)
throws IOException, PGPException {
InputStream plainText = element.toInputStream();
ByteArrayOutputStream cipherText = new ByteArrayOutputStream();
EncryptionStream cipherStream = PGPainless.createEncryptor().onOutputStream(cipherText)
.doNotEncrypt()
.signWith(getStore().getKeyRingProtector(), self.getSigningKeyRing())
.noArmor();
Streams.pipeAll(plainText, cipherStream);
plainText.close();
cipherStream.flush();
cipherStream.close();
cipherText.close();
String base64 = Base64.encodeToString(cipherText.toByteArray());
OpenPgpElement openPgpElement = new OpenPgpElement(base64);
return new OpenPgpElementAndMetadata(openPgpElement, cipherStream.getResult());
}
@Override
public OpenPgpElementAndMetadata encrypt(CryptElement element, OpenPgpSelf self, Collection<OpenPgpContact> recipients)
throws IOException, PGPException {
InputStream plainText = element.toInputStream();
ByteArrayOutputStream cipherText = new ByteArrayOutputStream();
ArrayList<PGPPublicKeyRingCollection> recipientKeys = new ArrayList<>();
for (OpenPgpContact contact : recipients) {
PGPPublicKeyRingCollection keys = contact.getTrustedAnnouncedKeys();
if (keys != null) {
recipientKeys.add(keys);
} else {
LOGGER.log(Level.WARNING, "There are no suitable keys for contact " + contact.getJid().toString());
}
}
EncryptionStream cipherStream = PGPainless.createEncryptor().onOutputStream(cipherText)
.toRecipients(recipientKeys.toArray(new PGPPublicKeyRingCollection[] {}))
.andToSelf(self.getTrustedAnnouncedKeys())
.usingSecureAlgorithms()
.doNotSign()
.noArmor();
Streams.pipeAll(plainText, cipherStream);
plainText.close();
cipherStream.flush();
cipherStream.close();
cipherText.close();
String base64 = Base64.encodeToString(cipherText.toByteArray());
OpenPgpElement openPgpElement = new OpenPgpElement(base64);
return new OpenPgpElementAndMetadata(openPgpElement, cipherStream.getResult());
}
@Override
public OpenPgpMessage decryptAndOrVerify(OpenPgpElement element, final OpenPgpSelf self, final OpenPgpContact sender) throws IOException, PGPException {
ByteArrayOutputStream plainText = new ByteArrayOutputStream();
InputStream cipherText = element.toInputStream();
PGPPublicKeyRingCollection announcedPublicKeys = sender.getAnnouncedPublicKeys();
if (announcedPublicKeys == null) {
LOGGER.log(Level.INFO, "Received a message from " + sender.getJid() + " but we have no keys yet. Try fetching them.");
try {
sender.updateKeys(connection);
announcedPublicKeys = sender.getAnnouncedPublicKeys();
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Fetching keys of " + sender.getJid() + " failed. Abort decryption and discard message.", e);
throw new PGPException("Abort decryption due to lack of keys.", e);
}
}
MissingPublicKeyCallback missingPublicKeyCallback = new MissingPublicKeyCallback() {
@Override
public PGPPublicKey onMissingPublicKeyEncountered(Long keyId) {
try {
sender.updateKeys(connection);
return sender.getAnyPublicKeys().getPublicKey(keyId);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Cannot fetch missing key " + keyId, e);
return null;
}
}
};
DecryptionStream cipherStream = PGPainless.createDecryptor().onInputStream(cipherText)
.decryptWith(getStore().getKeyRingProtector(), self.getSecretKeys())
.verifyWith(announcedPublicKeys)
.handleMissingPublicKeysWith(missingPublicKeyCallback)
.build();
Streams.pipeAll(cipherStream, plainText);
cipherText.close();
cipherStream.close();
plainText.close();
OpenPgpMetadata info = cipherStream.getResult();
OpenPgpMessage.State state;
if (info.isSigned()) {
if (info.isEncrypted()) {
state = OpenPgpMessage.State.signcrypt;
} else {
state = OpenPgpMessage.State.sign;
}
} else if (info.isEncrypted()) {
state = OpenPgpMessage.State.crypt;
} else {
throw new PGPException("Received message appears to be neither encrypted, nor signed.");
}
return new OpenPgpMessage(plainText.toByteArray(), state, info);
}
}

View file

@ -0,0 +1,20 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Crypto Providers for XEP-0373: OpenPGP for XMPP using Bouncycastle.
*/
package org.jivesoftware.smackx.ox.crypto;

View file

@ -0,0 +1,56 @@
/**
*
* Copyright 2017 Florian Schmaus, 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.element;
import java.util.Date;
import java.util.List;
import java.util.Set;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jxmpp.jid.Jid;
/**
* This class describes an OpenPGP content element which is encrypted, but not signed.
*/
public class CryptElement extends EncryptedOpenPgpContentElement {
public static final String ELEMENT = "crypt";
public CryptElement(Set<Jid> to, String rpad, Date timestamp, List<ExtensionElement> payload) {
super(to, rpad, timestamp, payload);
}
public CryptElement(Set<Jid> to, List<ExtensionElement> payload) {
super(to, payload);
}
@Override
public String getElementName() {
return ELEMENT;
}
@Override
public XmlStringBuilder toXML(String enclosingNamespace) {
XmlStringBuilder xml = new XmlStringBuilder(this).rightAngleBracket();
addCommonXml(xml);
xml.closeElement(this);
return xml;
}
}

View file

@ -0,0 +1,67 @@
/**
*
* Copyright 2017 Florian Schmaus, 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.element;
import java.security.SecureRandom;
import java.util.Date;
import java.util.List;
import java.util.Set;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jxmpp.jid.Jid;
/**
* Abstract class that bundles functionality of encrypted OpenPGP content elements ({@link CryptElement},
* {@link SigncryptElement}) together.
*/
public abstract class EncryptedOpenPgpContentElement extends OpenPgpContentElement {
public static final String ELEM_RPAD = "rpad";
private final String rpad;
protected EncryptedOpenPgpContentElement(Set<Jid> to, String rpad, Date timestamp, List<ExtensionElement> payload) {
super(Objects.requireNonNullNorEmpty(
to, "Encrypted OpenPGP content elements must have at least one 'to' attribute."),
timestamp, payload);
this.rpad = Objects.requireNonNull(rpad);
}
protected EncryptedOpenPgpContentElement(Set<Jid> to, List<ExtensionElement> payload) {
super(Objects.requireNonNullNorEmpty(
to, "Encrypted OpenPGP content elements must have at least one 'to' attribute."),
new Date(), payload);
this.rpad = createRandomPadding();
}
private static String createRandomPadding() {
SecureRandom secRan = new SecureRandom();
int len = secRan.nextInt(256);
return StringUtils.randomString(len);
}
@Override
protected void addCommonXml(XmlStringBuilder xml) {
super.addCommonXml(xml);
xml.openElement("rpad").escape(rpad).closeElement("rpad");
}
}

View file

@ -0,0 +1,189 @@
/**
*
* Copyright 2017 Florian Schmaus, 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.element;
import static org.jivesoftware.smack.util.StringUtils.requireNotNullNorEmpty;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.util.MultiMap;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.PacketUtil;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jxmpp.jid.Jid;
import org.jxmpp.util.XmppDateTime;
import org.jxmpp.util.XmppStringUtils;
/**
* This class describes an OpenPGP content element. It defines the elements and fields that OpenPGP content elements
* do have in common.
*/
public abstract class OpenPgpContentElement implements ExtensionElement {
public static final String ELEM_TO = "to";
public static final String ATTR_JID = "jid";
public static final String ELEM_TIME = "time";
public static final String ATTR_STAMP = "stamp";
public static final String ELEM_PAYLOAD = "payload";
private final Set<Jid> to;
private final Date timestamp;
private final MultiMap<String, ExtensionElement> payload;
private String timestampString;
protected OpenPgpContentElement(Set<Jid> to, Date timestamp, List<ExtensionElement> payload) {
this.to = to;
this.timestamp = Objects.requireNonNull(timestamp);
this.payload = new MultiMap<>();
for (ExtensionElement e : payload) {
this.payload.put(XmppStringUtils.generateKey(e.getElementName(), e.getNamespace()), e);
}
}
/**
* Return the set of recipients.
*
* @return recipients.
*/
public final Set<Jid> getTo() {
return to;
}
/**
* Return the timestamp on which the encrypted element has been created.
* This should be checked for sanity by the client.
*
* @return timestamp.
*/
public final Date getTimestamp() {
return timestamp;
}
/**
* Return the payload of the message.
*
* @return payload.
*/
public final List<ExtensionElement> getExtensions() {
synchronized (payload) {
return payload.values();
}
}
/**
* Return a list of all extensions with the given element name <em>and</em> namespace.
* <p>
* Changes to the returned set will update the stanza extensions, if the returned set is not the empty set.
* </p>
*
* @param elementName the element name, must not be null.
* @param namespace the namespace of the element(s), must not be null.
* @return a set of all matching extensions.
*/
public List<ExtensionElement> getExtensions(String elementName, String namespace) {
requireNotNullNorEmpty(elementName, "elementName must not be null or empty");
requireNotNullNorEmpty(namespace, "namespace must not be null or empty");
String key = XmppStringUtils.generateKey(elementName, namespace);
return payload.getAll(key);
}
/**
* Returns the first extension of this stanza that has the given namespace.
* <p>
* When possible, use {@link #getExtension(String,String)} instead.
* </p>
*
* @param namespace the namespace of the extension that is desired.
* @return the stanza extension with the given namespace.
*/
public ExtensionElement getExtension(String namespace) {
return PacketUtil.extensionElementFrom(getExtensions(), null, namespace);
}
/**
* Returns the first extension that matches the specified element name and
* namespace, or <tt>null</tt> if it doesn't exist. If the provided elementName is null,
* only the namespace is matched. Extensions are
* are arbitrary XML elements in standard XMPP stanzas.
*
* @param elementName the XML element name of the extension. (May be null)
* @param namespace the XML element namespace of the extension.
* @param <PE> type of the ExtensionElement.
* @return the extension, or <tt>null</tt> if it doesn't exist.
*/
@SuppressWarnings("unchecked")
public <PE extends ExtensionElement> PE getExtension(String elementName, String namespace) {
if (namespace == null) {
return null;
}
String key = XmppStringUtils.generateKey(elementName, namespace);
ExtensionElement packetExtension;
synchronized (payload) {
packetExtension = payload.getFirst(key);
}
if (packetExtension == null) {
return null;
}
return (PE) packetExtension;
}
@Override
public String getNamespace() {
return OpenPgpElement.NAMESPACE;
}
protected void ensureTimestampStringSet() {
if (timestampString != null) return;
timestampString = XmppDateTime.formatXEP0082Date(timestamp);
}
protected void addCommonXml(XmlStringBuilder xml) {
for (Jid toJid : (to != null ? to : Collections.<Jid>emptySet())) {
xml.halfOpenElement(ELEM_TO).attribute(ATTR_JID, toJid).closeEmptyElement();
}
ensureTimestampStringSet();
xml.halfOpenElement(ELEM_TIME).attribute(ATTR_STAMP, timestampString).closeEmptyElement();
xml.openElement(ELEM_PAYLOAD);
for (ExtensionElement element : payload.values()) {
xml.append(element.toXML(getNamespace()));
}
xml.closeElement(ELEM_PAYLOAD);
}
/**
* Return a {@link ByteArrayInputStream} that reads the bytes of the XML representation of this element.
*
* @return InputStream over xml.
*/
public InputStream toInputStream() {
byte[] encoded = toXML(null).toString().getBytes(Charset.forName("UTF-8"));
return new ByteArrayInputStream(encoded);
}
}

View file

@ -0,0 +1,82 @@
/**
*
* Copyright 2017 Florian Schmaus, 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.element;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smackx.ox.util.Util;
/**
* Class that represents an OpenPGP message.
* The content of this elements text is an base64 encoded , OpenPGP encrypted/signed content element ({@link SignElement},
* {@link SigncryptElement}, {@link CryptElement}).
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html#exchange">
* XEP-0373: §3.1 Exchanging OpenPGP Encrypted and Signed Data</a>
*/
public class OpenPgpElement implements ExtensionElement {
public static final String ELEMENT = "openpgp";
public static final String NAMESPACE = "urn:xmpp:openpgp:0";
// Represents the OpenPGP message, but encoded using base64.
private final String base64EncodedOpenPgpMessage;
public OpenPgpElement(String base64EncodedOpenPgpMessage) {
this.base64EncodedOpenPgpMessage = StringUtils.requireNotNullNorEmpty(base64EncodedOpenPgpMessage,
"base64 encoded message MUST NOT be null nor empty.");
}
public InputStream toInputStream() {
return new ByteArrayInputStream(base64EncodedOpenPgpMessage.getBytes(Util.UTF8));
}
/**
* Return the OpenPGP encrypted payload.
*
* @return OpenPGP encrypted payload.
*/
public String getEncryptedBase64MessageContent() {
return base64EncodedOpenPgpMessage;
}
@Override
public String getElementName() {
return ELEMENT;
}
@Override
public String getNamespace() {
return NAMESPACE;
}
@Override
public XmlStringBuilder toXML(String enclosingNamespace) {
XmlStringBuilder xml = new XmlStringBuilder(this);
xml.rightAngleBracket().append(base64EncodedOpenPgpMessage).closeElement(this);
return xml;
}
public static OpenPgpElement fromStanza(Stanza stanza) {
return stanza.getExtension(ELEMENT, NAMESPACE);
}
}

View file

@ -0,0 +1,121 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.element;
import java.nio.charset.Charset;
import java.util.Date;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.NamedElement;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.XmlStringBuilder;
/**
* Class representing a pubkey element which is used to transport OpenPGP public keys.
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html#announcing-pubkey">
* XEP-0373: §4.1 The OpenPGP Public-Key Data Node</a>
*/
public class PubkeyElement implements ExtensionElement {
public static final String NAMESPACE = OpenPgpElement.NAMESPACE;
public static final String ELEMENT = "pubkey";
public static final String ATTR_DATE = "date";
private final PubkeyDataElement dataElement;
private final Date date;
public PubkeyElement(PubkeyDataElement dataElement, Date date) {
this.dataElement = Objects.requireNonNull(dataElement);
this.date = date;
}
/**
* Return the &lt;data&gt; element containing the base64 encoded public key.
*
* @return data element
*/
public PubkeyDataElement getDataElement() {
return dataElement;
}
/**
* Date on which the key was last modified.
*
* @return last modification date
*/
public Date getDate() {
return date;
}
@Override
public String getNamespace() {
return NAMESPACE;
}
@Override
public String getElementName() {
return ELEMENT;
}
@Override
public XmlStringBuilder toXML(String enclosingNamespace) {
XmlStringBuilder xml = new XmlStringBuilder(this)
.optAttribute(ATTR_DATE, date)
.rightAngleBracket()
.element(getDataElement())
.closeElement(this);
return xml;
}
/**
* Element that contains the base64 encoded public key.
*/
public static class PubkeyDataElement implements NamedElement {
public static final String ELEMENT = "data";
private final byte[] b64Data;
public PubkeyDataElement(byte[] b64Data) {
this.b64Data = Objects.requireNonNull(b64Data);
}
/**
* Base64 encoded public key.
*
* @return public key bytes.
*/
public byte[] getB64Data() {
return b64Data;
}
@Override
public String getElementName() {
return ELEMENT;
}
@Override
public XmlStringBuilder toXML(String enclosingNamespace) {
XmlStringBuilder xml = new XmlStringBuilder(this)
.rightAngleBracket()
.append(new String(b64Data, Charset.forName("UTF-8")))
.closeElement(this);
return xml;
}
}
}

View file

@ -0,0 +1,156 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.element;
import java.util.Collections;
import java.util.Date;
import java.util.Map;
import java.util.TreeMap;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.NamedElement;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.pgpainless.key.OpenPgpV4Fingerprint;
/**
* Class that represents a public key list which was announced to a users metadata node.
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html#announcing-pubkey-list">
* XEP-0373: §4.2 The OpenPGP Public Key Metadata Node</a>
*/
public final class PublicKeysListElement implements ExtensionElement {
public static final String NAMESPACE = OpenPgpElement.NAMESPACE;
public static final String ELEMENT = "public-keys-list";
private final Map<OpenPgpV4Fingerprint, PubkeyMetadataElement> metadata;
private PublicKeysListElement(TreeMap<OpenPgpV4Fingerprint, PubkeyMetadataElement> metadata) {
this.metadata = Collections.unmodifiableMap(Objects.requireNonNull(metadata));
}
public static Builder builder() {
return new Builder();
}
public TreeMap<OpenPgpV4Fingerprint, PubkeyMetadataElement> getMetadata() {
return new TreeMap<>(metadata);
}
@Override
public String getNamespace() {
return NAMESPACE;
}
@Override
public String getElementName() {
return ELEMENT;
}
@Override
public XmlStringBuilder toXML(String enclosingNamespace) {
XmlStringBuilder xml = new XmlStringBuilder(this).rightAngleBracket();
for (PubkeyMetadataElement metadataElement : metadata.values()) {
xml.element(metadataElement);
}
xml.closeElement(this);
return xml;
}
public static final class Builder {
private final TreeMap<OpenPgpV4Fingerprint, PubkeyMetadataElement> metadata = new TreeMap<>();
private Builder() {
// Empty
}
public Builder addMetadata(PubkeyMetadataElement key) {
Objects.requireNonNull(key);
metadata.put(key.getV4Fingerprint(), key);
return this;
}
public PublicKeysListElement build() {
return new PublicKeysListElement(metadata);
}
}
public static class PubkeyMetadataElement implements NamedElement {
public static final String ELEMENT = "pubkey-metadata";
public static final String ATTR_V4_FINGERPRINT = "v4-fingerprint";
public static final String ATTR_DATE = "date";
private final OpenPgpV4Fingerprint v4_fingerprint;
private final Date date;
public PubkeyMetadataElement(OpenPgpV4Fingerprint v4_fingerprint, Date date) {
this.v4_fingerprint = Objects.requireNonNull(v4_fingerprint);
this.date = Objects.requireNonNull(date);
if (v4_fingerprint.length() != 40) {
throw new IllegalArgumentException("OpenPGP v4 fingerprint must be 40 characters long.");
}
}
public OpenPgpV4Fingerprint getV4Fingerprint() {
return v4_fingerprint;
}
public Date getDate() {
return date;
}
@Override
public String getElementName() {
return ELEMENT;
}
@Override
public XmlStringBuilder toXML(String enclosingNamespace) {
XmlStringBuilder xml = new XmlStringBuilder(this)
.attribute(ATTR_V4_FINGERPRINT, getV4Fingerprint())
.attribute(ATTR_DATE, date).closeEmptyElement();
return xml;
}
@Override
public int hashCode() {
return getV4Fingerprint().hashCode() + 3 * getDate().hashCode();
}
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
if (!(o instanceof PubkeyMetadataElement)) {
return false;
}
if (o == this) {
return true;
}
return hashCode() == o.hashCode();
}
}
}

View file

@ -0,0 +1,65 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.element;
import java.nio.charset.Charset;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.XmlStringBuilder;
/**
* This class represents a secretkey element which contains a users OpenPGP secret key.
*
* TODO: Update reflink
* @see <a href="https://xmpp.org/extensions/xep-0373.html#sect-idm46443026813600">
* XEP-0373: §5.2.2 PEP Service Success Response</a>
*/
public class SecretkeyElement implements ExtensionElement {
public static final String NAMESPACE = OpenPgpElement.NAMESPACE;
public static final String ELEMENT = "secretkey";
private final byte[] b64Data;
public SecretkeyElement(byte[] b64Data) {
this.b64Data = Objects.requireNonNull(b64Data);
}
public byte[] getB64Data() {
return b64Data;
}
@Override
public String getNamespace() {
return NAMESPACE;
}
@Override
public String getElementName() {
return ELEMENT;
}
@Override
public XmlStringBuilder toXML(String enclosingNamespace) {
XmlStringBuilder xml = new XmlStringBuilder(this)
.rightAngleBracket()
.append(new String(b64Data, Charset.forName("UTF-8")))
.closeElement(this);
return xml;
}
}

View file

@ -0,0 +1,55 @@
/**
*
* Copyright 2017 Florian Schmaus, 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.element;
import java.util.Date;
import java.util.List;
import java.util.Set;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jxmpp.jid.Jid;
/**
* This class represents an OpenPGP content element which is not encrypted but signed.
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html#exchange">
* XEP-0373: §3.1 Exchanging OpenPGP Encrypted and Signed Data</a>
*/
public class SignElement extends OpenPgpContentElement {
public SignElement(Set<Jid> to, Date timestamp, List<ExtensionElement> payload) {
super(to, timestamp, payload);
}
public static final String ELEMENT = "sign";
@Override
public String getElementName() {
return ELEMENT;
}
@Override
public XmlStringBuilder toXML(String enclosingNamespace) {
XmlStringBuilder xml = new XmlStringBuilder(this).rightAngleBracket();
addCommonXml(xml);
xml.closeElement(this);
return xml;
}
}

View file

@ -0,0 +1,59 @@
/**
*
* Copyright 2017 Florian Schmaus, 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.element;
import java.util.Date;
import java.util.List;
import java.util.Set;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jxmpp.jid.Jid;
/**
* This class represents an OpenPGP content element which is encrypted and signed.
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html#exchange">
* XEP-0373: §3.1 Exchanging OpenPGP Encrypted and Signed Data</a>
*/
public class SigncryptElement extends EncryptedOpenPgpContentElement {
public static final String ELEMENT = "signcrypt";
public SigncryptElement(Set<Jid> to, String rpad, Date timestamp, List<ExtensionElement> payload) {
super(to, rpad, timestamp, payload);
}
public SigncryptElement(Set<Jid> to, List<ExtensionElement> payload) {
super(to, payload);
}
@Override
public String getElementName() {
return ELEMENT;
}
@Override
public XmlStringBuilder toXML(String enclosingNamespace) {
XmlStringBuilder xml = new XmlStringBuilder(this).rightAngleBracket();
addCommonXml(xml);
xml.closeElement(this);
return xml;
}
}

View file

@ -0,0 +1,20 @@
/**
*
* Copyright 2017 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* XML elements for XEP-0373: OpenPGP for XMPP.
*/
package org.jivesoftware.smackx.ox.element;

View file

@ -0,0 +1,29 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.exception;
/**
* Exception that gets thrown if the backup code entered by the user is invalid.
*/
public class InvalidBackupCodeException extends Exception {
private static final long serialVersionUID = 1L;
public InvalidBackupCodeException(String message, Throwable e) {
super(message, e);
}
}

View file

@ -0,0 +1,68 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.exception;
import org.jxmpp.jid.BareJid;
import org.pgpainless.key.OpenPgpV4Fingerprint;
/**
* Exception that gets thrown when an operation is missing an OpenPGP key.
*/
public class MissingOpenPgpKeyException extends Exception {
private static final long serialVersionUID = 1L;
private final BareJid owner;
private final OpenPgpV4Fingerprint fingerprint;
/**
* Create a new {@link MissingOpenPgpKeyException}.
*
* @param owner {@link BareJid} of the keys owner.
* @param fingerprint {@link OpenPgpV4Fingerprint} of the missing key.
*/
public MissingOpenPgpKeyException(BareJid owner, OpenPgpV4Fingerprint fingerprint) {
super("Missing key " + fingerprint.toString() + " for owner " + owner + ".");
this.owner = owner;
this.fingerprint = fingerprint;
}
public MissingOpenPgpKeyException(BareJid owner, OpenPgpV4Fingerprint fingerprint, Throwable e) {
super("Missing key " + fingerprint.toString() + " for owner " + owner + ".", e);
this.owner = owner;
this.fingerprint = fingerprint;
}
/**
* Return the {@link BareJid} of the owner of the missing key.
*
* @return owner of missing key.
*/
public BareJid getOwner() {
return owner;
}
/**
* Return the {@link OpenPgpV4Fingerprint} of the missing key.
*
* @return {@link OpenPgpV4Fingerprint} of the missing key.
*/
public OpenPgpV4Fingerprint getFingerprint() {
return fingerprint;
}
}

View file

@ -0,0 +1,33 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.exception;
import org.jxmpp.jid.BareJid;
import org.pgpainless.key.OpenPgpV4Fingerprint;
/**
* This exception gets thrown, if the user tries to import a key of a user which is lacking a user-id with the users
* jid.
*/
public class MissingUserIdOnKeyException extends Exception {
private static final long serialVersionUID = 1L;
public MissingUserIdOnKeyException(BareJid owner, OpenPgpV4Fingerprint fingerprint) {
super("Key " + fingerprint.toString() + " does not have a user-id of \"xmpp:" + owner.toString() + "\".");
}
}

View file

@ -0,0 +1,26 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.exception;
/**
* This exception gets thrown in case the user tries to restore a secret key backup from PubSub, but no backup has been
* found.
*/
public class NoBackupFoundException extends Exception {
private static final long serialVersionUID = 1L;
}

View file

@ -0,0 +1,20 @@
/**
*
* Copyright 2017 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Exceptions for XEP-0373: OpenPGP for XMPP.
*/
package org.jivesoftware.smackx.ox.exception;

View file

@ -0,0 +1,36 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.listener;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.ox.OpenPgpContact;
import org.jivesoftware.smackx.ox.element.CryptElement;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
public interface CryptElementReceivedListener {
/**
* A {@link CryptElement} has been received and successfully decrypted.
* This listener is intended to be used by implementors of different OX usage profiles.
* @param contact sender of the message
* @param originalMessage original message which contains the {@link CryptElement}.
* @param cryptElement the {@link CryptElement} itself.
* @param metadata metadata about the encryption
*/
void cryptElementReceived(OpenPgpContact contact, Message originalMessage, CryptElement cryptElement, OpenPgpMetadata metadata);
}

View file

@ -0,0 +1,37 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.listener;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.ox.OpenPgpContact;
import org.jivesoftware.smackx.ox.element.SignElement;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
public interface SignElementReceivedListener {
/**
* A {@link SignElement} has been received and successfully been verified.
* This listener is intended to be used by implementors of different OX usage profiles.
* @param contact sender of the message
* @param originalMessage original message containing the {@link SignElement}
* @param signElement the {@link SignElement} itself
* @param metadata metadata about the signing
*/
void signElementReceived(OpenPgpContact contact, Message originalMessage, SignElement signElement, OpenPgpMetadata metadata);
}

View file

@ -0,0 +1,38 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.listener;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.ox.OpenPgpContact;
import org.jivesoftware.smackx.ox.element.SigncryptElement;
import org.jivesoftware.smackx.ox_im.OxMessageListener;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
public interface SigncryptElementReceivedListener {
/**
* A {@link SigncryptElement} has been received and successfully decrypted and verified.
* This listener is intended to be used by implementors of different OX usage profiles. In order to listen for
* OX-IM messages, please refer to the {@link OxMessageListener} instead.
* @param contact sender of the message
* @param originalMessage original message containing the the {@link SigncryptElement}
* @param signcryptElement the {@link SigncryptElement} itself
* @param metadata metadata about the encryption and signing
*/
void signcryptElementReceived(OpenPgpContact contact, Message originalMessage, SigncryptElement signcryptElement, OpenPgpMetadata metadata);
}

View file

@ -0,0 +1,20 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Internal OpenPgpContentElement listeners for XEP-0373: OpenPGP for XMPP.
*/
package org.jivesoftware.smackx.ox.listener;

View file

@ -0,0 +1,20 @@
/**
*
* Copyright 2017 Florian Schmaus, 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Smack API for XEP-0373: OpenPGP for XMPP.
*/
package org.jivesoftware.smackx.ox;

View file

@ -0,0 +1,38 @@
/**
*
* Copyright 2017 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.provider;
import org.jivesoftware.smackx.ox.element.CryptElement;
import org.xmlpull.v1.XmlPullParser;
/**
* {@link org.jivesoftware.smack.provider.ExtensionElementProvider} implementation for the {@link CryptElement}.
*/
public class CryptElementProvider extends OpenPgpContentElementProvider<CryptElement> {
public static final CryptElementProvider INSTANCE = new CryptElementProvider();
@Override
public CryptElement parse(XmlPullParser parser, int initialDepth)
throws Exception {
OpenPgpContentElementData data = parseOpenPgpContentElementData(parser, initialDepth);
return new CryptElement(data.to, data.rpad, data.timestamp, data.payload);
}
}

View file

@ -0,0 +1,163 @@
/**
*
* Copyright 2017 Florian Schmaus, 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.provider;
import static org.xmlpull.v1.XmlPullParser.END_TAG;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import java.io.IOException;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smack.provider.ProviderManager;
import org.jivesoftware.smack.util.PacketParserUtils;
import org.jivesoftware.smackx.ox.element.CryptElement;
import org.jivesoftware.smackx.ox.element.EncryptedOpenPgpContentElement;
import org.jivesoftware.smackx.ox.element.OpenPgpContentElement;
import org.jivesoftware.smackx.ox.element.SignElement;
import org.jivesoftware.smackx.ox.element.SigncryptElement;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.util.XmppDateTime;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
/**
* Abstract {@link ExtensionElementProvider} implementation for the also abstract {@link OpenPgpContentElement}.
*
* @param <O> Specialized subclass of {@link OpenPgpContentElement}.
*/
public abstract class OpenPgpContentElementProvider<O extends OpenPgpContentElement> extends ExtensionElementProvider<O> {
private static final Logger LOGGER = Logger.getLogger(OpenPgpContentElementProvider.class.getName());
public static OpenPgpContentElement parseOpenPgpContentElement(String element)
throws XmlPullParserException, IOException {
XmlPullParser parser = PacketParserUtils.getParserFor(element);
return parseOpenPgpContentElement(parser);
}
public static OpenPgpContentElement parseOpenPgpContentElement(XmlPullParser parser)
throws XmlPullParserException {
try {
switch (parser.getName()) {
case SigncryptElement.ELEMENT:
return SigncryptElementProvider.INSTANCE.parse(parser);
case SignElement.ELEMENT:
return SignElementProvider.INSTANCE.parse(parser);
case CryptElement.ELEMENT:
return CryptElementProvider.INSTANCE.parse(parser);
default: throw new XmlPullParserException("Expected <crypt/>, <sign/> or <signcrypt/> element, " +
"but got neither of them.");
}
} catch (Exception e) {
throw new XmlPullParserException(e.getMessage());
}
}
@Override
public abstract O parse(XmlPullParser parser, int initialDepth) throws Exception;
protected static OpenPgpContentElementData parseOpenPgpContentElementData(XmlPullParser parser, int initialDepth)
throws Exception {
Set<Jid> to = new HashSet<>();
Date timestamp = null;
String rpad = null;
List<ExtensionElement> payload = new LinkedList<>();
outerloop: while (true) {
int tag = parser.next();
String name = parser.getName();
switch (tag) {
case START_TAG:
switch (name) {
case OpenPgpContentElement.ELEM_TIME:
String stamp = parser.getAttributeValue("", OpenPgpContentElement.ATTR_STAMP);
timestamp = XmppDateTime.parseDate(stamp);
break;
case OpenPgpContentElement.ELEM_TO:
String jid = parser.getAttributeValue("", OpenPgpContentElement.ATTR_JID);
to.add(JidCreate.bareFrom(jid));
break;
case EncryptedOpenPgpContentElement.ELEM_RPAD:
rpad = parser.nextText();
break;
case OpenPgpContentElement.ELEM_PAYLOAD:
innerloop: while (true) {
int ptag = parser.next();
String pname = parser.getName();
String pns = parser.getNamespace();
switch (ptag) {
case START_TAG:
ExtensionElementProvider<ExtensionElement> provider =
ProviderManager.getExtensionProvider(pname, pns);
if (provider == null) {
LOGGER.log(Level.INFO, "No provider found for " + pname + " " + pns);
continue innerloop;
}
payload.add(provider.parse(parser));
break;
case END_TAG:
break innerloop;
}
}
break;
}
break;
case END_TAG:
switch (name) {
case CryptElement.ELEMENT:
case SigncryptElement.ELEMENT:
case SignElement.ELEMENT:
break outerloop;
}
break;
}
}
return new OpenPgpContentElementData(to, timestamp, rpad, payload);
}
protected static final class OpenPgpContentElementData {
protected final Set<Jid> to;
protected final Date timestamp;
protected final String rpad;
protected final List<ExtensionElement> payload;
private OpenPgpContentElementData(Set<Jid> to, Date timestamp, String rpad, List<ExtensionElement> payload) {
this.to = to;
this.timestamp = timestamp;
this.rpad = rpad;
this.payload = payload;
}
}
}

View file

@ -0,0 +1,37 @@
/**
*
* Copyright 2017 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.provider;
import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smackx.ox.element.OpenPgpElement;
import org.xmlpull.v1.XmlPullParser;
/**
* {@link ExtensionElementProvider} implementation for the {@link OpenPgpElement}.
*/
public class OpenPgpElementProvider extends ExtensionElementProvider<OpenPgpElement> {
public static final OpenPgpElementProvider TEST_INSTANCE = new OpenPgpElementProvider();
@Override
public OpenPgpElement parse(XmlPullParser parser, int initialDepth) throws Exception {
String base64EncodedOpenPgpMessage = parser.nextText();
return new OpenPgpElement(base64EncodedOpenPgpMessage);
}
}

View file

@ -0,0 +1,56 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.provider;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import java.nio.charset.Charset;
import java.util.Date;
import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smackx.ox.element.PubkeyElement;
import org.jxmpp.util.XmppDateTime;
import org.xmlpull.v1.XmlPullParser;
/**
* {@link ExtensionElementProvider} implementation for the {@link PubkeyElement}.
*/
public class PubkeyElementProvider extends ExtensionElementProvider<PubkeyElement> {
public static final PubkeyElementProvider TEST_INSTANCE = new PubkeyElementProvider();
@Override
public PubkeyElement parse(XmlPullParser parser, int initialDepth) throws Exception {
String dateString = parser.getAttributeValue(null, PubkeyElement.ATTR_DATE);
Date date = dateString != null ? XmppDateTime.parseXEP0082Date(dateString) : null;
while (true) {
int tag = parser.next();
String name = parser.getName();
if (tag == START_TAG) {
switch (name) {
case PubkeyElement.PubkeyDataElement.ELEMENT:
String data = parser.nextText();
if (data != null) {
byte[] bytes = data.getBytes(Charset.forName("UTF-8"));
return new PubkeyElement(new PubkeyElement.PubkeyDataElement(bytes), date);
}
}
}
}
}
}

View file

@ -0,0 +1,67 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.provider;
import static org.xmlpull.v1.XmlPullParser.END_TAG;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import java.util.Date;
import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smackx.ox.element.PublicKeysListElement;
import org.jxmpp.util.XmppDateTime;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.xmlpull.v1.XmlPullParser;
public final class PublicKeysListElementProvider extends ExtensionElementProvider<PublicKeysListElement> {
public static final PublicKeysListElementProvider TEST_INSTANCE = new PublicKeysListElementProvider();
@Override
public PublicKeysListElement parse(XmlPullParser parser, int initialDepth) throws Exception {
PublicKeysListElement.Builder builder = PublicKeysListElement.builder();
while (true) {
int tag = parser.nextTag();
String name = parser.getName();
switch (tag) {
case START_TAG:
if (PublicKeysListElement.PubkeyMetadataElement.ELEMENT.equals(name)) {
String finger = parser.getAttributeValue(null,
PublicKeysListElement.PubkeyMetadataElement.ATTR_V4_FINGERPRINT);
String dt = parser.getAttributeValue(null,
PublicKeysListElement.PubkeyMetadataElement.ATTR_DATE);
OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(finger);
Date date = XmppDateTime.parseXEP0082Date(dt);
builder.addMetadata(new PublicKeysListElement.PubkeyMetadataElement(fingerprint, date));
}
break;
case END_TAG:
if (name.equals(PublicKeysListElement.ELEMENT)) {
return builder.build();
}
}
}
}
}

View file

@ -0,0 +1,38 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.provider;
import java.nio.charset.Charset;
import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smackx.ox.element.SecretkeyElement;
import org.xmlpull.v1.XmlPullParser;
/**
* {@link ExtensionElementProvider} implementation for the {@link SecretkeyElement}.
*/
public class SecretkeyElementProvider extends ExtensionElementProvider<SecretkeyElement> {
public static final SecretkeyElementProvider TEST_INSTANCE = new SecretkeyElementProvider();
@Override
public SecretkeyElement parse(XmlPullParser parser, int initialDepth) throws Exception {
String data = parser.nextText();
return new SecretkeyElement(data.getBytes(Charset.forName("UTF-8")));
}
}

View file

@ -0,0 +1,45 @@
/**
*
* Copyright 2017 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.provider;
import java.util.logging.Logger;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.ox.element.SignElement;
import org.xmlpull.v1.XmlPullParser;
/**
* {@link org.jivesoftware.smack.provider.ExtensionElementProvider} implementation for the {@link SignElement}.
*/
public class SignElementProvider extends OpenPgpContentElementProvider<SignElement> {
private static final Logger LOGGER = Logger.getLogger(SigncryptElementProvider.class.getName());
public static final SignElementProvider INSTANCE = new SignElementProvider();
@Override
public SignElement parse(XmlPullParser parser, int initialDepth) throws Exception {
OpenPgpContentElementData data = parseOpenPgpContentElementData(parser, initialDepth);
if (StringUtils.isNotEmpty(data.rpad)) {
LOGGER.warning("Ignoring rpad in XEP-0373 <sign/> element");
}
return new SignElement(data.to, data.timestamp, data.payload);
}
}

View file

@ -0,0 +1,36 @@
/**
*
* Copyright 2017 Florian Schmaus, 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.provider;
import org.jivesoftware.smackx.ox.element.SigncryptElement;
import org.xmlpull.v1.XmlPullParser;
/**
* {@link org.jivesoftware.smack.provider.ExtensionElementProvider} implementation for the {@link SigncryptElement}.
*/
public class SigncryptElementProvider extends OpenPgpContentElementProvider<SigncryptElement> {
public static final SigncryptElementProvider INSTANCE = new SigncryptElementProvider();
@Override
public SigncryptElement parse(XmlPullParser parser, int initialDepth) throws Exception {
OpenPgpContentElementData data = parseOpenPgpContentElementData(parser, initialDepth);
return new SigncryptElement(data.to, data.rpad, data.timestamp, data.payload);
}
}

View file

@ -0,0 +1,20 @@
/**
*
* Copyright 2017 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Providers for XEP-0373: OpenPGP for XMPP.
*/
package org.jivesoftware.smackx.ox.provider;

View file

@ -0,0 +1,45 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.selection_strategy;
import java.util.Date;
import java.util.Map;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.selection.keyring.PublicKeyRingSelectionStrategy;
import org.pgpainless.key.selection.keyring.SecretKeyRingSelectionStrategy;
public class AnnouncedKeys {
public static class PubKeyRingSelectionStrategy extends PublicKeyRingSelectionStrategy<Map<OpenPgpV4Fingerprint, Date>> {
@Override
public boolean accept(Map<OpenPgpV4Fingerprint, Date> announcedKeys, PGPPublicKeyRing publicKeys) {
return announcedKeys.keySet().contains(new OpenPgpV4Fingerprint(publicKeys));
}
}
public static class SecKeyRingSelectionStrategy extends SecretKeyRingSelectionStrategy<Map<OpenPgpV4Fingerprint, Date>> {
@Override
public boolean accept(Map<OpenPgpV4Fingerprint, Date> announcedKeys, PGPSecretKeyRing secretKeys) {
return announcedKeys.keySet().contains(new OpenPgpV4Fingerprint(secretKeys));
}
}
}

View file

@ -0,0 +1,58 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.selection_strategy;
import java.util.Iterator;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.jxmpp.jid.BareJid;
import org.pgpainless.key.selection.keyring.PublicKeyRingSelectionStrategy;
import org.pgpainless.key.selection.keyring.SecretKeyRingSelectionStrategy;
public class BareJidUserId {
public static class PubRingSelectionStrategy extends PublicKeyRingSelectionStrategy<BareJid> {
@Override
public boolean accept(BareJid jid, PGPPublicKeyRing ring) {
Iterator<String> userIds = ring.getPublicKey().getUserIDs();
while (userIds.hasNext()) {
String userId = userIds.next();
if (userId.equals("xmpp:" + jid.toString())) {
return true;
}
}
return false;
}
}
public static class SecRingSelectionStrategy extends SecretKeyRingSelectionStrategy<BareJid> {
@Override
public boolean accept(BareJid jid, PGPSecretKeyRing ring) {
Iterator<String> userIds = ring.getPublicKey().getUserIDs();
while (userIds.hasNext()) {
String userId = userIds.next();
if (userId.equals("xmpp:" + jid.toString())) {
return true;
}
}
return false;
}
}
}

View file

@ -0,0 +1,20 @@
/**
*
* Copyright 2017 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Providers for XEP-0373: OpenPGP for XMPP using Bouncycastle.
*/
package org.jivesoftware.smackx.ox.selection_strategy;

View file

@ -0,0 +1,258 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.store.abstr;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smackx.ox.exception.MissingUserIdOnKeyException;
import org.jivesoftware.smackx.ox.selection_strategy.BareJidUserId;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpKeyStore;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.jxmpp.jid.BareJid;
import org.pgpainless.PGPainless;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.collection.PGPKeyRing;
import org.pgpainless.util.BCUtil;
public abstract class AbstractOpenPgpKeyStore implements OpenPgpKeyStore {
private static final Logger LOGGER = Logger.getLogger(AbstractOpenPgpKeyStore.class.getName());
protected Map<BareJid, PGPPublicKeyRingCollection> publicKeyRingCollections = new HashMap<>();
protected Map<BareJid, PGPSecretKeyRingCollection> secretKeyRingCollections = new HashMap<>();
protected Map<BareJid, Map<OpenPgpV4Fingerprint, Date>> keyFetchDates = new HashMap<>();
/**
* Read a {@link PGPPublicKeyRingCollection} from local storage.
* This method returns null, if no keys were found.
*
* @param owner owner of the keys
* @return public keys
*
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
*/
protected abstract PGPPublicKeyRingCollection readPublicKeysOf(BareJid owner) throws IOException, PGPException;
/**
* Write the {@link PGPPublicKeyRingCollection} of a user to local storage.
*
* @param owner owner of the keys
* @param publicKeys keys
*
* @throws IOException IO is dangerous
*/
protected abstract void writePublicKeysOf(BareJid owner, PGPPublicKeyRingCollection publicKeys) throws IOException;
/**
* Read a {@link PGPSecretKeyRingCollection} from local storage.
* This method returns null, if no keys were found.
*
* @param owner owner of the keys
* @return secret keys
*
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
*/
protected abstract PGPSecretKeyRingCollection readSecretKeysOf(BareJid owner) throws IOException, PGPException;
/**
* Write the {@link PGPSecretKeyRingCollection} of a user to local storage.
*
* @param owner owner of the keys
* @param secretKeys secret keys
*
* @throws IOException IO is dangerous
*/
protected abstract void writeSecretKeysOf(BareJid owner, PGPSecretKeyRingCollection secretKeys) throws IOException;
/**
* Read the key fetch dates for a users keys from local storage.
*
* @param owner owner
* @return fetch dates for the owners keys
*
* @throws IOException IO is dangerous
*/
protected abstract Map<OpenPgpV4Fingerprint, Date> readKeyFetchDates(BareJid owner) throws IOException;
/**
* Write the key fetch dates for a users keys to local storage.
*
* @param owner owner
* @param dates fetch dates for the owners keys
*
* @throws IOException IO is dangerous
*/
protected abstract void writeKeyFetchDates(BareJid owner, Map<OpenPgpV4Fingerprint, Date> dates) throws IOException;
@Override
public Map<OpenPgpV4Fingerprint, Date> getPublicKeyFetchDates(BareJid contact) throws IOException {
Map<OpenPgpV4Fingerprint, Date> dates = keyFetchDates.get(contact);
if (dates == null) {
dates = readKeyFetchDates(contact);
keyFetchDates.put(contact, dates);
}
return dates;
}
@Override
public void setPublicKeyFetchDates(BareJid contact, Map<OpenPgpV4Fingerprint, Date> dates) throws IOException {
keyFetchDates.put(contact, dates);
writeKeyFetchDates(contact, dates);
}
@Override
public PGPPublicKeyRingCollection getPublicKeysOf(BareJid owner) throws IOException, PGPException {
PGPPublicKeyRingCollection keys = publicKeyRingCollections.get(owner);
if (keys == null) {
keys = readPublicKeysOf(owner);
if (keys != null) {
publicKeyRingCollections.put(owner, keys);
}
}
return keys;
}
@Override
public PGPSecretKeyRingCollection getSecretKeysOf(BareJid owner) throws IOException, PGPException {
PGPSecretKeyRingCollection keys = secretKeyRingCollections.get(owner);
if (keys == null) {
keys = readSecretKeysOf(owner);
if (keys != null) {
secretKeyRingCollections.put(owner, keys);
}
}
return keys;
}
@Override
public void importSecretKey(BareJid owner, PGPSecretKeyRing secretKeys)
throws IOException, PGPException, MissingUserIdOnKeyException {
if (!new BareJidUserId.SecRingSelectionStrategy().accept(owner, secretKeys)) {
throw new MissingUserIdOnKeyException(owner, new OpenPgpV4Fingerprint(secretKeys));
}
PGPSecretKeyRing importKeys = BCUtil.removeUnassociatedKeysFromKeyRing(secretKeys, secretKeys.getPublicKey());
PGPSecretKeyRingCollection secretKeyRings = getSecretKeysOf(owner);
try {
if (secretKeyRings != null) {
secretKeyRings = PGPSecretKeyRingCollection.addSecretKeyRing(secretKeyRings, importKeys);
} else {
secretKeyRings = BCUtil.keyRingsToKeyRingCollection(importKeys);
}
} catch (IllegalArgumentException e) {
LOGGER.log(Level.INFO, "Skipping secret key ring " + Long.toHexString(importKeys.getPublicKey().getKeyID()) +
" as it is already in the key ring of " + owner.toString());
}
this.secretKeyRingCollections.put(owner, secretKeyRings);
writeSecretKeysOf(owner, secretKeyRings);
}
@Override
public void importPublicKey(BareJid owner, PGPPublicKeyRing publicKeys) throws IOException, PGPException, MissingUserIdOnKeyException {
if (!new BareJidUserId.PubRingSelectionStrategy().accept(owner, publicKeys)) {
throw new MissingUserIdOnKeyException(owner, new OpenPgpV4Fingerprint(publicKeys));
}
PGPPublicKeyRing importKeys = BCUtil.removeUnassociatedKeysFromKeyRing(publicKeys, publicKeys.getPublicKey());
PGPPublicKeyRingCollection publicKeyRings = getPublicKeysOf(owner);
try {
if (publicKeyRings != null) {
publicKeyRings = PGPPublicKeyRingCollection.addPublicKeyRing(publicKeyRings, importKeys);
} else {
publicKeyRings = BCUtil.keyRingsToKeyRingCollection(importKeys);
}
} catch (IllegalArgumentException e) {
LOGGER.log(Level.INFO, "Skipping public key ring " + Long.toHexString(importKeys.getPublicKey().getKeyID()) +
" as it is already in the key ring of " + owner.toString());
}
this.publicKeyRingCollections.put(owner, publicKeyRings);
writePublicKeysOf(owner, publicKeyRings);
}
@Override
public PGPPublicKeyRing getPublicKeyRing(BareJid owner, OpenPgpV4Fingerprint fingerprint) throws IOException, PGPException {
PGPPublicKeyRingCollection publicKeyRings = getPublicKeysOf(owner);
if (publicKeyRings != null) {
return publicKeyRings.getPublicKeyRing(fingerprint.getKeyId());
}
return null;
}
@Override
public PGPSecretKeyRing getSecretKeyRing(BareJid owner, OpenPgpV4Fingerprint fingerprint) throws IOException, PGPException {
PGPSecretKeyRingCollection secretKeyRings = getSecretKeysOf(owner);
if (secretKeyRings != null) {
return secretKeyRings.getSecretKeyRing(fingerprint.getKeyId());
}
return null;
}
@Override
public void deletePublicKeyRing(BareJid owner, OpenPgpV4Fingerprint fingerprint) throws IOException, PGPException {
PGPPublicKeyRingCollection publicKeyRings = getPublicKeysOf(owner);
if (publicKeyRings.contains(fingerprint.getKeyId())) {
publicKeyRings = PGPPublicKeyRingCollection.removePublicKeyRing(publicKeyRings, publicKeyRings.getPublicKeyRing(fingerprint.getKeyId()));
if (!publicKeyRings.iterator().hasNext()) {
publicKeyRings = null;
}
this.publicKeyRingCollections.put(owner, publicKeyRings);
writePublicKeysOf(owner, publicKeyRings);
}
}
@Override
public void deleteSecretKeyRing(BareJid owner, OpenPgpV4Fingerprint fingerprint) throws IOException, PGPException {
PGPSecretKeyRingCollection secretKeyRings = getSecretKeysOf(owner);
if (secretKeyRings.contains(fingerprint.getKeyId())) {
secretKeyRings = PGPSecretKeyRingCollection.removeSecretKeyRing(secretKeyRings, secretKeyRings.getSecretKeyRing(fingerprint.getKeyId()));
if (!secretKeyRings.iterator().hasNext()) {
secretKeyRings = null;
}
this.secretKeyRingCollections.put(owner, secretKeyRings);
writeSecretKeysOf(owner, secretKeyRings);
}
}
@Override
public PGPKeyRing generateKeyRing(BareJid owner)
throws PGPException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException {
return PGPainless.generateKeyRing().simpleEcKeyRing("xmpp:" + owner.toString());
}
}

View file

@ -0,0 +1,68 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.store.abstr;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpMetadataStore;
import org.jxmpp.jid.BareJid;
import org.pgpainless.key.OpenPgpV4Fingerprint;
public abstract class AbstractOpenPgpMetadataStore implements OpenPgpMetadataStore {
private final Map<BareJid, Map<OpenPgpV4Fingerprint, Date>> announcedFingerprints = new HashMap<>();
@Override
public Map<OpenPgpV4Fingerprint, Date> getAnnouncedFingerprintsOf(BareJid contact) throws IOException {
Map<OpenPgpV4Fingerprint, Date> fingerprints = announcedFingerprints.get(contact);
if (fingerprints == null) {
fingerprints = readAnnouncedFingerprintsOf(contact);
announcedFingerprints.put(contact, fingerprints);
}
return fingerprints;
}
@Override
public void setAnnouncedFingerprintsOf(BareJid contact, Map<OpenPgpV4Fingerprint, Date> data) throws IOException {
announcedFingerprints.put(contact, data);
writeAnnouncedFingerprintsOf(contact, data);
}
/**
* Read the fingerprints and modification dates of announced keys of a user from local storage.
*
* @param contact contact
* @return contacts announced key fingerprints and latest modification dates
*
* @throws IOException IO is dangerous
*/
protected abstract Map<OpenPgpV4Fingerprint, Date> readAnnouncedFingerprintsOf(BareJid contact) throws IOException;
/**
* Write the fingerprints and modification dates of announced keys of a user to local storage.
*
* @param contact contact
* @param metadata announced key fingerprints and latest modification dates
*
* @throws IOException IO is dangerous
*/
protected abstract void writeAnnouncedFingerprintsOf(BareJid contact, Map<OpenPgpV4Fingerprint, Date> metadata) throws IOException;
}

View file

@ -0,0 +1,177 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.store.abstr;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Observable;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smackx.ox.OpenPgpContact;
import org.jivesoftware.smackx.ox.callback.SecretKeyPassphraseCallback;
import org.jivesoftware.smackx.ox.exception.MissingUserIdOnKeyException;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpKeyStore;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpMetadataStore;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpStore;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpTrustStore;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.jxmpp.jid.BareJid;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.collection.PGPKeyRing;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.key.protection.UnprotectedKeysProtector;
public abstract class AbstractOpenPgpStore extends Observable implements OpenPgpStore {
protected final OpenPgpKeyStore keyStore;
protected final OpenPgpMetadataStore metadataStore;
protected final OpenPgpTrustStore trustStore;
protected SecretKeyPassphraseCallback secretKeyPassphraseCallback;
protected SecretKeyRingProtector unlocker = new UnprotectedKeysProtector();
protected final Map<BareJid, OpenPgpContact> contacts = new HashMap<>();
@Override
public void deletePublicKeyRing(BareJid owner, OpenPgpV4Fingerprint fingerprint) throws IOException, PGPException {
keyStore.deletePublicKeyRing(owner, fingerprint);
}
@Override
public void deleteSecretKeyRing(BareJid owner, OpenPgpV4Fingerprint fingerprint) throws IOException, PGPException {
keyStore.deleteSecretKeyRing(owner, fingerprint);
}
protected AbstractOpenPgpStore(OpenPgpKeyStore keyStore,
OpenPgpMetadataStore metadataStore,
OpenPgpTrustStore trustStore) {
this.keyStore = Objects.requireNonNull(keyStore);
this.metadataStore = Objects.requireNonNull(metadataStore);
this.trustStore = Objects.requireNonNull(trustStore);
}
@Override
public OpenPgpContact getOpenPgpContact(BareJid jid) {
OpenPgpContact contact = contacts.get(jid);
if (contact == null) {
contact = new OpenPgpContact(jid, this);
contacts.put(jid, contact);
}
return contact;
}
@Override
public void setKeyRingProtector(SecretKeyRingProtector protector) {
this.unlocker = protector;
}
@Override
public SecretKeyRingProtector getKeyRingProtector() {
return unlocker;
}
@Override
public void setSecretKeyPassphraseCallback(SecretKeyPassphraseCallback callback) {
this.secretKeyPassphraseCallback = callback;
}
/*
OpenPgpKeyStore
*/
@Override
public PGPPublicKeyRingCollection getPublicKeysOf(BareJid owner) throws IOException, PGPException {
return keyStore.getPublicKeysOf(owner);
}
@Override
public PGPSecretKeyRingCollection getSecretKeysOf(BareJid owner) throws IOException, PGPException {
return keyStore.getSecretKeysOf(owner);
}
@Override
public PGPPublicKeyRing getPublicKeyRing(BareJid owner, OpenPgpV4Fingerprint fingerprint) throws IOException, PGPException {
return keyStore.getPublicKeyRing(owner, fingerprint);
}
@Override
public PGPSecretKeyRing getSecretKeyRing(BareJid owner, OpenPgpV4Fingerprint fingerprint) throws IOException, PGPException {
return keyStore.getSecretKeyRing(owner, fingerprint);
}
@Override
public PGPKeyRing generateKeyRing(BareJid owner) throws PGPException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException {
return keyStore.generateKeyRing(owner);
}
@Override
public void importSecretKey(BareJid owner, PGPSecretKeyRing secretKeys) throws IOException, PGPException, MissingUserIdOnKeyException {
keyStore.importSecretKey(owner, secretKeys);
}
@Override
public void importPublicKey(BareJid owner, PGPPublicKeyRing publicKeys) throws IOException, PGPException, MissingUserIdOnKeyException {
keyStore.importPublicKey(owner, publicKeys);
}
@Override
public Map<OpenPgpV4Fingerprint, Date> getPublicKeyFetchDates(BareJid contact) throws IOException {
return keyStore.getPublicKeyFetchDates(contact);
}
@Override
public void setPublicKeyFetchDates(BareJid contact, Map<OpenPgpV4Fingerprint, Date> dates) throws IOException {
keyStore.setPublicKeyFetchDates(contact, dates);
}
/*
OpenPgpMetadataStore
*/
@Override
public Map<OpenPgpV4Fingerprint, Date> getAnnouncedFingerprintsOf(BareJid contact) throws IOException {
return metadataStore.getAnnouncedFingerprintsOf(contact);
}
@Override
public void setAnnouncedFingerprintsOf(BareJid contact, Map<OpenPgpV4Fingerprint, Date> data) throws IOException {
metadataStore.setAnnouncedFingerprintsOf(contact, data);
}
/*
OpenPgpTrustStore
*/
@Override
public Trust getTrust(BareJid owner, OpenPgpV4Fingerprint fingerprint) throws IOException {
return trustStore.getTrust(owner, fingerprint);
}
@Override
public void setTrust(BareJid owner, OpenPgpV4Fingerprint fingerprint, Trust trust) throws IOException {
trustStore.setTrust(owner, fingerprint, trust);
}
}

View file

@ -0,0 +1,91 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.store.abstr;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpTrustStore;
import org.jxmpp.jid.BareJid;
import org.pgpainless.key.OpenPgpV4Fingerprint;
public abstract class AbstractOpenPgpTrustStore implements OpenPgpTrustStore {
private final Map<BareJid, Map<OpenPgpV4Fingerprint, Trust>> trustCache = new HashMap<>();
/**
* Read the trust record for the key with fingerprint {@code fingerprint} of user {@code owner} from local storage.
* This method returns {@link Trust#undecided} in case that no trust record has been found.
*
* @param owner owner of the key
* @param fingerprint fingerprint of the key
* @return trust state of the key
*
* @throws IOException IO is dangerous
*/
protected abstract Trust readTrust(BareJid owner, OpenPgpV4Fingerprint fingerprint) throws IOException;
/**
* Write the trust record for the key with fingerprint {@code fingerprint} of user {@code owner} to local storage.
*
* @param owner owner of the key
* @param fingerprint fingerprint of the key
* @param trust trust state of the key
*
* @throws IOException IO is dangerous
*/
protected abstract void writeTrust(BareJid owner, OpenPgpV4Fingerprint fingerprint, Trust trust) throws IOException;
@Override
public Trust getTrust(BareJid owner, OpenPgpV4Fingerprint fingerprint) throws IOException {
Trust trust;
Map<OpenPgpV4Fingerprint, Trust> trustMap = trustCache.get(owner);
if (trustMap != null) {
trust = trustMap.get(fingerprint);
if (trust != null) {
return trust;
}
} else {
trustMap = new HashMap<>();
trustCache.put(owner, trustMap);
}
trust = readTrust(owner, fingerprint);
trustMap.put(fingerprint, trust);
return trust;
}
@Override
public void setTrust(BareJid owner, OpenPgpV4Fingerprint fingerprint, Trust trust) throws IOException {
Map<OpenPgpV4Fingerprint, Trust> trustMap = trustCache.get(owner);
if (trustMap == null) {
trustMap = new HashMap<>();
trustCache.put(owner, trustMap);
}
if (trustMap.get(fingerprint) == trust) {
return;
}
trustMap.put(fingerprint, trust);
writeTrust(owner, fingerprint, trust);
}
}

View file

@ -0,0 +1,20 @@
/**
*
* Copyright 2017 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Abstract OpenPGP store implementations.
*/
package org.jivesoftware.smackx.ox.store.abstr;

View file

@ -0,0 +1,177 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.store.definition;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Date;
import java.util.Map;
import org.jivesoftware.smackx.ox.exception.MissingUserIdOnKeyException;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.jxmpp.jid.BareJid;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.collection.PGPKeyRing;
public interface OpenPgpKeyStore {
/**
* Return the {@link PGPPublicKeyRingCollection} containing all public keys of {@code owner} that are locally
* available.
* This method might return null.
*
* @param owner {@link BareJid} of the user we want to get keys from.
* @return {@link PGPPublicKeyRingCollection} of the user.
*
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
*/
PGPPublicKeyRingCollection getPublicKeysOf(BareJid owner) throws IOException, PGPException;
/**
* Return the {@link PGPSecretKeyRingCollection} containing all secret keys of {@code owner} which are locally
* available.
* This method might return null.
*
* @param owner {@link BareJid} of the user we want to get keys from.
* @return {@link PGPSecretKeyRingCollection} of the user.
*
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
*/
PGPSecretKeyRingCollection getSecretKeysOf(BareJid owner) throws IOException, PGPException;
/**
* Return the {@link PGPPublicKeyRing} of {@code owner} which contains the key described by {@code fingerprint}.
* This method might return null.
*
* @param owner {@link BareJid} of the keys owner
* @param fingerprint {@link OpenPgpV4Fingerprint} of a key contained in the key ring
* @return {@link PGPPublicKeyRing} which contains the key described by {@code fingerprint}.
*
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
*/
PGPPublicKeyRing getPublicKeyRing(BareJid owner, OpenPgpV4Fingerprint fingerprint) throws IOException, PGPException;
/**
* Return the {@link PGPSecretKeyRing} of {@code owner} which contains the key described by {@code fingerprint}.
* This method might return null.
*
* @param owner {@link BareJid} of the keys owner
* @param fingerprint {@link OpenPgpV4Fingerprint} of a key contained in the key ring
* @return {@link PGPSecretKeyRing} which contains the key described by {@code fingerprint}.
*
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
*/
PGPSecretKeyRing getSecretKeyRing(BareJid owner, OpenPgpV4Fingerprint fingerprint) throws IOException, PGPException;
/**
* Remove a {@link PGPPublicKeyRing} which contains the key described by {@code fingerprint} from the
* {@link PGPPublicKeyRingCollection} of {@code owner}.
*
* @param owner owner of the key ring
* @param fingerprint fingerprint of the key whose key ring will be removed.
*
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
*/
void deletePublicKeyRing(BareJid owner, OpenPgpV4Fingerprint fingerprint) throws IOException, PGPException;
/**
* Remove a {@link PGPSecretKeyRing} which contains the key described by {@code fingerprint} from the
* {@link PGPSecretKeyRingCollection} of {@code owner}.
*
* @param owner owner of the key ring
* @param fingerprint fingerprint of the key whose key ring will be removed.
*
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
*/
void deleteSecretKeyRing(BareJid owner, OpenPgpV4Fingerprint fingerprint) throws IOException, PGPException;
/**
* Generate a new {@link PGPKeyRing} for {@code owner}.
* The key will have a user-id containing the users {@link BareJid} (eg. "xmpp:juliet@capulet.lit").
* This method MUST NOT return null.
*
* @param owner owner of the key ring.
* @return key ring
*
* @throws PGPException PGP is brittle
* @throws NoSuchAlgorithmException in case there is no {@link java.security.Provider} registered for the used
* OpenPGP algorithms.
* @throws NoSuchProviderException in case there is no suitable {@link java.security.Provider} registered.
* @throws InvalidAlgorithmParameterException in case an invalid algorithms configuration is used.
*/
PGPKeyRing generateKeyRing(BareJid owner) throws PGPException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException;
/**
* Import a {@link PGPSecretKeyRing} of {@code owner}.
* In case the key ring is already available locally, the keys are skipped.
*
* @param owner owner of the keys
* @param secretKeys secret keys
*
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
* @throws MissingUserIdOnKeyException in case the secret keys are lacking a user-id with the owners jid.
*/
void importSecretKey(BareJid owner, PGPSecretKeyRing secretKeys) throws IOException, PGPException, MissingUserIdOnKeyException;
/**
* Import a {@link PGPPublicKeyRing} of {@code owner}.
* In case the key ring is already available locally, the keys are skipped.
*
* @param owner owner of the keys
* @param publicKeys public keys
*
* @throws IOException IO is dangerous
* @throws PGPException PGP is brittle
* @throws MissingUserIdOnKeyException in case the public keys are lacking a user-id with the owners jid.
*/
void importPublicKey(BareJid owner, PGPPublicKeyRing publicKeys) throws IOException, PGPException, MissingUserIdOnKeyException;
/**
* Return the last date on which keys of {@code contact} were fetched from PubSub.
* This method MUST NOT return null.
*
* @param contact contact in which we are interested.
* @return dates of last key fetching.
*
* @throws IOException IO is dangerous
*/
Map<OpenPgpV4Fingerprint, Date> getPublicKeyFetchDates(BareJid contact) throws IOException;
/**
* Set the last date on which keys of {@code contact} were fetched from PubSub.
*
* @param contact contact in which we are interested.
* @param dates dates of last key fetching.
*
* @throws IOException IO is dangerous
*/
void setPublicKeyFetchDates(BareJid contact, Map<OpenPgpV4Fingerprint, Date> dates) throws IOException;
}

View file

@ -0,0 +1,49 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.store.definition;
import java.io.IOException;
import java.util.Date;
import java.util.Map;
import org.jxmpp.jid.BareJid;
import org.pgpainless.key.OpenPgpV4Fingerprint;
public interface OpenPgpMetadataStore {
/**
* Return a {@link Map} containing all announced fingerprints of a contact, as well as the dates on which they were
* last modified by {@code contact}.
* This method MUST NOT return null.
*
* @param contact contact in which we are interested.
* @return announced fingerprints
*
* @throws IOException IO is dangerous
*/
Map<OpenPgpV4Fingerprint, Date> getAnnouncedFingerprintsOf(BareJid contact) throws IOException;
/**
* Store a contacts announced fingerprints and dates of last modification.
*
* @param contact contact in which we are interested.
* @param data {@link Map} containing the contacts announced fingerprints and dates of last modification.
*
* @throws IOException IO is dangerous
*/
void setAnnouncedFingerprintsOf(BareJid contact, Map<OpenPgpV4Fingerprint, Date> data) throws IOException;
}

View file

@ -0,0 +1,59 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.store.definition;
import org.jivesoftware.smackx.ox.OpenPgpContact;
import org.jivesoftware.smackx.ox.callback.SecretKeyPassphraseCallback;
import org.jxmpp.jid.BareJid;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.key.protection.UnprotectedKeysProtector;
public interface OpenPgpStore extends OpenPgpKeyStore, OpenPgpMetadataStore, OpenPgpTrustStore {
/**
* Return an {@link OpenPgpContact} for a contacts jid.
*
* @param contactsJid {@link BareJid} of the contact.
* @return {@link OpenPgpContact} object of the contact.
*/
OpenPgpContact getOpenPgpContact(BareJid contactsJid);
/**
* Set a {@link SecretKeyRingProtector} which is used to decrypt password protected secret keys.
*
* @param unlocker unlocker which unlocks encrypted secret keys.
*/
void setKeyRingProtector(SecretKeyRingProtector unlocker);
/**
* Return the {@link SecretKeyRingProtector} which is used to decrypt password protected secret keys.
* In case no {@link SecretKeyRingProtector} has been set, this method MUST return an {@link UnprotectedKeysProtector}.
*
* @return secret key unlocker.
*/
SecretKeyRingProtector getKeyRingProtector();
/**
* Set a {@link SecretKeyPassphraseCallback} which is called in case we stumble over a secret key for which we have
* no passphrase.
*
* @param callback callback. MUST NOT be null.
*/
void setSecretKeyPassphraseCallback(SecretKeyPassphraseCallback callback);
}

View file

@ -0,0 +1,64 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.store.definition;
import java.io.IOException;
import org.jxmpp.jid.BareJid;
import org.pgpainless.key.OpenPgpV4Fingerprint;
public interface OpenPgpTrustStore {
/**
* Return the {@link Trust} state of {@code owner}s key with fingerprint {@code fingerprint}.
* The trust state describes, whether the user trusts a certain key of a contact.
* If no {@link Trust} record has been found, this method MUST return not null, nut {@link Trust#undecided}.
*
* @param owner owner of the key
* @param fingerprint fingerprint of the key
* @return trust state
*
* @throws IOException IO is dangerous
*/
Trust getTrust(BareJid owner, OpenPgpV4Fingerprint fingerprint) throws IOException;
/**
* Store the {@link Trust} state of {@code owner}s key with fingerprint {@code fingerprint}.
*
* @param owner owner of the key
* @param fingerprint fingerprint of the key
* @param trust trust record
*
* @throws IOException IO is dangerous
*/
void setTrust(BareJid owner, OpenPgpV4Fingerprint fingerprint, Trust trust) throws IOException;
enum Trust {
/**
* The user explicitly trusts the key.
*/
trusted,
/**
* The user explicitly distrusts the key.
*/
untrusted,
/**
* The user didn't yet describe, whether to trust the key or not.
*/
undecided
}
}

View file

@ -0,0 +1,20 @@
/**
*
* Copyright 2017 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* OpenPgp store class definitions.
*/
package org.jivesoftware.smackx.ox.store.definition;

View file

@ -0,0 +1,183 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.store.filebased;
import static org.jivesoftware.smackx.ox.util.FileUtils.prepareFileInputStream;
import static org.jivesoftware.smackx.ox.util.FileUtils.prepareFileOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;
import java.util.Map;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smackx.ox.store.abstr.AbstractOpenPgpKeyStore;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpKeyStore;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.jxmpp.jid.BareJid;
import org.pgpainless.PGPainless;
import org.pgpainless.key.OpenPgpV4Fingerprint;
/**
* This class is an implementation of the {@link OpenPgpKeyStore}, which stores keys in a file structure.
* The keys are stored in the following directory structure:
*
* <pre>
* {@code
* <basePath>/
* <userjid@server.tld>/
* pubring.pkr // public keys of the user/contact
* secring.pkr // secret keys of the user
* fetchDates.list // date of the last time we fetched the users keys
* }
* </pre>
*/
public class FileBasedOpenPgpKeyStore extends AbstractOpenPgpKeyStore {
private static final String PUB_RING = "pubring.pkr";
private static final String SEC_RING = "secring.skr";
private static final String FETCH_DATES = "fetchDates.list";
private final File basePath;
public FileBasedOpenPgpKeyStore(File basePath) {
this.basePath = Objects.requireNonNull(basePath);
}
@Override
public void writePublicKeysOf(BareJid owner, PGPPublicKeyRingCollection publicKeys) throws IOException {
File file = getPublicKeyRingPath(owner);
if (publicKeys == null) {
if (!file.exists()) {
return;
}
if (!file.delete()) {
throw new IOException("Could not delete file " + file.getAbsolutePath());
}
return;
}
OutputStream outputStream = null;
try {
outputStream = prepareFileOutputStream(file);
publicKeys.encode(outputStream);
outputStream.close();
} catch (IOException e) {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException ignored) {
// Don't care
}
}
throw e;
}
}
@Override
public void writeSecretKeysOf(BareJid owner, PGPSecretKeyRingCollection secretKeys) throws IOException {
File file = getSecretKeyRingPath(owner);
if (secretKeys == null) {
if (!file.exists()) {
return;
}
if (!file.delete()) {
throw new IOException("Could not delete file " + file.getAbsolutePath());
}
return;
}
OutputStream outputStream = null;
try {
outputStream = prepareFileOutputStream(file);
secretKeys.encode(outputStream);
outputStream.close();
} catch (IOException e) {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException ignored) {
// Don't care
}
}
throw e;
}
}
@Override
public PGPPublicKeyRingCollection readPublicKeysOf(BareJid owner)
throws IOException, PGPException {
File file = getPublicKeyRingPath(owner);
FileInputStream inputStream;
try {
inputStream = prepareFileInputStream(file);
} catch (FileNotFoundException e) {
return null;
}
PGPPublicKeyRingCollection collection = PGPainless.readKeyRing().publicKeyRingCollection(inputStream);
inputStream.close();
return collection;
}
@Override
public PGPSecretKeyRingCollection readSecretKeysOf(BareJid owner) throws IOException, PGPException {
File file = getSecretKeyRingPath(owner);
FileInputStream inputStream;
try {
inputStream = prepareFileInputStream(file);
} catch (FileNotFoundException e) {
return null;
}
PGPSecretKeyRingCollection collection = PGPainless.readKeyRing().secretKeyRingCollection(inputStream);
inputStream.close();
return collection;
}
@Override
protected Map<OpenPgpV4Fingerprint, Date> readKeyFetchDates(BareJid owner) throws IOException {
return FileBasedOpenPgpMetadataStore.readFingerprintsAndDates(getFetchDatesPath(owner));
}
@Override
protected void writeKeyFetchDates(BareJid owner, Map<OpenPgpV4Fingerprint, Date> dates) throws IOException {
FileBasedOpenPgpMetadataStore.writeFingerprintsAndDates(dates, getFetchDatesPath(owner));
}
private File getPublicKeyRingPath(BareJid jid) {
return new File(FileBasedOpenPgpStore.getContactsPath(basePath, jid), PUB_RING);
}
private File getSecretKeyRingPath(BareJid jid) {
return new File(FileBasedOpenPgpStore.getContactsPath(basePath, jid), SEC_RING);
}
private File getFetchDatesPath(BareJid jid) {
return new File(FileBasedOpenPgpStore.getContactsPath(basePath, jid), FETCH_DATES);
}
}

View file

@ -0,0 +1,190 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.store.filebased;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.text.ParseException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smackx.ox.store.abstr.AbstractOpenPgpMetadataStore;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpMetadataStore;
import org.jivesoftware.smackx.ox.util.FileUtils;
import org.jivesoftware.smackx.ox.util.Util;
import org.jxmpp.jid.BareJid;
import org.jxmpp.util.XmppDateTime;
import org.pgpainless.key.OpenPgpV4Fingerprint;
/**
* Implementation of the {@link OpenPgpMetadataStore}, which stores metadata information in a file structure.
* The information is stored in the following directory structure:
*
* <pre>
* {@code
* <basePath>/
* <userjid@server.tld>/
* announced.list // list of the users announced key fingerprints and modification dates
* }
* </pre>
*/
public class FileBasedOpenPgpMetadataStore extends AbstractOpenPgpMetadataStore {
public static final String ANNOUNCED = "announced.list";
public static final String RETRIEVED = "retrieved.list";
private static final Logger LOGGER = Logger.getLogger(FileBasedOpenPgpMetadataStore.class.getName());
private final File basePath;
public FileBasedOpenPgpMetadataStore(File basePath) {
this.basePath = basePath;
}
@Override
public Map<OpenPgpV4Fingerprint, Date> readAnnouncedFingerprintsOf(BareJid contact) throws IOException {
return readFingerprintsAndDates(getAnnouncedFingerprintsPath(contact));
}
@Override
public void writeAnnouncedFingerprintsOf(BareJid contact, Map<OpenPgpV4Fingerprint, Date> metadata)
throws IOException {
File destination = getAnnouncedFingerprintsPath(contact);
writeFingerprintsAndDates(metadata, destination);
}
static Map<OpenPgpV4Fingerprint, Date> readFingerprintsAndDates(File source) throws IOException {
if (!source.exists() || source.isDirectory()) {
return new HashMap<>();
}
BufferedReader reader = null;
try {
InputStream inputStream = FileUtils.prepareFileInputStream(source);
InputStreamReader isr = new InputStreamReader(inputStream, Util.UTF8);
reader = new BufferedReader(isr);
Map<OpenPgpV4Fingerprint, Date> fingerprintDateMap = new HashMap<>();
String line; int lineNr = 0;
while ((line = reader.readLine()) != null) {
lineNr++;
line = line.trim();
String[] split = line.split(" ");
if (split.length != 2) {
LOGGER.log(Level.FINE, "Skipping invalid line " + lineNr + " in file " + source.getAbsolutePath());
continue;
}
try {
OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(split[0]);
Date date = XmppDateTime.parseXEP0082Date(split[1]);
fingerprintDateMap.put(fingerprint, date);
} catch (IllegalArgumentException | ParseException e) {
LOGGER.log(Level.WARNING, "Error parsing fingerprint/date touple in line " + lineNr +
" of file " + source.getAbsolutePath(), e);
}
}
reader.close();
return fingerprintDateMap;
} catch (IOException e) {
if (reader != null) {
try {
reader.close();
} catch (IOException ignored) {
// Don't care
}
}
throw e;
}
}
static void writeFingerprintsAndDates(Map<OpenPgpV4Fingerprint, Date> data, File destination)
throws IOException {
if (data == null || data.isEmpty()) {
if (!destination.exists()) {
return;
} else {
if (!destination.delete()) {
throw new IOException("Cannot delete file " + destination.getAbsolutePath());
}
}
return;
}
if (!destination.exists()) {
File parent = destination.getParentFile();
if (!parent.exists() && !parent.mkdirs()) {
throw new IOException("Cannot create directory " + parent.getAbsolutePath());
}
if (!destination.createNewFile()) {
throw new IOException("Cannot create file " + destination.getAbsolutePath());
}
}
if (destination.isDirectory()) {
throw new IOException("File " + destination.getAbsolutePath() + " is a directory.");
}
BufferedWriter writer = null;
try {
OutputStream outputStream = FileUtils.prepareFileOutputStream(destination);
OutputStreamWriter osw = new OutputStreamWriter(outputStream, Util.UTF8);
writer = new BufferedWriter(osw);
for (OpenPgpV4Fingerprint fingerprint : data.keySet()) {
Date date = data.get(fingerprint);
String line = fingerprint.toString() + " " +
(date != null ? XmppDateTime.formatXEP0082Date(date) : XmppDateTime.formatXEP0082Date(new Date()));
writer.write(line);
writer.newLine();
}
writer.flush();
writer.close();
} catch (IOException e) {
if (writer != null) {
try {
writer.close();
} catch (IOException ignored) {
// Don't care
}
}
throw e;
}
}
private File getAnnouncedFingerprintsPath(BareJid contact) {
return new File(FileBasedOpenPgpStore.getContactsPath(basePath, contact), ANNOUNCED);
}
private File getRetrievedFingerprintsPath(BareJid contact) {
return new File(FileBasedOpenPgpStore.getContactsPath(basePath, contact), RETRIEVED);
}
}

View file

@ -0,0 +1,42 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.store.filebased;
import java.io.File;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smackx.ox.store.abstr.AbstractOpenPgpStore;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpStore;
import org.jxmpp.jid.BareJid;
/**
* Implementation of the {@link OpenPgpStore} which stores all information in a directory structure.
*/
public class FileBasedOpenPgpStore extends AbstractOpenPgpStore {
public FileBasedOpenPgpStore(File basePath) {
super(new FileBasedOpenPgpKeyStore(basePath),
new FileBasedOpenPgpMetadataStore(basePath),
new FileBasedOpenPgpTrustStore(basePath));
}
public static File getContactsPath(File basePath, BareJid jid) {
return new File(basePath, Objects.requireNonNull(jid.toString()));
}
}

View file

@ -0,0 +1,154 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.store.filebased;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smackx.ox.store.abstr.AbstractOpenPgpTrustStore;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpTrustStore;
import org.jivesoftware.smackx.ox.util.FileUtils;
import org.jivesoftware.smackx.ox.util.Util;
import org.jxmpp.jid.BareJid;
import org.pgpainless.key.OpenPgpV4Fingerprint;
/**
* Implementation of the {@link OpenPgpTrustStore} which stores information in a directory structure.
*
* <pre>
* {@code
* <basePath>/
* <userjid@server.tld>/
* <fingerprint>.trust // Trust record for a key
* }
* </pre>
*/
public class FileBasedOpenPgpTrustStore extends AbstractOpenPgpTrustStore {
private static final Logger LOGGER = Logger.getLogger(FileBasedOpenPgpTrustStore.class.getName());
private final File basePath;
public static String TRUST_RECORD(OpenPgpV4Fingerprint fingerprint) {
return fingerprint.toString() + ".trust";
}
public FileBasedOpenPgpTrustStore(File basePath) {
this.basePath = basePath;
}
@Override
protected Trust readTrust(BareJid owner, OpenPgpV4Fingerprint fingerprint) throws IOException {
File file = getTrustPath(owner, fingerprint);
BufferedReader reader = null;
try {
InputStream inputStream = FileUtils.prepareFileInputStream(file);
InputStreamReader isr = new InputStreamReader(inputStream, Util.UTF8);
reader = new BufferedReader(isr);
Trust trust = null;
String line; int lineNr = 0;
while ((line = reader.readLine()) != null) {
lineNr++;
try {
trust = Trust.valueOf(line);
break;
} catch (IllegalArgumentException e) {
LOGGER.log(Level.WARNING, "Skipping invalid trust record in line " + lineNr + " of file " +
file.getAbsolutePath());
}
}
reader.close();
return trust != null ? trust : Trust.undecided;
} catch (IOException e) {
if (reader != null) {
try {
reader.close();
} catch (IOException ignored) {
// Don't care
}
}
if (e instanceof FileNotFoundException) {
return Trust.undecided;
}
throw e;
}
}
@Override
protected void writeTrust(BareJid owner, OpenPgpV4Fingerprint fingerprint, Trust trust) throws IOException {
File file = getTrustPath(owner, fingerprint);
if (trust == null || trust == Trust.undecided) {
if (!file.exists()) {
return;
}
if (!file.delete()) {
throw new IOException("Could not delete file " + file.getAbsolutePath());
}
}
File parent = file.getParentFile();
if (!parent.exists() && !parent.mkdirs()) {
throw new IOException("Cannot create directory " + parent.getAbsolutePath());
}
if (!file.exists()) {
if (!file.createNewFile()) {
throw new IOException("Cannot create file " + file.getAbsolutePath());
}
} else {
if (file.isDirectory()) {
throw new IOException("File " + file.getAbsolutePath() + " is a directory.");
}
}
BufferedWriter writer = null;
try {
OutputStream outputStream = FileUtils.prepareFileOutputStream(file);
OutputStreamWriter osw = new OutputStreamWriter(outputStream, Util.UTF8);
writer = new BufferedWriter(osw);
writer.write(trust.toString());
writer.flush();
writer.close();
} catch (IOException e) {
if (writer != null) {
try {
writer.close();
} catch (IOException ignored) {
// Don't care
}
}
throw e;
}
}
private File getTrustPath(BareJid owner, OpenPgpV4Fingerprint fingerprint) {
return new File(FileBasedOpenPgpStore.getContactsPath(basePath, owner), TRUST_RECORD(fingerprint));
}
}

View file

@ -0,0 +1,20 @@
/**
*
* Copyright 2017 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* File based store implementations.
*/
package org.jivesoftware.smackx.ox.store.filebased;

View file

@ -0,0 +1,20 @@
/**
*
* Copyright 2017 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* OpenPGP store implementations.
*/
package org.jivesoftware.smackx.ox.store;

View file

@ -0,0 +1,60 @@
/**
*
* Copyright 2017 Florian Schmaus, 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileUtils {
public static FileOutputStream prepareFileOutputStream(File file) throws IOException {
if (!file.exists()) {
// Create parent directory
File parent = file.getParentFile();
if (!parent.exists() && !parent.mkdirs()) {
throw new IOException("Cannot create directory " + parent.getAbsolutePath());
}
// Create file
if (!file.createNewFile()) {
throw new IOException("Cannot create file " + file.getAbsolutePath());
}
}
if (file.isDirectory()) {
throw new AssertionError("File " + file.getAbsolutePath() + " is not a file!");
}
return new FileOutputStream(file);
}
public static FileInputStream prepareFileInputStream(File file) throws IOException {
if (file.exists()) {
if (file.isFile()) {
return new FileInputStream(file);
} else {
throw new IOException("File " + file.getAbsolutePath() + " is not a file!");
}
} else {
throw new FileNotFoundException("File " + file.getAbsolutePath() + " not found.");
}
}
}

View file

@ -0,0 +1,30 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.util;
import org.jivesoftware.smack.initializer.UrlInitializer;
/**
* Initializer class which registers ExtensionElementProviders on startup.
*/
public class OpenPgpInitializer extends UrlInitializer {
@Override
protected String getProvidersUri() {
return "classpath:org.jivesoftware.smackx.ox/openpgp.providers";
}
}

View file

@ -0,0 +1,484 @@
/**
*
* Copyright 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox.util;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.StanzaError;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.ox.OpenPgpManager;
import org.jivesoftware.smackx.ox.element.PubkeyElement;
import org.jivesoftware.smackx.ox.element.PublicKeysListElement;
import org.jivesoftware.smackx.ox.element.SecretkeyElement;
import org.jivesoftware.smackx.pubsub.AccessModel;
import org.jivesoftware.smackx.pubsub.ConfigureForm;
import org.jivesoftware.smackx.pubsub.Item;
import org.jivesoftware.smackx.pubsub.LeafNode;
import org.jivesoftware.smackx.pubsub.Node;
import org.jivesoftware.smackx.pubsub.PayloadItem;
import org.jivesoftware.smackx.pubsub.PubSubException;
import org.jivesoftware.smackx.pubsub.PubSubManager;
import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jxmpp.jid.BareJid;
import org.pgpainless.key.OpenPgpV4Fingerprint;
public class OpenPgpPubSubUtil {
private static final Logger LOGGER = Logger.getLogger(OpenPgpPubSubUtil.class.getName());
/**
* Name of the OX metadata node.
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html#announcing-pubkey-list">XEP-0373 §4.2</a>
*/
public static final String PEP_NODE_PUBLIC_KEYS = "urn:xmpp:openpgp:0:public-keys";
/**
* Name of the OX secret key node.
* TODO: Update once my PR gets merged.
* @see <a href="https://github.com/xsf/xeps/pull/669">xsf/xeps#669</a>
*/
public static final String PEP_NODE_SECRET_KEY = "urn:xmpp:openpgp:secret-key:0";
/**
* Feature to be announced using the {@link ServiceDiscoveryManager} to subscribe to the OX metadata node.
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html#pubsub-notifications">XEP-0373 §4.4</a>
*/
public static final String PEP_NODE_PUBLIC_KEYS_NOTIFY = PEP_NODE_PUBLIC_KEYS + "+notify";
/**
* Name of the OX public key node, which contains the key with id {@code id}.
*
* @param id upper case hex encoded OpenPGP v4 fingerprint of the key.
* @return PEP node name.
*/
public static String PEP_NODE_PUBLIC_KEY(OpenPgpV4Fingerprint id) {
return PEP_NODE_PUBLIC_KEYS + ":" + id;
}
/**
* Query the access model of {@code node}. If it is different from {@code accessModel}, change the access model
* of the node to {@code accessModel}.
*
* @see <a href="https://xmpp.org/extensions/xep-0060.html#accessmodels">XEP-0060 §4.5 - Node Access Models</a>
*
* @param node {@link LeafNode} whose PubSub access model we want to change
* @param accessModel new access model.
*
* @throws XMPPException.XMPPErrorException in case of an XMPP protocol error.
* @throws SmackException.NotConnectedException if we are not connected.
* @throws InterruptedException if the thread is interrupted.
* @throws SmackException.NoResponseException if the server doesn't respond.
*/
public static void changeAccessModelIfNecessary(LeafNode node, AccessModel accessModel)
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException {
ConfigureForm current = node.getNodeConfiguration();
if (current.getAccessModel() != accessModel) {
ConfigureForm updateConfig = new ConfigureForm(DataForm.Type.submit);
updateConfig.setAccessModel(accessModel);
node.sendConfigurationForm(updateConfig);
}
}
/**
* Publish the users OpenPGP public key to the public key node if necessary.
* Also announce the key to other users by updating the metadata node.
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html#annoucning-pubkey">XEP-0373 §4.1</a>
*
* @param connection XMPP connection
* @param pubkeyElement {@link PubkeyElement} containing the public key
* @param fingerprint fingerprint of the public key
*
* @throws InterruptedException if the thread gets interrupted.
* @throws PubSubException.NotALeafNodeException if either the metadata node or the public key node is not a
* {@link LeafNode}.
* @throws XMPPException.XMPPErrorException in case of an XMPP protocol error.
* @throws SmackException.NotConnectedException if we are not connected.
* @throws SmackException.NoResponseException if the server doesn't respond.
*/
public static void publishPublicKey(XMPPConnection connection, PubkeyElement pubkeyElement, OpenPgpV4Fingerprint fingerprint)
throws InterruptedException, PubSubException.NotALeafNodeException,
XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException {
String keyNodeName = PEP_NODE_PUBLIC_KEY(fingerprint);
PubSubManager pm = PubSubManager.getInstance(connection, connection.getUser().asBareJid());
// Check if key available at data node
// If not, publish key to data node
LeafNode keyNode = pm.getOrCreateLeafNode(keyNodeName);
changeAccessModelIfNecessary(keyNode, AccessModel.open);
List<Item> items = keyNode.getItems(1);
if (items.isEmpty()) {
LOGGER.log(Level.FINE, "Node " + keyNodeName + " is empty. Publish.");
keyNode.publish(new PayloadItem<>(pubkeyElement));
} else {
LOGGER.log(Level.FINE, "Node " + keyNodeName + " already contains key. Skip.");
}
// Fetch IDs from metadata node
LeafNode metadataNode = pm.getOrCreateLeafNode(PEP_NODE_PUBLIC_KEYS);
changeAccessModelIfNecessary(metadataNode, AccessModel.open);
List<PayloadItem<PublicKeysListElement>> 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()));
// Publish IDs to metadata node
metadataNode.publish(new PayloadItem<>(builder.build()));
}
/**
* Consult the public key metadata node and fetch a list of all of our published OpenPGP public keys.
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html#discover-pubkey-list">
* XEP-0373 §4.3: Discovering Public Keys of a User</a>
*
* @param connection XMPP connection
* @return content of our metadata node.
*
* @throws InterruptedException if the thread gets interrupted.
* @throws XMPPException.XMPPErrorException in case of an XMPP protocol exception.
* @throws PubSubException.NotAPubSubNodeException in case the queried entity is not a PubSub node
* @throws PubSubException.NotALeafNodeException in case the queried node is not a {@link LeafNode}
* @throws SmackException.NotConnectedException in case we are not connected
* @throws SmackException.NoResponseException in case the server doesn't respond
*/
public static PublicKeysListElement fetchPubkeysList(XMPPConnection connection)
throws InterruptedException, XMPPException.XMPPErrorException, PubSubException.NotAPubSubNodeException,
PubSubException.NotALeafNodeException, SmackException.NotConnectedException, SmackException.NoResponseException {
return fetchPubkeysList(connection, connection.getUser().asBareJid());
}
/**
* Consult the public key metadata node of {@code contact} to fetch the list of their published OpenPGP public keys.
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html#discover-pubkey-list">
* XEP-0373 §4.3: Discovering Public Keys of a User</a>
*