/* * Copyright 2016 OpenMarket Ltd * * 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.matrix.androidsdk.crypto; import android.text.TextUtils; import android.util.Base64; import org.matrix.androidsdk.util.Log; import org.matrix.androidsdk.rest.model.EncryptedFileInfo; import org.matrix.androidsdk.rest.model.EncryptedFileKey; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.Serializable; import java.security.MessageDigest; import java.security.SecureRandom; import java.util.Arrays; import java.util.HashMap; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; public class MXEncryptedAttachments implements Serializable { private static final String LOG_TAG = "MXEncryptAtt"; private static final int CRYPTO_BUFFER_SIZE = 32 * 1024; private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding"; private static final String SECRET_KEY_SPEC_ALGORITHM = "AES"; private static final String MESSAGE_DIGEST_ALGORITHM = "SHA-256"; /** * Define the result of an encryption file */ public static class EncryptionResult { public EncryptedFileInfo mEncryptedFileInfo; public InputStream mEncryptedStream; public EncryptionResult() { } } /*** * Encrypt an attachment stream. * @param attachmentStream the attachment stream * @return the encryption file info */ public static EncryptionResult encryptAttachment(InputStream attachmentStream, String mimetype) { long t0 = System.currentTimeMillis(); SecureRandom secureRandom = new SecureRandom(); // generate a random iv key // Half of the IV is random, the lower order bits are zeroed // such that the counter never wraps. // See https://github.com/matrix-org/matrix-ios-kit/blob/3dc0d8e46b4deb6669ed44f72ad79be56471354c/MatrixKit/Models/Room/MXEncryptedAttachments.m#L75 byte[] initVectorBytes = new byte[16]; Arrays.fill(initVectorBytes, (byte)0); byte[] ivRandomPart = new byte[8]; secureRandom.nextBytes(ivRandomPart); System.arraycopy(ivRandomPart, 0, initVectorBytes, 0, ivRandomPart.length); byte[] key = new byte[32]; secureRandom.nextBytes(key); ByteArrayOutputStream outStream = new ByteArrayOutputStream(); try { Cipher encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM); SecretKeySpec secretKeySpec = new SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM); IvParameterSpec ivParameterSpec = new IvParameterSpec(initVectorBytes); encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); MessageDigest messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM); byte[] data = new byte[CRYPTO_BUFFER_SIZE]; int read; byte[] encodedBytes; while (-1 != (read = attachmentStream.read(data))) { encodedBytes = encryptCipher.update(data, 0, read); messageDigest.update(encodedBytes, 0, encodedBytes.length); outStream.write(encodedBytes); } // encrypt the latest chunk encodedBytes = encryptCipher.doFinal(); messageDigest.update(encodedBytes, 0, encodedBytes.length); outStream.write(encodedBytes); EncryptionResult result = new EncryptionResult(); result.mEncryptedFileInfo = new EncryptedFileInfo(); result.mEncryptedFileInfo.key = new EncryptedFileKey(); result.mEncryptedFileInfo.mimetype = mimetype; result.mEncryptedFileInfo.key.alg = "A256CTR"; result.mEncryptedFileInfo.key.ext = true; result.mEncryptedFileInfo.key.key_ops = Arrays.asList("encrypt", "decrypt"); result.mEncryptedFileInfo.key.kty = "oct"; result.mEncryptedFileInfo.key.k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT)); result.mEncryptedFileInfo.iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""); result.mEncryptedFileInfo.v = "v2"; result.mEncryptedFileInfo.hashes = new HashMap(); result.mEncryptedFileInfo.hashes.put("sha256", base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))); result.mEncryptedStream = new ByteArrayInputStream(outStream.toByteArray()); outStream.close(); Log.d(LOG_TAG, "Encrypt in " + (System.currentTimeMillis() - t0) + " ms"); return result; } catch (OutOfMemoryError oom) { Log.e(LOG_TAG, "## encryptAttachment failed " + oom.getMessage()); } catch (Exception e) { Log.e(LOG_TAG, "## encryptAttachment failed " + e.getMessage()); } try { outStream.close(); } catch (Exception e) { Log.e(LOG_TAG, "## encryptAttachment() : fail to close outStream"); } return null; } /** * Decrypt an attachment * @param attachmentStream the attahcment stream * @param encryptedFileInfo the encryption file info * @return the decrypted attachment stream */ public static InputStream decryptAttachment(InputStream attachmentStream, EncryptedFileInfo encryptedFileInfo) { // sanity checks if ((null == attachmentStream) || (null == encryptedFileInfo)) { Log.e(LOG_TAG, "## decryptAttachment() : null parameters"); return null; } if (TextUtils.isEmpty(encryptedFileInfo.iv) || (null == encryptedFileInfo.key) || (null == encryptedFileInfo.hashes) || !encryptedFileInfo.hashes.containsKey("sha256") ) { Log.e(LOG_TAG, "## decryptAttachment() : some fields are not defined"); return null; } if (!TextUtils.equals(encryptedFileInfo.key.alg, "A256CTR") || !TextUtils.equals(encryptedFileInfo.key.kty, "oct") || TextUtils.isEmpty(encryptedFileInfo.key.k)) { Log.e(LOG_TAG, "## decryptAttachment() : invalid key fields"); return null; } // detect if there is no data to decrypt try { if (0 == attachmentStream.available()) { return new ByteArrayInputStream(new byte[0]); } } catch (Exception e) { Log.e(LOG_TAG, "Fail to retrieve the file size"); } long t0 = System.currentTimeMillis(); ByteArrayOutputStream outStream = new ByteArrayOutputStream(); try { byte[] key = Base64.decode(base64UrlToBase64(encryptedFileInfo.key.k), Base64.DEFAULT); byte[] initVectorBytes = Base64.decode(encryptedFileInfo.iv, Base64.DEFAULT); Cipher decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM); SecretKeySpec secretKeySpec = new SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM); IvParameterSpec ivParameterSpec = new IvParameterSpec(initVectorBytes); decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); MessageDigest messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM); int read; byte[] data = new byte[CRYPTO_BUFFER_SIZE]; byte[] decodedBytes; while (-1 != (read = attachmentStream.read(data))) { messageDigest.update(data, 0, read); decodedBytes = decryptCipher.update(data, 0, read); outStream.write(decodedBytes); } // decrypt the last chunk decodedBytes = decryptCipher.doFinal(); messageDigest.update(decodedBytes); outStream.write(decodedBytes); String currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT)); if (!TextUtils.equals(encryptedFileInfo.hashes.get("sha256"), currentDigestValue)) { Log.e(LOG_TAG, "## decryptAttachment() : Digest value mismatch"); outStream.close(); return null; } InputStream decryptedStream = new ByteArrayInputStream(outStream.toByteArray()); outStream.close(); Log.d(LOG_TAG, "Decrypt in " + (System.currentTimeMillis() - t0) + " ms"); return decryptedStream; } catch (OutOfMemoryError oom) { Log.e(LOG_TAG, "## decryptAttachment() : failed " + oom.getMessage()); } catch (Exception e) { Log.e(LOG_TAG, "## decryptAttachment() : failed " + e.getMessage()); } try { outStream.close(); } catch (Exception closeException) { Log.e(LOG_TAG, "## decryptAttachment() : fail to close the file"); } return null; } /** * Base64 URL conversion methods */ private static String base64UrlToBase64(String base64Url) { if (null != base64Url) { base64Url = base64Url.replaceAll("-", "+"); base64Url = base64Url.replaceAll("_", "/"); } return base64Url; } private static String base64ToBase64Url(String base64) { if (null != base64) { base64 = base64.replaceAll("\n", ""); base64 = base64.replaceAll("\\+", "-"); base64 = base64.replaceAll("/", "_"); base64 = base64.replaceAll("=", ""); } return base64; } private static String base64ToUnpaddedBase64(String base64) { if (null != base64) { base64 = base64.replaceAll("\n", ""); base64 = base64.replaceAll("=", ""); } return base64; } }