|
25 | 25 | import java.io.OutputStream; |
26 | 26 | import java.net.HttpURLConnection; |
27 | 27 | import java.net.URL; |
| 28 | +import java.security.InvalidAlgorithmParameterException; |
| 29 | +import java.security.InvalidKeyException; |
| 30 | +import java.security.NoSuchAlgorithmException; |
28 | 31 | import java.util.List; |
29 | 32 | import java.util.Map; |
30 | 33 | import java.util.Objects; |
31 | 34 | import java.util.WeakHashMap; |
32 | 35 | import java.util.logging.Level; |
33 | 36 | import java.util.logging.Logger; |
34 | 37 |
|
| 38 | +import javax.crypto.Cipher; |
| 39 | +import javax.crypto.CipherInputStream; |
| 40 | +import javax.crypto.NoSuchPaddingException; |
35 | 41 | import javax.net.ssl.HttpsURLConnection; |
36 | 42 | import javax.net.ssl.SSLContext; |
37 | 43 | import javax.net.ssl.SSLSocketFactory; |
|
53 | 59 | import org.jivesoftware.smackx.httpfileupload.element.Slot; |
54 | 60 | import org.jivesoftware.smackx.httpfileupload.element.SlotRequest; |
55 | 61 | 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; |
56 | 64 | import org.jivesoftware.smackx.xdata.FormField; |
57 | 65 | import org.jivesoftware.smackx.xdata.packet.DataForm; |
58 | 66 |
|
59 | 67 | import org.jxmpp.jid.DomainBareJid; |
60 | 68 |
|
61 | 69 | /** |
62 | 70 | * A manager for XEP-0363: HTTP File Upload. |
| 71 | + * This manager is also capable of XEP-0454: OMEMO Media Sharing. |
63 | 72 | * |
64 | 73 | * @author Grigory Fedorov |
65 | 74 | * @author Florian Schmaus |
| 75 | + * @author Paul Schaub |
66 | 76 | * @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> |
67 | 78 | */ |
68 | 79 | public final class HttpFileUploadManager extends Manager { |
69 | 80 |
|
@@ -315,6 +326,92 @@ public URL uploadFile(InputStream inputStream, String fileName, long fileSize, U |
315 | 326 | return slot.getGetUrl(); |
316 | 327 | } |
317 | 328 |
|
| 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 | + |
318 | 415 | /** |
319 | 416 | * Request a new upload slot from default upload service (if discovered). When you get slot you should upload file |
320 | 417 | * 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 |
476 | 573 | try { |
477 | 574 | inputStream.close(); |
478 | 575 | } |
479 | | - catch (IOException e) { |
| 576 | + // Must include IllegalStateException: GCM cipher cannot be reused for encryption (happen on Note-5) |
| 577 | + catch (IOException | IllegalStateException e) { |
480 | 578 | LOGGER.log(Level.WARNING, "Exception while closing input stream", e); |
481 | 579 | } |
482 | 580 | try { |
|
0 commit comments