Browse Source

Add support for XEP-XXXX: OMEMO Media Sharing

omemo_media_sharing
Paul Schaub 1 year ago
parent
commit
9a635e7a25
No known key found for this signature in database GPG Key ID: 62BEE9264BF17311
11 changed files with 678 additions and 6 deletions
  1. +1
    -0
      documentation/extensions/index.md
  2. +36
    -0
      smack-core/src/main/java/org/jivesoftware/smack/util/RandomUtils.java
  3. +18
    -0
      smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java
  4. +86
    -5
      smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadManager.java
  5. +161
    -0
      smack-experimental/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/AesgcmUrl.java
  6. +116
    -0
      smack-experimental/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingUtils.java
  7. +24
    -0
      smack-experimental/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/package-info.java
  8. +71
    -0
      smack-experimental/src/test/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingUtilsTest.java
  9. +8
    -1
      smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java
  10. +134
    -0
      smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingIntegrationTest.java
  11. +23
    -0
      smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/package-info.java

+ 1
- 0
documentation/extensions/index.md View File

@@ -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


+ 36
- 0
smack-core/src/main/java/org/jivesoftware/smack/util/RandomUtils.java View File

@@ -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;
}
}

+ 18
- 0
smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java View File

@@ -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);
}


+ 86
- 5
smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadManager.java View File

@@ -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 <a href="http://xmpp.org/extensions/xep-0363.html">XEP-0363: HTTP File Upload</a>
* @see <a href="https://xmpp.org/extensions/inbox/omemo-media-sharing.html">XEP-XXXX: OMEMO Media Sharing</a>
*/
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 <a href="https://xmpp.org/extensions/inbox/omemo-media-sharing.html">XEP-XXXX: OMEMO Media Sharing</a>
*/
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 <a href="https://xmpp.org/extensions/inbox/omemo-media-sharing.html">XEP-XXXX: OMEMO Media Sharing</a>
*/
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<String, String> header : slot.getHeaders().entrySet()) {
urlConnection.setRequestProperty(header.getKey(), header.getValue());


+ 161
- 0
smack-experimental/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/AesgcmUrl.java View File

@@ -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 <a href="https://xmpp.org/extensions/inbox/omemo-media-sharing.html">XEP-XXXX: OMEMO Media Sharing</a>
*/
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};
}
}

+ 116
- 0
smack-experimental/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingUtils.java View File

@@ -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 <a href="https://xmpp.org/extensions/inbox/omemo-media-sharing.html">XEP-XXXX: OMEMO Media Sharing</a>
*/
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;
}
}

+ 24
- 0
smack-experimental/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/package-info.java View File

@@ -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 <a href="https://xmpp.org/extensions/inbox/omemo-media-sharing.html">XEP-XXXX: OMEMO Media Sharing</a>
*/
package org.jivesoftware.smackx.omemo_media_sharing;

+ 71
- 0
smack-experimental/src/test/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingUtilsTest.java View File

@@ -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());
}
}

+ 8
- 1
smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java View File

@@ -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<DC extends AbstractXMPPConnection> {

public static boolean SINTTEST_UNIT_TEST = false;

static {
if (Security.getProvider("BC") == null) {
Security.insertProviderAt(new BouncyCastleProvider(), 0);
}
}

private final Class<DC> defaultConnectionClass;

protected final Configuration config;


+ 134
- 0
smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/OmemoMediaSharingIntegrationTest.java View File

@@ -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);
}
}

+ 23
- 0
smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo_media_sharing/package-info.java View File

@@ -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 <a href="https://xmpp.org/extensions/inbox/omemo-media-sharing.html">XEP-XXXX: OMEMO Media Sharing</a>
*/
package org.jivesoftware.smackx.omemo_media_sharing;

Loading…
Cancel
Save