Commit ca00131d authored by Bastien Le Querrec's avatar Bastien Le Querrec

send and calculate digest for MMS messages

parent 476a256c
Pipeline #185 passed with stages
in 8 minutes and 1 second
......@@ -10,13 +10,23 @@ public class UriAttachment extends Attachment {
private final @Nullable Uri thumbnailUri;
public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size) {
this(uri, uri, contentType, transferState, size);
this(uri, uri, contentType, transferState, size, null);
}
public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size, byte[] digest) {
this(uri, uri, contentType, transferState, size, digest);
}
public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri,
@NonNull String contentType, int transferState, long size)
{
super(contentType, transferState, size, null, null, null, null);
this(dataUri, thumbnailUri, contentType, transferState, size, null);
}
public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri,
@NonNull String contentType, int transferState, long size, byte[] digest)
{
super(contentType, transferState, size, null, null, null, digest);
this.dataUri = dataUri;
this.thumbnailUri = thumbnailUri;
}
......
......@@ -22,6 +22,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.lang.System;
......@@ -50,25 +51,30 @@ public class DecryptingPartInputStream extends FileInputStream {
private static final int IV_LENGTH = 16;
private static final int MAC_LENGTH = 20;
private Cipher cipher;
private Mac mac;
private Cipher cipher;
private Mac mac;
private MessageDigest digest;
private byte[] theirDigest;
private boolean done;
private long totalDataSize;
private long totalRead;
private byte[] overflowBuffer;
public DecryptingPartInputStream(File file, MasterSecret masterSecret) throws FileNotFoundException {
public DecryptingPartInputStream(File file, MasterSecret masterSecret, byte[] theirDigest) throws FileNotFoundException {
super(file);
try {
if (file.length() <= IV_LENGTH + MAC_LENGTH)
throw new FileNotFoundException("Part shorter than crypto overhead!");
done = false;
digest = initializeDigest();
mac = initializeMac(masterSecret.getMacKey());
cipher = initializeCipher(masterSecret.getEncryptionKey());
totalDataSize = file.length() - cipher.getBlockSize() - mac.getMacLength();
totalRead = 0;
this.theirDigest = theirDigest;
} catch (InvalidKeyException ike) {
Log.w(TAG, ike);
throw new FileNotFoundException("Invalid key!");
......@@ -125,6 +131,12 @@ public class DecryptingPartInputStream extends FileInputStream {
if (!Arrays.equals(ourMac, theirMac))
throw new IOException("MAC doesn't match! Potential tampering?");
byte[] ourDigest = digest.digest(ourMac);
if (theirDigest != null && !MessageDigest.isEqual(ourDigest, theirDigest)) {
throw new IOException("Digest doesn't match!");
}
done = true;
return flourish;
} catch (IllegalBlockSizeException e) {
......@@ -168,6 +180,7 @@ public class DecryptingPartInputStream extends FileInputStream {
try {
mac.update(internalBuffer, 0, read);
digest.update(internalBuffer, 0, read);
int outputLen = cipher.getOutputSize(read);
......@@ -210,11 +223,18 @@ public class DecryptingPartInputStream extends FileInputStream {
return cipher;
}
private MessageDigest initializeDigest()
throws NoSuchAlgorithmException
{
return MessageDigest.getInstance("SHA256");
}
private IvParameterSpec readIv(int size) throws IOException {
byte[] iv = new byte[size];
readFully(iv);
mac.update(iv);
digest.update(iv);
return new IvParameterSpec(iv);
}
......
......@@ -21,6 +21,7 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
......@@ -40,25 +41,37 @@ import android.util.Log;
public class EncryptingPartOutputStream extends FileOutputStream {
private Cipher cipher;
private Mac mac;
private static final String TAG = EncryptingPartOutputStream.class.getSimpleName();
private Cipher cipher;
private Mac mac;
private MessageDigest messageDigest;
private boolean closed;
private byte[] digest;
public EncryptingPartOutputStream(File file, MasterSecret masterSecret) throws FileNotFoundException {
super(file);
try {
mac = initializeMac(masterSecret.getMacKey());
cipher = initializeCipher(mac, masterSecret.getEncryptionKey());
this.cipher = initializeCipher();
this.mac = initializeMac();
this.messageDigest = MessageDigest.getInstance("SHA256");
this.cipher.init(Cipher.ENCRYPT_MODE, masterSecret.getEncryptionKey());
this.mac.init(masterSecret.getMacKey());
mac.update(cipher.getIV());
messageDigest.update(cipher.getIV());
super.write(cipher.getIV(), 0, cipher.getIV().length);
closed = false;
} catch (IOException ioe) {
Log.w("EncryptingPartOutputStream", ioe);
Log.w(TAG, ioe);
throw new FileNotFoundException("Couldn't write IV");
} catch (InvalidKeyException e) {
throw new AssertionError(e);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (NoSuchPaddingException e) {
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
......@@ -74,6 +87,7 @@ public class EncryptingPartOutputStream extends FileOutputStream {
if (encryptedBuffer != null) {
mac.update(encryptedBuffer);
messageDigest.update(encryptedBuffer);
super.write(encryptedBuffer, 0, encryptedBuffer.length);
}
}
......@@ -87,6 +101,9 @@ public class EncryptingPartOutputStream extends FileOutputStream {
byte[] macBytes = mac.doFinal();
messageDigest.update(encryptedRemainder);
this.digest = messageDigest.digest(macBytes);
super.write(encryptedRemainder, 0, encryptedRemainder.length);
super.write(macBytes, 0, macBytes.length);
......@@ -101,22 +118,24 @@ public class EncryptingPartOutputStream extends FileOutputStream {
}
}
private Mac initializeMac(SecretKeySpec key) throws NoSuchAlgorithmException, InvalidKeyException {
Mac hmac = Mac.getInstance("HmacSHA1");
hmac.init(key);
return hmac;
public byte[] getAttachmentDigest() {
return digest;
}
private Cipher initializeCipher(Mac mac, SecretKeySpec key) throws IOException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] ivBytes = cipher.getIV();
mac.update(ivBytes);
super.write(ivBytes, 0, ivBytes.length);
private Mac initializeMac() {
try {
return Mac.getInstance("HmacSHA1");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
return cipher;
private Cipher initializeCipher() {
try {
return Cipher.getInstance("AES/CBC/PKCS5Padding");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new AssertionError(e);
}
}
}
......@@ -42,6 +42,7 @@ import org.smssecure.smssecure.crypto.MasterSecret;
import org.smssecure.smssecure.mms.MediaStream;
import org.smssecure.smssecure.mms.MmsException;
import org.smssecure.smssecure.mms.PartAuthority;
import org.smssecure.smssecure.util.Hex;
import org.smssecure.smssecure.util.MediaUtil;
import org.smssecure.smssecure.util.MediaUtil.ThumbnailData;
import org.smssecure.smssecure.util.Util;
......@@ -52,6 +53,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
......@@ -251,36 +253,6 @@ public class AttachmentDatabase extends Database {
}
}
public long insertAttachmentsForPlaceholder(@NonNull MasterSecret masterSecret, long mmsId,
@NonNull AttachmentId attachmentId,
@NonNull InputStream inputStream)
throws MmsException
{
SQLiteDatabase database = databaseHelper.getWritableDatabase();
Pair<File, Long> partData = setAttachmentData(masterSecret, inputStream);
ContentValues values = new ContentValues();
values.put(DATA, partData.first.getAbsolutePath());
values.put(SIZE, partData.second);
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
values.put(CONTENT_LOCATION, (String)null);
values.put(CONTENT_DISPOSITION, (String)null);
values.put(DIGEST, (byte[])null);
values.put(NAME, (String) null);
if (database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()) == 0) {
//noinspection ResultOfMethodCallIgnored
partData.first.delete();
} else {
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(mmsId));
notifyConversationListListeners();
}
thumbnailExecutor.submit(new ThumbnailFetchCallable(masterSecret, attachmentId));
return partData.second;
}
void insertAttachmentsForMessage(@NonNull MasterSecret masterSecret,
long mmsId,
@NonNull List<Attachment> attachments)
......@@ -307,10 +279,11 @@ public class AttachmentDatabase extends Database {
throw new MmsException("No attachment data found!");
}
long dataSize = setAttachmentData(masterSecret, dataFile, mediaStream.getStream());
Pair<Long, byte[]> dataSizeAndDigest = setAttachmentData(masterSecret, dataFile, mediaStream.getStream());
ContentValues contentValues = new ContentValues();
contentValues.put(SIZE, dataSize);
contentValues.put(SIZE, dataSizeAndDigest.first);
contentValues.put(DIGEST, dataSizeAndDigest.second);
contentValues.put(CONTENT_TYPE, mediaStream.getMimeType());
database.update(TABLE_NAME, contentValues, PART_ID_WHERE, databaseAttachment.getAttachmentId().toStrings());
......@@ -321,11 +294,11 @@ public class AttachmentDatabase extends Database {
databaseAttachment.hasThumbnail(),
mediaStream.getMimeType(),
databaseAttachment.getTransferState(),
dataSize,
dataSizeAndDigest.first,
databaseAttachment.getLocation(),
databaseAttachment.getKey(),
databaseAttachment.getRelay(),
databaseAttachment.getDigest());
dataSizeAndDigest.second);
}
......@@ -362,8 +335,10 @@ public class AttachmentDatabase extends Database {
{
File dataFile = getAttachmentDataFile(attachmentId, dataType);
byte[] digest = (!dataType.equals(THUMBNAIL)) ? getAttachment(attachmentId).getDigest() : null;
try {
if (dataFile != null) return new DecryptingPartInputStream(dataFile, masterSecret);
if (dataFile != null) return new DecryptingPartInputStream(dataFile, masterSecret, digest);
else return null;
} catch (FileNotFoundException e) {
Log.w(TAG, e);
......@@ -397,7 +372,7 @@ public class AttachmentDatabase extends Database {
}
private @NonNull Pair<File, Long> setAttachmentData(@NonNull MasterSecret masterSecret,
private @NonNull Pair<File, Pair<Long,byte[]>> setAttachmentData(@NonNull MasterSecret masterSecret,
@NonNull Uri uri)
throws MmsException
{
......@@ -409,7 +384,7 @@ public class AttachmentDatabase extends Database {
}
}
private @NonNull Pair<File, Long> setAttachmentData(@NonNull MasterSecret masterSecret,
private @NonNull Pair<File, Pair<Long,byte[]>> setAttachmentData(@NonNull MasterSecret masterSecret,
@NonNull InputStream in)
throws MmsException
{
......@@ -423,14 +398,14 @@ public class AttachmentDatabase extends Database {
}
}
private long setAttachmentData(@NonNull MasterSecret masterSecret,
private @NonNull Pair<Long, byte[]> setAttachmentData(@NonNull MasterSecret masterSecret,
@NonNull File destination,
@NonNull InputStream in)
throws MmsException
{
try {
OutputStream out = new EncryptingPartOutputStream(destination, masterSecret);
return Util.copy(in, out);
EncryptingPartOutputStream out = new EncryptingPartOutputStream(destination, masterSecret);
return new Pair<>(Util.copy(in, (OutputStream) out), out.getAttachmentDigest());
} catch (IOException e) {
throw new MmsException(e);
}
......@@ -457,9 +432,9 @@ public class AttachmentDatabase extends Database {
{
Log.w(TAG, "Inserting attachment for mms id: " + mmsId);
SQLiteDatabase database = databaseHelper.getWritableDatabase();
Pair<File, Long> partData = null;
long uniqueId = System.currentTimeMillis();
SQLiteDatabase database = databaseHelper.getWritableDatabase();
Pair<File, Pair<Long,byte[]>> partData = null;
long uniqueId = System.currentTimeMillis();
if (masterSecret != null && attachment.getDataUri() != null) {
partData = setAttachmentData(masterSecret, attachment.getDataUri());
......@@ -477,8 +452,9 @@ public class AttachmentDatabase extends Database {
contentValues.put(NAME, attachment.getRelay());
if (partData != null) {
contentValues.put(DATA, partData.first.getAbsolutePath());
contentValues.put(SIZE, partData.second);
contentValues.put(DATA, partData.first.getAbsolutePath());
contentValues.put(SIZE, partData.second.first);
contentValues.put(DIGEST, partData.second.second);
}
long rowId = database.insert(TABLE_NAME, null, contentValues);
......@@ -499,12 +475,12 @@ public class AttachmentDatabase extends Database {
{
Log.w(TAG, "updating part thumbnail for #" + attachmentId);
Pair<File, Long> thumbnailFile = setAttachmentData(masterSecret, in);
File thumbnailFile = setAttachmentData(masterSecret, in).first;
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues(2);
values.put(THUMBNAIL, thumbnailFile.first.getAbsolutePath());
values.put(THUMBNAIL, thumbnailFile.getAbsolutePath());
values.put(THUMBNAIL_ASPECT_RATIO, aspectRatio);
database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings());
......
......@@ -37,9 +37,11 @@ import org.smssecure.smssecure.protocol.WirePrefix;
import org.smssecure.smssecure.providers.SingleUseBlobProvider;
import org.smssecure.smssecure.service.KeyCachingService;
import org.smssecure.smssecure.util.dualsim.DualSimUtil;
import org.smssecure.smssecure.util.Hex;
import org.smssecure.smssecure.util.Util;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.jobqueue.util.Base64;
import org.whispersystems.libsignal.DuplicateMessageException;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.LegacyMessageException;
......@@ -223,10 +225,31 @@ public class MmsDownloadJob extends MasterSecretJob {
PduPart part = media.getPart(i);
if (part.getData() != null) {
byte[] decodedDigest = null;
if (isSecure) {
PduPart digestPart = pduBody.getPartByName(Util.toIsoString(part.getName()) + ".digest");
byte[] digestBytes = null;
if (digestPart != null) {
digestBytes = digestPart.getData();
}
if (digestBytes != null) {
decodedDigest = Base64.decode(digestBytes, Base64.NO_WRAP);
}
if (decodedDigest != null) {
Log.w(TAG, "Available digest for part name " + Util.toIsoString(part.getName()) + " (content id " + Util.toIsoString(part.getContentId()) + "): " + Hex.toString(decodedDigest));
} else {
Log.w(TAG, "No available digest for part name " + Util.toIsoString(part.getName()) + " (content id " + Util.toIsoString(part.getContentId()) + ")");
}
}
Uri uri = provider.createUri(part.getData());
attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()),
AttachmentDatabase.TRANSFER_PROGRESS_DONE,
part.getData().length));
part.getData().length, decodedDigest));
}
}
}
......
......@@ -40,9 +40,11 @@ import org.smssecure.smssecure.transport.UndeliverableMessageException;
import org.smssecure.smssecure.util.dualsim.DualSimUtil;
import org.smssecure.smssecure.util.Hex;
import org.smssecure.smssecure.util.NumberUtil;
import org.smssecure.smssecure.util.MediaUtil;
import org.smssecure.smssecure.util.Util;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.jobqueue.util.Base64;
import org.whispersystems.libsignal.NoSessionException;
import org.whispersystems.libsignal.UntrustedIdentityException;
......@@ -192,7 +194,7 @@ public class MmsSendJob extends SendJob {
SendReq req = new SendReq();
String lineNumber = Utils.getMyPhoneNumber(context);
List<String> numbers = message.getRecipients().toNumberStringList(true);
MediaConstraints mediaConstraints = MediaConstraints.getMmsMediaConstraints(message.getSubscriptionId(), message.isSecure());
MediaConstraints mediaConstraints = MediaConstraints.getMmsMediaConstraints(message.getSubscriptionId(), message.isSecure());
List<Attachment> scaledAttachments = scaleAttachments(masterSecret, mediaConstraints, message.getAttachments());
if (!TextUtils.isEmpty(lineNumber)) {
......@@ -226,7 +228,7 @@ public class MmsSendJob extends SendJob {
try {
if (attachment.getDataUri() == null) throw new IOException("Assertion failed, attachment for outgoing MMS has no data!");
PduPart part = new PduPart();
PduPart part = new PduPart();
String fileName = String.valueOf(Math.abs(Util.getSecureRandom().nextLong()));
String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(attachment.getContentType());
......@@ -248,6 +250,29 @@ public class MmsSendJob extends SendJob {
body.addPart(part);
size += getPartSize(part);
if (message.isSecure()) {
if (attachment.getDigest() != null) {
byte[] base64Digest = Base64.encode(attachment.getDigest(), Base64.NO_WRAP);
PduPart digestPart = new PduPart();
String name = fileName + ".digest";
String digestContentId = contentId + "-digest";
digestPart.setData(base64Digest);
digestPart.setCharset(CharacterSets.UTF_8);
digestPart.setContentType(MediaUtil.CONTENT_TYPE_OCTET_STREAM.getBytes());
digestPart.setContentId(digestContentId.getBytes());
digestPart.setContentLocation((name + ".txt").getBytes());
digestPart.setName(name.getBytes());
Log.w(TAG, "Inserting digest for file " + fileName);
body.addPart(digestPart);
size += getPartSize(digestPart);
} else {
Log.w(TAG, "Digest is null!");
}
}
} catch (IOException e) {
Log.w(TAG, e);
}
......
......@@ -5,6 +5,7 @@ import android.content.Context;
import android.content.UriMatcher;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.util.Log;
import org.smssecure.smssecure.attachments.AttachmentId;
import org.smssecure.smssecure.crypto.MasterSecret;
......
......@@ -128,7 +128,7 @@ public class PersistentBlobProvider {
public @NonNull InputStream getStream(MasterSecret masterSecret, long id) throws IOException {
final byte[] cached = cache.get(id);
return cached != null ? new ByteArrayInputStream(cached)
: new DecryptingPartInputStream(getFile(id), masterSecret);
: new DecryptingPartInputStream(getFile(id), masterSecret, null);
}
private File getFile(long id) {
......
......@@ -39,6 +39,8 @@ public class MediaUtil {
public static final String AUDIO_UNSPECIFIED = "audio/*";
public static final String VIDEO_UNSPECIFIED = "video/*";
public static final String CONTENT_TYPE_OCTET_STREAM = "application/octet-stream";
public static @Nullable ThumbnailData generateThumbnail(Context context, MasterSecret masterSecret, String contentType, Uri uri)
throws BitmapDecodingException
{
......
......@@ -25,7 +25,7 @@ public class EncryptedMediaDataSource extends MediaDataSource {
@Override
public int readAt(long position, byte[] bytes, int offset, int length) throws IOException {
DecryptingPartInputStream inputStream = new DecryptingPartInputStream(mediaFile, masterSecret);
DecryptingPartInputStream inputStream = new DecryptingPartInputStream(mediaFile, masterSecret, null);
byte[] buffer = new byte[4096];
long headerRemaining = position;
......@@ -44,7 +44,7 @@ public class EncryptedMediaDataSource extends MediaDataSource {
@Override
public long getSize() throws IOException {
DecryptingPartInputStream inputStream = new DecryptingPartInputStream(mediaFile, masterSecret);
DecryptingPartInputStream inputStream = new DecryptingPartInputStream(mediaFile, masterSecret, null);
byte[] buffer = new byte[4096];
long size = 0;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment