diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md index 527c0dd10..a20df252d 100644 --- a/documentation/extensions/index.md +++ b/documentation/extensions/index.md @@ -113,6 +113,7 @@ Unofficial XMPP Extensions | Name | XEP | Version | Description | |---------------------------------------------|--------------------------------------------------------|-----------|----------------------------------------------------------------------------------------------------------| | [Multi-User Chat Light](muclight.md) | [XEP-xxxx](https://mongooseim.readthedocs.io/en/latest/open-extensions/xeps/xep-muc-light.html) | n/a | Multi-User Chats for mobile XMPP applications and specific environment. | +| OMEMO Media Sharing | [XEP-XXXX](https://xmpp.org/extensions/inbox/omemo-media-sharing.html) | 0.0.1 | Share files via HTTP File Upload in an encrypted fashion. | | Google GCM JSON payload | n/a | n/a | Semantically the same as XEP-0335: JSON Containers. | Legacy Smack Extensions and currently supported XEPs of smack-legacy 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 000000000..90ede532f --- /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 0798c1ab0..ecbfa6c55 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 @@ -261,6 +261,24 @@ public class StringUtils { return new String(hexChars); } + /** + * Convert a hexadecimal String to bytes. + * + * Source: https://stackoverflow.com/a/140861/11150851 + * + * @param s hex string + * @return byte array + */ + public static byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i + 1), 16)); + } + return data; + } + public static byte[] toUtf8Bytes(String string) { return string.getBytes(StandardCharsets.UTF_8); } 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 29f78c48c..6a6879be6 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 @@ -21,16 +21,22 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.WeakHashMap; import java.util.logging.Level; import java.util.logging.Logger; - +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.NoSuchPaddingException; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; @@ -43,13 +49,14 @@ import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPConnectionRegistry; import org.jivesoftware.smack.XMPPException; - 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; @@ -57,10 +64,13 @@ import org.jxmpp.jid.DomainBareJid; /** * A manager for XEP-0363: HTTP File Upload. + * This manager is also capable of XEP-XXXX: OMEMO Media Sharing. * * @author Grigory Fedorov * @author Florian Schmaus + * @author Paul Schaub * @see XEP-0363: HTTP File Upload + * @see XEP-XXXX: OMEMO Media Sharing */ public final class HttpFileUploadManager extends Manager { @@ -245,7 +255,7 @@ public final class HttpFileUploadManager extends Manager { * Note that this is a synchronous call -- Smack must wait for the server response. * * @param file file to be uploaded - * @param listener upload progress listener of null + * @param listener upload progress listener or null * @return public URL for sharing uploaded file * * @throws InterruptedException @@ -265,6 +275,74 @@ public final class HttpFileUploadManager extends Manager { return slot.getGetUrl(); } + /** + * Upload a file encrypted using the scheme described in OMEMO Media Sharing. + * The file is being encrypted using a random 256 bit AES key in Galois Counter Mode using a random 16 byte IV and + * then uploaded to the server. + * The URL that is returned has a modified scheme (aesgcm:// instead of https://) and has the IV and key attached + * as ref part. + * + * Note: The URL contains the used key and IV in plain text. Keep in mind to only share this URL though a secured + * channel (i.e. end-to-end encrypted message), as anybody who can read the URL can also decrypt the file. + * + * Note: This method uses a IV of length 16 instead of 12. Although not specified in the ProtoXEP, 16 byte IVs are + * currently used by most implementations. This implementation also supports 12 byte IVs when decrypting. + * + * @param file file + * @return AESGCM URL which contains the key and IV of the encrypted file. + * + * @see XEP-XXXX: OMEMO Media Sharing + */ + public AesgcmUrl uploadFileEncrypted(File file) throws InterruptedException, IOException, + XMPPException.XMPPErrorException, SmackException, InvalidAlgorithmParameterException, + NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException { + return uploadFileEncrypted(file, null); + } + /** + * Upload a file encrypted using the scheme described in OMEMO Media Sharing. + * The file is being encrypted using a random 256 bit AES key in Galois Counter Mode using a random 16 byte IV and + * then uploaded to the server. + * The URL that is returned has a modified scheme (aesgcm:// instead of https://) and has the IV and key attached + * as ref part. + * + * Note: The URL contains the used key and IV in plain text. Keep in mind to only share this URL though a secured + * channel (i.e. end-to-end encrypted message), as anybody who can read the URL can also decrypt the file. + * + * Note: This method uses a IV of length 16 instead of 12. Although not specified in the ProtoXEP, 16 byte IVs are + * currently used by most implementations. This implementation also supports 12 byte IVs when decrypting. + * + * @param file file + * @param listener progress listener or null + * @return AESGCM URL which contains the key and IV of the encrypted file. + * + * @see XEP-XXXX: OMEMO Media Sharing + */ + public AesgcmUrl uploadFileEncrypted(File file, UploadProgressListener listener) throws IOException, + InterruptedException, XMPPException.XMPPErrorException, SmackException, NoSuchPaddingException, + NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException { + if (!file.isFile()) { + throw new FileNotFoundException("The path " + file.getAbsolutePath() + " is not a file"); + } + + // The encrypted file will contain an extra block with the AEAD MAC. + long cipherFileLength = file.length() + 16; + + final Slot slot = requestSlot(file.getName(), cipherFileLength, "application/octet-stream"); + URL slotUrl = slot.getGetUrl(); + + // fresh AES key + iv + byte[] key = OmemoMediaSharingUtils.generateRandomKey(); + byte[] iv = OmemoMediaSharingUtils.generateRandomIV(); + Cipher cipher = OmemoMediaSharingUtils.encryptionCipherFrom(key, iv); + + FileInputStream fis = new FileInputStream(file); + // encrypt the file on the fly - encryption actually happens below in uploadFile() + CipherInputStream cis = new CipherInputStream(fis, cipher); + + uploadFile(cis, cipherFileLength, slot, listener); + + return new AesgcmUrl(slotUrl, key, iv); + } /** * Request a new upload slot from default upload service (if discovered). When you get slot you should upload file @@ -391,10 +469,13 @@ public final class HttpFileUploadManager extends Manager { if (fileSize >= Integer.MAX_VALUE) { throw new IllegalArgumentException("File size " + fileSize + " must be less than " + Integer.MAX_VALUE); } - final int fileSizeInt = (int) fileSize; // Construct the FileInputStream first to make sure we can actually read the file. final FileInputStream fis = new FileInputStream(file); + uploadFile(fis, fileSize, slot, listener); + } + + private void uploadFile(final InputStream fis, long fileSize, final Slot slot, UploadProgressListener listener) throws IOException { final URL putUrl = slot.getPutUrl(); @@ -404,7 +485,7 @@ public final class HttpFileUploadManager extends Manager { urlConnection.setUseCaches(false); urlConnection.setDoOutput(true); // TODO Change to using fileSize once Smack's minimum Android API level is 19 or higher. - urlConnection.setFixedLengthStreamingMode(fileSizeInt); + urlConnection.setFixedLengthStreamingMode((int) fileSize); urlConnection.setRequestProperty("Content-Type", "application/octet-stream;"); for (Entry header : slot.getHeaders().entrySet()) { urlConnection.setRequestProperty(header.getKey(), header.getValue()); 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 000000000..348d429b1 --- /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 000000000..b6a1c2cd1 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingUtils.java @@ -0,0 +1,116 @@ +/** + * + * 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; + + @SuppressWarnings("unused") + 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 000000000..048c6e405 --- /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/omemo_media_sharing/OmemoMediaSharingUtilsTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingUtilsTest.java new file mode 100644 index 000000000..e8c1767a6 --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingUtilsTest.java @@ -0,0 +1,71 @@ +/** + * + * 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.jupiter.api.Assertions.assertEquals; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.jivesoftware.smack.util.StringUtils; + +import org.junit.jupiter.api.Test; + + +public class OmemoMediaSharingUtilsTest { + + private static final String iv_12 = "8c3d050e9386ec173861778f"; + private static final String iv_16 = "1ad857dcbb119e2642e4f8f7c137819e"; + private static final String key = "4f15af8f1a28100d0101fb1c2e119b0c18c34396c68ad379f5912ee21dca6b0b"; + private static final String key_iv_12 = iv_12 + key; + private static final String key_iv_16 = iv_16 + key; + + private static final String file = "download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/tr%C3%A8s%20cool.jpg"; + private static final String file_https = "https://" + file; + private static final String file_aesgcm_12 = "aesgcm://" + file + "#" + key_iv_12; + private static final String file_aesgcm_16 = "aesgcm://" + file + "#" + key_iv_16; + + @Test + public void test12byteIvVariant() throws MalformedURLException { + 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 AesgcmUrl(url, StringUtils.hexStringToByteArray(key), + StringUtils.hexStringToByteArray(iv_12)); + assertEquals(file_aesgcm_12, aesgcm.getAesgcmUrl()); + assertEquals(file_https, aesgcm.getDownloadUrl().toString()); + } + + @Test + public void test16byteIvVariant() throws MalformedURLException { + 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 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 962da4bee..0797d911e 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.ParameterizedType; 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.XMPPException; 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/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 000000000..1e058a611 --- /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 000000000..cb3306e7e --- /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;