From be5f81172a785a3319270db1231199b682a8ff84 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 16 May 2019 22:59:48 +0200 Subject: [PATCH] Fix issues pointed out by code review --- .../jivesoftware/smack/util/RandomUtils.java | 36 +++ .../jivesoftware/smack/util/StringUtils.java | 3 +- .../httpfileupload/HttpFileUploadManager.java | 241 +----------------- .../smackx/omemo_media_sharing/AesgcmUrl.java | 161 ++++++++++++ .../OmemoMediaSharingUtils.java | 115 +++++++++ .../omemo_media_sharing/package-info.java | 24 ++ .../OmemoMediaSharingUtilsTest.java | 10 +- .../SmackIntegrationTestFramework.java | 9 +- .../HttpFileUploadIntegrationTest.java | 60 ----- .../OmemoMediaSharingIntegrationTest.java | 134 ++++++++++ .../omemo_media_sharing/package-info.java | 23 ++ 11 files changed, 510 insertions(+), 306 deletions(-) create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/util/RandomUtils.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/AesgcmUrl.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingUtils.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/package-info.java rename smack-experimental/src/test/java/org/jivesoftware/smackx/{httpfileupload => omemo_media_sharing}/OmemoMediaSharingUtilsTest.java (85%) create mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingIntegrationTest.java create mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/package-info.java diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/RandomUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/RandomUtils.java new file mode 100644 index 0000000000..90ede532f3 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/RandomUtils.java @@ -0,0 +1,36 @@ +/** + * + * Copyright © 2019 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.smack.util; + +import java.security.SecureRandom; + +public class RandomUtils { + + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + /** + * Generate a securely random byte array. + * + * @param len length of the byte array + * @return byte array + */ + public static byte[] secureRandomBytes(int len) { + byte[] bytes = new byte[len]; + SECURE_RANDOM.nextBytes(bytes); + return bytes; + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java index f9e69d6200..96e812f5d4 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java @@ -246,7 +246,8 @@ public static String encodeHex(byte[] bytes) { /** * Convert a hexadecimal String to bytes. - * Stolen from https://stackoverflow.com/a/140861/11150851 + * + * Source: https://stackoverflow.com/a/140861/11150851 * * @param s hex string * @return byte array diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadManager.java index 87cb00b576..6a6879be66 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadManager.java @@ -24,12 +24,10 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; -import java.net.MalformedURLException; import java.net.URL; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -38,11 +36,7 @@ import java.util.logging.Logger; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; -import javax.crypto.KeyGenerator; import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; @@ -55,14 +49,14 @@ import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPConnectionRegistry; import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.util.Objects; -import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.disco.packet.DiscoverInfo; import org.jivesoftware.smackx.httpfileupload.UploadService.Version; import org.jivesoftware.smackx.httpfileupload.element.Slot; import org.jivesoftware.smackx.httpfileupload.element.SlotRequest; import org.jivesoftware.smackx.httpfileupload.element.SlotRequest_V0_2; +import org.jivesoftware.smackx.omemo_media_sharing.AesgcmUrl; +import org.jivesoftware.smackx.omemo_media_sharing.OmemoMediaSharingUtils; import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.packet.DataForm; @@ -95,7 +89,6 @@ public final class HttpFileUploadManager extends Manager { public static final String NAMESPACE_0_2 = "urn:xmpp:http:upload"; private static final Logger LOGGER = Logger.getLogger(HttpFileUploadManager.class.getName()); - private static final SecureRandom SECURE_RANDOM = new SecureRandom(); static { XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { @@ -579,234 +572,4 @@ public static UploadService.Version namespaceToVersion(String namespace) { private static boolean containsHttpFileUploadNamespace(DiscoverInfo discoverInfo) { return discoverInfo.containsFeature(NAMESPACE) || discoverInfo.containsFeature(NAMESPACE_0_2); } - - /** - * Generate a securely random byte array. - * - * @param len length of the byte array - * @return byte array - */ - private static byte[] secureRandomBytes(int len) { - byte[] bytes = new byte[len]; - SECURE_RANDOM.nextBytes(bytes); - return bytes; - } - - /** - * Utility code for XEP-XXXX: OMEMO Media Sharing. - * - * @see XEP-XXXX: OMEMO Media Sharing - */ - static class OmemoMediaSharingUtils { - - private static final String KEYTYPE = "AES"; - private static final String CIPHERMODE = "AES/GCM/NoPadding"; - // 256 bit = 32 byte - private static final int LEN_KEY = 32; - private static final int LEN_KEY_BITS = LEN_KEY * 8; - - private static final int LEN_IV_12 = 12; - private static final int LEN_IV_16 = 16; - // Note: Contrary to what the ProtoXEP states, 16 byte IV length is used in the wild instead of 12. - // At some point we should switch to 12 bytes though. - private static final int LEN_IV = LEN_IV_16; - - static byte[] generateRandomIV() { - return generateRandomIV(LEN_IV); - } - - static byte[] generateRandomIV(int len) { - return secureRandomBytes(len); - } - - /** - * Generate a random 256 bit AES key. - * - * @return encoded AES key - * @throws NoSuchAlgorithmException if the JVM doesn't provide the given key type. - */ - static byte[] generateRandomKey() throws NoSuchAlgorithmException { - KeyGenerator generator = KeyGenerator.getInstance(KEYTYPE); - generator.init(LEN_KEY_BITS); - return generator.generateKey().getEncoded(); - } - - /** - * Create a {@link Cipher} from a given key and iv which is in encryption mode. - * - * @param key aes encryption key - * @param iv initialization vector - * - * @return cipher in encryption mode - * - * @throws NoSuchPaddingException if the JVM doesn't provide the padding specified in the ciphermode. - * @throws NoSuchAlgorithmException if the JVM doesn't provide the encryption method specified in the ciphermode. - * @throws InvalidAlgorithmParameterException if the cipher cannot be initiated. - * @throws InvalidKeyException if the key is invalid. - */ - private static Cipher encryptionCipherFrom(byte[] key, byte[] iv) - throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, - InvalidKeyException { - SecretKey secretKey = new SecretKeySpec(key, KEYTYPE); - IvParameterSpec ivSpec = new IvParameterSpec(iv); - Cipher cipher = Cipher.getInstance(CIPHERMODE); - cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); - return cipher; - } - - /** - * Create a {@link Cipher} from a given key and iv which is in decryption mode. - * - * @param key aes encryption key - * @param iv initialization vector - * - * @return cipher in decryption mode - * - * @throws NoSuchPaddingException if the JVM doesn't provide the padding specified in the ciphermode. - * @throws NoSuchAlgorithmException if the JVM doesn't provide the encryption method specified in the ciphermode. - * @throws InvalidAlgorithmParameterException if the cipher cannot be initiated. - * @throws InvalidKeyException if the key is invalid. - */ - private static Cipher decryptionCipherFrom(byte[] key, byte[] iv) - throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, - InvalidKeyException { - SecretKey secretKey = new SecretKeySpec(key, KEYTYPE); - IvParameterSpec ivSpec = new IvParameterSpec(iv); - Cipher cipher = Cipher.getInstance(CIPHERMODE); - cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); - return cipher; - } - } - - /** - * This class represents a aesgcm URL as described in XEP-XXXX: OMEMO Media Sharing. - * As the builtin {@link URL} class cannot handle the aesgcm protocol identifier, this class - * is used as a utility class that bundles together a {@link URL}, key and IV. - * - * @see XEP-XXXX: OMEMO Media Sharing - */ - public static class AesgcmUrl { - - public static final String PROTOCOL = "aesgcm"; - - private final URL httpsUrl; - private final byte[] keyBytes; - private final byte[] ivBytes; - - /** - * Private constructor that constructs the {@link AesgcmUrl} from a normal https {@link URL}, a key and iv. - * - * @param httpsUrl normal https url as given by the {@link Slot}. - * @param key byte array of an encoded 256 bit aes key - * @param iv 16 or 12 byte initialization vector - */ - AesgcmUrl(URL httpsUrl, byte[] key, byte[] iv) { - this.httpsUrl = Objects.requireNonNull(httpsUrl); - this.keyBytes = Objects.requireNonNull(key); - this.ivBytes = Objects.requireNonNull(iv); - } - - /** - * Parse a {@link AesgcmUrl} from a {@link String}. - * The parsed object will provide a normal {@link URL} under which the offered file can be downloaded, - * as well as a {@link Cipher} that can be used to decrypt it. - * - * @param aesgcmUrlString aesgcm URL as a {@link String} - */ - public AesgcmUrl(String aesgcmUrlString) { - if (!aesgcmUrlString.startsWith(PROTOCOL)) { - throw new IllegalArgumentException("Provided String does not resemble a aesgcm URL."); - } - - // Convert aesgcm Url to https URL - this.httpsUrl = extractHttpsUrl(aesgcmUrlString); - - // Extract IV and Key - byte[][] ivAndKey = extractIVAndKey(aesgcmUrlString); - this.ivBytes = ivAndKey[0]; - this.keyBytes = ivAndKey[1]; - } - - /** - * Return a https {@link URL} under which the file can be downloaded. - * - * @return https URL - */ - public URL getDownloadUrl() { - return httpsUrl; - } - - /** - * Returns the {@link String} representation of this aesgcm URL. - * - * @return aesgcm URL with key and IV. - */ - public String getAesgcmUrl() { - String aesgcmUrl = httpsUrl.toString().replaceFirst(httpsUrl.getProtocol(), PROTOCOL); - return aesgcmUrl + "#" + StringUtils.encodeHex(ivBytes) + StringUtils.encodeHex(keyBytes); - } - - /** - * Returns a {@link Cipher} in decryption mode, which can be used to decrypt the offered file. - * - * @return cipher - * - * @throws NoSuchPaddingException if the JVM cannot provide the specified cipher mode - * @throws NoSuchAlgorithmException if the JVM cannot provide the specified cipher mode - * @throws InvalidAlgorithmParameterException if the JVM cannot provide the specified cipher - * (eg. if no BC provider is added) - * @throws InvalidKeyException if the provided key is invalid - */ - public Cipher getDecryptionCipher() throws NoSuchPaddingException, NoSuchAlgorithmException, - InvalidAlgorithmParameterException, InvalidKeyException { - return OmemoMediaSharingUtils.decryptionCipherFrom(keyBytes, ivBytes); - } - - private static URL extractHttpsUrl(String aesgcmUrlString) { - // aesgcm -> https - String httpsUrlString = aesgcmUrlString.replaceFirst(PROTOCOL, "https"); - // remove #ref - httpsUrlString = httpsUrlString.substring(0, httpsUrlString.indexOf("#")); - - try { - return new URL(httpsUrlString); - } catch (MalformedURLException e) { - throw new AssertionError("Failed to convert aesgcm URL to https URL: '" + aesgcmUrlString + "'", e); - } - } - - private static byte[][] extractIVAndKey(String aesgcmUrlString) { - int startOfRef = aesgcmUrlString.lastIndexOf("#"); - if (startOfRef == -1) { - throw new IllegalArgumentException("The provided aesgcm Url does not have a ref part which is " + - "supposed to contain the encryption key for file encryption."); - } - - String ref = aesgcmUrlString.substring(startOfRef + 1); - byte[] refBytes = StringUtils.hexStringToByteArray(ref); - - byte[] key = new byte[32]; - byte[] iv; - int ivLen; - // determine the length of the initialization vector part - switch (refBytes.length) { - // 32 bytes key + 16 bytes IV - case 48: - ivLen = 16; - break; - - // 32 bytes key + 12 bytes IV - case 44: - ivLen = 12; - break; - default: - throw new IllegalArgumentException("Provided URL has an invalid ref tag (" + ref.length() + "): '" + ref + "'"); - } - iv = new byte[ivLen]; - System.arraycopy(refBytes, 0, iv, 0, ivLen); - System.arraycopy(refBytes, ivLen, key, 0, 32); - - return new byte[][] {iv,key}; - } - } } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/AesgcmUrl.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/AesgcmUrl.java new file mode 100644 index 0000000000..ce3d75bdf5 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/AesgcmUrl.java @@ -0,0 +1,161 @@ +/** + * + * Copyright © 2019 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.omemo_media_sharing; + +import java.net.MalformedURLException; +import java.net.URL; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; + +import org.jivesoftware.smack.util.Objects; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.httpfileupload.element.Slot; + +/** + * This class represents a aesgcm URL as described in XEP-XXXX: OMEMO Media Sharing. + * As the builtin {@link URL} class cannot handle the aesgcm protocol identifier, this class + * is used as a utility class that bundles together a {@link URL}, key and IV. + * + * @see XEP-XXXX: OMEMO Media Sharing + */ +public class AesgcmUrl { + + public static final String PROTOCOL = "aesgcm"; + + private final URL httpsUrl; + private final byte[] keyBytes; + private final byte[] ivBytes; + + /** + * Private constructor that constructs the {@link AesgcmUrl} from a normal https {@link URL}, a key and iv. + * + * @param httpsUrl normal https url as given by the {@link Slot}. + * @param key byte array of an encoded 256 bit aes key + * @param iv 16 or 12 byte initialization vector + */ + public AesgcmUrl(URL httpsUrl, byte[] key, byte[] iv) { + this.httpsUrl = Objects.requireNonNull(httpsUrl); + this.keyBytes = Objects.requireNonNull(key); + this.ivBytes = Objects.requireNonNull(iv); + } + + /** + * Parse a {@link AesgcmUrl} from a {@link String}. + * The parsed object will provide a normal {@link URL} under which the offered file can be downloaded, + * as well as a {@link Cipher} that can be used to decrypt it. + * + * @param aesgcmUrlString aesgcm URL as a {@link String} + */ + public AesgcmUrl(String aesgcmUrlString) { + if (!aesgcmUrlString.startsWith(PROTOCOL)) { + throw new IllegalArgumentException("Provided String does not resemble a aesgcm URL."); + } + + // Convert aesgcm Url to https URL + this.httpsUrl = extractHttpsUrl(aesgcmUrlString); + + // Extract IV and Key + byte[][] ivAndKey = extractIVAndKey(aesgcmUrlString); + this.ivBytes = ivAndKey[0]; + this.keyBytes = ivAndKey[1]; + } + + /** + * Return a https {@link URL} under which the file can be downloaded. + * + * @return https URL + */ + public URL getDownloadUrl() { + return httpsUrl; + } + + /** + * Returns the {@link String} representation of this aesgcm URL. + * + * @return aesgcm URL with key and IV. + */ + public String getAesgcmUrl() { + String aesgcmUrl = httpsUrl.toString().replaceFirst(httpsUrl.getProtocol(), PROTOCOL); + return aesgcmUrl + "#" + StringUtils.encodeHex(ivBytes) + StringUtils.encodeHex(keyBytes); + } + + /** + * Returns a {@link Cipher} in decryption mode, which can be used to decrypt the offered file. + * + * @return cipher + * + * @throws NoSuchPaddingException if the JVM cannot provide the specified cipher mode + * @throws NoSuchAlgorithmException if the JVM cannot provide the specified cipher mode + * @throws InvalidAlgorithmParameterException if the JVM cannot provide the specified cipher + * (eg. if no BC provider is added) + * @throws InvalidKeyException if the provided key is invalid + */ + public Cipher getDecryptionCipher() throws NoSuchPaddingException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException, InvalidKeyException { + return OmemoMediaSharingUtils.decryptionCipherFrom(keyBytes, ivBytes); + } + + private static URL extractHttpsUrl(String aesgcmUrlString) { + // aesgcm -> https + String httpsUrlString = aesgcmUrlString.replaceFirst(PROTOCOL, "https"); + // remove #ref + httpsUrlString = httpsUrlString.substring(0, httpsUrlString.indexOf("#")); + + try { + return new URL(httpsUrlString); + } catch (MalformedURLException e) { + throw new AssertionError("Failed to convert aesgcm URL to https URL: '" + aesgcmUrlString + "'", e); + } + } + + private static byte[][] extractIVAndKey(String aesgcmUrlString) { + int startOfRef = aesgcmUrlString.lastIndexOf("#"); + if (startOfRef == -1) { + throw new IllegalArgumentException("The provided aesgcm Url does not have a ref part which is " + + "supposed to contain the encryption key for file encryption."); + } + + String ref = aesgcmUrlString.substring(startOfRef + 1); + byte[] refBytes = StringUtils.hexStringToByteArray(ref); + + byte[] key = new byte[32]; + byte[] iv; + int ivLen; + // determine the length of the initialization vector part + switch (refBytes.length) { + // 32 bytes key + 16 bytes IV + case 48: + ivLen = 16; + break; + + // 32 bytes key + 12 bytes IV + case 44: + ivLen = 12; + break; + default: + throw new IllegalArgumentException("Provided URL has an invalid ref tag (" + ref.length() + "): '" + ref + "'"); + } + iv = new byte[ivLen]; + System.arraycopy(refBytes, 0, iv, 0, ivLen); + System.arraycopy(refBytes, ivLen, key, 0, 32); + + return new byte[][] {iv,key}; + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingUtils.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingUtils.java new file mode 100644 index 0000000000..95d2fd754a --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingUtils.java @@ -0,0 +1,115 @@ +/** + * + * Copyright © 2019 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.omemo_media_sharing; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +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; + +import org.jivesoftware.smack.util.RandomUtils; + +/** + * Utility code for XEP-XXXX: OMEMO Media Sharing. + * + * @see XEP-XXXX: OMEMO Media Sharing + */ +public class OmemoMediaSharingUtils { + + private static final String KEYTYPE = "AES"; + private static final String CIPHERMODE = "AES/GCM/NoPadding"; + // 256 bit = 32 byte + private static final int LEN_KEY = 32; + private static final int LEN_KEY_BITS = LEN_KEY * 8; + + private static final int LEN_IV_12 = 12; + private static final int LEN_IV_16 = 16; + // Note: Contrary to what the ProtoXEP states, 16 byte IV length is used in the wild instead of 12. + // At some point we should switch to 12 bytes though. + private static final int LEN_IV = LEN_IV_16; + + public static byte[] generateRandomIV() { + return generateRandomIV(LEN_IV); + } + + public static byte[] generateRandomIV(int len) { + return RandomUtils.secureRandomBytes(len); + } + + /** + * Generate a random 256 bit AES key. + * + * @return encoded AES key + * @throws NoSuchAlgorithmException if the JVM doesn't provide the given key type. + */ + public static byte[] generateRandomKey() throws NoSuchAlgorithmException { + KeyGenerator generator = KeyGenerator.getInstance(KEYTYPE); + generator.init(LEN_KEY_BITS); + return generator.generateKey().getEncoded(); + } + + /** + * Create a {@link Cipher} from a given key and iv which is in encryption mode. + * + * @param key aes encryption key + * @param iv initialization vector + * + * @return cipher in encryption mode + * + * @throws NoSuchPaddingException if the JVM doesn't provide the padding specified in the ciphermode. + * @throws NoSuchAlgorithmException if the JVM doesn't provide the encryption method specified in the ciphermode. + * @throws InvalidAlgorithmParameterException if the cipher cannot be initiated. + * @throws InvalidKeyException if the key is invalid. + */ + public static Cipher encryptionCipherFrom(byte[] key, byte[] iv) + throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, + InvalidKeyException { + SecretKey secretKey = new SecretKeySpec(key, KEYTYPE); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + Cipher cipher = Cipher.getInstance(CIPHERMODE); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); + return cipher; + } + + /** + * Create a {@link Cipher} from a given key and iv which is in decryption mode. + * + * @param key aes encryption key + * @param iv initialization vector + * + * @return cipher in decryption mode + * + * @throws NoSuchPaddingException if the JVM doesn't provide the padding specified in the ciphermode. + * @throws NoSuchAlgorithmException if the JVM doesn't provide the encryption method specified in the ciphermode. + * @throws InvalidAlgorithmParameterException if the cipher cannot be initiated. + * @throws InvalidKeyException if the key is invalid. + */ + public static Cipher decryptionCipherFrom(byte[] key, byte[] iv) + throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, + InvalidKeyException { + SecretKey secretKey = new SecretKeySpec(key, KEYTYPE); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + Cipher cipher = Cipher.getInstance(CIPHERMODE); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); + return cipher; + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/package-info.java new file mode 100644 index 0000000000..048c6e4057 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/package-info.java @@ -0,0 +1,24 @@ +/** + * + * Copyright © 2017 Grigory Fedorov + * + * 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: OMEMO Media Sharing. + * + * @author Paul Schaub + * @see XEP-XXXX: OMEMO Media Sharing + */ +package org.jivesoftware.smackx.omemo_media_sharing; diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/OmemoMediaSharingUtilsTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingUtilsTest.java similarity index 85% rename from smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/OmemoMediaSharingUtilsTest.java rename to smack-experimental/src/test/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingUtilsTest.java index 335704f3c4..d8b8c301b4 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/OmemoMediaSharingUtilsTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingUtilsTest.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jivesoftware.smackx.httpfileupload; +package org.jivesoftware.smackx.omemo_media_sharing; import static junit.framework.TestCase.assertEquals; @@ -40,14 +40,14 @@ public class OmemoMediaSharingUtilsTest { @Test public void test12byteIvVariant() throws MalformedURLException { - HttpFileUploadManager.AesgcmUrl aesgcm = new HttpFileUploadManager.AesgcmUrl(file_aesgcm_12); + AesgcmUrl aesgcm = new AesgcmUrl(file_aesgcm_12); // Make sure, that parsed aesgcm url still equals input string assertEquals(file_aesgcm_12, aesgcm.getAesgcmUrl()); assertEquals(file_https, aesgcm.getDownloadUrl().toString()); URL url = new URL(file_https); - aesgcm = new HttpFileUploadManager.AesgcmUrl(url, StringUtils.hexStringToByteArray(key), + aesgcm = new AesgcmUrl(url, StringUtils.hexStringToByteArray(key), StringUtils.hexStringToByteArray(iv_12)); assertEquals(file_aesgcm_12, aesgcm.getAesgcmUrl()); assertEquals(file_https, aesgcm.getDownloadUrl().toString()); @@ -55,14 +55,14 @@ public void test12byteIvVariant() throws MalformedURLException { @Test public void test16byteIvVariant() throws MalformedURLException { - HttpFileUploadManager.AesgcmUrl aesgcm = new HttpFileUploadManager.AesgcmUrl(file_aesgcm_16); + AesgcmUrl aesgcm = new AesgcmUrl(file_aesgcm_16); // Make sure, that parsed aesgcm url still equals input string assertEquals(file_aesgcm_16, aesgcm.getAesgcmUrl()); assertEquals(file_https, aesgcm.getDownloadUrl().toString()); URL url = new URL(file_https); - aesgcm = new HttpFileUploadManager.AesgcmUrl(url, StringUtils.hexStringToByteArray(key), + aesgcm = new AesgcmUrl(url, StringUtils.hexStringToByteArray(key), StringUtils.hexStringToByteArray(iv_16)); assertEquals(file_aesgcm_16, aesgcm.getAesgcmUrl()); assertEquals(file_https, aesgcm.getDownloadUrl().toString()); diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java index 5ff2b1e820..5710321e24 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java @@ -31,6 +31,7 @@ import java.lang.reflect.Type; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; +import java.security.Security; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -56,10 +57,10 @@ import org.jivesoftware.smack.tcp.XMPPTCPConnection; import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; import org.jivesoftware.smack.util.StringUtils; - import org.jivesoftware.smackx.debugger.EnhancedDebuggerWindow; import org.jivesoftware.smackx.iqregister.AccountManager; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.igniterealtime.smack.inttest.Configuration.AccountRegistration; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -75,6 +76,12 @@ public class SmackIntegrationTestFramework { public static boolean SINTTEST_UNIT_TEST = false; + static { + if (Security.getProvider("BC") == null) { + Security.insertProviderAt(new BouncyCastleProvider(), 0); + } + } + private final Class defaultConnectionClass; protected final Configuration config; diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadIntegrationTest.java index 96673d0c13..6390a2e948 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadIntegrationTest.java @@ -26,20 +26,12 @@ import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.Security; -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.NoSuchPaddingException; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; @@ -54,9 +46,6 @@ public class HttpFileUploadIntegrationTest extends AbstractSmackIntegrationTest public HttpFileUploadIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPErrorException, NotConnectedException, NoResponseException, InterruptedException, TestNotPossibleException { super(environment); - if (Security.getProvider("BC") == null) { - Security.addProvider(new BouncyCastleProvider()); - } hfumOne = HttpFileUploadManager.getInstanceFor(conOne); if (!hfumOne.discoverUploadService()) { throw new TestNotPossibleException( @@ -113,53 +102,4 @@ public void onUploadProgress(long uploadedBytes, long totalBytes) { assertArrayEquals(upBytes, downBytes); } - - @SmackIntegrationTest - public void omemoMediaSharingTest() throws IOException, NoSuchPaddingException, InterruptedException, - InvalidKeyException, NoSuchAlgorithmException, XMPPErrorException, SmackException, - InvalidAlgorithmParameterException { - final int fileSize = FILE_SIZE; - File file = createNewTempFile(); - FileOutputStream fos = new FileOutputStream(file.getCanonicalPath()); - byte[] upBytes; - try { - upBytes = new byte[fileSize]; - INSECURE_RANDOM.nextBytes(upBytes); - fos.write(upBytes); - } - finally { - fos.close(); - } - - HttpFileUploadManager.AesgcmUrl aesgcmUrl = hfumOne.uploadFileEncrypted(file, new UploadProgressListener() { - @Override - public void onUploadProgress(long uploadedBytes, long totalBytes) { - double progress = uploadedBytes / totalBytes; - LOGGER.fine("Encrypted HTTP File Upload progress " + progress + "% (" + uploadedBytes + '/' + totalBytes + ')'); - } - }); - - URL httpsUrl = aesgcmUrl.getDownloadUrl(); - Cipher decryptionCipher = aesgcmUrl.getDecryptionCipher(); - - HttpURLConnection urlConnection = getHttpUrlConnectionFor(httpsUrl); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(fileSize); - byte[] buffer = new byte[4096]; - int n; - try { - InputStream is = new CipherInputStream(urlConnection.getInputStream(), decryptionCipher); - while ((n = is.read(buffer)) != -1) { - baos.write(buffer, 0, n); - } - is.close(); - } - finally { - urlConnection.disconnect(); - } - - byte[] downBytes = baos.toByteArray(); - - assertArrayEquals(upBytes, downBytes); - } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingIntegrationTest.java new file mode 100644 index 0000000000..1e058a6111 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingIntegrationTest.java @@ -0,0 +1,134 @@ +/** + * + * Copyright 2019 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.omemo_media_sharing; + +import static org.junit.Assert.assertArrayEquals; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.NoSuchPaddingException; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.httpfileupload.HttpFileUploadManager; +import org.jivesoftware.smackx.httpfileupload.UploadProgressListener; +import org.jivesoftware.smackx.httpfileupload.UploadService; + +import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; +import org.igniterealtime.smack.inttest.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; +import org.igniterealtime.smack.inttest.TestNotPossibleException; + +public class OmemoMediaSharingIntegrationTest extends AbstractSmackIntegrationTest { + + private static final int FILE_SIZE = 1024 * 128; + + private final HttpFileUploadManager hfumOne; + + public OmemoMediaSharingIntegrationTest(SmackIntegrationTestEnvironment environment) + throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException, TestNotPossibleException { + super(environment); + hfumOne = HttpFileUploadManager.getInstanceFor(conOne); + if (!hfumOne.discoverUploadService()) { + throw new TestNotPossibleException( + "HttpFileUploadManager was unable to discover a HTTP File Upload service"); + } + UploadService uploadService = hfumOne.getDefaultUploadService(); + if (!uploadService.acceptsFileOfSize(FILE_SIZE)) { + throw new TestNotPossibleException("The upload service at " + uploadService.getAddress() + + " does not accept files of size " + FILE_SIZE + + ". It only accepts files with a maximum size of " + uploadService.getMaxFileSize()); + } + hfumOne.setTlsContext(environment.configuration.tlsContext); + } + + /** + * Test OMEMO Media Sharing by uploading an encrypted file to the server and downloading it again to see, whether + * encryption and decryption works. + * + * @throws IOException + * @throws NoSuchPaddingException + * @throws InterruptedException + * @throws InvalidKeyException + * @throws NoSuchAlgorithmException + * @throws XMPPException.XMPPErrorException + * @throws SmackException + * @throws InvalidAlgorithmParameterException + */ + @SmackIntegrationTest + public void omemoMediaSharingTest() throws IOException, NoSuchPaddingException, InterruptedException, + InvalidKeyException, NoSuchAlgorithmException, XMPPException.XMPPErrorException, SmackException, + InvalidAlgorithmParameterException { + final int fileSize = FILE_SIZE; + File file = createNewTempFile(); + FileOutputStream fos = new FileOutputStream(file.getCanonicalPath()); + byte[] upBytes; + try { + upBytes = new byte[fileSize]; + INSECURE_RANDOM.nextBytes(upBytes); + fos.write(upBytes); + } + finally { + fos.close(); + } + + AesgcmUrl aesgcmUrl = hfumOne.uploadFileEncrypted(file, new UploadProgressListener() { + @Override + public void onUploadProgress(long uploadedBytes, long totalBytes) { + double progress = uploadedBytes / totalBytes; + LOGGER.fine("Encrypted HTTP File Upload progress " + progress + "% (" + uploadedBytes + '/' + totalBytes + ')'); + } + }); + + URL httpsUrl = aesgcmUrl.getDownloadUrl(); + Cipher decryptionCipher = aesgcmUrl.getDecryptionCipher(); + + HttpURLConnection urlConnection = getHttpUrlConnectionFor(httpsUrl); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(fileSize); + byte[] buffer = new byte[4096]; + int n; + try { + InputStream is = new CipherInputStream(urlConnection.getInputStream(), decryptionCipher); + while ((n = is.read(buffer)) != -1) { + baos.write(buffer, 0, n); + } + is.close(); + } + finally { + urlConnection.disconnect(); + } + + byte[] downBytes = baos.toByteArray(); + + // In a real deployment, you want to check the AES TAG, not just cut it away! + byte[] withoutAesTag = new byte[fileSize]; + System.arraycopy(downBytes, 0, withoutAesTag, 0, fileSize); + assertArrayEquals(upBytes, withoutAesTag); + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/package-info.java new file mode 100644 index 0000000000..cb3306e7ee --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/package-info.java @@ -0,0 +1,23 @@ +/** + * + * Copyright 2019 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 Test classes for OMEMO Media Sharing. + * + * @author Paul Schaub + * @see XEP-XXXX: OMEMO Media Sharing + */ +package org.jivesoftware.smackx.omemo_media_sharing;