/** * * 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 XEP-XXXX: OMEMO Media Sharing */ 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}; } }