Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OMEMO Media Sharing via HTTP File Upload #317

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions documentation/extensions/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,24 @@ public static String encodeHex(byte[] bytes) {
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) {
vanitasvitae marked this conversation as resolved.
Show resolved Hide resolved
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,22 @@
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;
Expand All @@ -43,24 +49,28 @@
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;

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 {

Expand Down Expand Up @@ -245,7 +255,7 @@ public URL uploadFile(File file) throws InterruptedException, XMPPException.XMPP
* 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
Expand All @@ -265,6 +275,74 @@ public URL uploadFile(File file, UploadProgressListener listener) throws Interru
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
Expand Down Expand Up @@ -391,10 +469,13 @@ private void uploadFile(final File file, final Slot slot, UploadProgressListener
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();

Expand All @@ -404,7 +485,7 @@ private void uploadFile(final File file, final Slot slot, UploadProgressListener
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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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};
}
}
Loading