1
0
Fork 0
mirror of https://codeberg.org/Mercury-IM/Smack synced 2024-11-22 14:22:05 +01:00

Merge pull request #254 from vanitasvitae/openpgp

Add support for XEP-0373, XEP-0374: OpenPGP for XMPP: Instant Messaging
This commit is contained in:
Florian Schmaus 2018-07-30 10:56:52 +02:00 committed by GitHub
commit 0c2efbd342
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
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>
*
* @param connection XMPP connection
* @param contact {@link BareJid} of the user we want to fetch the list from.
* @return content of {@code contact}'s metadata node.
*
* @throws InterruptedException if the thread gets interrupted.
* @throws XMPPException.XMPPErrorException in case of an XMPP protocol exception.
* @throws SmackException.NoResponseException in case the server doesn't respond
* @throws PubSubException.NotALeafNodeException in case the queried node is not a {@link LeafNode}
* @throws SmackException.NotConnectedException in case we are not connected
* @throws PubSubException.NotAPubSubNodeException in case the queried entity is not a PubSub node
*/
public static PublicKeysListElement fetchPubkeysList(XMPPConnection connection, BareJid contact)
throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NoResponseException,
PubSubException.NotALeafNodeException, SmackException.NotConnectedException, PubSubException.NotAPubSubNodeException {
PubSubManager pm = PubSubManager.getInstance(connection, contact);
LeafNode node = getLeafNode(pm, PEP_NODE_PUBLIC_KEYS);
List<PayloadItem<PublicKeysListElement>> list = node.getItems(1);
if (list.isEmpty()) {
return null;
}
return list.get(0).getPayload();
}
/**
* Delete our metadata node.
*
* @param connection XMPP connection
*
* @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 deletePubkeysListNode(XMPPConnection connection)
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException {
PubSubManager pm = PubSubManager.getInstance(connection, connection.getUser().asBareJid());
try {
pm.deleteNode(PEP_NODE_PUBLIC_KEYS);
} catch (XMPPException.XMPPErrorException e) {
if (e.getStanzaError().getCondition() == StanzaError.Condition.item_not_found) {
LOGGER.log(Level.FINE, "Node does not exist. No need to delete it.");
} else {
throw e;
}
}
}
/**
* Delete the public key node of the key with fingerprint {@code fingerprint}.
*
* @param connection XMPP connection
* @param fingerprint fingerprint of the key we want to delete
*
* @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.
*/
public static void deletePublicKeyNode(XMPPConnection connection, OpenPgpV4Fingerprint fingerprint)
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException {
PubSubManager pm = PubSubManager.getInstance(connection, connection.getUser().asBareJid());
try {
pm.deleteNode(PEP_NODE_PUBLIC_KEY(fingerprint));
} catch (XMPPException.XMPPErrorException e) {
if (e.getStanzaError().getCondition() == StanzaError.Condition.item_not_found) {
LOGGER.log(Level.FINE, "Node does not exist. No need to delete it.");
} else {
throw e;
}
}
}
/**
* Fetch the OpenPGP public key of a {@code contact}, identified by its OpenPGP {@code v4_fingerprint}.
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html#discover-pubkey">XEP-0373 §4.3</a>
*
* @param connection XMPP connection
* @param contact {@link BareJid} of the contact we want to fetch a key from.
* @param v4_fingerprint upper case, hex encoded v4 fingerprint of the contacts key.
* @return {@link PubkeyElement} containing the requested public key.
*
* @throws InterruptedException if the thread gets interrupted.A
* @throws XMPPException.XMPPErrorException in case of an XMPP protocol error.
* @throws PubSubException.NotAPubSubNodeException in case the targeted entity is not a PubSub node.
* @throws PubSubException.NotALeafNodeException in case the fetched node is not a {@link LeafNode}.
* @throws SmackException.NotConnectedException in case we are not connected.
* @throws SmackException.NoResponseException if the server doesn't respond.
*/
public static PubkeyElement fetchPubkey(XMPPConnection connection, BareJid contact, OpenPgpV4Fingerprint v4_fingerprint)
throws InterruptedException, XMPPException.XMPPErrorException, PubSubException.NotAPubSubNodeException,
PubSubException.NotALeafNodeException, SmackException.NotConnectedException, SmackException.NoResponseException {
PubSubManager pm = PubSubManager.getInstance(connection, contact);
String nodeName = PEP_NODE_PUBLIC_KEY(v4_fingerprint);
LeafNode node = getLeafNode(pm, nodeName);
List<PayloadItem<PubkeyElement>> list = node.getItems(1);
if (list.isEmpty()) {
return null;
}
return list.get(0).getPayload();
}
/**
* Try to get a {@link LeafNode} the traditional way (first query information using disco#info), then query the node.
* If that fails, query the node directly.
*
* @param pm PubSubManager
* @param nodeName name of the node
* @return node
*
* @throws XMPPException.XMPPErrorException in case of an XMPP protocol error.
* @throws PubSubException.NotALeafNodeException if the queried node is not a {@link LeafNode}.
* @throws InterruptedException in case the thread gets interrupted
* @throws PubSubException.NotAPubSubNodeException in case the queried entity is not a PubSub node.
* @throws SmackException.NotConnectedException in case the connection is not connected.
* @throws SmackException.NoResponseException in case the server doesn't respond.
*/
static LeafNode getLeafNode(PubSubManager pm, String nodeName)
throws XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException, InterruptedException,
PubSubException.NotAPubSubNodeException, SmackException.NotConnectedException, SmackException.NoResponseException {
LeafNode node;
try {
node = pm.getLeafNode(nodeName);
} catch (XMPPException.XMPPErrorException e) {
// It might happen, that the server doesn't allow disco#info queries from strangers.
// In that case we have to fetch the node directly
if (e.getStanzaError().getCondition() == StanzaError.Condition.subscription_required) {
node = getOpenLeafNode(pm, nodeName);
} else {
throw e;
}
}
return node;
}
/**
* Publishes a {@link SecretkeyElement} to the secret key node.
* The node will be configured to use the whitelist access model to prevent access from subscribers.
*
* @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 connection {@link XMPPConnection} of the user
* @param element a {@link SecretkeyElement} containing the encrypted secret key of the user
*
* @throws InterruptedException if the thread gets interrupted.
* @throws PubSubException.NotALeafNodeException if something is wrong with the PubSub node
* @throws XMPPException.XMPPErrorException in case of an protocol related error
* @throws SmackException.NotConnectedException if we are not connected
* @throws SmackException.NoResponseException /watch?v=0peBq89ZTrc
* @throws SmackException.FeatureNotSupportedException if the Server doesn't support the whitelist access model
*/
public static void depositSecretKey(XMPPConnection connection, SecretkeyElement element)
throws InterruptedException, PubSubException.NotALeafNodeException,
XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException,
SmackException.FeatureNotSupportedException {
if (!OpenPgpManager.serverSupportsSecretKeyBackups(connection)) {
throw new SmackException.FeatureNotSupportedException("http://jabber.org/protocol/pubsub#access-whitelist");
}
PubSubManager pm = PubSubManager.getInstance(connection, connection.getUser().asBareJid());
LeafNode secretKeyNode = pm.getOrCreateLeafNode(PEP_NODE_SECRET_KEY);
OpenPgpPubSubUtil.changeAccessModelIfNecessary(secretKeyNode, AccessModel.whitelist);
secretKeyNode.publish(new PayloadItem<>(element));
}
/**
* Fetch the latest {@link SecretkeyElement} from the private backup node.
*
* @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 connection {@link XMPPConnection} of the user.
* @return the secret key node or null, if it doesn't exist.
*
* @throws InterruptedException if the thread gets interrupted
* @throws PubSubException.NotALeafNodeException if there is an issue with the PubSub node
* @throws XMPPException.XMPPErrorException if there is an XMPP protocol related issue
* @throws SmackException.NotConnectedException if we are not connected
* @throws SmackException.NoResponseException /watch?v=7U0FzQzJzyI
*/
public static SecretkeyElement fetchSecretKey(XMPPConnection connection)
throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException,
SmackException.NotConnectedException, SmackException.NoResponseException {
PubSubManager pm = PubSubManager.getInstance(connection, connection.getUser().asBareJid());
LeafNode secretKeyNode = pm.getOrCreateLeafNode(PEP_NODE_SECRET_KEY);
List<PayloadItem<SecretkeyElement>> list = secretKeyNode.getItems(1);
if (list.size() == 0) {
LOGGER.log(Level.INFO, "No secret key published!");
return null;
}
SecretkeyElement secretkeyElement = list.get(0).getPayload();
return secretkeyElement;
}
/**
* Delete the private backup node.
*
* @param connection {@link XMPPConnection} of the user.
*
* @throws XMPPException.XMPPErrorException if there is an XMPP protocol related issue
* @throws SmackException.NotConnectedException if we are not connected
* @throws InterruptedException if the thread gets interrupted
* @throws SmackException.NoResponseException if the server sends no response
*/
public static void deleteSecretKeyNode(XMPPConnection connection)
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException {
PubSubManager pm = PubSubManager.getInstance(connection);
pm.deleteNode(PEP_NODE_SECRET_KEY);
}
/**
* Use reflection magic to get a {@link LeafNode} without doing a disco#info query.
* This method is useful for fetching nodes that are configured with the access model 'open', since
* some servers that announce support for that access model do not allow disco#info queries from contacts
* which are not subscribed to the node owner. Therefore this method fetches the node directly and puts it
* into the {@link PubSubManager}s node map.
*
* Note: Due to the alck of a disco#info query, it might happen, that the node doesn't exist on the server,
* even though we add it to the node map.
*
* @see <a href="https://github.com/processone/ejabberd/issues/2483">Ejabberd bug tracker about the issue</a>
* @see <a href="https://mail.jabber.org/pipermail/standards/2018-June/035206.html">
* Topic on the standards mailing list</a>
*
* @param pubSubManager pubsub manager
* @param nodeName name of the node
* @return leafNode
*
* @throws PubSubException.NotALeafNodeException in case we already have the node cached, but it is not a LeafNode.
*/
@SuppressWarnings("unchecked")
public static LeafNode getOpenLeafNode(PubSubManager pubSubManager, String nodeName)
throws PubSubException.NotALeafNodeException {
try {
// Get access to the PubSubManager's nodeMap
Field field = pubSubManager.getClass().getDeclaredField("nodeMap");
field.setAccessible(true);
Map<String, Node> nodeMap = (Map) field.get(pubSubManager);
// Check, if the node already exists
Node existingNode = nodeMap.get(nodeName);
if (existingNode != null) {
if (existingNode instanceof LeafNode) {
// We already know that node
return (LeafNode) existingNode;
} else {
// Throw a new NotALeafNodeException, as the node is not a LeafNode.
// Again use reflections to access the exceptions constructor.
Constructor<PubSubException.NotALeafNodeException> exceptionConstructor =
PubSubException.NotALeafNodeException.class.getDeclaredConstructor(String.class, BareJid.class);
exceptionConstructor.setAccessible(true);
throw exceptionConstructor.newInstance(nodeName, pubSubManager.getServiceJid());
}
}
// Node does not exist. Create the node
Constructor<LeafNode> constructor;
constructor = LeafNode.class.getDeclaredConstructor(PubSubManager.class, String.class);
constructor.setAccessible(true);
LeafNode node = constructor.newInstance(pubSubManager, nodeName);
// Add it to the node map
nodeMap.put(nodeName, node);
// And return
return node;
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException |
NoSuchFieldException e) {
LOGGER.log(Level.SEVERE, "Using reflections to create a LeafNode and put it into PubSubManagers nodeMap failed.", e);
throw new AssertionError(e);
}
}
}

