Skip to content

Commit 71efd88

Browse files
committed
PR for XEP-0454 Media File Sharing
Replaces: igniterealtime#317 pull request
1 parent 8c359ee commit 71efd88

File tree

9 files changed

+655
-1
lines changed

9 files changed

+655
-1
lines changed

documentation/extensions/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ Experimental Smack Extensions and currently supported XEPs of smack-experimental
127127
| 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. |
128128
| Message Retraction | [XEP-0424](https://xmpp.org/extensions/xep-0424.html) | 0.2.0 | Mark messages as retracted. |
129129
| 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. |
130+
| 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. |
130131

131132
Unofficial XMPP Extensions
132133
--------------------------

smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadManager.java

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,19 @@
2525
import java.io.OutputStream;
2626
import java.net.HttpURLConnection;
2727
import java.net.URL;
28+
import java.security.InvalidAlgorithmParameterException;
29+
import java.security.InvalidKeyException;
30+
import java.security.NoSuchAlgorithmException;
2831
import java.util.List;
2932
import java.util.Map;
3033
import java.util.Objects;
3134
import java.util.WeakHashMap;
3235
import java.util.logging.Level;
3336
import java.util.logging.Logger;
3437

38+
import javax.crypto.Cipher;
39+
import javax.crypto.CipherInputStream;
40+
import javax.crypto.NoSuchPaddingException;
3541
import javax.net.ssl.HttpsURLConnection;
3642
import javax.net.ssl.SSLContext;
3743
import javax.net.ssl.SSLSocketFactory;
@@ -53,17 +59,22 @@
5359
import org.jivesoftware.smackx.httpfileupload.element.Slot;
5460
import org.jivesoftware.smackx.httpfileupload.element.SlotRequest;
5561
import org.jivesoftware.smackx.httpfileupload.element.SlotRequest_V0_2;
62+
import org.jivesoftware.smackx.omemo_media_sharing.AesgcmUrl;
63+
import org.jivesoftware.smackx.omemo_media_sharing.OmemoMediaSharingUtils;
5664
import org.jivesoftware.smackx.xdata.FormField;
5765
import org.jivesoftware.smackx.xdata.packet.DataForm;
5866

5967
import org.jxmpp.jid.DomainBareJid;
6068

6169
/**
6270
* A manager for XEP-0363: HTTP File Upload.
71+
* This manager is also capable of XEP-0454: OMEMO Media Sharing.
6372
*
6473
* @author Grigory Fedorov
6574
* @author Florian Schmaus
75+
* @author Paul Schaub
6676
* @see <a href="http://xmpp.org/extensions/xep-0363.html">XEP-0363: HTTP File Upload</a>
77+
* @see <a href="http://xmpp.org/extensions/inbox/omemo-media-sharing.html">XEP-0454: OMEMO Media Sharing</a>
6778
*/
6879
public final class HttpFileUploadManager extends Manager {
6980

@@ -315,6 +326,92 @@ public URL uploadFile(InputStream inputStream, String fileName, long fileSize, U
315326
return slot.getGetUrl();
316327
}
317328

329+
/**
330+
* Upload a file encrypted using the scheme described in OMEMO Media Sharing.
331+
* The file is being encrypted using a random 256 bit AES key in Galois Counter Mode using a random 16 byte IV and
332+
* then uploaded to the server.
333+
* The URL that is returned has a modified scheme (aesgcm:// instead of https://) and has the IV and key attached
334+
* as ref part.
335+
*
336+
* Note: The URL contains the used key and IV in plain text. Keep in mind to only share this URL though a secured
337+
* channel (i.e. end-to-end encrypted message), as anybody who can read the URL can also decrypt the file.
338+
*
339+
* Note: This method uses a IV of length 16 instead of 12. Although not specified in the ProtoXEP, 16 byte IVs are
340+
* currently used by most implementations. This implementation also supports 12 byte IVs when decrypting.
341+
*
342+
* @param file file
343+
* @return AESGCM URL which contains the key and IV of the encrypted file.
344+
* @throws InterruptedException If the calling thread was interrupted.
345+
* @throws IOException If an I/O error occurred.
346+
* @throws XMPPException.XMPPErrorException if there was an XMPP error returned.
347+
* @throws SmackException If Smack detected an exceptional situation.
348+
* @throws InvalidAlgorithmParameterException if the provided arguments are invalid.
349+
* @throws NoSuchAlgorithmException if no such algorithm is available.
350+
* @throws InvalidKeyException if the key is invalid.
351+
* @throws NoSuchPaddingException if the requested padding mechanism is not available.
352+
*
353+
* @see <a href="https://xmpp.org/extensions/inbox/omemo-media-sharing.html">XEP-0454: OMEMO Media Sharing</a>
354+
*/
355+
/**
356+
public AesgcmUrl uploadFileEncrypted(File file) throws InterruptedException, IOException,
357+
XMPPException.XMPPErrorException, SmackException, InvalidAlgorithmParameterException,
358+
NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException {
359+
return uploadFileEncrypted(file, null);
360+
}
361+
362+
/**
363+
* Upload a file encrypted using the scheme described in OMEMO Media Sharing.
364+
* The file is being encrypted using a random 256 bit AES key in Galois Counter Mode using a random 16 byte IV and
365+
* then uploaded to the server.
366+
* The URL that is returned has a modified scheme (aesgcm:// instead of https://) and has the IV and key attached
367+
* as ref part.
368+
* <p>
369+
* Note: The URL contains the used key and IV in plain text. Keep in mind to only share this URL though a secured
370+
* channel (i.e. end-to-end encrypted message), as anybody who can read the URL can also decrypt the file.
371+
* <p>
372+
* Note: This method uses a IV of length 16 instead of 12. Although not specified in the ProtoXEP, 16 byte IVs are
373+
* currently used by most implementations. This implementation also supports 12 byte IVs when decrypting.
374+
*
375+
* @param file file
376+
* @param listener progress listener or null
377+
* @return AESGCM URL which contains the key and IV of the encrypted file.
378+
* @throws IOException If an I/O error occurred.
379+
* @throws InterruptedException If the calling thread was interrupted.
380+
* @throws XMPPException.XMPPErrorException if there was an XMPP error returned.
381+
* @throws SmackException If Smack detected an exceptional situation.
382+
* @throws NoSuchPaddingException if the requested padding mechanism is not available.
383+
* @throws NoSuchAlgorithmException if no such algorithm is available.
384+
* @throws InvalidAlgorithmParameterException if the provided arguments are invalid.
385+
* @throws InvalidKeyException if the key is invalid.
386+
*
387+
* @see <a href="https://xmpp.org/extensions/inbox/omemo-media-sharing.html">XEP-0454: OMEMO Media Sharing</a>
388+
*/
389+
public AesgcmUrl uploadFileEncrypted(File file, UploadProgressListener listener) throws IOException,
390+
InterruptedException, XMPPException.XMPPErrorException, SmackException, NoSuchPaddingException,
391+
NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
392+
if (!file.isFile()) {
393+
throw new FileNotFoundException("The path " + file.getAbsolutePath() + " is not a file");
394+
}
395+
396+
// The encrypted file will contain an extra block with the AEAD MAC.
397+
long cipherFileLength = file.length() + 16;
398+
399+
final Slot slot = requestSlot(file.getName(), cipherFileLength, "application/octet-stream");
400+
URL slotUrl = slot.getGetUrl();
401+
402+
// fresh AES key + iv
403+
byte[] key = OmemoMediaSharingUtils.generateRandomKey();
404+
byte[] iv = OmemoMediaSharingUtils.generateRandomIV();
405+
Cipher cipher = OmemoMediaSharingUtils.encryptionCipherFrom(key, iv);
406+
407+
FileInputStream fis = new FileInputStream(file);
408+
// encrypt the file on the fly - encryption actually happens below in uploadFile()
409+
CipherInputStream cis = new CipherInputStream(fis, cipher);
410+
411+
upload(cis, cipherFileLength, slot, listener);
412+
return new AesgcmUrl(slotUrl, key, iv);
413+
}
414+
318415
/**
319416
* Request a new upload slot from default upload service (if discovered). When you get slot you should upload file
320417
* to PUT URL and share GET URL. Note that this is a synchronous call -- Smack must wait for the server response.
@@ -476,7 +573,8 @@ private void upload(InputStream iStream, long fileSize, Slot slot, UploadProgres
476573
try {
477574
inputStream.close();
478575
}
479-
catch (IOException e) {
576+
// Must include IllegalStateException: GCM cipher cannot be reused for encryption (happen on Note-5)
577+
catch (IOException | IllegalStateException e) {
480578
LOGGER.log(Level.WARNING, "Exception while closing input stream", e);
481579
}
482580
try {
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/**
2+
*
3+
* Copyright © 2019 Paul Schaub
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.jivesoftware.smackx.omemo_media_sharing;
18+
19+
import java.net.MalformedURLException;
20+
import java.net.URL;
21+
import java.security.InvalidAlgorithmParameterException;
22+
import java.security.InvalidKeyException;
23+
import java.security.NoSuchAlgorithmException;
24+
import javax.crypto.Cipher;
25+
import javax.crypto.NoSuchPaddingException;
26+
27+
import org.jivesoftware.smack.util.Objects;
28+
import org.jivesoftware.smack.util.StringUtils;
29+
import org.jivesoftware.smackx.httpfileupload.element.Slot;
30+
31+
/**
32+
* This class represents a aesgcm URL as described in XEP-0454: OMEMO Media Sharing.
33+
* As the builtin {@link URL} class cannot handle the aesgcm protocol identifier, this class
34+
* is used as a utility class that bundles together a {@link URL}, key and IV.
35+
*
36+
* @see <a href="https://xmpp.org/extensions/inbox/omemo-media-sharing.html">XEP-0454: OMEMO Media Sharing</a>
37+
*/
38+
public class AesgcmUrl {
39+
40+
public static final String PROTOCOL = "aesgcm";
41+
42+
private final URL httpsUrl;
43+
private final byte[] keyBytes;
44+
private final byte[] ivBytes;
45+
46+
/**
47+
* Private constructor that constructs the {@link AesgcmUrl} from a normal https {@link URL}, a key and iv.
48+
*
49+
* @param httpsUrl normal https url as given by the {@link Slot}.
50+
* @param key byte array of an encoded 256 bit aes key
51+
* @param iv 16 or 12 byte initialization vector
52+
*/
53+
public AesgcmUrl(URL httpsUrl, byte[] key, byte[] iv) {
54+
this.httpsUrl = Objects.requireNonNull(httpsUrl);
55+
this.keyBytes = Objects.requireNonNull(key);
56+
this.ivBytes = Objects.requireNonNull(iv);
57+
}
58+
59+
/**
60+
* Parse a {@link AesgcmUrl} from a {@link String}.
61+
* The parsed object will provide a normal {@link URL} under which the offered file can be downloaded,
62+
* as well as a {@link Cipher} that can be used to decrypt it.
63+
*
64+
* @param aesgcmUrlString aesgcm URL as a {@link String}
65+
*/
66+
public AesgcmUrl(String aesgcmUrlString) {
67+
if (!aesgcmUrlString.startsWith(PROTOCOL)) {
68+
throw new IllegalArgumentException("Provided String does not resemble a aesgcm URL.");
69+
}
70+
71+
// Convert aesgcm Url to https URL
72+
this.httpsUrl = extractHttpsUrl(aesgcmUrlString);
73+
74+
// Extract IV and Key
75+
byte[][] ivAndKey = extractIVAndKey(aesgcmUrlString);
76+
this.ivBytes = ivAndKey[0];
77+
this.keyBytes = ivAndKey[1];
78+
}
79+
80+
/**
81+
* Return a https {@link URL} under which the file can be downloaded.
82+
*
83+
* @return https URL
84+
*/
85+
public URL getDownloadUrl() {
86+
return httpsUrl;
87+
}
88+
89+
/**
90+
* Returns the {@link String} representation of this aesgcm URL.
91+
*
92+
* @return aesgcm URL with key and IV.
93+
*/
94+
public String getAesgcmUrl() {
95+
String aesgcmUrl = httpsUrl.toString().replaceFirst(httpsUrl.getProtocol(), PROTOCOL);
96+
return aesgcmUrl + "#" + StringUtils.encodeHex(ivBytes) + StringUtils.encodeHex(keyBytes);
97+
}
98+
99+
/**
100+
* Returns a {@link Cipher} in decryption mode, which can be used to decrypt the offered file.
101+
*
102+
* @return cipher
103+
*
104+
* @throws NoSuchPaddingException if the JVM cannot provide the specified cipher mode
105+
* @throws NoSuchAlgorithmException if the JVM cannot provide the specified cipher mode
106+
* @throws InvalidAlgorithmParameterException if the JVM cannot provide the specified cipher
107+
* (eg. if no BC provider is added)
108+
* @throws InvalidKeyException if the provided key is invalid
109+
*/
110+
public Cipher getDecryptionCipher() throws NoSuchPaddingException, NoSuchAlgorithmException,
111+
InvalidAlgorithmParameterException, InvalidKeyException {
112+
return OmemoMediaSharingUtils.decryptionCipherFrom(keyBytes, ivBytes);
113+
}
114+
115+
private static URL extractHttpsUrl(String aesgcmUrlString) {
116+
// aesgcm -> https
117+
String httpsUrlString = aesgcmUrlString.replaceFirst(PROTOCOL, "https");
118+
// remove #ref
119+
httpsUrlString = httpsUrlString.substring(0, httpsUrlString.indexOf("#"));
120+
121+
try {
122+
return new URL(httpsUrlString);
123+
} catch (MalformedURLException e) {
124+
throw new AssertionError("Failed to convert aesgcm URL to https URL: '" + aesgcmUrlString + "'", e);
125+
}
126+
}
127+
128+
private static byte[][] extractIVAndKey(String aesgcmUrlString) {
129+
int startOfRef = aesgcmUrlString.lastIndexOf("#");
130+
if (startOfRef == -1) {
131+
throw new IllegalArgumentException("The provided aesgcm Url does not have a ref part which is " +
132+
"supposed to contain the encryption key for file encryption.");
133+
}
134+
135+
String ref = aesgcmUrlString.substring(startOfRef + 1);
136+
byte[] refBytes = hexStringToByteArray(ref);
137+
138+
byte[] key = new byte[32];
139+
byte[] iv;
140+
int ivLen;
141+
// determine the length of the initialization vector part
142+
switch (refBytes.length) {
143+
// 32 bytes key + 16 bytes IV
144+
case 48:
145+
ivLen = 16;
146+
break;
147+
148+
// 32 bytes key + 12 bytes IV
149+
case 44:
150+
ivLen = 12;
151+
break;
152+
default:
153+
throw new IllegalArgumentException("Provided URL has an invalid ref tag (" + ref.length() + "): '" + ref + "'");
154+
}
155+
iv = new byte[ivLen];
156+
System.arraycopy(refBytes, 0, iv, 0, ivLen);
157+
System.arraycopy(refBytes, ivLen, key, 0, 32);
158+
159+
return new byte[][] {iv, key};
160+
}
161+
162+
/**
163+
* Convert a hexadecimal String to bytes.
164+
*
165+
* Source: https://stackoverflow.com/a/140861/11150851
166+
*
167+
* @param s hex string
168+
* @return byte array
169+
*/
170+
public static byte[] hexStringToByteArray(String s) {
171+
int len = s.length();
172+
byte[] data = new byte[len / 2];
173+
for (int i = 0; i < len; i += 2) {
174+
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
175+
+ Character.digit(s.charAt(i + 1), 16));
176+
}
177+
return data;
178+
}
179+
}

0 commit comments

Comments
 (0)