From c77aee7010e4c8908aed63222340e3732c2faa01 Mon Sep 17 00:00:00 2001 From: vanitasvitae Date: Sun, 20 Aug 2017 17:42:10 +0200 Subject: [PATCH] Add support for XEP-xxxx Jingle Encrypted Transports --- .../smackx/ciphers/Aes128GcmNoPadding.java | 43 ++++ .../smackx/ciphers/Aes256GcmNoPadding.java | 43 ++++ .../smackx/ciphers/AesGcmNoPadding.java | 144 ++++++++++++++ .../smackx/ciphers/package-info.java | 22 +++ .../jivesoftware/smackx/jet/JetManager.java | 187 ++++++++++++++++++ .../smackx/jet/JetSecurityAdapter.java | 35 ++++ .../smackx/jet/JingleEnvelopeManager.java | 52 +++++ .../smackx/jet/component/JetSecurity.java | 142 +++++++++++++ .../JetSecurityBytestreamSession.java | 51 +++++ .../smackx/jet/component/package-info.java | 22 +++ .../jet/element/JetSecurityElement.java | 81 ++++++++ .../smackx/jet/element/package-info.java | 22 +++ .../jivesoftware/smackx/jet/package-info.java | 21 ++ .../jet/provider/JetSecurityProvider.java | 65 ++++++ .../smackx/jet/provider/package-info.java | 22 +++ .../smackx/ciphers/AesGcmNoPaddingTest.java | 101 ++++++++++ .../smackx/jet/JetElementTest.java | 97 +++++++++ .../smackx/jet/JetIntegrationTest.java | 145 ++++++++++++++ .../jivesoftware/smackx/jet/package-info.java | 21 ++ .../omemo/OmemoIntegrationTestHelper.java | 11 +- .../smackx/omemo/OmemoStoreTest.java | 3 + ...ckOmemoSignalIntegrationTestFramework.java | 2 +- .../smackx/omemo/OmemoManager.java | 60 +++++- .../smackx/omemo/OmemoService.java | 1 - 24 files changed, 1382 insertions(+), 11 deletions(-) create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/ciphers/Aes128GcmNoPadding.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/ciphers/Aes256GcmNoPadding.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/ciphers/AesGcmNoPadding.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/ciphers/package-info.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/jet/JetManager.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/jet/JetSecurityAdapter.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/jet/JingleEnvelopeManager.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/jet/component/JetSecurity.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/jet/component/JetSecurityBytestreamSession.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/jet/component/package-info.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/jet/element/JetSecurityElement.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/jet/element/package-info.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/jet/package-info.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/jet/provider/JetSecurityProvider.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/jet/provider/package-info.java create mode 100644 smack-experimental/src/test/java/org/jivesoftware/smackx/ciphers/AesGcmNoPaddingTest.java create mode 100644 smack-experimental/src/test/java/org/jivesoftware/smackx/jet/JetElementTest.java create mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/jet/JetIntegrationTest.java create mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/jet/package-info.java diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/ciphers/Aes128GcmNoPadding.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/ciphers/Aes128GcmNoPadding.java new file mode 100644 index 000000000..e52453bfe --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/ciphers/Aes128GcmNoPadding.java @@ -0,0 +1,43 @@ +/** + * + * Copyright 2017 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.ciphers; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import javax.crypto.NoSuchPaddingException; + +public class Aes128GcmNoPadding extends AesGcmNoPadding { + public static final String NAMESPACE = "urn:xmpp:ciphers:aes-128-gcm-nopadding:0"; + + public Aes128GcmNoPadding(int MODE) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, + NoSuchProviderException, InvalidAlgorithmParameterException { + super(128, MODE); + } + + public Aes128GcmNoPadding(byte[] keyAndIv, int MODE) throws NoSuchProviderException, InvalidAlgorithmParameterException, + NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException { + super(AesGcmNoPadding.copyOfRange(keyAndIv, 0, 16), // 16 byte key + AesGcmNoPadding.copyOfRange(keyAndIv, 16, keyAndIv.length), MODE); // rest (12 byte) IV + } + + @Override + public String getNamespace() { + return NAMESPACE; + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/ciphers/Aes256GcmNoPadding.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/ciphers/Aes256GcmNoPadding.java new file mode 100644 index 000000000..e7a323264 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/ciphers/Aes256GcmNoPadding.java @@ -0,0 +1,43 @@ +/** + * + * Copyright 2017 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.ciphers; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import javax.crypto.NoSuchPaddingException; + +public class Aes256GcmNoPadding extends AesGcmNoPadding { + public static final String NAMESPACE = "urn:xmpp:ciphers:aes-256-gcm-nopadding:0"; + + public Aes256GcmNoPadding(int MODE) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, + NoSuchProviderException, InvalidAlgorithmParameterException { + super(256, MODE); + } + + public Aes256GcmNoPadding(byte[] keyAndIv, int MODE) throws NoSuchProviderException, InvalidAlgorithmParameterException, + NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException { + super(AesGcmNoPadding.copyOfRange(keyAndIv, 0, 32), // 32 byte key + AesGcmNoPadding.copyOfRange(keyAndIv, 32, keyAndIv.length), MODE); // rest (12 byte) IV + } + + @Override + public String getNamespace() { + return NAMESPACE; + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/ciphers/AesGcmNoPadding.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/ciphers/AesGcmNoPadding.java new file mode 100644 index 000000000..c09b1a45a --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/ciphers/AesGcmNoPadding.java @@ -0,0 +1,144 @@ +/** + * + * Copyright 2017 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.ciphers; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +public abstract class AesGcmNoPadding { + + public static final String keyType = "AES"; + public static final String cipherMode = "AES/GCM/NoPadding"; + + private final int length; + protected final Cipher cipher; + private final byte[] key, iv, keyAndIv; + + AesGcmNoPadding(int bits, int MODE) throws NoSuchAlgorithmException, NoSuchProviderException, + NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException { + this.length = bits; + int bytes = bits / 8; + + KeyGenerator keyGenerator = KeyGenerator.getInstance(keyType); + keyGenerator.init(bits); + key = keyGenerator.generateKey().getEncoded(); + + SecureRandom secureRandom = new SecureRandom(); + iv = new byte[12]; + secureRandom.nextBytes(iv); + + keyAndIv = new byte[bytes + 12]; + System.arraycopy(key, 0, keyAndIv, 0, bytes); + System.arraycopy(iv, 0, keyAndIv, bytes, 12); + + cipher = Cipher.getInstance(cipherMode, "BC"); + SecretKey keySpec = new SecretKeySpec(key, keyType); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + cipher.init(MODE, keySpec, ivSpec); + } + + public static AesGcmNoPadding createEncryptionKey(String cipherName) + throws NoSuchProviderException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, + InvalidAlgorithmParameterException { + + switch (cipherName) { + case Aes128GcmNoPadding.NAMESPACE: + return new Aes128GcmNoPadding(Cipher.ENCRYPT_MODE); + case Aes256GcmNoPadding.NAMESPACE: + return new Aes256GcmNoPadding(Cipher.ENCRYPT_MODE); + default: throw new NoSuchAlgorithmException("Invalid cipher."); + } + } + + /** + * Create a new AES key. + * @param key key + * @param iv iv + * @param MODE cipher mode (Cipher.ENCRYPT_MODE / Cipher.DECRYPT_MODE) + * @throws NoSuchPaddingException + * @throws NoSuchAlgorithmException + * @throws NoSuchProviderException + * @throws InvalidAlgorithmParameterException + * @throws InvalidKeyException + */ + public AesGcmNoPadding(byte[] key, byte[] iv, int MODE) throws NoSuchPaddingException, NoSuchAlgorithmException, + NoSuchProviderException, InvalidAlgorithmParameterException, InvalidKeyException { + assert iv.length == 12; + this.length = key.length * 8; + this.key = key; + this.iv = iv; + + keyAndIv = new byte[key.length + iv.length]; + System.arraycopy(key, 0, keyAndIv, 0, key.length); + System.arraycopy(iv, 0, keyAndIv, key.length, iv.length); + + cipher = Cipher.getInstance(cipherMode, "BC"); + SecretKeySpec keySpec = new SecretKeySpec(key, keyType); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + cipher.init(MODE, keySpec, ivSpec); + } + + public static AesGcmNoPadding createDecryptionKey(String namespace, byte[] serialized) + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchProviderException, + InvalidKeyException, NoSuchPaddingException { + + switch (namespace) { + case Aes128GcmNoPadding.NAMESPACE: + return new Aes128GcmNoPadding(serialized, Cipher.DECRYPT_MODE); + case Aes256GcmNoPadding.NAMESPACE: + return new Aes256GcmNoPadding(serialized, Cipher.DECRYPT_MODE); + default: throw new NoSuchAlgorithmException("Invalid cipher."); + } + } + + public byte[] getKeyAndIv() { + return keyAndIv.clone(); + } + + public byte[] getKey() { + return key.clone(); + } + + public byte[] getIv() { + return iv.clone(); + } + + public int getLength() { + return length; + } + + public Cipher getCipher() { + return cipher; + } + + public abstract String getNamespace(); + + static byte[] copyOfRange(byte[] source, int start, int end) { + byte[] copy = new byte[end - start]; + System.arraycopy(source, start, copy, 0, end - start); + return copy; + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/ciphers/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/ciphers/package-info.java new file mode 100644 index 000000000..846c31720 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/ciphers/package-info.java @@ -0,0 +1,22 @@ +/** + * + * Copyright 2017 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's API for XEP-XXXX: Ciphers. + * This contains some AES cipher utility functions. + */ +package org.jivesoftware.smackx.ciphers; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/JetManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/JetManager.java new file mode 100644 index 000000000..e84834fa2 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/JetManager.java @@ -0,0 +1,187 @@ +/** + * + * Copyright 2017 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.jet; + +import java.io.File; +import java.io.InputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.HashMap; +import java.util.WeakHashMap; +import java.util.logging.Logger; + +import javax.crypto.NoSuchPaddingException; + +import org.jivesoftware.smack.Manager; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.provider.ExtensionElementProvider; +import org.jivesoftware.smackx.ciphers.Aes256GcmNoPadding; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; +import org.jivesoftware.smackx.jet.component.JetSecurity; +import org.jivesoftware.smackx.jet.provider.JetSecurityProvider; +import org.jivesoftware.smackx.jingle.JingleDescriptionManager; +import org.jivesoftware.smackx.jingle.JingleManager; +import org.jivesoftware.smackx.jingle.JingleTransportManager; +import org.jivesoftware.smackx.jingle.component.JingleContent; +import org.jivesoftware.smackx.jingle.component.JingleSession; +import org.jivesoftware.smackx.jingle.element.JingleContentElement; +import org.jivesoftware.smackx.jingle.util.Role; +import org.jivesoftware.smackx.jingle_filetransfer.JingleFileTransferManager; +import org.jivesoftware.smackx.jingle_filetransfer.component.JingleFile; +import org.jivesoftware.smackx.jingle_filetransfer.component.JingleOutgoingFileOffer; +import org.jivesoftware.smackx.jingle_filetransfer.controller.OutgoingFileOfferController; + +import org.jxmpp.jid.FullJid; + +/** + * Manager for Jingle Encrypted Transfers (XEP-XXXX). + */ +public final class JetManager extends Manager implements JingleDescriptionManager { + + private static final Logger LOGGER = Logger.getLogger(JetManager.class.getName()); + + private static final WeakHashMap INSTANCES = new WeakHashMap<>(); + private static final HashMap envelopeManagers = new HashMap<>(); + private static final HashMap> envelopeProviders = new HashMap<>(); + + private final JingleManager jingleManager; + + static { + JingleManager.addJingleSecurityAdapter(new JetSecurityAdapter()); + JingleManager.addJingleSecurityProvider(new JetSecurityProvider()); + } + + private JetManager(XMPPConnection connection) { + super(connection); + this.jingleManager = JingleManager.getInstanceFor(connection); + ServiceDiscoveryManager.getInstanceFor(connection).addFeature(getNamespace()); + jingleManager.addJingleDescriptionManager(this); + } + + public static JetManager getInstanceFor(XMPPConnection connection) { + JetManager manager = INSTANCES.get(connection); + + if (manager == null) { + manager = new JetManager(connection); + INSTANCES.put(connection, manager); + } + + return manager; + } + + public OutgoingFileOfferController sendEncryptedFile(File file, FullJid recipient, JingleEnvelopeManager envelopeManager) throws Exception { + return sendEncryptedFile(file, JingleFile.fromFile(file, null, null, null), recipient, envelopeManager); + } + + public OutgoingFileOfferController sendEncryptedFile(File file, JingleFile metadata, FullJid recipient, JingleEnvelopeManager envelopeManager) throws Exception { + if (file == null || !file.exists()) { + throw new IllegalArgumentException("File MUST NOT be null and MUST exist."); + } + + throwIfRecipientLacksSupport(recipient); + + JingleSession session = jingleManager.createSession(Role.initiator, recipient); + + JingleContent content = new JingleContent(JingleContentElement.Creator.initiator, JingleContentElement.Senders.initiator); + session.addContent(content); + + JingleOutgoingFileOffer offer = new JingleOutgoingFileOffer(file, metadata); + content.setDescription(offer); + + JingleTransportManager transportManager = jingleManager.getBestAvailableTransportManager(recipient); + content.setTransport(transportManager.createTransportForInitiator(content)); + + JetSecurity security = new JetSecurity(envelopeManager, recipient, content.getName(), Aes256GcmNoPadding.NAMESPACE); + content.setSecurity(security); + session.sendInitiate(connection()); + + return offer; + } + + public OutgoingFileOfferController sendEncryptedStream(InputStream inputStream, JingleFile metadata, FullJid recipient, JingleEnvelopeManager envelopeManager) + throws XMPPException.XMPPErrorException, SmackException.FeatureNotSupportedException, SmackException.NotConnectedException, + InterruptedException, SmackException.NoResponseException, NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, + JingleEnvelopeManager.JingleEncryptionException, NoSuchProviderException, InvalidAlgorithmParameterException { + + throwIfRecipientLacksSupport(recipient); + JingleSession session = jingleManager.createSession(Role.initiator, recipient); + + JingleContent content = new JingleContent(JingleContentElement.Creator.initiator, JingleContentElement.Senders.initiator); + session.addContent(content); + + JingleOutgoingFileOffer offer = new JingleOutgoingFileOffer(inputStream, metadata); + content.setDescription(offer); + + JingleTransportManager transportManager = jingleManager.getBestAvailableTransportManager(recipient); + content.setTransport(transportManager.createTransportForInitiator(content)); + + JetSecurity security = new JetSecurity(envelopeManager, recipient, content.getName(), Aes256GcmNoPadding.NAMESPACE); + content.setSecurity(security); + session.sendInitiate(connection()); + + return offer; + } + + public void registerEnvelopeManager(JingleEnvelopeManager method) { + envelopeManagers.put(method.getJingleEnvelopeNamespace(), method); + } + + public void unregisterEnvelopeManager(String namespace) { + envelopeManagers.remove(namespace); + } + + public JingleEnvelopeManager getEnvelopeManager(String namespace) { + return envelopeManagers.get(namespace); + } + + public static void registerEnvelopeProvider(String namespace, ExtensionElementProvider provider) { + envelopeProviders.put(namespace, provider); + } + + public static void unregisterEnvelopeProvider(String namespace) { + envelopeProviders.remove(namespace); + } + + public static ExtensionElementProvider getEnvelopeProvider(String namespace) { + return envelopeProviders.get(namespace); + } + + @Override + public String getNamespace() { + return JetSecurity.NAMESPACE; + } + + @Override + public void notifySessionInitiate(JingleSession session) { + JingleFileTransferManager.getInstanceFor(connection()).notifySessionInitiate(session); + } + + @Override + public void notifyContentAdd(JingleSession session, JingleContent content) { + JingleFileTransferManager.getInstanceFor(connection()).notifyContentAdd(session, content); + } + + private void throwIfRecipientLacksSupport(FullJid recipient) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, SmackException.FeatureNotSupportedException { + if (!ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(recipient, getNamespace())) { + throw new SmackException.FeatureNotSupportedException(getNamespace(), recipient); + } + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/JetSecurityAdapter.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/JetSecurityAdapter.java new file mode 100644 index 000000000..32e9831da --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/JetSecurityAdapter.java @@ -0,0 +1,35 @@ +/** + * + * Copyright 2017 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.jet; + +import org.jivesoftware.smackx.jet.component.JetSecurity; +import org.jivesoftware.smackx.jet.element.JetSecurityElement; +import org.jivesoftware.smackx.jingle.adapter.JingleSecurityAdapter; +import org.jivesoftware.smackx.jingle.element.JingleContentSecurityElement; + +public class JetSecurityAdapter implements JingleSecurityAdapter { + + @Override + public JetSecurity securityFromElement(JingleContentSecurityElement element) { + return new JetSecurity((JetSecurityElement) element); + } + + @Override + public String getNamespace() { + return JetSecurity.NAMESPACE; + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/JingleEnvelopeManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/JingleEnvelopeManager.java new file mode 100644 index 000000000..88464f47d --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/JingleEnvelopeManager.java @@ -0,0 +1,52 @@ +/** + * + * Copyright 2017 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.jet; + +import java.security.NoSuchAlgorithmException; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.ExtensionElement; + +import org.jxmpp.jid.FullJid; + +/** + * Classes that implement this interface can be used to encrypt Jingle File Transfers. + */ +public interface JingleEnvelopeManager { + + ExtensionElement encryptJingleTransfer(FullJid recipient, byte[] keyData) + throws JingleEncryptionException, InterruptedException, NoSuchAlgorithmException, + SmackException.NotConnectedException, SmackException.NoResponseException; + + byte[] decryptJingleTransfer(FullJid sender, ExtensionElement envelope) + throws JingleEncryptionException, InterruptedException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, SmackException.NoResponseException; + + class JingleEncryptionException extends Exception { + private static final long serialVersionUID = 1L; + + public JingleEncryptionException(Throwable throwable) { + super(throwable); + } + } + + XMPPConnection getConnection(); + + String getJingleEnvelopeNamespace(); +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/component/JetSecurity.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/component/JetSecurity.java new file mode 100644 index 000000000..6d2609618 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/component/JetSecurity.java @@ -0,0 +1,142 @@ +/** + * + * Copyright 2017 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.jet.component; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.crypto.NoSuchPaddingException; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smackx.bytestreams.BytestreamSession; +import org.jivesoftware.smackx.ciphers.AesGcmNoPadding; +import org.jivesoftware.smackx.jet.JetManager; +import org.jivesoftware.smackx.jet.JingleEnvelopeManager; +import org.jivesoftware.smackx.jet.element.JetSecurityElement; +import org.jivesoftware.smackx.jingle.callbacks.JingleSecurityCallback; +import org.jivesoftware.smackx.jingle.component.JingleSecurity; +import org.jivesoftware.smackx.jingle.element.JingleContentSecurityInfoElement; +import org.jivesoftware.smackx.jingle.element.JingleElement; + +import org.jxmpp.jid.FullJid; + +/** + * Created by vanitas on 22.07.17. + */ +public class JetSecurity extends JingleSecurity { + private static final Logger LOGGER = Logger.getLogger(JetSecurity.class.getName()); + + public static final String NAMESPACE_V0 = "urn:xmpp:jingle:jet:0"; + public static final String NAMESPACE = NAMESPACE_V0; + + private final String envelopeNamespace; + + private AesGcmNoPadding aesKey; + private final ExtensionElement child; + private final String cipherName; + private final String contentName; + + public JetSecurity(JetSecurityElement element) { + super(); + this.child = element.getChild(); + this.envelopeNamespace = element.getEnvelopeNamespace(); + this.contentName = element.getContentName(); + this.cipherName = element.getCipherName(); + } + + public JetSecurity(JingleEnvelopeManager envelopeManager, FullJid recipient, String contentName, String cipherName) + throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, + InvalidAlgorithmParameterException, InvalidKeyException, InterruptedException, + JingleEnvelopeManager.JingleEncryptionException, SmackException.NotConnectedException, + SmackException.NoResponseException { + + this.envelopeNamespace = envelopeManager.getJingleEnvelopeNamespace(); + this.aesKey = AesGcmNoPadding.createEncryptionKey(cipherName); + this.child = envelopeManager.encryptJingleTransfer(recipient, aesKey.getKeyAndIv()); + this.contentName = contentName; + this.cipherName = cipherName; + } + + private void decryptEncryptionKey(JingleEnvelopeManager method, FullJid sender) + throws InterruptedException, JingleEnvelopeManager.JingleEncryptionException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, SmackException.NoResponseException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException, NoSuchProviderException, InvalidKeyException, NoSuchPaddingException { + byte[] keyAndIv = method.decryptJingleTransfer(sender, child); + aesKey = AesGcmNoPadding.createDecryptionKey(cipherName, keyAndIv); + } + + @Override + public JetSecurityElement getElement() { + return new JetSecurityElement(contentName, cipherName, child); + } + + @Override + public JingleElement handleSecurityInfo(JingleContentSecurityInfoElement element, JingleElement wrapping) { + return null; + } + + @Override + public void decryptIncomingBytestream(BytestreamSession bytestreamSession, JingleSecurityCallback callback) { + if (aesKey == null) { + throw new IllegalStateException("Encryption key has not yet been decrypted."); + } + JetSecurityBytestreamSession securityBytestreamSession = new JetSecurityBytestreamSession(bytestreamSession, aesKey.getCipher()); + callback.onSecurityReady(securityBytestreamSession); + } + + @Override + public void encryptOutgoingBytestream(BytestreamSession bytestreamSession, JingleSecurityCallback callback) { + JetSecurityBytestreamSession securityBytestreamSession = new JetSecurityBytestreamSession(bytestreamSession, aesKey.getCipher()); + callback.onSecurityReady(securityBytestreamSession); + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public void prepare(XMPPConnection connection, FullJid sender) { + if (getParent().getParent().isInitiator()) { + return; + } + + if (aesKey != null) { + return; + } + + JingleEnvelopeManager method = JetManager.getInstanceFor(connection).getEnvelopeManager(getEnvelopeNamespace()); + if (method == null) { + throw new AssertionError("No JingleEncryptionMethodManager found for " + getEnvelopeNamespace()); + } + try { + decryptEncryptionKey(method, sender); + } catch (InterruptedException | NoSuchPaddingException | InvalidKeyException | NoSuchProviderException | InvalidAlgorithmParameterException | NoSuchAlgorithmException | SmackException.NoResponseException | SmackException.NotConnectedException | XMPPException.XMPPErrorException | JingleEnvelopeManager.JingleEncryptionException e) { + LOGGER.log(Level.SEVERE, "Could not decrypt security key: " + e, e); + } + } + + public String getEnvelopeNamespace() { + return envelopeNamespace; + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/component/JetSecurityBytestreamSession.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/component/JetSecurityBytestreamSession.java new file mode 100644 index 000000000..6426e4c93 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/component/JetSecurityBytestreamSession.java @@ -0,0 +1,51 @@ +/** + * + * Copyright 2017 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.jet.component; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; + +import org.jivesoftware.smackx.bytestreams.BytestreamSession; +import org.jivesoftware.smackx.jingle.component.JingleSecurityBytestreamSession; + +public class JetSecurityBytestreamSession extends JingleSecurityBytestreamSession { + private final Cipher cipher; + + public JetSecurityBytestreamSession(BytestreamSession session, Cipher cipher) { + super(session); + this.cipher = cipher; + } + + @Override + public InputStream getInputStream() throws IOException { + return new CipherInputStream(wrapped.getInputStream(), cipher); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return new CipherOutputStream(wrapped.getOutputStream(), cipher); + } + + @Override + public void close() throws IOException { + wrapped.close(); + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/component/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/component/package-info.java new file mode 100644 index 000000000..e3468c8c9 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/component/package-info.java @@ -0,0 +1,22 @@ +/** + * + * Copyright 2017 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's API for XEP-XXXX: Jingle Encrypted Transfers. + * Internal classes. + */ +package org.jivesoftware.smackx.jet.component; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/element/JetSecurityElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/element/JetSecurityElement.java new file mode 100644 index 000000000..c976a99f2 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/element/JetSecurityElement.java @@ -0,0 +1,81 @@ +/** + * + * Copyright 2017 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.jet.element; + +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.util.XmlStringBuilder; +import org.jivesoftware.smackx.jet.component.JetSecurity; +import org.jivesoftware.smackx.jingle.element.JingleContentSecurityElement; + +/** + * Implementation of the Jingle security element as specified in XEP-XXXX (Jingle Encrypted Transfers). + * + * + * + * + * <- You are here. + * + * + */ +public class JetSecurityElement extends JingleContentSecurityElement { + public static final String ATTR_CONTENT_NAME = "name"; + public static final String ATTR_ENVELOPE_TYPE = "type"; + public static final String ATTR_CIPHER_TYPE = "cipher"; + + private final ExtensionElement child; + private final String contentName; + private final String cipherName; + + public JetSecurityElement(String contentName, String cipherName, ExtensionElement child) { + this.contentName = contentName; + this.child = child; + this.cipherName = cipherName; + } + + @Override + public CharSequence toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.attribute(ATTR_CONTENT_NAME, contentName) + .attribute(ATTR_CIPHER_TYPE, cipherName) + .attribute(ATTR_ENVELOPE_TYPE, child.getNamespace()); + xml.rightAngleBracket(); + xml.element(child); + xml.closeElement(this); + return xml; + } + + @Override + public String getNamespace() { + return JetSecurity.NAMESPACE; + } + + public String getEnvelopeNamespace() { + return child.getNamespace(); + } + + public ExtensionElement getChild() { + return child; + } + + public String getContentName() { + return contentName; + } + + public String getCipherName() { + return cipherName; + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/element/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/element/package-info.java new file mode 100644 index 000000000..442412fc7 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/element/package-info.java @@ -0,0 +1,22 @@ +/** + * + * Copyright 2017 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's API for XEP-XXXX: Jingle Encrypted Transfers. + * Elements. + */ +package org.jivesoftware.smackx.jet.element; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/package-info.java new file mode 100644 index 000000000..927a7b356 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/package-info.java @@ -0,0 +1,21 @@ +/** + * + * Copyright 2017 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's API for XEP-XXXX: Jingle Encrypted Transfers. + */ +package org.jivesoftware.smackx.jet; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/provider/JetSecurityProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/provider/JetSecurityProvider.java new file mode 100644 index 000000000..e997f5019 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/provider/JetSecurityProvider.java @@ -0,0 +1,65 @@ +/** + * + * Copyright 2017 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.jet.provider; + +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.util.Objects; +import org.jivesoftware.smackx.jet.JetManager; +import org.jivesoftware.smackx.jet.component.JetSecurity; +import org.jivesoftware.smackx.jet.element.JetSecurityElement; +import org.jivesoftware.smackx.jingle.provider.JingleContentSecurityProvider; + +import org.xmlpull.v1.XmlPullParser; + +/** + * Provider for the Jingle security element for XEP-XXXX (Jingle Encrypted Transfers). + */ +public class JetSecurityProvider extends JingleContentSecurityProvider { + private static final Logger LOGGER = Logger.getLogger(JetSecurityProvider.class.getName()); + + @Override + public JetSecurityElement parse(XmlPullParser parser, int initialDepth) throws Exception { + String name = parser.getAttributeValue("", JetSecurityElement.ATTR_CONTENT_NAME); + String cipher = parser.getAttributeValue("", JetSecurityElement.ATTR_CIPHER_TYPE); + String type = parser.getAttributeValue("", JetSecurityElement.ATTR_ENVELOPE_TYPE); + ExtensionElement child; + + Objects.requireNonNull(type); + Objects.requireNonNull(cipher); + + ExtensionElementProvider encryptionElementProvider = + JetManager.getEnvelopeProvider(type); + + if (encryptionElementProvider != null) { + child = encryptionElementProvider.parse(parser); + } else { + LOGGER.log(Level.WARNING, "Unknown child element in JetSecurityElement: " + type); + return null; + } + + return new JetSecurityElement(name, cipher, child); + } + + @Override + public String getNamespace() { + return JetSecurity.NAMESPACE; + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/provider/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/provider/package-info.java new file mode 100644 index 000000000..5e5d6c654 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/jet/provider/package-info.java @@ -0,0 +1,22 @@ +/** + * + * Copyright 2017 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's API for XEP-XXXX: Jingle Encrypted Transfers. + * Providers. + */ +package org.jivesoftware.smackx.jet.provider; diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/ciphers/AesGcmNoPaddingTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/ciphers/AesGcmNoPaddingTest.java new file mode 100644 index 000000000..0366ed5d3 --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/ciphers/AesGcmNoPaddingTest.java @@ -0,0 +1,101 @@ +/** + * + * Copyright 2017 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.ciphers; + +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.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.Arrays; +import java.util.Random; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; + +import org.jivesoftware.smack.test.util.SmackTestSuite; + +import org.junit.Test; + +public class AesGcmNoPaddingTest extends SmackTestSuite { + + @Test + public void Aes128Test() throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException, InvalidKeyException, InvalidAlgorithmParameterException { + AesGcmNoPadding aes128 = AesGcmNoPadding.createEncryptionKey(Aes128GcmNoPadding.NAMESPACE); + assertNotNull(aes128); + assertEquals(16, aes128.getKey().length); + assertEquals(12, aes128.getIv().length); + assertEquals(28, aes128.getKeyAndIv().length); + assertNotNull(aes128.getCipher()); + assertEquals(128, aes128.getLength()); + } + + @Test + public void Aes256Test() throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException, InvalidKeyException, InvalidAlgorithmParameterException { + AesGcmNoPadding aes256 = AesGcmNoPadding.createEncryptionKey(Aes256GcmNoPadding.NAMESPACE); + assertNotNull(aes256); + assertEquals(32, aes256.getKey().length); + assertEquals(12, aes256.getIv().length); + assertEquals(44, aes256.getKeyAndIv().length); + assertNotNull(aes256.getCipher()); + assertEquals(256, aes256.getLength()); + } + + @Test(expected = NoSuchAlgorithmException.class) + public void invalidEncryptionCipher() throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException, InvalidKeyException, InvalidAlgorithmParameterException { + AesGcmNoPadding.createEncryptionKey("invalid"); + } + + @Test(expected = NoSuchAlgorithmException.class) + public void invalidDecryptionCipher() throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException, InvalidKeyException, InvalidAlgorithmParameterException { + AesGcmNoPadding.createDecryptionKey("invalid", null); + } + + @Test + public void encryption128Test() throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException, InvalidKeyException, InvalidAlgorithmParameterException, BadPaddingException, IllegalBlockSizeException { + AesGcmNoPadding aes1 = AesGcmNoPadding.createEncryptionKey(Aes128GcmNoPadding.NAMESPACE); + AesGcmNoPadding aes2 = AesGcmNoPadding.createDecryptionKey(Aes128GcmNoPadding.NAMESPACE, aes1.getKeyAndIv()); + + byte[] data = new byte[4096]; + new Random().nextBytes(data); + + byte[] enc = aes1.getCipher().doFinal(data); + assertFalse(Arrays.equals(data, enc)); + + byte[] dec = aes2.getCipher().doFinal(enc); + assertTrue(Arrays.equals(dec, data)); + } + + @Test + public void encryption256Test() throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException, InvalidKeyException, InvalidAlgorithmParameterException, BadPaddingException, IllegalBlockSizeException { + AesGcmNoPadding aes1 = AesGcmNoPadding.createEncryptionKey(Aes256GcmNoPadding.NAMESPACE); + AesGcmNoPadding aes2 = AesGcmNoPadding.createDecryptionKey(Aes256GcmNoPadding.NAMESPACE, aes1.getKeyAndIv()); + + byte[] data = new byte[4096]; + new Random().nextBytes(data); + + byte[] enc = aes1.getCipher().doFinal(data); + assertFalse(Arrays.equals(data, enc)); + + byte[] dec = aes2.getCipher().doFinal(enc); + assertTrue(Arrays.equals(dec, data)); + } +} diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/jet/JetElementTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/jet/JetElementTest.java new file mode 100644 index 000000000..537808804 --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/jet/JetElementTest.java @@ -0,0 +1,97 @@ +/** + * + * Copyright 2017 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.jet; + +import static junit.framework.TestCase.assertEquals; +import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.test.util.SmackTestSuite; +import org.jivesoftware.smackx.ciphers.Aes128GcmNoPadding; +import org.jivesoftware.smackx.jet.component.JetSecurity; +import org.jivesoftware.smackx.jet.element.JetSecurityElement; + +import org.junit.Test; +import org.jxmpp.jid.FullJid; +import org.xml.sax.SAXException; + +public class JetElementTest extends SmackTestSuite { + + @Test + public void jetTest() throws InterruptedException, JingleEnvelopeManager.JingleEncryptionException, NoSuchAlgorithmException, SmackException.NotConnectedException, SmackException.NoResponseException, IOException, SAXException { + ExtensionElement child = new SecurityStub().encryptJingleTransfer(null, null); + JetSecurityElement element = new JetSecurityElement("content1", Aes128GcmNoPadding.NAMESPACE, child); + JetSecurity security = new JetSecurity(element); + assertEquals(SecurityStub.NAMESPACE, security.getEnvelopeNamespace()); + assertEquals(Aes128GcmNoPadding.NAMESPACE, element.getCipherName()); + assertEquals(SecurityStub.NAMESPACE, element.getEnvelopeNamespace()); + assertEquals("content1", element.getContentName()); + + String xml = "" + + "" + + ""; + assertXMLEqual(xml, security.getElement().toXML().toString()); + } + + private static class SecurityStub implements JingleEnvelopeManager { + public static final String NAMESPACE = "urn:xmpp:security-stub"; + + @Override + public ExtensionElement encryptJingleTransfer(FullJid recipient, byte[] keyData) throws JingleEncryptionException, InterruptedException, NoSuchAlgorithmException, SmackException.NotConnectedException, SmackException.NoResponseException { + return new ExtensionElement() { + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public String getElementName() { + return "security-stub"; + } + + @Override + public CharSequence toXML() { + return ""; + } + }; + } + + @Override + public byte[] decryptJingleTransfer(FullJid sender, ExtensionElement envelope) throws JingleEncryptionException, InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException { + return new byte[0]; + } + + @Override + public XMPPConnection getConnection() { + return null; + } + + @Override + public String getJingleEnvelopeNamespace() { + return NAMESPACE; + } + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/jet/JetIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/jet/JetIntegrationTest.java new file mode 100644 index 000000000..6b5ff08f5 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/jet/JetIntegrationTest.java @@ -0,0 +1,145 @@ +/** + * + * Copyright 2017 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.jet; + +import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.cleanServerSideTraces; +import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.setUpOmemoManager; +import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.subscribe; +import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.unidirectionalTrust; +import static org.junit.Assert.assertArrayEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Random; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.jingle.transport.jingle_ibb.JingleIBBTransportManager; +import org.jivesoftware.smackx.jingle.transport.jingle_s5b.JingleS5BTransportManager; +import org.jivesoftware.smackx.jingle_filetransfer.JingleFileTransferManager; +import org.jivesoftware.smackx.jingle_filetransfer.component.JingleFile; +import org.jivesoftware.smackx.jingle_filetransfer.controller.IncomingFileOfferController; +import org.jivesoftware.smackx.jingle_filetransfer.listener.IncomingFileOfferListener; +import org.jivesoftware.smackx.jingle_filetransfer.listener.ProgressListener; +import org.jivesoftware.smackx.omemo.AbstractOmemoIntegrationTest; +import org.jivesoftware.smackx.omemo.OmemoManager; +import org.jivesoftware.smackx.omemo.OmemoService; +import org.jivesoftware.smackx.omemo.OmemoStore; +import org.jivesoftware.smackx.omemo.provider.OmemoVAxolotlProvider; +import org.jivesoftware.smackx.omemo.util.OmemoConstants; + +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; + +public class JetIntegrationTest extends AbstractOmemoIntegrationTest { + + private OmemoManager oa, ob; + private JetManager ja, jb; + private JingleIBBTransportManager ia, ib; + private JingleS5BTransportManager sa, sb; + private OmemoStore store; + + public JetIntegrationTest(SmackIntegrationTestEnvironment environment) + throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException, TestNotPossibleException { + super(environment); + } + + @Override + public void before() { + store = OmemoService.getInstance().getOmemoStoreBackend(); + oa = OmemoManager.getInstanceFor(conOne, 666); + ob = OmemoManager.getInstanceFor(conTwo, 777); + ja = JetManager.getInstanceFor(conOne); + jb = JetManager.getInstanceFor(conTwo); + ia = JingleIBBTransportManager.getInstanceFor(conOne); + ib = JingleIBBTransportManager.getInstanceFor(conTwo); + sa = JingleS5BTransportManager.getInstanceFor(conOne); + sb = JingleS5BTransportManager.getInstanceFor(conTwo); + JetManager.registerEnvelopeProvider(OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL, new OmemoVAxolotlProvider()); + } + + @SmackIntegrationTest + public void JingleEncryptedFileTransferTest() + throws Exception { + + final SimpleResultSyncPoint received = new SimpleResultSyncPoint(); + + Random weakRandom = new Random(); + + //Setup OMEMO + subscribe(oa, ob, "Bob"); + subscribe(ob, oa, "Alice"); + setUpOmemoManager(oa); + setUpOmemoManager(ob); + unidirectionalTrust(oa, ob); + unidirectionalTrust(ob, oa); + + ja.registerEnvelopeManager(oa); + jb.registerEnvelopeManager(ob); + + byte[] sourceBytes = new byte[16000]; + weakRandom.nextBytes(sourceBytes); + InputStream sourceStream = new ByteArrayInputStream(sourceBytes); + final ByteArrayOutputStream targetStream = new ByteArrayOutputStream(16000); + + JingleFileTransferManager.getInstanceFor(conTwo).addIncomingFileOfferListener(new IncomingFileOfferListener() { + @Override + public void onIncomingFileOffer(IncomingFileOfferController offer) { + try { + offer.addProgressListener(new ProgressListener() { + @Override + public void started() { + + } + + @Override + public void progress(float percent) { + + } + + @Override + public void finished() { + received.signal(); + } + }); + offer.accept(conTwo, targetStream); + } catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException | IOException e) { + received.signal(e); + } + } + }); + + ja.sendEncryptedStream(sourceStream, new JingleFile("test", "desc", (long) sourceBytes.length, null, null, null), conTwo.getUser().asFullJidOrThrow(), oa); + + received.waitForResult(60 * 1000); + + assertArrayEquals(sourceBytes, targetStream.toByteArray()); + } + + @Override + public void after() { + oa.shutdown(); + ob.shutdown(); + cleanServerSideTraces(oa); + cleanServerSideTraces(ob); + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/jet/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/jet/package-info.java new file mode 100644 index 000000000..804d14a10 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/jet/package-info.java @@ -0,0 +1,21 @@ +/** + * + * Copyright 2017 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. + */ + +/** + * Tests for XEP-XXXX - Jingle Encrypted Transfers. + */ +package org.jivesoftware.smackx.jet; diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoIntegrationTestHelper.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoIntegrationTestHelper.java index 78895cf08..63469b162 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoIntegrationTestHelper.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoIntegrationTestHelper.java @@ -28,7 +28,6 @@ import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.roster.Roster; import org.jivesoftware.smack.roster.RosterEntry; - import org.jivesoftware.smackx.omemo.element.OmemoBundleElement; import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException; import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; @@ -41,11 +40,11 @@ import org.jivesoftware.smackx.pubsub.PubSubManager; /** * Class containing some helper methods for OmemoIntegrationTests. */ -final class OmemoIntegrationTestHelper { +public final class OmemoIntegrationTestHelper { private static final Logger LOGGER = Logger.getLogger(OmemoIntegrationTestHelper.class.getSimpleName()); - static void cleanServerSideTraces(OmemoManager omemoManager) { + public static void cleanServerSideTraces(OmemoManager omemoManager) { cleanUpPubSub(omemoManager); cleanUpRoster(omemoManager); } @@ -115,7 +114,7 @@ final class OmemoIntegrationTestHelper { * @throws InterruptedException * @throws SmackException.NoResponseException */ - static void subscribe(OmemoManager alice, OmemoManager bob, String nick) + public static void subscribe(OmemoManager alice, OmemoManager bob, String nick) throws SmackException.NotLoggedInException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { @@ -127,7 +126,7 @@ final class OmemoIntegrationTestHelper { } - static void unidirectionalTrust(OmemoManager alice, OmemoManager bob) throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, CannotEstablishOmemoSessionException { + public static void unidirectionalTrust(OmemoManager alice, OmemoManager bob) throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, CannotEstablishOmemoSessionException { //Fetch deviceList alice.requestDeviceListUpdateFor(bob.getOwnJid()); LOGGER.log(Level.INFO, "Current deviceList state: " + alice.getOwnDevice() + " knows " + bob.getOwnDevice() + ": " @@ -147,7 +146,7 @@ final class OmemoIntegrationTestHelper { } - static void setUpOmemoManager(OmemoManager omemoManager) throws CorruptedOmemoKeyException, InterruptedException, SmackException.NoResponseException, SmackException.NotConnectedException, XMPPException.XMPPErrorException, SmackException.NotLoggedInException, PubSubException.NotALeafNodeException, NotAPubSubNodeException { + public static void setUpOmemoManager(OmemoManager omemoManager) throws CorruptedOmemoKeyException, InterruptedException, SmackException.NoResponseException, SmackException.NotConnectedException, XMPPException.XMPPErrorException, SmackException.NotLoggedInException, PubSubException.NotALeafNodeException, NotAPubSubNodeException { omemoManager.initialize(); OmemoBundleElement bundle = OmemoService.fetchBundle(omemoManager, omemoManager.getOwnDevice()); assertNotNull("Bundle must not be null.", bundle); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java index 91dca385b..e79e65c34 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoStoreTest.java @@ -22,6 +22,7 @@ import static junit.framework.TestCase.assertNotNull; import static junit.framework.TestCase.assertNotSame; import static junit.framework.TestCase.assertNull; import static junit.framework.TestCase.assertTrue; +import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.cleanServerSideTraces; import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.deletePath; import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.setUpOmemoManager; @@ -154,6 +155,8 @@ public class OmemoStoreTest extends AbstractOmemoIntegrationTest { @Override public void after() { + cleanServerSideTraces(alice); + cleanServerSideTraces(bob); alice.shutdown(); bob.shutdown(); } diff --git a/smack-omemo-signal-integration-test/src/main/java/org/igniterealtime/smack/inttest/smack_omemo_signal/SmackOmemoSignalIntegrationTestFramework.java b/smack-omemo-signal-integration-test/src/main/java/org/igniterealtime/smack/inttest/smack_omemo_signal/SmackOmemoSignalIntegrationTestFramework.java index cdf1754f7..29239807a 100644 --- a/smack-omemo-signal-integration-test/src/main/java/org/igniterealtime/smack/inttest/smack_omemo_signal/SmackOmemoSignalIntegrationTestFramework.java +++ b/smack-omemo-signal-integration-test/src/main/java/org/igniterealtime/smack/inttest/smack_omemo_signal/SmackOmemoSignalIntegrationTestFramework.java @@ -48,7 +48,7 @@ public class SmackOmemoSignalIntegrationTestFramework { SignalOmemoService.acknowledgeLicense(); SignalOmemoService.setup(); - final String[] smackOmemoPackages = new String[] { "org.jivesoftware.smackx.omemo" }; + final String[] smackOmemoPackages = new String[] { "org.jivesoftware.smackx.omemo", "org.jivesoftware.smackx.jet" }; SmackIntegrationTestFramework.main(smackOmemoPackages); } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoManager.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoManager.java index d17582172..c4ecd7c35 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoManager.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoManager.java @@ -41,11 +41,12 @@ import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.util.Async; - +import org.jivesoftware.smack.util.stringencoder.Base64; import org.jivesoftware.smackx.carbons.CarbonManager; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.eme.element.ExplicitMessageEncryptionElement; import org.jivesoftware.smackx.hints.element.StoreHint; +import org.jivesoftware.smackx.jet.JingleEnvelopeManager; import org.jivesoftware.smackx.mam.MamManager; import org.jivesoftware.smackx.muc.MultiUserChat; import org.jivesoftware.smackx.muc.MultiUserChatManager; @@ -66,6 +67,7 @@ import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation; import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener; import org.jivesoftware.smackx.omemo.listener.OmemoMucMessageListener; +import org.jivesoftware.smackx.omemo.util.OmemoConstants; import org.jivesoftware.smackx.pep.PEPListener; import org.jivesoftware.smackx.pep.PEPManager; import org.jivesoftware.smackx.pubsub.EventElement; @@ -78,6 +80,7 @@ import org.jxmpp.jid.BareJid; import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.EntityFullJid; +import org.jxmpp.jid.FullJid; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.stringprep.XmppStringprepException; @@ -88,7 +91,7 @@ import org.jxmpp.stringprep.XmppStringprepException; * @author Paul Schaub */ -public final class OmemoManager extends Manager { +public final class OmemoManager extends Manager implements JingleEnvelopeManager { private static final Logger LOGGER = Logger.getLogger(OmemoManager.class.getName()); private static final WeakHashMap> INSTANCES = new WeakHashMap<>(); @@ -133,6 +136,7 @@ public final class OmemoManager extends Manager { }); service = OmemoService.getInstance(); + ServiceDiscoveryManager.getInstanceFor(connection).addFeature(OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL); } /** @@ -751,10 +755,20 @@ public final class OmemoManager extends Manager { * * @return the connection of this manager */ - XMPPConnection getConnection() { + @Override + public XMPPConnection getConnection() { return connection(); } + public static String getNamespace() { + return OMEMO_NAMESPACE_V_AXOLOTL; + } + + @Override + public String getJingleEnvelopeNamespace() { + return getNamespace(); + } + /** * Return the OMEMO service object. * @@ -840,4 +854,44 @@ public final class OmemoManager extends Manager { } return omemoCarbonCopyListener; } + + @Override + public ExtensionElement encryptJingleTransfer(FullJid recipient, byte[] keyData) throws JingleEncryptionException, InterruptedException, NoSuchAlgorithmException, SmackException.NotConnectedException, SmackException.NoResponseException { + BareJid bareJid = recipient.asBareJid(); + Message EncryptedMessage; + try { + EncryptedMessage = encrypt(bareJid, Base64.encodeToString(keyData)); + } catch (CryptoFailedException | UndecidedOmemoIdentityException | CannotEstablishOmemoSessionException e) { + throw new JingleEncryptionException(e); + } + + ExtensionElement encryptionElement = EncryptedMessage.getExtension(OmemoElement.ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); + if (encryptionElement == null) { + throw new AssertionError("OmemoElement MUST NOT be null."); + } + + return encryptionElement; + } + + @Override + public byte[] decryptJingleTransfer(FullJid sender, ExtensionElement envelope) throws JingleEncryptionException, InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException { + if (!envelope.getNamespace().equals(OMEMO_NAMESPACE_V_AXOLOTL) + || !envelope.getElementName().equals(OmemoElement.ENCRYPTED)) { + throw new IllegalArgumentException("Passed ExtensionElement MUST be an OmemoElement!"); + } + + OmemoElement omemoElement = (OmemoElement) envelope; + Message pseudoMessage = new Message(); + pseudoMessage.setFrom(sender.asBareJid()); + pseudoMessage.addExtension(omemoElement); + + ClearTextMessage decryptedPseudoMessage; + try { + decryptedPseudoMessage = decrypt(sender.asBareJid(), pseudoMessage); + } catch (CryptoFailedException | CorruptedOmemoKeyException | NoRawSessionException e) { + throw new JingleEncryptionException(e); + } + + return Base64.decode(decryptedPseudoMessage.getBody()); + } } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java index 093767166..abb793308 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java @@ -51,7 +51,6 @@ import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smack.util.Async; - import org.jivesoftware.smackx.carbons.CarbonCopyReceivedListener; import org.jivesoftware.smackx.carbons.CarbonManager; import org.jivesoftware.smackx.carbons.packet.CarbonExtension;