View file

@ -0,0 +1,158 @@
/**
*
* 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.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Set;
import org.jivesoftware.smack.util.stringencoder.Base64;
import org.jivesoftware.smackx.ox.crypto.OpenPgpProvider;
import org.jivesoftware.smackx.ox.element.SecretkeyElement;
import org.jivesoftware.smackx.ox.exception.InvalidBackupCodeException;
import org.jivesoftware.smackx.ox.exception.MissingOpenPgpKeyException;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.jxmpp.jid.BareJid;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.util.Passphrase;
/**
* Helper class which provides some functions needed for backup/restore of the users secret key to/from their private
* PubSub node.
*/
public class SecretKeyBackupHelper {
/**
* Generate a secure backup code.
* This code can be used to encrypt a secret key backup and follows the form described in XEP-0373 §5.3.
*
* TODO: Update reflink
* @see <a href="https://xmpp.org/extensions/xep-0373.html#sect-idm140425111347232">XEP-0373 §5.3</a>
*
* @return backup code
*/
public static String generateBackupPassword() {
final String alphabet = "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ";
final int len = alphabet.length();
SecureRandom random = new SecureRandom();
StringBuilder code = new StringBuilder(29);
// 6 blocks
for (int i = 0; i < 6; i++) {
// of 4 chars
for (int j = 0; j < 4; j++) {
char c = alphabet.charAt(random.nextInt(len));
code.append(c);
}
// dash after every block except the last one
if (i != 5) {
code.append('-');
}
}
return code.toString();
}
/**
* Create a {@link SecretkeyElement} which contains the secret keys listed in {@code fingerprints} and is encrypted
* symmetrically using the {@code backupCode}.
*
* @param provider {@link OpenPgpProvider} for symmetric encryption.
* @param owner owner of the secret keys (usually our jid).
* @param fingerprints set of {@link OpenPgpV4Fingerprint}s of the keys which are going to be backed up.
* @param backupCode passphrase for symmetric encryption.
* @return {@link SecretkeyElement}
*
* @throws PGPException PGP is brittle
* @throws IOException IO is dangerous
* @throws MissingOpenPgpKeyException in case one of the keys whose fingerprint is in {@code fingerprints} is
* not accessible.
*/
public static SecretkeyElement createSecretkeyElement(OpenPgpProvider provider,
BareJid owner,
Set<OpenPgpV4Fingerprint> fingerprints,
String backupCode)
throws PGPException, IOException, MissingOpenPgpKeyException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
for (OpenPgpV4Fingerprint fingerprint : fingerprints) {
PGPSecretKeyRing key = provider.getStore().getSecretKeyRing(owner, fingerprint);
if (key == null) {
throw new MissingOpenPgpKeyException(owner, fingerprint);
}
byte[] bytes = key.getEncoded();
buffer.write(bytes);
}
return createSecretkeyElement(buffer.toByteArray(), backupCode);
}
/**
* Create a {@link SecretkeyElement} which contains the secret keys which are serialized in {@code keys} and is
* symmetrically encrypted using the {@code backupCode}.
*
* TODO: Update reflink
* @see <a href="https://xmpp.org/extensions/xep-0373.html#sect-idm45948286262768">
* XEP-0373 about secret key backup requirements</a>
*
* @param keys serialized OpenPGP secret keys in transferable key format
* @param backupCode passphrase for symmetric encryption
* @return {@link SecretkeyElement}
*
* @throws PGPException PGP is brittle
* @throws IOException IO is dangerous
*/
public static SecretkeyElement createSecretkeyElement(byte[] keys,
String backupCode)
throws PGPException, IOException {
byte[] encrypted = PGPainless.encryptWithPassword(keys, new Passphrase(backupCode.toCharArray()),
SymmetricKeyAlgorithm.AES_256);
return new SecretkeyElement(Base64.encode(encrypted));
}
/**
* Decrypt a secret key backup and return the {@link PGPSecretKeyRing} contained in it.
* TODO: Return a PGPSecretKeyRingCollection instead?
*
* @param backup encrypted {@link SecretkeyElement} containing the backup
* @param backupCode passphrase for decrypting the {@link SecretkeyElement}.
* @return the
* @throws InvalidBackupCodeException in case the provided backup code is invalid.
* @throws IOException IO is dangerous.
* @throws PGPException PGP is brittle.
*/
public static PGPSecretKeyRing restoreSecretKeyBackup(SecretkeyElement backup, String backupCode)
throws InvalidBackupCodeException, IOException, PGPException {
byte[] encrypted = Base64.decode(backup.getB64Data());
byte[] decrypted;
try {
decrypted = PGPainless.decryptWithPassword(encrypted, new Passphrase(backupCode.toCharArray()));
} catch (IOException | PGPException e) {
throw new InvalidBackupCodeException("Could not decrypt secret key backup. Possibly wrong passphrase?", e);
}
return PGPainless.readKeyRing().secretKeyRing(decrypted);
}
}

View file

@ -0,0 +1,24 @@
/**
*
* 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.nio.charset.Charset;
public class Util {
public static final Charset UTF8 = Charset.forName("UTF-8");
}

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.
*/
/**
* Utility classes for XEP-0373: OpenPGP for XMPP.
*/
package org.jivesoftware.smackx.ox.util;

View file

