Skip to content
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 @@ -127,6 +127,7 @@ Experimental Smack Extensions and currently supported XEPs of smack-experimental
| Message Fastening | [XEP-0422](https://xmpp.org/extensions/xep-0422.html) | 0.1.1 | Mark payloads on a message to be logistically fastened to a previous message. |
| Message Retraction | [XEP-0424](https://xmpp.org/extensions/xep-0424.html) | 0.2.0 | Mark messages as retracted. |
| Fallback Indication | [XEP-0428](https://xmpp.org/extensions/xep-0428.html) | 0.1.0 | Declare body elements of a message as ignorable fallback for naive legacy clients. |
| OMEMO Media Sharing | [XEP-0454](https://xmpp.org/extensions/xep-0454.html) | 0.1.0 | Share files via HTTP File Upload in an encrypted fashion. |

Unofficial XMPP Extensions
--------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,19 @@
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.Objects;
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 @@ -53,17 +59,22 @@
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-0454: 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="http://xmpp.org/extensions/inbox/omemo-media-sharing.html">XEP-0454: OMEMO Media Sharing</a>
*/
public final class HttpFileUploadManager extends Manager {

Expand Down Expand Up @@ -315,6 +326,92 @@ public URL uploadFile(InputStream inputStream, String fileName, long fileSize, U
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.
* @throws InterruptedException If the calling thread was interrupted.
* @throws IOException If an I/O error occurred.
* @throws XMPPException.XMPPErrorException if there was an XMPP error returned.
* @throws SmackException If Smack detected an exceptional situation.
* @throws InvalidAlgorithmParameterException if the provided arguments are invalid.
* @throws NoSuchAlgorithmException if no such algorithm is available.
* @throws InvalidKeyException if the key is invalid.
* @throws NoSuchPaddingException if the requested padding mechanism is not available.
*
* @see <a href="https://xmpp.org/extensions/inbox/omemo-media-sharing.html">XEP-0454: 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.
* <p>
* 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.
* <p>
* 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.
* @throws IOException If an I/O error occurred.
* @throws InterruptedException If the calling thread was interrupted.
* @throws XMPPException.XMPPErrorException if there was an XMPP error returned.
* @throws SmackException If Smack detected an exceptional situation.
* @throws NoSuchPaddingException if the requested padding mechanism is not available.
* @throws NoSuchAlgorithmException if no such algorithm is available.
* @throws InvalidAlgorithmParameterException if the provided arguments are invalid.
* @throws InvalidKeyException if the key is invalid.
*
* @see <a href="https://xmpp.org/extensions/inbox/omemo-media-sharing.html">XEP-0454: 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);

upload(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
* to PUT URL and share GET URL. Note that this is a synchronous call -- Smack must wait for the server response.
Expand Down Expand Up @@ -476,7 +573,8 @@ private void upload(InputStream iStream, long fileSize, Slot slot, UploadProgres
try {
inputStream.close();
}
catch (IOException e) {
// Must include IllegalStateException: GCM cipher cannot be reused for encryption (happen on Note-5)
catch (IOException | IllegalStateException e) {
LOGGER.log(Level.WARNING, "Exception while closing input stream", e);
}
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
*
* 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-0454: 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-0454: 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 = 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};
}

/**
* 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;
}
}
Loading