Basic support for encrypted push-based attachments.

1) Move the attachment structures into the encrypted message body.

2) Encrypt attachments with symmetric keys transmitted in the
   encryptd attachment pointer structure.

3) Correctly handle asynchronous decryption and categorization of
   encrypted push messages.

TODO: Correct notification process and network/interruption
      retries.
This commit is contained in:
Moxie Marlinspike
2013-09-08 18:19:05 -07:00
parent cddba2738f
commit 0dd36c64a4
47 changed files with 2381 additions and 1003 deletions

View File

@@ -51,7 +51,8 @@ public class DatabaseFactory {
private static final int INTRODUCED_MMS_BODY_VERSION = 7;
private static final int INTRODUCED_MMS_FROM_VERSION = 8;
private static final int INTRODUCED_TOFU_IDENTITY_VERSION = 9;
private static final int DATABASE_VERSION = 9;
private static final int INTRODUCED_PUSH_DATABASE_VERSION = 10;
private static final int DATABASE_VERSION = 10;
private static final String DATABASE_NAME = "messages.db";
private static final Object lock = new Object();
@@ -71,6 +72,7 @@ public class DatabaseFactory {
private final MmsSmsDatabase mmsSmsDatabase;
private final IdentityDatabase identityDatabase;
private final DraftDatabase draftDatabase;
private final PushDatabase pushDatabase;
public static DatabaseFactory getInstance(Context context) {
synchronized (lock) {
@@ -132,6 +134,10 @@ public class DatabaseFactory {
return getInstance(context).draftDatabase;
}
public static PushDatabase getPushDatabase(Context context) {
return getInstance(context).pushDatabase;
}
private DatabaseFactory(Context context) {
this.databaseHelper = new DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
this.sms = new SmsDatabase(context, databaseHelper);
@@ -144,6 +150,7 @@ public class DatabaseFactory {
this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper);
this.identityDatabase = new IdentityDatabase(context, databaseHelper);
this.draftDatabase = new DraftDatabase(context, databaseHelper);
this.pushDatabase = new PushDatabase(context, databaseHelper);
}
public void reset(Context context) {
@@ -425,6 +432,7 @@ public class DatabaseFactory {
db.execSQL(MmsAddressDatabase.CREATE_TABLE);
db.execSQL(IdentityDatabase.CREATE_TABLE);
db.execSQL(DraftDatabase.CREATE_TABLE);
db.execSQL(PushDatabase.CREATE_TABLE);
executeStatements(db, SmsDatabase.CREATE_INDEXS);
executeStatements(db, MmsDatabase.CREATE_INDEXS);
@@ -617,6 +625,12 @@ public class DatabaseFactory {
db.execSQL("CREATE TABLE identities (_id INTEGER PRIMARY KEY, recipient INTEGER UNIQUE, key TEXT, mac TEXT);");
}
if (oldVersion < INTRODUCED_PUSH_DATABASE_VERSION) {
db.execSQL("CREATE TABLE push (_id INTEGER PRIMARY KEY, type INTEGER, source TEXT, destinations TEXT, body TEXT, TIMESTAMP INTEGER);");
db.execSQL("ALTER TABLE part ADD COLUMN pending_push INTEGER;");
db.execSQL("CREATE INDEX IF NOT EXISTS pending_push_index ON parts (pending_push);");
}
db.setTransactionSuccessful();
db.endTransaction();
}

View File

@@ -51,6 +51,7 @@ import java.io.UnsupportedEncodingException;
import java.lang.ref.SoftReference;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
@@ -63,6 +64,7 @@ import ws.com.google.android.mms.pdu.EncodedStringValue;
import ws.com.google.android.mms.pdu.NotificationInd;
import ws.com.google.android.mms.pdu.PduBody;
import ws.com.google.android.mms.pdu.PduHeaders;
import ws.com.google.android.mms.pdu.PduPart;
import ws.com.google.android.mms.pdu.SendReq;
// XXXX Clean up MMS efficiency:
@@ -289,11 +291,11 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
public SendReq[] getOutgoingMessages(MasterSecret masterSecret, long messageId)
throws MmsException
{
MmsAddressDatabase addr = DatabaseFactory.getMmsAddressDatabase(context);
PartDatabase parts = getPartDatabase(masterSecret);
SQLiteDatabase database = databaseHelper.getReadableDatabase();
MasterCipher masterCipher = masterSecret == null ? null : new MasterCipher(masterSecret);
Cursor cursor = null;
MmsAddressDatabase addr = DatabaseFactory.getMmsAddressDatabase(context);
PartDatabase partDatabase = getPartDatabase(masterSecret);
SQLiteDatabase database = databaseHelper.getReadableDatabase();
MasterCipher masterCipher = masterSecret == null ? null : new MasterCipher(masterSecret);
Cursor cursor = null;
String selection;
@@ -322,8 +324,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
String messageText = cursor.getString(cursor.getColumnIndexOrThrow(BODY));
PduHeaders headers = getHeadersFromCursor(cursor);
addr.getAddressesForId(messageId, headers);
PduBody body = parts.getParts(messageId, true);
PduBody body = getPartsAsBody(partDatabase.getParts(messageId, true));
try {
if (!Util.isEmpty(messageText) && Types.isSymmetricEncryption(outboxType)) {
@@ -864,9 +866,12 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
if (masterSecret == null)
return null;
PduBody body = getPartDatabase(masterSecret).getParts(id, false);
PduBody body = getPartsAsBody(getPartDatabase(masterSecret).getParts(id, false));
SlideDeck slideDeck = new SlideDeck(context, masterSecret, body);
slideCache.put(id, new SoftReference<SlideDeck>(slideDeck));
if (!body.containsPushInProgress()) {
slideCache.put(id, new SoftReference<SlideDeck>(slideDeck));
}
return slideDeck;
}
@@ -907,4 +912,14 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
}
}
private PduBody getPartsAsBody(List<Pair<Long, PduPart>> parts) {
PduBody body = new PduBody();
for (Pair<Long, PduPart> part : parts) {
body.addPart(part.second);
}
return body;
}
}

View File

@@ -23,10 +23,12 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.providers.PartProvider;
import org.thoughtcrime.securesms.util.Util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
@@ -34,6 +36,8 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList;
import java.util.List;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.MmsException;
@@ -42,31 +46,34 @@ import ws.com.google.android.mms.pdu.PduPart;
public class PartDatabase extends Database {
private static final String TABLE_NAME = "part";
private static final String ID = "_id";
private static final String MMS_ID = "mid";
private static final String SEQUENCE = "seq";
private static final String CONTENT_TYPE = "ct";
private static final String NAME = "name";
private static final String CHARSET = "chset";
private static final String CONTENT_DISPOSITION = "cd";
private static final String FILENAME = "fn";
private static final String CONTENT_ID = "cid";
private static final String CONTENT_LOCATION = "cl";
private static final String CONTENT_TYPE_START = "ctt_s";
private static final String CONTENT_TYPE_TYPE = "ctt_t";
private static final String ENCRYPTED = "encrypted";
private static final String DATA = "_data";
private static final String TABLE_NAME = "part";
private static final String ID = "_id";
private static final String MMS_ID = "mid";
private static final String SEQUENCE = "seq";
private static final String CONTENT_TYPE = "ct";
private static final String NAME = "name";
private static final String CHARSET = "chset";
private static final String CONTENT_DISPOSITION = "cd";
private static final String FILENAME = "fn";
private static final String CONTENT_ID = "cid";
private static final String CONTENT_LOCATION = "cl";
private static final String CONTENT_TYPE_START = "ctt_s";
private static final String CONTENT_TYPE_TYPE = "ctt_t";
private static final String ENCRYPTED = "encrypted";
private static final String DATA = "_data";
private static final String PENDING_PUSH_ATTACHMENT = "pending_push";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
MMS_ID + " INTEGER, " + SEQUENCE + " INTEGER DEFAULT 0, " +
CONTENT_TYPE + " TEXT, " + NAME + " TEXT, " + CHARSET + " INTEGER, " +
CONTENT_DISPOSITION + " TEXT, " + FILENAME + " TEXT, " + CONTENT_ID + " TEXT, " +
CONTENT_LOCATION + " TEXT, " + CONTENT_TYPE_START + " INTEGER, " +
CONTENT_TYPE_TYPE + " TEXT, " + ENCRYPTED + " INTEGER, " + DATA + " TEXT);";
CONTENT_TYPE_TYPE + " TEXT, " + ENCRYPTED + " INTEGER, " +
PENDING_PUSH_ATTACHMENT + " INTEGER, "+ DATA + " TEXT);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");"
"CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");",
"CREATE INDEX IF NOT EXISTS pending_push_index ON " + TABLE_NAME + " (" + PENDING_PUSH_ATTACHMENT + ");",
};
public PartDatabase(Context context, SQLiteOpenHelper databaseHelper) {
@@ -113,6 +120,11 @@ public class PartDatabase extends Database {
if (!cursor.isNull(encryptedColumn))
part.setEncrypted(cursor.getInt(encryptedColumn) == 1);
int pendingPushColumn = cursor.getColumnIndexOrThrow(PENDING_PUSH_ATTACHMENT);
if (!cursor.isNull(pendingPushColumn))
part.setPendingPush(cursor.getInt(pendingPushColumn) == 1);
}
@@ -126,8 +138,9 @@ public class PartDatabase extends Database {
if (part.getContentType() != null) {
contentValues.put(CONTENT_TYPE, Util.toIsoString(part.getContentType()));
if (Util.toIsoString(part.getContentType()).equals(ContentType.APP_SMIL))
if (Util.toIsoString(part.getContentType()).equals(ContentType.APP_SMIL)) {
contentValues.put(SEQUENCE, -1);
}
} else {
throw new MmsException("There is no content type for this part.");
}
@@ -153,6 +166,7 @@ public class PartDatabase extends Database {
}
contentValues.put(ENCRYPTED, part.getEncrypted() ? 1 : 0);
contentValues.put(PENDING_PUSH_ATTACHMENT, part.isPendingPush() ? 1 : 0);
return contentValues;
}
@@ -186,35 +200,42 @@ public class PartDatabase extends Database {
}
}
private File writePartData(PduPart part) throws MmsException {
private File writePartData(PduPart part, InputStream in) throws MmsException {
try {
File partsDirectory = context.getDir("parts", Context.MODE_PRIVATE);
File dataFile = File.createTempFile("part", ".mms", partsDirectory);
FileOutputStream fout = getPartOutputStream(dataFile, part);
byte[] buf = new byte[512];
int read;
while ((read = in.read(buf)) != -1) {
fout.write(buf, 0, read);
}
fout.close();
in.close();
return dataFile;
} catch (IOException e) {
throw new AssertionError(e);
}
}
private File writePartData(PduPart part) throws MmsException {
try {
if (part.getData() != null) {
Log.w("PartDatabase", "Writing part data from buffer");
fout.write(part.getData());
fout.close();
return dataFile;
return writePartData(part, new ByteArrayInputStream(part.getData()));
} else if (part.getDataUri() != null) {
Log.w("PartDatabase", "Writing part dat from URI");
byte[] buf = new byte[512];
InputStream in = context.getContentResolver().openInputStream(part.getDataUri());
int read;
while ((read = in.read(buf)) != -1)
fout.write(buf, 0, read);
fout.close();
in.close();
return dataFile;
return writePartData(part, in);
} else {
throw new MmsException("Part is empty!");
}
} catch (FileNotFoundException e) {
throw new AssertionError(e);
} catch (IOException e) {
throw new AssertionError(e);
}
}
@@ -224,7 +245,7 @@ public class PartDatabase extends Database {
long partId = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
getPartValues(part, cursor);
if (includeData)
if (includeData && !part.isPendingPush())
readPartData(part, dataLocation);
part.setDataUri(ContentUris.withAppendedId(PartProvider.CONTENT_URI, partId));
@@ -232,14 +253,20 @@ public class PartDatabase extends Database {
}
private long insertPart(PduPart part, long mmsId) throws MmsException {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
File dataFile = writePartData(part);
SQLiteDatabase database = databaseHelper.getWritableDatabase();
File dataFile = null;
if (!part.isPendingPush()) {
dataFile = writePartData(part);
Log.w("PartDatabase", "Wrote part to file: " + dataFile.getAbsolutePath());
}
Log.w("PartDatabase", "Wrote part to file: " + dataFile.getAbsolutePath());
ContentValues contentValues = getContentValuesForPart(part);
contentValues.put(MMS_ID, mmsId);
contentValues.put(DATA, dataFile.getAbsolutePath());
if (dataFile != null) {
contentValues.put(DATA, dataFile.getAbsolutePath());
}
return database.insert(TABLE_NAME, null, contentValues);
}
@@ -256,6 +283,10 @@ public class PartDatabase extends Database {
PduPart part = new PduPart();
part.setEncrypted(cursor.getInt(1) == 1);
if (cursor.isNull(0)) {
throw new FileNotFoundException("No part data for id: " + partId);
}
return getPartInputStream(new File(cursor.getString(0)), part);
} else {
throw new FileNotFoundException("No part for id: " + partId);
@@ -273,6 +304,41 @@ public class PartDatabase extends Database {
}
}
public void updateDownloadedPart(long messageId, long partId, PduPart part, InputStream data)
throws MmsException
{
SQLiteDatabase database = databaseHelper.getWritableDatabase();
File partData = writePartData(part, data);
part.setContentDisposition(new byte[0]);
part.setPendingPush(false);
ContentValues values = getContentValuesForPart(part);
if (partData != null) {
values.put(DATA, partData.getAbsolutePath());
}
database.update(TABLE_NAME, values, ID_WHERE, new String[] {partId+""});
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId));
}
public void updateFailedDownloadedPart(long messageId, long partId, PduPart part)
throws MmsException
{
SQLiteDatabase database = databaseHelper.getWritableDatabase();
part.setContentDisposition(new byte[0]);
part.setPendingPush(false);
ContentValues values = getContentValuesForPart(part);
values.put(DATA, (String)null);
database.update(TABLE_NAME, values, ID_WHERE, new String[] {partId+""});
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId));
}
public PduPart getPart(long partId, boolean includeData) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null;
@@ -290,26 +356,50 @@ public class PartDatabase extends Database {
}
}
public PduBody getParts(long mmsId, boolean includeData) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
PduBody body = new PduBody();
Cursor cursor = null;
public List<Pair<Long, PduPart>> getParts(long mmsId, boolean includeData) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
List<Pair<Long, PduPart>> results = new LinkedList<Pair<Long, PduPart>>();
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, null, MMS_ID + " = ?", new String[] {mmsId+""}, null, null, null);
while (cursor != null && cursor.moveToNext()) {
PduPart part = getPart(cursor, includeData);
body.addPart(part);
results.add(new Pair<Long, PduPart>(cursor.getLong(cursor.getColumnIndexOrThrow(ID)),
part));
}
return body;
return results;
} finally {
if (cursor != null)
cursor.close();
}
}
public List<Pair<Long, Pair<Long, PduPart>>> getPushPendingParts() {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
List<Pair<Long, Pair<Long, PduPart>>> results = new LinkedList<Pair<Long, Pair<Long, PduPart>>>();
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, null, PENDING_PUSH_ATTACHMENT + " = ?", new String[] {"1"}, null, null, null);
while (cursor != null && cursor.moveToNext()) {
PduPart part = getPart(cursor, false);
results.add(new Pair<Long, Pair<Long, PduPart>>(cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)),
new Pair<Long, PduPart>(cursor.getLong(cursor.getColumnIndexOrThrow(ID)),
part)));
}
return results;
} finally {
if (cursor != null)
cursor.close();
}
}
public void deleteParts(long mmsId) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
Cursor cursor = null;

View File

@@ -0,0 +1,42 @@
package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteOpenHelper;
import org.spongycastle.util.encoders.Base64;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.util.Util;
public class PushDatabase extends Database {
private static final String TABLE_NAME = "push";
public static final String ID = "_id";
public static final String TYPE = "type";
public static final String SOURCE = "source";
public static final String DESTINATIONS = "destinations";
public static final String BODY = "body";
public static final String TIMESTAMP = "timestamp";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
TYPE + " INTEGER, " + SOURCE + " TEXT, " + DESTINATIONS + " TEXT, " + BODY + " TEXT, " + TIMESTAMP + " INTEGER);";
public PushDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
public long insert(IncomingPushMessage message) {
ContentValues values = new ContentValues();
values.put(TYPE, message.getType());
values.put(SOURCE, message.getSource());
values.put(DESTINATIONS, Util.join(message.getDestinations(), ","));
values.put(BODY, Base64.encode(message.getBody()));
values.put(TIMESTAMP, message.getTimestampMillis());
return databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, values);
}
public void delete(long id) {
databaseHelper.getWritableDatabase().delete(TABLE_NAME, ID_WHERE, new String[] {id+""});
}
}