@ -0,0 +1,334 @@
/**
*
* 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 java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import org.jivesoftware.smack.Manager;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.chat2.ChatManager;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.eme.element.ExplicitMessageEncryptionElement;
import org.jivesoftware.smackx.hints.element.StoreHint;
import org.jivesoftware.smackx.ox.OpenPgpContact;
import org.jivesoftware.smackx.ox.OpenPgpManager;
import org.jivesoftware.smackx.ox.OpenPgpMessage;
import org.jivesoftware.smackx.ox.crypto.OpenPgpElementAndMetadata;
import org.jivesoftware.smackx.ox.element.OpenPgpElement;
import org.jivesoftware.smackx.ox.element.SigncryptElement;
import org.jivesoftware.smackx.ox.listener.SigncryptElementReceivedListener;
import org.bouncycastle.openpgp.PGPException;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.Jid;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.key.OpenPgpV4Fingerprint;
/**
* Entry point of Smacks API for XEP-0374: OpenPGP for XMPP: Instant Messaging.
*
* <h2>Setup</h2>
*
* In order to set up OX Instant Messaging, please first follow the setup routines of the {@link OpenPgpManager}, then
* do the following steps:
*
* <h3>Acquire an {@link OXInstantMessagingManager} instance.</h3>
*
* <pre>
* {@code
* OXInstantMessagingManager instantManager = OXInstantMessagingManager.getInstanceFor(connection);
* }
* </pre>
*
* <h3>Listen for OX messages</h3>
* In order to listen for incoming OX:IM messages, you have to register a listener.
*
* <pre>
* {@code
* instantManager.addOxMessageListener(
* new OxMessageListener() {
* void newIncomingOxMessage(OpenPgpContact contact,
* Message originalMessage,
* SigncryptElement decryptedPayload) {
* Message.Body body = decryptedPayload.<Message.Body>getExtension(Message.Body.ELEMENT, Message.Body.NAMESPACE);
* ...
* }
* });
* }
* </pre>
*
* <h3>Finally, announce support for OX:IM</h3>
* In order to let your contacts know, that you support message encrypting using the OpenPGP for XMPP: Instant Messaging
* profile, you have to announce support for OX:IM.
*
* <pre>
* {@code
* instantManager.announceSupportForOxInstantMessaging();
* }
* </pre>
*
* <h2>Sending messages</h2>
* In order to send an OX:IM message, just do
*
* <pre>
* {@code
* instantManager.sendOxMessage(openPgpManager.getOpenPgpContact(contactsJid), "Hello World");
* }
* </pre>
*
* Note, that you have to decide, whether to trust the contacts keys prior to sending a message, otherwise undecided
* keys are not included in the encryption process. You can trust keys by calling
* {@link OpenPgpContact#trust(OpenPgpV4Fingerprint)}. Same goes for your own keys! In order to determine, whether
* there are undecided keys, call {@link OpenPgpContact#hasUndecidedKeys()}. The trust state of a single key can be
* determined using {@link OpenPgpContact#getTrust(OpenPgpV4Fingerprint)}.
*
* Note: This implementation does not yet have support for sending/receiving messages to/from MUCs.
*
* @see <a href="https://xmpp.org/extensions/xep-0374.html">
* XEP-0374: OpenPGP for XMPP: Instant Messaging</a>
*/
public final class OXInstantMessagingManager extends Manager {
public static final String NAMESPACE_0 = "urn:xmpp:openpgp:im:0";
private static final Map<XMPPConnection, OXInstantMessagingManager> INSTANCES = new WeakHashMap<>();
private final Set<OxMessageListener> oxMessageListeners = new HashSet<>();
private final OpenPgpManager openPgpManager;
private OXInstantMessagingManager(final XMPPConnection connection) {
super(connection);
openPgpManager = OpenPgpManager.getInstanceFor(connection);
openPgpManager.registerSigncryptReceivedListener(signcryptElementReceivedListener);
announceSupportForOxInstantMessaging();
}
/**
* Return an instance of the {@link OXInstantMessagingManager} that belongs to the given {@code connection}.
*
* @param connection XMPP connection
* @return manager instance
*/
public static OXInstantMessagingManager getInstanceFor(XMPPConnection connection) {
OXInstantMessagingManager manager = INSTANCES.get(connection);
if (manager == null) {
manager = new OXInstantMessagingManager(connection);
INSTANCES.put(connection, manager);
}
return manager;
}
/**
* Add the OX:IM namespace as a feature to our disco features.
*/
public void announceSupportForOxInstantMessaging() {
ServiceDiscoveryManager.getInstanceFor(connection())
.addFeature(NAMESPACE_0);
}
/**
* Determine, whether a contact announces support for XEP-0374: OpenPGP for XMPP: Instant Messaging.
*
* @param jid {@link BareJid} of the contact in question.
* @return true if contact announces support, 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 gets interrupted
* @throws SmackException.NoResponseException if the server doesn't respond
*/
public boolean contactSupportsOxInstantMessaging(BareJid jid)
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException {
return ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(jid, NAMESPACE_0);
}
/**
* Determine, whether a contact announces support for XEP-0374: OpenPGP for XMPP: Instant Messaging.
*
* @param contact {@link OpenPgpContact} in question.
* @return true if contact announces support, 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 boolean contactSupportsOxInstantMessaging(OpenPgpContact contact)
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException {
return contactSupportsOxInstantMessaging(contact.getJid());
}
/**
* Add an {@link OxMessageListener}. The listener gets notified about incoming {@link OpenPgpMessage}s which
* contained an OX-IM message.
*
* @param listener listener
* @return true if the listener gets added, otherwise false.
*/
public boolean addOxMessageListener(OxMessageListener listener) {
return oxMessageListeners.add(listener);
}
/**
* Remove an {@link OxMessageListener}. The listener will no longer be notified about OX-IM messages.
*
* @param listener listener
* @return true, if the listener gets removed, otherwise false
*/
public boolean removeOxMessageListener(OxMessageListener listener) {
return oxMessageListeners.remove(listener);
}
/**
* Send an OX message to a {@link OpenPgpContact}. The message will be encrypted to all active keys of the contact,
* as well as all of our active keys. The message is also signed with our key.
*
* @param contact contact capable of OpenPGP for XMPP: Instant Messaging.
* @param body message body.
*
* @return {@link OpenPgpMetadata} about the messages encryption + signatures.
*
* @throws InterruptedException if the thread is interrupted
* @throws IOException IO is dangerous
* @throws SmackException.NotConnectedException if we are not connected
* @throws SmackException.NotLoggedInException if we are not logged in
* @throws PGPException PGP is brittle
*/
public OpenPgpMetadata sendOxMessage(OpenPgpContact contact, CharSequence body)
throws InterruptedException, IOException,
SmackException.NotConnectedException, SmackException.NotLoggedInException, PGPException {
Message message = new Message(contact.getJid());
Message.Body mBody = new Message.Body(null, body.toString());
OpenPgpMetadata metadata = addOxMessage(message, contact, Collections.<ExtensionElement>singletonList(mBody));
ChatManager.getInstanceFor(connection()).chatWith(contact.getJid().asEntityBareJidIfPossible()).send(message);
return metadata;
}
/**
* Add an OX-IM message element to a message.
*
* @param message message
* @param contact recipient of the message
* @param payload payload which will be encrypted and signed
*
* @return {@link OpenPgpMetadata} about the messages encryption + metadata.
*
* @throws SmackException.NotLoggedInException in case we are not logged in
* @throws PGPException in case something goes wrong during encryption
* @throws IOException IO is dangerous (we need to read keys)
*/
public OpenPgpMetadata addOxMessage(Message message, OpenPgpContact contact, List<ExtensionElement> payload)
throws SmackException.NotLoggedInException, PGPException, IOException {
return addOxMessage(message, Collections.singleton(contact), payload);
}
/**
* Add an OX-IM message element to a message.
*
* @param message message
* @param contacts recipients of the message
* @param payload payload which will be encrypted and signed
*
* @return metadata about the messages encryption + signatures.
*
* @throws SmackException.NotLoggedInException in case we are not logged in
* @throws PGPException in case something goes wrong during encryption
* @throws IOException IO is dangerous (we need to read keys)
*/
public OpenPgpMetadata addOxMessage(Message message, Set<OpenPgpContact> contacts, List<ExtensionElement> payload)
throws SmackException.NotLoggedInException, IOException, PGPException {
HashSet<OpenPgpContact> recipients = new HashSet<>(contacts);
OpenPgpContact self = openPgpManager.getOpenPgpSelf();
recipients.add(self);
OpenPgpElementAndMetadata openPgpElementAndMetadata = signAndEncrypt(recipients, payload);
message.addExtension(openPgpElementAndMetadata.getElement());
// Set hints on message
ExplicitMessageEncryptionElement.set(message,
ExplicitMessageEncryptionElement.ExplicitMessageEncryptionProtocol.openpgpV0);
StoreHint.set(message);
setOXBodyHint(message);
return openPgpElementAndMetadata.getMetadata();
}
/**
* Wrap some {@code payload} into a {@link SigncryptElement}, sign and encrypt it for {@code contacts} and ourselves.
*
* @param contacts recipients of the message
* @param payload payload which will be encrypted and signed
*
* @return encrypted and signed {@link OpenPgpElement}, along with {@link OpenPgpMetadata} about the
* encryption + signatures.
*
* @throws SmackException.NotLoggedInException in case we are not logged in
* @throws IOException IO is dangerous (we need to read keys)
* @throws PGPException in case encryption goes wrong
*/
public OpenPgpElementAndMetadata signAndEncrypt(Set<OpenPgpContact> contacts, List<ExtensionElement> payload)
throws SmackException.NotLoggedInException, IOException, PGPException {
Set<Jid> jids = new HashSet<>();
for (OpenPgpContact contact : contacts) {
jids.add(contact.getJid());
}
jids.add(openPgpManager.getOpenPgpSelf().getJid());
SigncryptElement signcryptElement = new SigncryptElement(jids, payload);
OpenPgpElementAndMetadata encrypted = openPgpManager.getOpenPgpProvider().signAndEncrypt(signcryptElement,
openPgpManager.getOpenPgpSelf(), contacts);
return encrypted;
}
/**
* Set a hint about the message being OX-IM encrypted as body of the message.
*
* @param message message
*/
private static void setOXBodyHint(Message message) {
message.setBody("This message is encrypted using XEP-0374: OpenPGP for XMPP: Instant Messaging.");
}
private final SigncryptElementReceivedListener signcryptElementReceivedListener = new SigncryptElementReceivedListener() {
@Override
public void signcryptElementReceived(OpenPgpContact contact, Message originalMessage, SigncryptElement signcryptElement, OpenPgpMetadata metadata) {
for (OxMessageListener listener : oxMessageListeners) {
listener.newIncomingOxMessage(contact, originalMessage, signcryptElement, metadata);
}
}
};
}

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_im;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.ox.OpenPgpContact;
import org.jivesoftware.smackx.ox.element.OpenPgpElement;
import org.jivesoftware.smackx.ox.element.SigncryptElement;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
public interface OxMessageListener {
/**
* This method gets invoked, whenever an OX-IM encrypted message gets received.
*
* @see <a href="https://xmpp.org/extensions/xep-0374.html">
* XEP-0374: OpenPGP for XMPP: Instant Messaging (OX-IM)</a>
*@param contact {@link OpenPgpContact} which sent the message.
* @param originalMessage the received message that is carrying the encrypted {@link OpenPgpElement}.
* @param decryptedPayload decrypted {@link SigncryptElement} which is carrying the payload.
* @param metadata metadata about the encryption and signing
*/
void newIncomingOxMessage(OpenPgpContact contact,
Message originalMessage,
SigncryptElement decryptedPayload,
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.
*/
/**
* Smack API for XEP-0374: OpenPGP for XMPP: Instant Messaging.
*/
package org.jivesoftware.smackx.ox_im;

View file

@ -0,0 +1,28 @@
<?xml version="1.0"?>
<!-- Providers for OpenPGP for XMPP -->
<smackProviders>
<!-- Stanza extensions -->
<extensionProvider>
<elementName>openpgp</elementName>
<namespace>urn:xmpp:openpgp:0</namespace>
<className>org.jivesoftware.smackx.ox.provider.OpenPgpElementProvider</className>
</extensionProvider>
<!-- PubSub extensions -->
<extensionProvider>
<elementName>pubkey</elementName>
<namespace>urn:xmpp:openpgp:0</namespace>
<className>org.jivesoftware.smackx.ox.provider.PubkeyElementProvider</className>
</extensionProvider>
<extensionProvider>
<elementName>secretkey</elementName>
<namespace>urn:xmpp:openpgp:0</namespace>
<className>org.jivesoftware.smackx.ox.provider.SecretkeyElementProvider</className>
</extensionProvider>
<extensionProvider>
<elementName>public-keys-list</elementName>
<namespace>urn:xmpp:openpgp:0</namespace>
<className>org.jivesoftware.smackx.ox.provider.PublicKeysListElementProvider</className>
</extensionProvider>
</smackProviders>

View file

@ -0,0 +1,185 @@
/**
*
* 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 org.custommonkey.xmlunit.XMLAssert.assertXMLEqual;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smack.test.util.TestUtils;
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.provider.OpenPgpContentElementProvider;
import org.jivesoftware.smackx.ox.provider.OpenPgpElementProvider;
import org.junit.Test;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
public class OpenPgpElementTest extends SmackTestSuite {
private final Set<Jid> recipients;
// 2014-07-10T15:06:00.000+00:00
private static final Date testDate = new Date(1405004760000L);
public OpenPgpElementTest() throws XmppStringprepException {
super();
Set<Jid> jids = new HashSet<>();
jids.add(JidCreate.bareFrom("alice@wonderland.lit"));
jids.add(JidCreate.bareFrom("bob@builder.lit"));
this.recipients = Collections.unmodifiableSet(jids);
}
@Test
public void providerTest() throws Exception {
String expected =
"<openpgp xmlns='urn:xmpp:openpgp:0'>" +
"BASE64_OPENPGP_MESSAGE" +
"</openpgp>";
OpenPgpElement element = new OpenPgpElement("BASE64_OPENPGP_MESSAGE");
assertXMLEqual(expected, element.toXML(null).toString());
XmlPullParser parser = TestUtils.getParser(expected);
OpenPgpElement parsed = OpenPgpElementProvider.TEST_INSTANCE.parse(parser);
assertEquals(element.getEncryptedBase64MessageContent(), parsed.getEncryptedBase64MessageContent());
}
@Test
public void simplifiedConstructorTest() {
ArrayList<ExtensionElement> payload = new ArrayList<>();
payload.add(new Message.Body("de", "Hallo Welt!"));
CryptElement element = new CryptElement(recipients, payload);
assertNotNull(element.getTimestamp());
}
@Test
public void signElementProviderTest() throws Exception {
String expected =
"<sign xmlns='urn:xmpp:openpgp:0'>" +
"<to jid='alice@wonderland.lit'/>" +
"<to jid='bob@builder.lit'/>" +
"<time stamp='2014-07-10T15:06:00.000+00:00'/>" +
"<payload>" +
"<body xmlns='jabber:client' xml:lang='en'>Hello World!</body>" +
"</payload>" +
"</sign>";
List<ExtensionElement> payload = new ArrayList<>();
payload.add(new Message.Body("en", "Hello World!"));
SignElement element = new SignElement(recipients, testDate, payload);
assertXMLEqual(expected, element.toXML(null).toString());
XmlPullParser parser = TestUtils.getParser(expected);
SignElement parsed = (SignElement) OpenPgpContentElementProvider.parseOpenPgpContentElement(parser);
assertEquals(element.getTimestamp(), parsed.getTimestamp());
assertEquals(element.getTo(), parsed.getTo());
assertEquals(element.getExtensions(), parsed.getExtensions());
}
@Test
public void cryptElementProviderTest() throws Exception {
String expected =
"<crypt xmlns='urn:xmpp:openpgp:0'>" +
"<to jid='alice@wonderland.lit'/>" +
"<time stamp='2014-07-10T15:06:00.000+00:00'/>" +
"<payload>" +
"<body xmlns='jabber:client' xml:lang='en'>The cake is a lie.</body>" +
"</payload>" +
"<rpad>f0rm1l4n4-mT8y33j!Y%fRSrcd^ZE4Q7VDt1L%WEgR!kv</rpad>" +
"</crypt>";
List<ExtensionElement> payload = new ArrayList<>();
payload.add(new Message.Body("en", "The cake is a lie."));
Set<Jid> to = new HashSet<>();
to.add(JidCreate.bareFrom("alice@wonderland.lit"));
CryptElement element = new CryptElement(to,
"f0rm1l4n4-mT8y33j!Y%fRSrcd^ZE4Q7VDt1L%WEgR!kv",
testDate,
payload);
assertXMLEqual(expected, element.toXML(null).toString());
XmlPullParser parser = TestUtils.getParser(expected);
CryptElement parsed = (CryptElement) OpenPgpContentElementProvider.parseOpenPgpContentElement(parser);
assertEquals(element.getTimestamp(), parsed.getTimestamp());
assertEquals(element.getTo(), parsed.getTo());
assertEquals(element.getExtensions(), parsed.getExtensions());
}
@Test
public void signcryptElementProviderTest() throws Exception {
String expected =
"<signcrypt xmlns='urn:xmpp:openpgp:0'>" +
"<to jid='juliet@example.org'/>" +
"<time stamp='2014-07-10T15:06:00.000+00:00'/>" +
"<payload>" +
"<body xmlns='jabber:client' xml:lang='en'>This is a secret message.</body>" +
"</payload>" +
"<rpad>f0rm1l4n4-mT8y33j!Y%fRSrcd^ZE4Q7VDt1L%WEgR!kv</rpad>" +
"</signcrypt>";
List<ExtensionElement> payload = new ArrayList<>();
payload.add(new Message.Body("en", "This is a secret message."));
Set<Jid> jids = new HashSet<>();
jids.add(JidCreate.bareFrom("juliet@example.org"));
SigncryptElement element = new SigncryptElement(jids,
"f0rm1l4n4-mT8y33j!Y%fRSrcd^ZE4Q7VDt1L%WEgR!kv",
testDate, payload);
assertXMLEqual(expected, element.toXML(null).toString());
XmlPullParser parser = TestUtils.getParser(expected);
SigncryptElement parsed = (SigncryptElement) OpenPgpContentElementProvider.parseOpenPgpContentElement(parser);
assertEquals(element.getTimestamp(), parsed.getTimestamp());
assertEquals(element.getTo(), parsed.getTo());
assertEquals(element.getExtensions(), parsed.getExtensions());
assertEquals(payload.get(0), element.getExtension(Message.Body.NAMESPACE));
assertEquals(payload.get(0), element.getExtension(Message.Body.ELEMENT, Message.Body.NAMESPACE));
}
@Test(expected = XmlPullParserException.class)
public void openPgpContentElementProvider_invalidElementTest() throws IOException, XmlPullParserException {
String invalidElementXML = "<payload>" +
"<body xmlns='jabber:client' xml:lang='en'>This is a secret message.</body>" +
"</payload>";
OpenPgpContentElementProvider.parseOpenPgpContentElement(invalidElementXML);
}
}

View file

@ -0,0 +1,353 @@
/**
*
* 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.assertFalse;
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.security.Security;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smack.util.FileUtils;
import org.jivesoftware.smackx.ox.callback.SecretKeyPassphraseCallback;
import org.jivesoftware.smackx.ox.exception.MissingUserIdOnKeyException;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpStore;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpTrustStore;
import org.jivesoftware.smackx.ox.store.filebased.FileBasedOpenPgpStore;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.junit.After;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.junit.runners.Parameterized;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.JidTestUtil;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.collection.PGPKeyRing;
import org.pgpainless.key.protection.UnprotectedKeysProtector;
import org.pgpainless.util.Passphrase;
@RunWith(Parameterized.class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class OpenPgpStoreTest extends SmackTestSuite {
private static File storagePath;
private static final BareJid alice = JidTestUtil.BARE_JID_1;
private static final BareJid bob = JidTestUtil.BARE_JID_2;
private static final OpenPgpV4Fingerprint finger1 = new OpenPgpV4Fingerprint("DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF");
private static final OpenPgpV4Fingerprint finger2 = new OpenPgpV4Fingerprint("C0FFEEC0FFEEC0FFEEC0FFEEC0FFEEC0FFEE1234");
private static final OpenPgpV4Fingerprint finger3 = new OpenPgpV4Fingerprint("0123012301230123012301230123012301230123");
private final OpenPgpStore openPgpStoreInstance1;
private final OpenPgpStore openPgpStoreInstance2;
static {
storagePath = FileUtils.getTempDir("storeTest");
Security.addProvider(new BouncyCastleProvider());
}
@Parameterized.Parameters
public static Collection<OpenPgpStore[]> data() {
return Arrays.asList(
new OpenPgpStore[][] {
{new FileBasedOpenPgpStore(storagePath), new FileBasedOpenPgpStore(storagePath)}
});
}
public OpenPgpStoreTest(OpenPgpStore firstInstance, OpenPgpStore secondInstance) {
if (firstInstance == secondInstance || !firstInstance.getClass().equals(secondInstance.getClass())) {
throw new IllegalArgumentException("firstInstance must be another instance of the same class as secondInstance.");
}
this.openPgpStoreInstance1 = firstInstance;
this.openPgpStoreInstance2 = secondInstance;
}
@Before
@After
public void deletePath() {
FileUtils.deleteDirectory(storagePath);
}
/*
Generic
*/
@Test
public void t00_store_protectorGetSet() {
openPgpStoreInstance1.setKeyRingProtector(new UnprotectedKeysProtector());
assertNotNull(openPgpStoreInstance1.getKeyRingProtector());
// TODO: Test method below
openPgpStoreInstance1.setSecretKeyPassphraseCallback(new SecretKeyPassphraseCallback() {
@Override
public Passphrase onPassphraseNeeded(OpenPgpV4Fingerprint fingerprint) {
return null;
}
});
}
/*
OpenPgpKeyStore
*/
@Test
public void t00_deleteTest() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException, MissingUserIdOnKeyException {
assertNull(openPgpStoreInstance1.getSecretKeysOf(alice));
assertNull(openPgpStoreInstance1.getPublicKeysOf(alice));
PGPKeyRing keys = openPgpStoreInstance1.generateKeyRing(alice);
openPgpStoreInstance1.importSecretKey(alice, keys.getSecretKeys());
openPgpStoreInstance1.importPublicKey(alice, keys.getPublicKeys());
assertNotNull(openPgpStoreInstance1.getSecretKeysOf(alice));
assertNotNull(openPgpStoreInstance1.getPublicKeysOf(alice));
openPgpStoreInstance1.deleteSecretKeyRing(alice, new OpenPgpV4Fingerprint(keys.getSecretKeys()));
openPgpStoreInstance1.deletePublicKeyRing(alice, new OpenPgpV4Fingerprint(keys.getSecretKeys()));
assertNull(openPgpStoreInstance1.getPublicKeysOf(alice));
assertNull(openPgpStoreInstance1.getSecretKeysOf(alice));
}
@Test
public void t01_key_emptyStoreTest() throws IOException, PGPException {
assertNull(openPgpStoreInstance1.getPublicKeysOf(alice));
assertNull(openPgpStoreInstance1.getSecretKeysOf(alice));
assertNull(openPgpStoreInstance1.getPublicKeyRing(alice, finger1));
assertNull(openPgpStoreInstance1.getSecretKeyRing(alice, finger1));
}
@Test
public void t02_key_importKeysTest() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException, MissingUserIdOnKeyException {
// Test for nullity of all possible values.
PGPKeyRing keys = openPgpStoreInstance1.generateKeyRing(alice);
PGPSecretKeyRing secretKeys = keys.getSecretKeys();
PGPPublicKeyRing publicKeys = keys.getPublicKeys();
assertNotNull(secretKeys);
assertNotNull(publicKeys);
OpenPgpContact cAlice = openPgpStoreInstance1.getOpenPgpContact(alice);
assertNull(cAlice.getAnyPublicKeys());
OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(publicKeys);
assertEquals(fingerprint, new OpenPgpV4Fingerprint(secretKeys));
assertNull(openPgpStoreInstance1.getPublicKeysOf(alice));
assertNull(openPgpStoreInstance1.getSecretKeysOf(alice));
openPgpStoreInstance1.importPublicKey(alice, publicKeys);
assertTrue(Arrays.equals(publicKeys.getEncoded(), openPgpStoreInstance1.getPublicKeysOf(alice).getEncoded()));
assertNotNull(openPgpStoreInstance1.getPublicKeyRing(alice, fingerprint));
assertNull(openPgpStoreInstance1.getSecretKeysOf(alice));
cAlice = openPgpStoreInstance1.getOpenPgpContact(alice);
assertNotNull(cAlice.getAnyPublicKeys());
// Import keys a second time -> No change expected.
openPgpStoreInstance1.importPublicKey(alice, publicKeys);
assertTrue(Arrays.equals(publicKeys.getEncoded(), openPgpStoreInstance1.getPublicKeysOf(alice).getEncoded()));
openPgpStoreInstance1.importSecretKey(alice, secretKeys);
assertTrue(Arrays.equals(secretKeys.getEncoded(), openPgpStoreInstance1.getSecretKeysOf(alice).getEncoded()));
openPgpStoreInstance1.importSecretKey(alice, secretKeys);
assertNotNull(openPgpStoreInstance1.getSecretKeysOf(alice));
assertTrue(Arrays.equals(secretKeys.getEncoded(), openPgpStoreInstance1.getSecretKeysOf(alice).getEncoded()));
assertNotNull(openPgpStoreInstance1.getSecretKeyRing(alice, fingerprint));
assertTrue(Arrays.equals(secretKeys.getEncoded(), openPgpStoreInstance1.getSecretKeyRing(alice, fingerprint).getEncoded()));
assertTrue(Arrays.equals(publicKeys.getEncoded(), openPgpStoreInstance1.getPublicKeyRing(alice, fingerprint).getEncoded()));
// Clean up
openPgpStoreInstance1.deletePublicKeyRing(alice, fingerprint);
openPgpStoreInstance1.deleteSecretKeyRing(alice, fingerprint);
}
@Test(expected = MissingUserIdOnKeyException.class)
public void t04_key_wrongBareJidOnSecretKeyImportTest() throws PGPException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, IOException, MissingUserIdOnKeyException {
PGPSecretKeyRing secretKeys = openPgpStoreInstance1.generateKeyRing(alice).getSecretKeys();
openPgpStoreInstance1.importSecretKey(bob, secretKeys);
}
@Test(expected = MissingUserIdOnKeyException.class)
public void t05_key_wrongBareJidOnPublicKeyImportTest() throws PGPException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, IOException, MissingUserIdOnKeyException {
PGPPublicKeyRing publicKeys = openPgpStoreInstance1.generateKeyRing(alice).getPublicKeys();
openPgpStoreInstance1.importPublicKey(bob, publicKeys);
}
@Test
public void t06_key_keyReloadTest() throws PGPException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, IOException, MissingUserIdOnKeyException {
PGPKeyRing keys = openPgpStoreInstance1.generateKeyRing(alice);
PGPSecretKeyRing secretKeys = keys.getSecretKeys();
OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(secretKeys);
PGPPublicKeyRing publicKeys = keys.getPublicKeys();
openPgpStoreInstance1.importSecretKey(alice, secretKeys);
openPgpStoreInstance1.importPublicKey(alice, publicKeys);
assertNotNull(openPgpStoreInstance2.getSecretKeysOf(alice));
assertNotNull(openPgpStoreInstance2.getPublicKeysOf(alice));
// Clean up
openPgpStoreInstance1.deletePublicKeyRing(alice, fingerprint);
openPgpStoreInstance1.deleteSecretKeyRing(alice, fingerprint);
openPgpStoreInstance2.deletePublicKeyRing(alice, fingerprint);
openPgpStoreInstance2.deleteSecretKeyRing(alice, fingerprint);
}
@Test
public void t07_multipleKeysTest() throws PGPException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, IOException, MissingUserIdOnKeyException {
PGPKeyRing one = openPgpStoreInstance1.generateKeyRing(alice);
PGPKeyRing two = openPgpStoreInstance1.generateKeyRing(alice);
OpenPgpV4Fingerprint fingerprint1 = new OpenPgpV4Fingerprint(one.getSecretKeys());
OpenPgpV4Fingerprint fingerprint2 = new OpenPgpV4Fingerprint(two.getSecretKeys());
openPgpStoreInstance1.importSecretKey(alice, one.getSecretKeys());
openPgpStoreInstance1.importSecretKey(alice, two.getSecretKeys());
openPgpStoreInstance1.importPublicKey(alice, one.getPublicKeys());
openPgpStoreInstance1.importPublicKey(alice, two.getPublicKeys());
assertTrue(Arrays.equals(one.getSecretKeys().getEncoded(), openPgpStoreInstance1.getSecretKeyRing(alice, fingerprint1).getEncoded()));
assertTrue(Arrays.equals(two.getSecretKeys().getEncoded(), openPgpStoreInstance1.getSecretKeyRing(alice, fingerprint2).getEncoded()));
assertTrue(Arrays.equals(one.getSecretKeys().getEncoded(), openPgpStoreInstance1.getSecretKeysOf(alice).getSecretKeyRing(fingerprint1.getKeyId()).getEncoded()));
assertTrue(Arrays.equals(one.getPublicKeys().getEncoded(),
openPgpStoreInstance1.getPublicKeyRing(alice, fingerprint1).getEncoded()));
// Cleanup
openPgpStoreInstance1.deletePublicKeyRing(alice, fingerprint1);
openPgpStoreInstance1.deletePublicKeyRing(alice, fingerprint2);
openPgpStoreInstance1.deleteSecretKeyRing(alice, fingerprint1);
openPgpStoreInstance1.deleteSecretKeyRing(alice, fingerprint2);
}
/*
OpenPgpTrustStore
*/
@Test
public void t08_trust_emptyStoreTest() throws IOException {
assertEquals(OpenPgpTrustStore.Trust.undecided, openPgpStoreInstance1.getTrust(alice, finger2));
openPgpStoreInstance1.setTrust(alice, finger2, OpenPgpTrustStore.Trust.trusted);
assertEquals(OpenPgpTrustStore.Trust.trusted, openPgpStoreInstance1.getTrust(alice, finger2));
// Set trust a second time -> no change
openPgpStoreInstance1.setTrust(alice, finger2, OpenPgpTrustStore.Trust.trusted);
assertEquals(OpenPgpTrustStore.Trust.trusted, openPgpStoreInstance1.getTrust(alice, finger2));
assertEquals(OpenPgpTrustStore.Trust.undecided, openPgpStoreInstance1.getTrust(alice, finger3));
openPgpStoreInstance1.setTrust(bob, finger2, OpenPgpTrustStore.Trust.untrusted);
assertEquals(OpenPgpTrustStore.Trust.untrusted, openPgpStoreInstance1.getTrust(bob, finger2));
assertEquals(OpenPgpTrustStore.Trust.trusted, openPgpStoreInstance1.getTrust(alice, finger2));
// clean up
openPgpStoreInstance1.setTrust(alice, finger2, OpenPgpTrustStore.Trust.undecided);
openPgpStoreInstance1.setTrust(bob, finger2, OpenPgpTrustStore.Trust.undecided);
}
@Test
public void t09_trust_reloadTest() throws IOException {
openPgpStoreInstance1.setTrust(alice, finger1, OpenPgpTrustStore.Trust.trusted);
assertEquals(OpenPgpTrustStore.Trust.trusted, openPgpStoreInstance2.getTrust(alice, finger1));
// cleanup
openPgpStoreInstance1.setTrust(alice, finger1, OpenPgpTrustStore.Trust.undecided);
openPgpStoreInstance2.setTrust(alice, finger1, OpenPgpTrustStore.Trust.undecided);
}
/*
OpenPgpMetadataStore
*/
@Test
public void t10_meta_emptyStoreTest() throws IOException {
assertNotNull(openPgpStoreInstance1.getAnnouncedFingerprintsOf(alice));
assertTrue(openPgpStoreInstance1.getAnnouncedFingerprintsOf(alice).isEmpty());
Map<OpenPgpV4Fingerprint, Date> map = new HashMap<>();
Date date1 = new Date(12354563423L);
Date date2 = new Date(8274729879812L);
map.put(finger1, date1);
map.put(finger2, date2);
openPgpStoreInstance1.setAnnouncedFingerprintsOf(alice, map);
assertFalse(openPgpStoreInstance1.getAnnouncedFingerprintsOf(alice).isEmpty());
assertEquals(map, openPgpStoreInstance1.getAnnouncedFingerprintsOf(alice));
assertTrue(openPgpStoreInstance1.getAnnouncedFingerprintsOf(bob).isEmpty());
assertFalse(openPgpStoreInstance2.getAnnouncedFingerprintsOf(alice).isEmpty());
assertEquals(map, openPgpStoreInstance2.getAnnouncedFingerprintsOf(alice));
openPgpStoreInstance1.setAnnouncedFingerprintsOf(alice, Collections.<OpenPgpV4Fingerprint, Date>emptyMap());
openPgpStoreInstance2.setAnnouncedFingerprintsOf(alice, Collections.<OpenPgpV4Fingerprint, Date>emptyMap());
}
@Test
public void t11_key_fetchDateTest() throws IOException {
Map<OpenPgpV4Fingerprint, Date> fetchDates1 = openPgpStoreInstance1.getPublicKeyFetchDates(alice);
assertNotNull(fetchDates1);
assertTrue(fetchDates1.isEmpty());
Date date1 = new Date(85092830954L);
fetchDates1.put(finger1, date1);
openPgpStoreInstance1.setPublicKeyFetchDates(alice, fetchDates1);
Map<OpenPgpV4Fingerprint, Date> fetchDates2 = openPgpStoreInstance1.getPublicKeyFetchDates(alice);
assertNotNull(fetchDates2);
assertFalse(fetchDates2.isEmpty());
assertEquals(fetchDates1, fetchDates2);
Map<OpenPgpV4Fingerprint, Date> fetchDates3 = openPgpStoreInstance2.getPublicKeyFetchDates(alice);
assertNotNull(fetchDates3);
assertEquals(fetchDates1, fetchDates3);
openPgpStoreInstance1.setPublicKeyFetchDates(alice, null);
openPgpStoreInstance2.setPublicKeyFetchDates(alice, null);
assertNotNull(openPgpStoreInstance1.getPublicKeyFetchDates(alice));
assertTrue(openPgpStoreInstance1.getPublicKeyFetchDates(alice).isEmpty());
}
}

View file

@ -0,0 +1,187 @@
/**
*
* 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.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.Collections;
import java.util.Date;
import java.util.List;
import org.jivesoftware.smack.DummyConnection;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smack.util.FileUtils;
import org.jivesoftware.smackx.ox.crypto.OpenPgpElementAndMetadata;
import org.jivesoftware.smackx.ox.crypto.PainlessOpenPgpProvider;
import org.jivesoftware.smackx.ox.element.CryptElement;
import org.jivesoftware.smackx.ox.element.SignElement;
import org.jivesoftware.smackx.ox.element.SigncryptElement;
import org.jivesoftware.smackx.ox.exception.MissingUserIdOnKeyException;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpStore;
import org.jivesoftware.smackx.ox.store.filebased.FileBasedOpenPgpStore;
import org.bouncycastle.openpgp.PGPException;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.JidTestUtil;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.collection.PGPKeyRing;
import org.pgpainless.key.protection.UnprotectedKeysProtector;
import org.xmlpull.v1.XmlPullParserException;
public class PainlessOpenPgpProviderTest extends SmackTestSuite {
private static final File storagePath;
private static final BareJid alice = JidTestUtil.BARE_JID_1;
private static final BareJid bob = JidTestUtil.BARE_JID_2;
static {
storagePath = FileUtils.getTempDir("smack-painlessprovidertest");
}
@BeforeClass
@AfterClass
public static void deletePath() {
FileUtils.deleteDirectory(storagePath);
}
@Test
public void encryptDecryptTest() throws PGPException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, IOException, MissingUserIdOnKeyException, XmlPullParserException {
// Initialize
OpenPgpStore aliceStore = new FileBasedOpenPgpStore(storagePath);
OpenPgpStore bobStore = new FileBasedOpenPgpStore(storagePath);
aliceStore.setKeyRingProtector(new UnprotectedKeysProtector());
bobStore.setKeyRingProtector(new UnprotectedKeysProtector());
PainlessOpenPgpProvider aliceProvider = new PainlessOpenPgpProvider(new DummyConnection(), aliceStore);
PainlessOpenPgpProvider bobProvider = new PainlessOpenPgpProvider(new DummyConnection(), bobStore);
PGPKeyRing aliceKeys = aliceStore.generateKeyRing(alice);
PGPKeyRing bobKeys = bobStore.generateKeyRing(bob);
OpenPgpV4Fingerprint aliceFingerprint = new OpenPgpV4Fingerprint(aliceKeys.getPublicKeys());
OpenPgpV4Fingerprint bobFingerprint = new OpenPgpV4Fingerprint(bobKeys.getPublicKeys());
aliceStore.importSecretKey(alice, aliceKeys.getSecretKeys());
bobStore.importSecretKey(bob, bobKeys.getSecretKeys());
aliceStore.setAnnouncedFingerprintsOf(alice, Collections.singletonMap(new OpenPgpV4Fingerprint(aliceKeys.getPublicKeys()), new Date()));
bobStore.setAnnouncedFingerprintsOf(bob, Collections.singletonMap(new OpenPgpV4Fingerprint(bobKeys.getPublicKeys()), new Date()));
OpenPgpSelf aliceSelf = new OpenPgpSelf(alice, aliceStore);
aliceSelf.trust(aliceFingerprint);
OpenPgpSelf bobSelf = new OpenPgpSelf(bob, bobStore);
bobSelf.trust(bobFingerprint);
// Exchange keys
aliceStore.importPublicKey(bob, bobKeys.getPublicKeys());
bobStore.importPublicKey(alice, aliceKeys.getPublicKeys());
aliceStore.setAnnouncedFingerprintsOf(bob, Collections.singletonMap(new OpenPgpV4Fingerprint(bobKeys.getPublicKeys()), new Date()));
bobStore.setAnnouncedFingerprintsOf(alice, Collections.singletonMap(new OpenPgpV4Fingerprint(aliceKeys.getPublicKeys()), new Date()));
OpenPgpContact aliceForBob = new OpenPgpContact(alice, bobStore);
aliceForBob.trust(aliceFingerprint);
OpenPgpContact bobForAlice = new OpenPgpContact(bob, aliceStore);
bobForAlice.trust(bobFingerprint);
// Prepare message
Message.Body body = new Message.Body(null, "Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.");
List<ExtensionElement> payload = Collections.<ExtensionElement>singletonList(body);
OpenPgpElementAndMetadata encrypted;
OpenPgpMessage decrypted;
/*
test signcrypt
*/
SigncryptElement signcryptElement = new SigncryptElement(Collections.<Jid>singleton(bob), payload);
// Encrypt and Sign
encrypted = aliceProvider.signAndEncrypt(signcryptElement, aliceSelf, Collections.singleton(bobForAlice));
// Decrypt and Verify
decrypted = bobProvider.decryptAndOrVerify(encrypted.getElement(), bobSelf, aliceForBob);
OpenPgpV4Fingerprint decryptionFingerprint = decrypted.getMetadata().getDecryptionFingerprint();
assertTrue(bobSelf.getSecretKeys().contains(decryptionFingerprint.getKeyId()));
assertTrue(decrypted.getMetadata().getVerifiedSignaturesFingerprints().contains(aliceFingerprint));
assertEquals(OpenPgpMessage.State.signcrypt, decrypted.getState());
SigncryptElement decryptedSignCrypt = (SigncryptElement) decrypted.getOpenPgpContentElement();
assertEquals(body.getMessage(), decryptedSignCrypt.<Message.Body>getExtension(Message.Body.ELEMENT, Message.Body.NAMESPACE).getMessage());
/*
test crypt
*/
CryptElement cryptElement = new CryptElement(Collections.<Jid>singleton(bob), payload);
// Encrypt
encrypted = aliceProvider.encrypt(cryptElement, aliceSelf, Collections.singleton(bobForAlice));
decrypted = bobProvider.decryptAndOrVerify(encrypted.getElement(), bobSelf, aliceForBob);
decryptionFingerprint = decrypted.getMetadata().getDecryptionFingerprint();
assertTrue(bobSelf.getSecretKeys().contains(decryptionFingerprint.getKeyId()));
assertTrue(decrypted.getMetadata().getVerifiedSignaturesFingerprints().isEmpty());
assertEquals(OpenPgpMessage.State.crypt, decrypted.getState());
CryptElement decryptedCrypt = (CryptElement) decrypted.getOpenPgpContentElement();
assertEquals(body.getMessage(), decryptedCrypt.<Message.Body>getExtension(Message.Body.ELEMENT, Message.Body.NAMESPACE).getMessage());
/*
test sign
*/
SignElement signElement = new SignElement(Collections.<Jid>singleton(bob), new Date(), payload);
// Sign
encrypted = aliceProvider.sign(signElement, aliceSelf);
decrypted = bobProvider.decryptAndOrVerify(encrypted.getElement(), bobSelf, aliceForBob);
assertNull(decrypted.getMetadata().getDecryptionFingerprint());
assertTrue(decrypted.getMetadata().getVerifiedSignaturesFingerprints().contains(aliceFingerprint));
assertEquals(OpenPgpMessage.State.sign, decrypted.getState());
SignElement decryptedSign = (SignElement) decrypted.getOpenPgpContentElement();
assertEquals(body.getMessage(), decryptedSign.<Message.Body>getExtension(Message.Body.ELEMENT, Message.Body.NAMESPACE).getMessage());
}
}

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;
import static junit.framework.TestCase.assertEquals;
import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil;
import org.bouncycastle.openpgp.PGPException;
import org.junit.Test;
import org.pgpainless.key.OpenPgpV4Fingerprint;
public class PubSubDelegateTest extends SmackTestSuite {
@Test
public void pubkeyNodeNameTest() throws PGPException {
OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint("486f7065207520646f6e2068617665204f43640a");
assertEquals("urn:xmpp:openpgp:0:public-keys:486F7065207520646F6E2068617665204F43640A",
OpenPgpPubSubUtil.PEP_NODE_PUBLIC_KEY(fingerprint));
}
}

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;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertTrue;
import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Date;
import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smack.test.util.TestUtils;
import org.jivesoftware.smackx.ox.element.PubkeyElement;
import org.jivesoftware.smackx.ox.provider.PubkeyElementProvider;
import org.junit.Test;
import org.jxmpp.util.XmppDateTime;
import org.xmlpull.v1.XmlPullParser;
public class PubkeyElementTest extends SmackTestSuite {
@Test
public void providerTest() throws Exception {
String expected =
"<pubkey xmlns='urn:xmpp:openpgp:0' date='2018-01-21T10:46:21.000+00:00'>" +
"<data>" +
"BASE64_OPENPGP_PUBLIC_KEY" +
"</data>" +
"</pubkey>";
Date date = XmppDateTime.parseXEP0082Date("2018-01-21T10:46:21.000+00:00");
byte[] key = "BASE64_OPENPGP_PUBLIC_KEY".getBytes(Charset.forName("UTF-8"));
PubkeyElement element = new PubkeyElement(new PubkeyElement.PubkeyDataElement(key), date);
assertXMLEqual(expected, element.toXML(null).toString());
XmlPullParser parser = TestUtils.getParser(expected);
PubkeyElement parsed = PubkeyElementProvider.TEST_INSTANCE.parse(parser);
assertEquals(element.getDate(), parsed.getDate());
assertTrue(Arrays.equals(element.getDataElement().getB64Data(), parsed.getDataElement().getB64Data()));
}
}

View file

@ -0,0 +1,88 @@
/**
*
* 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 org.custommonkey.xmlunit.XMLAssert.assertXMLEqual;
import java.util.Date;
import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smack.test.util.TestUtils;
import org.jivesoftware.smackx.ox.element.PublicKeysListElement;
import org.jivesoftware.smackx.ox.provider.PublicKeysListElementProvider;
import org.bouncycastle.openpgp.PGPException;
import org.junit.Test;
import org.jxmpp.util.XmppDateTime;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.xmlpull.v1.XmlPullParser;
public class PublicKeysListElementTest extends SmackTestSuite {
@Test
public void providerTest() throws Exception {
String expected =
"<public-keys-list xmlns='urn:xmpp:openpgp:0'>" +
"<pubkey-metadata " +
"v4-fingerprint='1357B01865B2503C18453D208CAC2A9678548E35' " +
"date='2018-03-01T15:26:12.000+00:00'" +
"/>" +
"<pubkey-metadata " +
"v4-fingerprint='67819B343B2AB70DED9320872C6464AF2A8E4C02' " +
"date='1953-05-16T12:00:00.000+00:00'" +
"/>" +
"</public-keys-list>";
Date date1 = XmppDateTime.parseDate("2018-03-01T15:26:12.000+00:00");
Date date2 = XmppDateTime.parseDate("1953-05-16T12:00:00.000+00:00");
PublicKeysListElement.PubkeyMetadataElement child1 =
new PublicKeysListElement.PubkeyMetadataElement(
new OpenPgpV4Fingerprint("1357B01865B2503C18453D208CAC2A9678548E35"), date1);
PublicKeysListElement.PubkeyMetadataElement child2 =
new PublicKeysListElement.PubkeyMetadataElement(
new OpenPgpV4Fingerprint("67819B343B2AB70DED9320872C6464AF2A8E4C02"), date2);
PublicKeysListElement element = PublicKeysListElement.builder()
.addMetadata(child1)
.addMetadata(child2)
.build();
assertXMLEqual(expected, element.toXML(null).toString());
XmlPullParser parser = TestUtils.getParser(expected);
PublicKeysListElement parsed = PublicKeysListElementProvider.TEST_INSTANCE.parse(parser);
assertEquals(element.getMetadata(), parsed.getMetadata());
}
@Test
public void listBuilderRefusesDuplicatesTest() throws PGPException {
PublicKeysListElement.Builder builder = PublicKeysListElement.builder();
String fp40 = "49545320414c4c2041424f555420444120484558";
Date oneDate = new Date(12337883234L);
Date otherDate = new Date(8888348384L);
// Check if size of metadata is one after insert.
builder.addMetadata(new PublicKeysListElement.PubkeyMetadataElement(new OpenPgpV4Fingerprint(fp40), oneDate));
assertEquals(builder.build().getMetadata().size(), 1);
// Check if size is still one after inserting element with same fp.
builder.addMetadata(new PublicKeysListElement.PubkeyMetadataElement(new OpenPgpV4Fingerprint(fp40), otherDate));
assertEquals(builder.build().getMetadata().size(), 1);
}
}

View file

@ -0,0 +1,103 @@
/**
*
* 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.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.Collections;
import org.jivesoftware.smack.DummyConnection;
import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smack.util.FileUtils;
import org.jivesoftware.smackx.ox.crypto.PainlessOpenPgpProvider;
import org.jivesoftware.smackx.ox.element.SecretkeyElement;
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.store.filebased.FileBasedOpenPgpStore;
import org.jivesoftware.smackx.ox.util.SecretKeyBackupHelper;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.impl.JidCreate;
import org.pgpainless.PGPainless;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.collection.PGPKeyRing;
public class SecretKeyBackupHelperTest extends SmackTestSuite {
private static final File basePath;
static {
basePath = FileUtils.getTempDir("ox_secret_keys");
}
@Test
public void backupPasswordGenerationTest() {
final String alphabet = "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ";
String backupCode = SecretKeyBackupHelper.generateBackupPassword();
assertEquals(29, backupCode.length());
for (int i = 0; i < backupCode.length(); i++) {
if ((i + 1) % 5 == 0) {
assertEquals('-', backupCode.charAt(i));
} else {
assertTrue(alphabet.indexOf(backupCode.charAt(i)) != -1);
}
}
}
@Test
public void createAndDecryptSecretKeyElementTest()
throws PGPException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException,
IOException, MissingUserIdOnKeyException, MissingOpenPgpKeyException, InvalidBackupCodeException {
// Prepare store and provider and so on...
FileBasedOpenPgpStore store = new FileBasedOpenPgpStore(basePath);
PainlessOpenPgpProvider provider = new PainlessOpenPgpProvider(new DummyConnection(), store);
// Generate and import key
PGPKeyRing keyRing = PGPainless.generateKeyRing().simpleEcKeyRing("xmpp:alice@wonderland.lit");
BareJid jid = JidCreate.bareFrom("alice@wonderland.lit");
provider.getStore().importSecretKey(jid, keyRing.getSecretKeys());
// Create encrypted backup
String backupCode = SecretKeyBackupHelper.generateBackupPassword();
SecretkeyElement element = SecretKeyBackupHelper.createSecretkeyElement(provider, jid, Collections.singleton(new OpenPgpV4Fingerprint(keyRing.getSecretKeys())), backupCode);
// Decrypt backup and compare
PGPSecretKeyRing secretKeyRing = SecretKeyBackupHelper.restoreSecretKeyBackup(element, backupCode);
assertTrue(Arrays.equals(keyRing.getSecretKeys().getEncoded(), secretKeyRing.getEncoded()));
}
@AfterClass
@BeforeClass
public static void deleteDirs() {
FileUtils.deleteDirectory(basePath);
}
}

View file

@ -0,0 +1,52 @@
/**
*
* 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.assertTrue;
import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual;
import java.nio.charset.Charset;
import java.util.Arrays;
import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smack.test.util.TestUtils;
import org.jivesoftware.smackx.ox.element.SecretkeyElement;
import org.jivesoftware.smackx.ox.provider.SecretkeyElementProvider;
import org.junit.Test;
import org.xmlpull.v1.XmlPullParser;
public class SecretkeyElementTest extends SmackTestSuite {
@Test
public void providerTest() throws Exception {
String expected =
"<secretkey xmlns='urn:xmpp:openpgp:0'>" +
"BASE64_OPENPGP_ENCRYPTED_SECRET_KEY" +
"</secretkey>";
byte[] key = "BASE64_OPENPGP_ENCRYPTED_SECRET_KEY".getBytes(Charset.forName("UTF-8"));
SecretkeyElement element = new SecretkeyElement(key);
assertXMLEqual(expected, element.toXML(null).toString());
XmlPullParser parser = TestUtils.getParser(expected);
SecretkeyElement parsed = SecretkeyElementProvider.TEST_INSTANCE.parse(parser);
assertTrue(Arrays.equals(element.getB64Data(), parsed.getB64Data()));
}
}

View file

@ -0,0 +1,162 @@
/**
*
* 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.jxmpp.jid.BareJid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
public class TestKeys {
public TestKeys() {
}
public static final BareJid JULIET_JID;
public static final BareJid ROMEO_JID;
static {
try {
JULIET_JID = JidCreate.bareFrom("juliet@capulet.lit");
ROMEO_JID = JidCreate.bareFrom("romeo@montague.lit");
} catch (XmppStringprepException e) {
throw new AssertionError(e);
}
}
public static final String JULIET_UID = "xmpp:juliet@capulet.lit";
/**
* Public key of xmpp:juliet@capulet.lit.
*/
public static final String JULIET_PUB = "" +
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n" +
"\n" +
"mQENBFrxov4BCAChZwPrBxxIlwzpieR5T2pnaOZLWH0WqSON6rVjvfbJHWdDi3Th\n" +
"remHW4gg4IBSTXkVFDIeQNVcOvGNgMg3Oe/x0I6FK12jrw9prycmjFxQ7A0ix7ZG\n" +
"UkTF5jITgzJbkH100gYfXtZsfTyvgISSAT//6vvvQPZ3zCr09XvAG0CyQ1BhULsv\n" +
"mVRe4Oh5b0VK4kLdv+GiA/T+49UKZj6lne9Vdti16ZIj7teVCbicfdhpTzsjur42\n" +
"r8ptouKAuyFPw9KnGNwVlIiv5jt/Kit/LoOBenh74sitsCXq8IQ9kKp/eNt8TF4u\n" +
"D4IGpxnJfB8XCiixYHoFEajmQBVJXNYtvoPvABEBAAG0F3htcHA6anVsaWV0QGNh\n" +
"cHVsZXQubGl0iQFOBBMBCAA4FiEEHQGMdy34xe+GodzJtLUJy1k24D4FAlrxov4C\n" +
"Gy8FCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQtLUJy1k24D6H7AgAoTjx4ezc\n" +
"A83NeOY3tMHVQTM7hKuy0wMcSzQgVgJmhLYRZS8r+FocPZua/eke49GPhe2yozvl\n" +
"ByWHtotklQeJiwOKxuPKMzneVA1ZK3/9LdGvtZlHMcAkEKDhit8HIaEcsFd4Z1re\n" +
"EhF2lyvY/E+rrx9YxV0QjisSWV2dSptv6FeGSztr9e5E+Head6hEQhsugiTVRF+1\n" +
"6mG90te0WGQ9YNiJ2FJovx5kBLTTuhwUz8Oacqihd2+RDDI5p3wJoogVL31aNb4n\n" +
"c7dGo8ieJPHGlkBsOfmreSxijTodZz9MXsgcx7b//u0uQryViJoZHWbtnXOFjjNc\n" +
"GWBtS084NKWl9w==\n" +
"=ecwX\n" +
"-----END PGP PUBLIC KEY BLOCK-----";
/**
* Private key of xmpp:juliet@capulet.lit.
*/
public static final String JULIET_PRIV = "" +
"-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"\n" +
"lQOYBFrxov4BCAChZwPrBxxIlwzpieR5T2pnaOZLWH0WqSON6rVjvfbJHWdDi3Th\n" +
"remHW4gg4IBSTXkVFDIeQNVcOvGNgMg3Oe/x0I6FK12jrw9prycmjFxQ7A0ix7ZG\n" +
"UkTF5jITgzJbkH100gYfXtZsfTyvgISSAT//6vvvQPZ3zCr09XvAG0CyQ1BhULsv\n" +
"mVRe4Oh5b0VK4kLdv+GiA/T+49UKZj6lne9Vdti16ZIj7teVCbicfdhpTzsjur42\n" +
"r8ptouKAuyFPw9KnGNwVlIiv5jt/Kit/LoOBenh74sitsCXq8IQ9kKp/eNt8TF4u\n" +
"D4IGpxnJfB8XCiixYHoFEajmQBVJXNYtvoPvABEBAAEAB/4jMbXagW3q7DkOEZnm\n" +
"0+jVTLvu0QhRsScGEphj+++8sfMq+NVPQp9p+w0Hcjy49ZjB/mnhS+zaVCYI33yJ\n" +
"AlKubXYuVqLwBsO7HUzRrIiSwq4ol9jIo7bIWmYv+As6iRq6JvPb0k+6T2K0uDbw\n" +
"KWKduM0fwhAcVkJFsOO/o5GrbQaJc3oioFk8uFWTnO+FPBRTJ9oTlVG2M/tEatZK\n" +
"gl7I8Ukl0YYruCNUFKZ0tvO8HqulxBgUbGPBer1uOlfUD4RXdc8/PUiFKNo48XSu\n" +
"ZUEAZKGbFBjuX5Z8ha7+sUMEYEt70qlbkiLQxgHKAmpyridAk3q/SB3y2VB8Ik7I\n" +
"gpExBADInzLROYuUcXqmty+znVwm6nRIB75JBAy778zgIxx1v0O3QlVnR+YI8gJM\n" +
"mQ/9pD6LyP9hktWDmJxG8tX+kSuIp3wNJc5EMeXtCCmkUW0CP1gUhAbNW3MezKa5\n" +
"II5IhE9RgIsYqSU8ZgeIh72ON8XTp8i/wGipCXvJPggSAMXukQQAzfRmtLW+JHEK\n" +
"B8ETIYh8IUjXJ6TVlmuBwZ0eXjCpqy9arJi6tacesDJwnL3sqOMQWUmqGsCGSKA5\n" +
"cLITkVsxX/htIq8GFyludjg8t4Nr+fOGfChEq8QE0PHE2CgskQMHpfHvfIdnwKve\n" +
"Fg2Q8twoMw849O6PF3k/848Z65lDin8EAMDbuPWL7KU2sWeqvDEuoulS5K1gsq8X\n" +
"p3Od3+f0OG8YViMjKcVlSKHVvdlK4dlsccJrJJx6VzotV47LsmvVbzDwUE//MYq7\n" +
"QwwQetZbpdQZDysSGVqHMTuAg/1pr2u5rqh4cFqCYatgZwinEI2TQMXEqnSc+mj8\n" +
"xp/LNq5BZZQuO4y0F3htcHA6anVsaWV0QGNhcHVsZXQubGl0iQFOBBMBCAA4FiEE\n" +
"HQGMdy34xe+GodzJtLUJy1k24D4FAlrxov4CGy8FCwkIBwIGFQoJCAsCBBYCAwEC\n" +
"HgECF4AACgkQtLUJy1k24D6H7AgAoTjx4ezcA83NeOY3tMHVQTM7hKuy0wMcSzQg\n" +
"VgJmhLYRZS8r+FocPZua/eke49GPhe2yozvlByWHtotklQeJiwOKxuPKMzneVA1Z\n" +
"K3/9LdGvtZlHMcAkEKDhit8HIaEcsFd4Z1reEhF2lyvY/E+rrx9YxV0QjisSWV2d\n" +
"Sptv6FeGSztr9e5E+Head6hEQhsugiTVRF+16mG90te0WGQ9YNiJ2FJovx5kBLTT\n" +
"uhwUz8Oacqihd2+RDDI5p3wJoogVL31aNb4nc7dGo8ieJPHGlkBsOfmreSxijTod\n" +
"Zz9MXsgcx7b//u0uQryViJoZHWbtnXOFjjNcGWBtS084NKWl9w==\n" +
"=yPPE\n" +
"-----END PGP PRIVATE KEY BLOCK-----";
public static final String ROMEO_UID = "xmpp:romeo@montague.lit";
/**
* Public key of xmpp:romeo@montague.lit.
*/
public static final String ROMEO_PUB = "" +
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n" +
"\n" +
"mQENBFrxopkBCADiYg/+mEObXgxuMW6/LFKpEyaJK9pBMgutuxnYZ9PXWZmOhDIT\n" +
"Ugm9X9YJ3Qh94KaHge9F4uCeFASmM1vvUTRFTEb1W5RR9ZE/sy/cdAttnZ5JloPi\n" +
"CT3HDMIJAxIXhRJkeUR9GUb51ql27bMXl6lFh865VdNSXN/B8FzRQHENxv1Bq/6Z\n" +
"iQOViIETeRRgO+u6u2iZkYlHgYMaoMK7+YiNlHXanU9Atcuaz0ZCJS/XFNH89iqB\n" +
"Kvnv7KCQh4FhrNMLJRzNPXV8MY05nn0zF72qeEsniB16Xde18lMro8fQehg2mLwc\n" +
"XGtCwCKI6QbZVxYQt77r3ZACiwl66soFWijVABEBAAG0F3htcHA6cm9tZW9AbW9u\n" +
"dGFndWUubGl0iQFOBBMBCAA4FiEENdKZ0IovfYAjCwldBKMhguBeIfcFAlrxopkC\n" +
"Gy8FCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQBKMhguBeIfcj8AgAu1wubUwr\n" +
"2aQmDN3OqRM4M4yRL3oyYMkCKIjqD6KEeFsIXSSkXOuREJKEo8Mb1+ewV0SYmHCC\n" +
"K3bKKq3m71AQ7evDhKGshacPYesiDvMdHWQdQnjfaoHhyn9qIKl7H0Xv1yf/wyuG\n" +
"ANy1jYgtCEuYw7D+EsqNDdn8Xh+k/9s4aMI/6mfC0yGZgG8EyLTfbZkGPoS4aZfV\n" +
"AGFbuqryg48dXtnuzAPKcdgMTTMSnmR729YlfkjCffcFaldyXoe1VMbudUO7nkO9\n" +
"g65i5EXenkbc2h0TRDQ4lDFQyModqFTwYFYxAf/RA6tuhIQEoCnpCytFMvrRKMb3\n" +
"Bx5vYRDVmE3jeg==\n" +
"=2jSg\n" +
"-----END PGP PUBLIC KEY BLOCK-----";
/**
* Private key of xmpp:romeo@montague.lit.
*/
public static final String ROMEO_PRIV = "" +
"-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"\n" +
"lQOYBFrxopkBCADiYg/+mEObXgxuMW6/LFKpEyaJK9pBMgutuxnYZ9PXWZmOhDIT\n" +
"Ugm9X9YJ3Qh94KaHge9F4uCeFASmM1vvUTRFTEb1W5RR9ZE/sy/cdAttnZ5JloPi\n" +
"CT3HDMIJAxIXhRJkeUR9GUb51ql27bMXl6lFh865VdNSXN/B8FzRQHENxv1Bq/6Z\n" +
"iQOViIETeRRgO+u6u2iZkYlHgYMaoMK7+YiNlHXanU9Atcuaz0ZCJS/XFNH89iqB\n" +
"Kvnv7KCQh4FhrNMLJRzNPXV8MY05nn0zF72qeEsniB16Xde18lMro8fQehg2mLwc\n" +
"XGtCwCKI6QbZVxYQt77r3ZACiwl66soFWijVABEBAAEAB/4mu5p69/hRQ+UikWie\n" +
"Yun9rZ4hSBR+pR5kaifA4/rV1Km2PZ4HujiaYyRO6beDOgWkF7IlpezCfzBQc2ce\n" +
"ailkVemqHzIgV8CzQmhE8sHlzlr/wjXsXaJpRSCJxDG7PnRoJmt2b/W512WFSKQk\n" +
"vDklAVh4U1vlsqhCGWr4DmuJbJkRyDhcX01tplRwim283F7bGqRcMBmKMZHiMgVc\n" +
"0u84EYKKVizJ3YAaaVqZyHb4qdeKK2ak3fPNuGT/oGd2sxnkL+BZGjJpu3RGpTA1\n" +
"tbOvOQnJGHQtABFxE8n6H9dHPJGtgyz2+udjUhL/P/E3PDoXazZkXRq2oHZKgg0f\n" +
"AwOBBADsWncHgvz15rXPF7O6AivbGTJ5ctkgVy4U3Fu2sk9rf0fx0sryBSqtTBw1\n" +
"Uvn/p9RwTsKw6fng6Nf78xpZFlUDB00YCcuWkGodxvjTAyB0dtBmkhopeKi0dmHh\n" +
"ndnR6Pv0CsXu8nG7lUi+q6s3oc4h2OfDBhrqsyYY5M2gGit3dQQA9TNuinJD9XXv\n" +
"QRyauMnSJ5xRcfOu8QCxZlllCvffZjSGCPoVjUpJEe9qsVbXVj2GYCxjLCSXV0V+\n" +
"vlJfdPrl1BhZ3fmEpg0u7SyGDDOe8fe1ehk5sAeL8O0eFWlPSEaEccsjlpJ2FO0n\n" +
"P04SZdOeM6wmhDTEDzpFnjbPndQTH+ED/R1zNzr55DvxQodmrW/BvTmhGQ22rHtk\n" +
"IUfbeMaVfUvNLJA/JksrUIx3Gga9QCDZgfm1RsRhLUlHiqTQe23sPWgKOsbf5O1j\n" +
"XJZaCNZ7LloVQbkG7xFcnb/n1+JjBr4FxXjAA6cY/iRGlznjIIaasyklKm1/4LuQ\n" +
"hnH3QqTvCN3dOFS0F3htcHA6cm9tZW9AbW9udGFndWUubGl0iQFOBBMBCAA4FiEE\n" +
"NdKZ0IovfYAjCwldBKMhguBeIfcFAlrxopkCGy8FCwkIBwIGFQoJCAsCBBYCAwEC\n" +
"HgECF4AACgkQBKMhguBeIfcj8AgAu1wubUwr2aQmDN3OqRM4M4yRL3oyYMkCKIjq\n" +
"D6KEeFsIXSSkXOuREJKEo8Mb1+ewV0SYmHCCK3bKKq3m71AQ7evDhKGshacPYesi\n" +
"DvMdHWQdQnjfaoHhyn9qIKl7H0Xv1yf/wyuGANy1jYgtCEuYw7D+EsqNDdn8Xh+k\n" +
"/9s4aMI/6mfC0yGZgG8EyLTfbZkGPoS4aZfVAGFbuqryg48dXtnuzAPKcdgMTTMS\n" +
"nmR729YlfkjCffcFaldyXoe1VMbudUO7nkO9g65i5EXenkbc2h0TRDQ4lDFQyMod\n" +
"qFTwYFYxAf/RA6tuhIQEoCnpCytFMvrRKMb3Bx5vYRDVmE3jeg==\n" +
"=LZ1b\n" +
"-----END PGP PRIVATE KEY BLOCK-----";
}

View file

@ -0,0 +1,175 @@
/**
*
* 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.assertEquals;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNotNull;
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.Collections;
import java.util.Date;
import org.jivesoftware.smack.DummyConnection;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smack.util.FileUtils;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.eme.element.ExplicitMessageEncryptionElement;
import org.jivesoftware.smackx.ox.OpenPgpContact;
import org.jivesoftware.smackx.ox.OpenPgpManager;
import org.jivesoftware.smackx.ox.OpenPgpMessage;
import org.jivesoftware.smackx.ox.OpenPgpSelf;
import org.jivesoftware.smackx.ox.crypto.PainlessOpenPgpProvider;
import org.jivesoftware.smackx.ox.element.OpenPgpElement;
import org.jivesoftware.smackx.ox.element.SigncryptElement;
import org.jivesoftware.smackx.ox.exception.MissingUserIdOnKeyException;
import org.jivesoftware.smackx.ox.store.filebased.FileBasedOpenPgpStore;
import org.bouncycastle.openpgp.PGPException;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.JidTestUtil;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.xmlpull.v1.XmlPullParserException;
public class OXInstantMessagingManagerTest extends SmackTestSuite {
private static final File basePath;
static {
basePath = FileUtils.getTempDir("ox_im_test_" + StringUtils.randomString(10));
}
@Test
public void test() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException,
NoSuchProviderException, SmackException, MissingUserIdOnKeyException, InterruptedException, XMPPException,
XmlPullParserException {
DummyConnection aliceCon = new DummyConnection(
DummyConnection.DummyConnectionConfiguration.builder()
.setXmppDomain(JidTestUtil.EXAMPLE_ORG)
.setUsernameAndPassword("alice", "dummypass").build());
aliceCon.connect().login();
DummyConnection bobCon = new DummyConnection(
DummyConnection.DummyConnectionConfiguration.builder()
.setXmppDomain(JidTestUtil.EXAMPLE_ORG)
.setUsernameAndPassword("bob", "dummypass").build());
bobCon.connect().login();
FileBasedOpenPgpStore aliceStore = new FileBasedOpenPgpStore(new File(basePath, "alice"));
FileBasedOpenPgpStore bobStore = new FileBasedOpenPgpStore(new File(basePath, "bob"));
PainlessOpenPgpProvider aliceProvider = new PainlessOpenPgpProvider(aliceCon, aliceStore);
PainlessOpenPgpProvider bobProvider = new PainlessOpenPgpProvider(bobCon, bobStore);
OpenPgpManager aliceOpenPgp = OpenPgpManager.getInstanceFor(aliceCon);
OpenPgpManager bobOpenPgp = OpenPgpManager.getInstanceFor(bobCon);
aliceOpenPgp.setOpenPgpProvider(aliceProvider);
bobOpenPgp.setOpenPgpProvider(bobProvider);
OXInstantMessagingManager aliceOxim = OXInstantMessagingManager.getInstanceFor(aliceCon);
OXInstantMessagingManager bobOxim = OXInstantMessagingManager.getInstanceFor(bobCon);
OpenPgpSelf aliceSelf = aliceOpenPgp.getOpenPgpSelf();
OpenPgpSelf bobSelf = bobOpenPgp.getOpenPgpSelf();
assertFalse(aliceSelf.hasSecretKeyAvailable());
assertFalse(bobSelf.hasSecretKeyAvailable());
// Generate keys
aliceOpenPgp.generateAndImportKeyPair(aliceSelf.getJid());
bobOpenPgp.generateAndImportKeyPair(bobSelf.getJid());
assertTrue(aliceSelf.hasSecretKeyAvailable());
assertTrue(bobSelf.hasSecretKeyAvailable());
assertTrue(aliceSelf.isTrusted(aliceSelf.getSigningKeyFingerprint()));
assertTrue(bobSelf.isTrusted(bobSelf.getSigningKeyFingerprint()));
assertTrue(aliceSelf.getTrustedFingerprints().contains(aliceSelf.getSigningKeyFingerprint()));
// Exchange keys
aliceStore.importPublicKey(bobSelf.getJid(), bobSelf.getAnnouncedPublicKeys().iterator().next());
bobStore.importPublicKey(aliceSelf.getJid(), aliceSelf.getAnnouncedPublicKeys().iterator().next());
// Simulate key announcement
bobStore.setAnnouncedFingerprintsOf(bobSelf.getJid(), Collections.singletonMap(bobSelf.getSigningKeyFingerprint(), new Date()));
bobStore.setAnnouncedFingerprintsOf(aliceSelf.getJid(), Collections.singletonMap(aliceSelf.getSigningKeyFingerprint(), new Date()));
aliceStore.setAnnouncedFingerprintsOf(aliceSelf.getJid(), Collections.singletonMap(aliceSelf.getSigningKeyFingerprint(), new Date()));
aliceStore.setAnnouncedFingerprintsOf(bobSelf.getJid(), Collections.singletonMap(bobSelf.getSigningKeyFingerprint(), new Date()));
OpenPgpContact aliceForBob = bobOpenPgp.getOpenPgpContact((EntityBareJid) aliceSelf.getJid());
OpenPgpContact bobForAlice = aliceOpenPgp.getOpenPgpContact((EntityBareJid) bobSelf.getJid());
assertTrue(aliceForBob.hasUndecidedKeys());
assertTrue(bobForAlice.hasUndecidedKeys());
assertTrue(aliceForBob.getUndecidedFingerprints().contains(aliceSelf.getSigningKeyFingerprint()));
assertTrue(bobForAlice.getUndecidedFingerprints().contains(bobSelf.getSigningKeyFingerprint()));
bobForAlice.trust(bobSelf.getSigningKeyFingerprint());
aliceForBob.trust(aliceSelf.getSigningKeyFingerprint());
assertFalse(aliceForBob.hasUndecidedKeys());
assertFalse(bobForAlice.hasUndecidedKeys());
Message message = new Message();
assertFalse(ExplicitMessageEncryptionElement.hasProtocol(message, ExplicitMessageEncryptionElement.ExplicitMessageEncryptionProtocol.openpgpV0));
aliceOxim.addOxMessage(message, bobForAlice,
Collections.<ExtensionElement>singletonList(new Message.Body(null, "Hello World!")));
assertTrue(ExplicitMessageEncryptionElement.hasProtocol(message, ExplicitMessageEncryptionElement.ExplicitMessageEncryptionProtocol.openpgpV0));
assertNotNull(OpenPgpElement.fromStanza(message));
OpenPgpMessage decrypted = bobOpenPgp.decryptOpenPgpElement(OpenPgpElement.fromStanza(message), aliceForBob);
assertEquals(OpenPgpMessage.State.signcrypt, decrypted.getState());
SigncryptElement signcryptElement = (SigncryptElement) decrypted.getOpenPgpContentElement();
Message.Body body = signcryptElement.getExtension(Message.Body.ELEMENT, Message.Body.NAMESPACE);
assertNotNull(body);
assertEquals("Hello World!", body.getMessage());
OpenPgpMetadata metadata = decrypted.getMetadata();
assertTrue(metadata.isSigned() && metadata.isEncrypted());
// Check, if one of Bobs keys was used for decryption
assertNotNull(bobSelf.getSigningKeyRing().getPublicKey(metadata.getDecryptionFingerprint().getKeyId()));
// Check if one of Alice' keys was used for signing
assertTrue(metadata.containsVerifiedSignatureFrom(
aliceForBob.getTrustedAnnouncedKeys().iterator().next()));
}
@AfterClass
@BeforeClass
public static void deleteDirs() {
FileUtils.deleteDirectory(basePath);
}
}