mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-03 15:13:50 +00:00
Migrate from SQLite and ciphertext blobs to SQLCipher + KeyStore
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -16,11 +16,10 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.graphics.Bitmap;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
@@ -30,17 +29,17 @@ import android.support.annotation.Nullable;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream;
|
||||
import org.thoughtcrime.securesms.crypto.MasterCipher;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUnion;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
||||
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.mms.MediaStream;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
@@ -48,13 +47,13 @@ import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.video.EncryptedMediaDataSource;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
@@ -83,6 +82,8 @@ public class AttachmentDatabase extends Database {
|
||||
static final String DIGEST = "digest";
|
||||
static final String VOICE_NOTE = "voice_note";
|
||||
public static final String FAST_PREFLIGHT_ID = "fast_preflight_id";
|
||||
static final String DATA_RANDOM = "data_random";
|
||||
static final String THUMBNAIL_RANDOM = "thumbnail_random";
|
||||
|
||||
public static final int TRANSFER_PROGRESS_DONE = 0;
|
||||
public static final int TRANSFER_PROGRESS_STARTED = 1;
|
||||
@@ -95,7 +96,8 @@ public class AttachmentDatabase extends Database {
|
||||
MMS_ID, CONTENT_TYPE, NAME, CONTENT_DISPOSITION,
|
||||
CONTENT_LOCATION, DATA, THUMBNAIL, TRANSFER_STATE,
|
||||
SIZE, FILE_NAME, THUMBNAIL, THUMBNAIL_ASPECT_RATIO,
|
||||
UNIQUE_ID, DIGEST, FAST_PREFLIGHT_ID, VOICE_NOTE};
|
||||
UNIQUE_ID, DIGEST, FAST_PREFLIGHT_ID, VOICE_NOTE,
|
||||
DATA_RANDOM, THUMBNAIL_RANDOM};
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ROW_ID + " INTEGER PRIMARY KEY, " +
|
||||
MMS_ID + " INTEGER, " + "seq" + " INTEGER DEFAULT 0, " +
|
||||
@@ -106,7 +108,7 @@ public class AttachmentDatabase extends Database {
|
||||
TRANSFER_STATE + " INTEGER, "+ DATA + " TEXT, " + SIZE + " INTEGER, " +
|
||||
FILE_NAME + " TEXT, " + THUMBNAIL + " TEXT, " + THUMBNAIL_ASPECT_RATIO + " REAL, " +
|
||||
UNIQUE_ID + " INTEGER NOT NULL, " + DIGEST + " BLOB, " + FAST_PREFLIGHT_ID + " TEXT, " +
|
||||
VOICE_NOTE + " INTEGER DEFAULT 0);";
|
||||
VOICE_NOTE + " INTEGER DEFAULT 0, " + DATA_RANDOM + " BLOB, " + THUMBNAIL_RANDOM + " BLOB);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");",
|
||||
@@ -115,31 +117,34 @@ public class AttachmentDatabase extends Database {
|
||||
|
||||
private final ExecutorService thumbnailExecutor = Util.newSingleThreadedLifoExecutor();
|
||||
|
||||
public AttachmentDatabase(Context context, SQLiteOpenHelper databaseHelper) {
|
||||
private final AttachmentSecret attachmentSecret;
|
||||
|
||||
public AttachmentDatabase(Context context, SQLCipherOpenHelper databaseHelper, AttachmentSecret attachmentSecret) {
|
||||
super(context, databaseHelper);
|
||||
this.attachmentSecret = attachmentSecret;
|
||||
}
|
||||
|
||||
public @NonNull InputStream getAttachmentStream(MasterSecret masterSecret, AttachmentId attachmentId)
|
||||
public @NonNull InputStream getAttachmentStream(AttachmentId attachmentId)
|
||||
throws IOException
|
||||
{
|
||||
InputStream dataStream = getDataStream(masterSecret, attachmentId, DATA);
|
||||
InputStream dataStream = getDataStream(attachmentId, DATA);
|
||||
|
||||
if (dataStream == null) throw new IOException("No stream for: " + attachmentId);
|
||||
else return dataStream;
|
||||
}
|
||||
|
||||
public @NonNull InputStream getThumbnailStream(@NonNull MasterSecret masterSecret, @NonNull AttachmentId attachmentId)
|
||||
public @NonNull InputStream getThumbnailStream(@NonNull AttachmentId attachmentId)
|
||||
throws IOException
|
||||
{
|
||||
Log.w(TAG, "getThumbnailStream(" + attachmentId + ")");
|
||||
InputStream dataStream = getDataStream(masterSecret, attachmentId, THUMBNAIL);
|
||||
InputStream dataStream = getDataStream(attachmentId, THUMBNAIL);
|
||||
|
||||
if (dataStream != null) {
|
||||
return dataStream;
|
||||
}
|
||||
|
||||
try {
|
||||
InputStream generatedStream = thumbnailExecutor.submit(new ThumbnailFetchCallable(masterSecret, attachmentId)).get();
|
||||
InputStream generatedStream = thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId)).get();
|
||||
|
||||
if (generatedStream == null) throw new FileNotFoundException("No thumbnail stream available: " + attachmentId);
|
||||
else return generatedStream;
|
||||
@@ -162,7 +167,7 @@ public class AttachmentDatabase extends Database {
|
||||
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(mmsId));
|
||||
}
|
||||
|
||||
public @Nullable DatabaseAttachment getAttachment(@Nullable MasterSecret masterSecret, AttachmentId attachmentId)
|
||||
public @Nullable DatabaseAttachment getAttachment(@NonNull AttachmentId attachmentId)
|
||||
{
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = null;
|
||||
@@ -170,7 +175,7 @@ public class AttachmentDatabase extends Database {
|
||||
try {
|
||||
cursor = database.query(TABLE_NAME, PROJECTION, PART_ID_WHERE, attachmentId.toStrings(), null, null, null);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) return getAttachment(masterSecret, cursor);
|
||||
if (cursor != null && cursor.moveToFirst()) return getAttachment(cursor);
|
||||
else return null;
|
||||
|
||||
} finally {
|
||||
@@ -179,7 +184,7 @@ public class AttachmentDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull List<DatabaseAttachment> getAttachmentsForMessage(@Nullable MasterSecret masterSecret, long mmsId) {
|
||||
public @NonNull List<DatabaseAttachment> getAttachmentsForMessage(long mmsId) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
List<DatabaseAttachment> results = new LinkedList<>();
|
||||
Cursor cursor = null;
|
||||
@@ -189,7 +194,7 @@ public class AttachmentDatabase extends Database {
|
||||
null, null, null);
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
results.add(getAttachment(masterSecret, cursor));
|
||||
results.add(getAttachment(cursor));
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -199,7 +204,7 @@ public class AttachmentDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull List<DatabaseAttachment> getPendingAttachments(@NonNull MasterSecret masterSecret) {
|
||||
public @NonNull List<DatabaseAttachment> getPendingAttachments() {
|
||||
final SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
final List<DatabaseAttachment> attachments = new LinkedList<>();
|
||||
|
||||
@@ -207,7 +212,7 @@ public class AttachmentDatabase extends Database {
|
||||
try {
|
||||
cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(TRANSFER_PROGRESS_STARTED)}, null, null, null);
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
attachments.add(getAttachment(masterSecret, cursor));
|
||||
attachments.add(getAttachment(cursor));
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
@@ -217,7 +222,7 @@ public class AttachmentDatabase extends Database {
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public void deleteAttachmentsForMessage(long mmsId) {
|
||||
void deleteAttachmentsForMessage(long mmsId) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
Cursor cursor = null;
|
||||
|
||||
@@ -246,7 +251,7 @@ public class AttachmentDatabase extends Database {
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public void deleteAllAttachments() {
|
||||
void deleteAllAttachments() {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
database.delete(TABLE_NAME, null, null);
|
||||
|
||||
@@ -258,17 +263,16 @@ public class AttachmentDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
public long insertAttachmentsForPlaceholder(@NonNull MasterSecret masterSecret, long mmsId,
|
||||
@NonNull AttachmentId attachmentId,
|
||||
@NonNull InputStream inputStream)
|
||||
public void insertAttachmentsForPlaceholder(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();
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
DataInfo dataInfo = setAttachmentData(inputStream);
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
values.put(DATA, partData.first.getAbsolutePath());
|
||||
values.put(SIZE, partData.second);
|
||||
values.put(DATA, dataInfo.file.getAbsolutePath());
|
||||
values.put(SIZE, dataInfo.length);
|
||||
values.put(DATA_RANDOM, dataInfo.random);
|
||||
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
|
||||
values.put(CONTENT_LOCATION, (String)null);
|
||||
values.put(CONTENT_DISPOSITION, (String)null);
|
||||
@@ -278,47 +282,44 @@ public class AttachmentDatabase extends Database {
|
||||
|
||||
if (database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()) == 0) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
partData.first.delete();
|
||||
dataInfo.file.delete();
|
||||
} else {
|
||||
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(mmsId));
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
|
||||
thumbnailExecutor.submit(new ThumbnailFetchCallable(masterSecret, attachmentId));
|
||||
return partData.second;
|
||||
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId));
|
||||
}
|
||||
|
||||
void insertAttachmentsForMessage(@NonNull MasterSecretUnion masterSecret,
|
||||
long mmsId,
|
||||
@NonNull List<Attachment> attachments)
|
||||
void insertAttachmentsForMessage(long mmsId, @NonNull List<Attachment> attachments)
|
||||
throws MmsException
|
||||
{
|
||||
Log.w(TAG, "insertParts(" + attachments.size() + ")");
|
||||
|
||||
for (Attachment attachment : attachments) {
|
||||
AttachmentId attachmentId = insertAttachment(masterSecret, mmsId, attachment);
|
||||
AttachmentId attachmentId = insertAttachment(mmsId, attachment);
|
||||
Log.w(TAG, "Inserted attachment at ID: " + attachmentId);
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull Attachment updateAttachmentData(@NonNull MasterSecret masterSecret,
|
||||
@NonNull Attachment attachment,
|
||||
public @NonNull Attachment updateAttachmentData(@NonNull Attachment attachment,
|
||||
@NonNull MediaStream mediaStream)
|
||||
throws MmsException
|
||||
{
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
DatabaseAttachment databaseAttachment = (DatabaseAttachment) attachment;
|
||||
File dataFile = getAttachmentDataFile(databaseAttachment.getAttachmentId(), DATA);
|
||||
DataInfo dataInfo = getAttachmentDataFileInfo(databaseAttachment.getAttachmentId(), DATA);
|
||||
|
||||
if (dataFile == null) {
|
||||
if (dataInfo == null) {
|
||||
throw new MmsException("No attachment data found!");
|
||||
}
|
||||
|
||||
long dataSize = setAttachmentData(masterSecret, dataFile, mediaStream.getStream());
|
||||
dataInfo = setAttachmentData(dataInfo.file, mediaStream.getStream());
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(SIZE, dataSize);
|
||||
contentValues.put(SIZE, dataInfo.length);
|
||||
contentValues.put(CONTENT_TYPE, mediaStream.getMimeType());
|
||||
contentValues.put(DATA_RANDOM, dataInfo.random);
|
||||
|
||||
database.update(TABLE_NAME, contentValues, PART_ID_WHERE, databaseAttachment.getAttachmentId().toStrings());
|
||||
|
||||
@@ -328,7 +329,7 @@ public class AttachmentDatabase extends Database {
|
||||
databaseAttachment.hasThumbnail(),
|
||||
mediaStream.getMimeType(),
|
||||
databaseAttachment.getTransferState(),
|
||||
dataSize,
|
||||
dataInfo.length,
|
||||
databaseAttachment.getFileName(),
|
||||
databaseAttachment.getLocation(),
|
||||
databaseAttachment.getKey(),
|
||||
@@ -339,16 +340,11 @@ public class AttachmentDatabase extends Database {
|
||||
}
|
||||
|
||||
|
||||
public void updateAttachmentFileName(@NonNull MasterSecret masterSecret,
|
||||
@NonNull AttachmentId attachmentId,
|
||||
public void updateAttachmentFileName(@NonNull AttachmentId attachmentId,
|
||||
@Nullable String fileName)
|
||||
{
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
|
||||
if (fileName != null) {
|
||||
fileName = new MasterCipher(masterSecret).encryptBody(fileName);
|
||||
}
|
||||
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(FILE_NAME, fileName);
|
||||
|
||||
@@ -382,28 +378,43 @@ public class AttachmentDatabase extends Database {
|
||||
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId));
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@VisibleForTesting
|
||||
protected @Nullable InputStream getDataStream(MasterSecret masterSecret, AttachmentId attachmentId, String dataType)
|
||||
protected @Nullable InputStream getDataStream(AttachmentId attachmentId, String dataType)
|
||||
{
|
||||
File dataFile = getAttachmentDataFile(attachmentId, dataType);
|
||||
DataInfo dataInfo = getAttachmentDataFileInfo(attachmentId, dataType);
|
||||
|
||||
if (dataInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
if (dataFile != null) return DecryptingPartInputStream.createFor(masterSecret, dataFile);
|
||||
else return null;
|
||||
if (dataInfo.random != null && dataInfo.random.length == 32) {
|
||||
return ModernDecryptingPartInputStream.createFor(attachmentSecret, dataInfo.random, dataInfo.file);
|
||||
} else {
|
||||
return ClassicDecryptingPartInputStream.createFor(attachmentSecret, dataInfo.file);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable File getAttachmentDataFile(@NonNull AttachmentId attachmentId,
|
||||
@NonNull String dataType)
|
||||
private @Nullable DataInfo getAttachmentDataFileInfo(@NonNull AttachmentId attachmentId, @NonNull String dataType)
|
||||
{
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = null;
|
||||
|
||||
String randomColumn;
|
||||
|
||||
switch (dataType) {
|
||||
case DATA: randomColumn = DATA_RANDOM; break;
|
||||
case THUMBNAIL: randomColumn = THUMBNAIL_RANDOM; break;
|
||||
default:throw new AssertionError("Unknown data type: " + dataType);
|
||||
}
|
||||
|
||||
try {
|
||||
cursor = database.query(TABLE_NAME, new String[]{dataType}, PART_ID_WHERE, attachmentId.toStrings(),
|
||||
cursor = database.query(TABLE_NAME, new String[]{dataType, SIZE, randomColumn}, PART_ID_WHERE, attachmentId.toStrings(),
|
||||
null, null, null);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
@@ -411,7 +422,9 @@ public class AttachmentDatabase extends Database {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new File(cursor.getString(0));
|
||||
return new DataInfo(new File(cursor.getString(0)),
|
||||
cursor.getLong(1),
|
||||
cursor.getBlob(2));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -422,57 +435,46 @@ public class AttachmentDatabase extends Database {
|
||||
|
||||
}
|
||||
|
||||
private @NonNull Pair<File, Long> setAttachmentData(@NonNull MasterSecret masterSecret,
|
||||
@NonNull Uri uri)
|
||||
private @NonNull DataInfo setAttachmentData(@NonNull Uri uri)
|
||||
throws MmsException
|
||||
{
|
||||
try {
|
||||
InputStream inputStream = PartAuthority.getAttachmentStream(context, masterSecret, uri);
|
||||
return setAttachmentData(masterSecret, inputStream);
|
||||
InputStream inputStream = PartAuthority.getAttachmentStream(context, uri);
|
||||
return setAttachmentData(inputStream);
|
||||
} catch (IOException e) {
|
||||
throw new MmsException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull Pair<File, Long> setAttachmentData(@NonNull MasterSecret masterSecret,
|
||||
@NonNull InputStream in)
|
||||
private @NonNull DataInfo setAttachmentData(@NonNull InputStream in)
|
||||
throws MmsException
|
||||
{
|
||||
try {
|
||||
File partsDirectory = context.getDir("parts", Context.MODE_PRIVATE);
|
||||
File dataFile = File.createTempFile("part", ".mms", partsDirectory);
|
||||
|
||||
return new Pair<>(dataFile, setAttachmentData(masterSecret, dataFile, in));
|
||||
return setAttachmentData(dataFile, in);
|
||||
} catch (IOException e) {
|
||||
throw new MmsException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private long setAttachmentData(@NonNull MasterSecret masterSecret,
|
||||
@NonNull File destination,
|
||||
@NonNull InputStream in)
|
||||
private @NonNull DataInfo setAttachmentData(@NonNull File destination, @NonNull InputStream in)
|
||||
throws MmsException
|
||||
{
|
||||
try {
|
||||
OutputStream out = new EncryptingPartOutputStream(destination, masterSecret);
|
||||
return Util.copy(in, out);
|
||||
byte[] random = new byte[32];
|
||||
new SecureRandom().nextBytes(random);
|
||||
|
||||
OutputStream out = ModernEncryptingPartOutputStream.createFor(attachmentSecret, random, destination);
|
||||
long length = Util.copy(in, out);
|
||||
|
||||
return new DataInfo(destination, length, random);
|
||||
} catch (IOException e) {
|
||||
throw new MmsException(e);
|
||||
}
|
||||
}
|
||||
|
||||
DatabaseAttachment getAttachment(@Nullable MasterSecret masterSecret, Cursor cursor) {
|
||||
String encryptedFileName = cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME));
|
||||
String fileName = null;
|
||||
|
||||
if (masterSecret != null && !TextUtils.isEmpty(encryptedFileName)) {
|
||||
try {
|
||||
fileName = new MasterCipher(masterSecret).decryptBody(encryptedFileName);
|
||||
} catch (InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
DatabaseAttachment getAttachment(@NonNull Cursor cursor) {
|
||||
return new DatabaseAttachment(new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(ATTACHMENT_ID_ALIAS)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)),
|
||||
@@ -481,7 +483,7 @@ public class AttachmentDatabase extends Database {
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)),
|
||||
fileName,
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(NAME)),
|
||||
@@ -491,23 +493,18 @@ public class AttachmentDatabase extends Database {
|
||||
}
|
||||
|
||||
|
||||
private AttachmentId insertAttachment(MasterSecretUnion masterSecret, long mmsId, Attachment attachment)
|
||||
private AttachmentId insertAttachment(long mmsId, Attachment attachment)
|
||||
throws MmsException
|
||||
{
|
||||
Log.w(TAG, "Inserting attachment for mms id: " + mmsId);
|
||||
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
Pair<File, Long> partData = null;
|
||||
long uniqueId = System.currentTimeMillis();
|
||||
String fileName = null;
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
DataInfo dataInfo = null;
|
||||
long uniqueId = System.currentTimeMillis();
|
||||
|
||||
if (masterSecret.getMasterSecret().isPresent() && attachment.getDataUri() != null) {
|
||||
partData = setAttachmentData(masterSecret.getMasterSecret().get(), attachment.getDataUri());
|
||||
Log.w(TAG, "Wrote part to file: " + partData.first.getAbsolutePath());
|
||||
}
|
||||
|
||||
if (masterSecret.getMasterSecret().isPresent() && !TextUtils.isEmpty(attachment.getFileName())) {
|
||||
fileName = new MasterCipher(masterSecret.getMasterSecret().get()).encryptBody(attachment.getFileName());
|
||||
if (attachment.getDataUri() != null) {
|
||||
dataInfo = setAttachmentData(attachment.getDataUri());
|
||||
Log.w(TAG, "Wrote part to file: " + dataInfo.file.getAbsolutePath());
|
||||
}
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
@@ -519,33 +516,34 @@ public class AttachmentDatabase extends Database {
|
||||
contentValues.put(DIGEST, attachment.getDigest());
|
||||
contentValues.put(CONTENT_DISPOSITION, attachment.getKey());
|
||||
contentValues.put(NAME, attachment.getRelay());
|
||||
contentValues.put(FILE_NAME, fileName);
|
||||
contentValues.put(FILE_NAME, attachment.getFileName());
|
||||
contentValues.put(SIZE, attachment.getSize());
|
||||
contentValues.put(FAST_PREFLIGHT_ID, attachment.getFastPreflightId());
|
||||
contentValues.put(VOICE_NOTE, attachment.isVoiceNote() ? 1 : 0);
|
||||
|
||||
if (partData != null) {
|
||||
contentValues.put(DATA, partData.first.getAbsolutePath());
|
||||
contentValues.put(SIZE, partData.second);
|
||||
if (dataInfo != null) {
|
||||
contentValues.put(DATA, dataInfo.file.getAbsolutePath());
|
||||
contentValues.put(SIZE, dataInfo.length);
|
||||
contentValues.put(DATA_RANDOM, dataInfo.random);
|
||||
}
|
||||
|
||||
long rowId = database.insert(TABLE_NAME, null, contentValues);
|
||||
AttachmentId attachmentId = new AttachmentId(rowId, uniqueId);
|
||||
|
||||
if (partData != null) {
|
||||
if (dataInfo != null) {
|
||||
if (MediaUtil.hasVideoThumbnail(attachment.getDataUri())) {
|
||||
Bitmap bitmap = MediaUtil.getVideoThumbnail(context, attachment.getDataUri());
|
||||
|
||||
if (bitmap != null) {
|
||||
ThumbnailData thumbnailData = new ThumbnailData(bitmap);
|
||||
updateAttachmentThumbnail(masterSecret.getMasterSecret().get(), attachmentId, thumbnailData.toDataStream(), thumbnailData.getAspectRatio());
|
||||
updateAttachmentThumbnail(attachmentId, thumbnailData.toDataStream(), thumbnailData.getAspectRatio());
|
||||
} else {
|
||||
Log.w(TAG, "Retrieving video thumbnail failed, submitting thumbnail generation job...");
|
||||
thumbnailExecutor.submit(new ThumbnailFetchCallable(masterSecret.getMasterSecret().get(), attachmentId));
|
||||
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId));
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Submitting thumbnail generation job...");
|
||||
thumbnailExecutor.submit(new ThumbnailFetchCallable(masterSecret.getMasterSecret().get(), attachmentId));
|
||||
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,19 +551,21 @@ public class AttachmentDatabase extends Database {
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@VisibleForTesting
|
||||
protected void updateAttachmentThumbnail(MasterSecret masterSecret, AttachmentId attachmentId, InputStream in, float aspectRatio)
|
||||
protected void updateAttachmentThumbnail(AttachmentId attachmentId, InputStream in, float aspectRatio)
|
||||
throws MmsException
|
||||
{
|
||||
Log.w(TAG, "updating part thumbnail for #" + attachmentId);
|
||||
|
||||
Pair<File, Long> thumbnailFile = setAttachmentData(masterSecret, in);
|
||||
DataInfo thumbnailFile = setAttachmentData(in);
|
||||
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues(2);
|
||||
|
||||
values.put(THUMBNAIL, thumbnailFile.first.getAbsolutePath());
|
||||
values.put(THUMBNAIL, thumbnailFile.file.getAbsolutePath());
|
||||
values.put(THUMBNAIL_ASPECT_RATIO, aspectRatio);
|
||||
values.put(THUMBNAIL_RANDOM, thumbnailFile.random);
|
||||
|
||||
database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings());
|
||||
|
||||
@@ -584,24 +584,22 @@ public class AttachmentDatabase extends Database {
|
||||
@VisibleForTesting
|
||||
class ThumbnailFetchCallable implements Callable<InputStream> {
|
||||
|
||||
private final MasterSecret masterSecret;
|
||||
private final AttachmentId attachmentId;
|
||||
|
||||
ThumbnailFetchCallable(MasterSecret masterSecret, AttachmentId attachmentId) {
|
||||
this.masterSecret = masterSecret;
|
||||
ThumbnailFetchCallable(AttachmentId attachmentId) {
|
||||
this.attachmentId = attachmentId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable InputStream call() throws Exception {
|
||||
Log.w(TAG, "Executing thumbnail job...");
|
||||
final InputStream stream = getDataStream(masterSecret, attachmentId, THUMBNAIL);
|
||||
final InputStream stream = getDataStream(attachmentId, THUMBNAIL);
|
||||
|
||||
if (stream != null) {
|
||||
return stream;
|
||||
}
|
||||
|
||||
DatabaseAttachment attachment = getAttachment(masterSecret, attachmentId);
|
||||
DatabaseAttachment attachment = getAttachment(attachmentId);
|
||||
|
||||
if (attachment == null || !attachment.hasData()) {
|
||||
return null;
|
||||
@@ -610,34 +608,35 @@ public class AttachmentDatabase extends Database {
|
||||
ThumbnailData data;
|
||||
|
||||
if (MediaUtil.isVideoType(attachment.getContentType())) {
|
||||
data = generateVideoThumbnail(masterSecret, attachmentId);
|
||||
data = generateVideoThumbnail(attachmentId);
|
||||
} else{
|
||||
data = MediaUtil.generateThumbnail(context, masterSecret, attachment.getContentType(), attachment.getDataUri());
|
||||
data = MediaUtil.generateThumbnail(context, attachment.getContentType(), attachment.getDataUri());
|
||||
}
|
||||
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
updateAttachmentThumbnail(masterSecret, attachmentId, data.toDataStream(), data.getAspectRatio());
|
||||
updateAttachmentThumbnail(attachmentId, data.toDataStream(), data.getAspectRatio());
|
||||
|
||||
return getDataStream(masterSecret, attachmentId, THUMBNAIL);
|
||||
return getDataStream(attachmentId, THUMBNAIL);
|
||||
}
|
||||
|
||||
private ThumbnailData generateVideoThumbnail(MasterSecret masterSecret, AttachmentId attachmentId) {
|
||||
@SuppressLint("NewApi")
|
||||
private ThumbnailData generateVideoThumbnail(AttachmentId attachmentId) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
Log.w(TAG, "Video thumbnails not supported...");
|
||||
return null;
|
||||
}
|
||||
|
||||
File mediaFile = getAttachmentDataFile(attachmentId, DATA);
|
||||
DataInfo dataInfo = getAttachmentDataFileInfo(attachmentId, DATA);
|
||||
|
||||
if (mediaFile == null) {
|
||||
if (dataInfo == null) {
|
||||
Log.w(TAG, "No data file found for video thumbnail...");
|
||||
return null;
|
||||
}
|
||||
|
||||
EncryptedMediaDataSource dataSource = new EncryptedMediaDataSource(masterSecret, mediaFile);
|
||||
EncryptedMediaDataSource dataSource = new EncryptedMediaDataSource(attachmentSecret, dataInfo.file, dataInfo.random, dataInfo.length);
|
||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||
retriever.setDataSource(dataSource);
|
||||
|
||||
@@ -647,4 +646,16 @@ public class AttachmentDatabase extends Database {
|
||||
return new ThumbnailData(bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DataInfo {
|
||||
private final File file;
|
||||
private final long length;
|
||||
private final byte[] random;
|
||||
|
||||
private DataInfo(File file, long length, byte[] random) {
|
||||
this.file = file;
|
||||
this.length = length;
|
||||
this.random = random;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,10 @@ package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class Database {
|
||||
@@ -29,10 +30,10 @@ public abstract class Database {
|
||||
private static final String CONVERSATION_URI = "content://textsecure/thread/";
|
||||
private static final String CONVERSATION_LIST_URI = "content://textsecure/conversation-list";
|
||||
|
||||
protected SQLiteOpenHelper databaseHelper;
|
||||
protected final Context context;
|
||||
protected SQLCipherOpenHelper databaseHelper;
|
||||
protected final Context context;
|
||||
|
||||
public Database(Context context, SQLiteOpenHelper databaseHelper) {
|
||||
public Database(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
this.context = context;
|
||||
this.databaseHelper = databaseHelper;
|
||||
}
|
||||
@@ -58,7 +59,7 @@ public abstract class Database {
|
||||
cursor.setNotificationUri(context.getContentResolver(), Uri.parse(CONVERSATION_LIST_URI));
|
||||
}
|
||||
|
||||
public void reset(SQLiteOpenHelper databaseHelper) {
|
||||
public void reset(SQLCipherOpenHelper databaseHelper) {
|
||||
this.databaseHelper = databaseHelper;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,15 +3,13 @@ package org.thoughtcrime.securesms.database;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.thoughtcrime.securesms.crypto.MasterCipher;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -32,18 +30,18 @@ public class DraftDatabase extends Database {
|
||||
"CREATE INDEX IF NOT EXISTS draft_thread_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
|
||||
};
|
||||
|
||||
public DraftDatabase(Context context, SQLiteOpenHelper databaseHelper) {
|
||||
public DraftDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
public void insertDrafts(MasterCipher masterCipher, long threadId, List<Draft> drafts) {
|
||||
public void insertDrafts(long threadId, List<Draft> drafts) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
for (Draft draft : drafts) {
|
||||
ContentValues values = new ContentValues(3);
|
||||
values.put(THREAD_ID, threadId);
|
||||
values.put(DRAFT_TYPE, masterCipher.encryptBody(draft.getType()));
|
||||
values.put(DRAFT_VALUE, masterCipher.encryptBody(draft.getValue()));
|
||||
values.put(DRAFT_TYPE, draft.getType());
|
||||
values.put(DRAFT_VALUE, draft.getValue());
|
||||
|
||||
db.insert(TABLE_NAME, null, values);
|
||||
}
|
||||
@@ -54,7 +52,7 @@ public class DraftDatabase extends Database {
|
||||
db.delete(TABLE_NAME, THREAD_ID + " = ?", new String[] {threadId+""});
|
||||
}
|
||||
|
||||
public void clearDrafts(Set<Long> threadIds) {
|
||||
void clearDrafts(Set<Long> threadIds) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
StringBuilder where = new StringBuilder();
|
||||
List<String> arguments = new LinkedList<>();
|
||||
@@ -70,29 +68,24 @@ public class DraftDatabase extends Database {
|
||||
db.delete(TABLE_NAME, where.toString().substring(4), arguments.toArray(new String[0]));
|
||||
}
|
||||
|
||||
public void clearAllDrafts() {
|
||||
void clearAllDrafts() {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, null, null);
|
||||
}
|
||||
|
||||
public List<Draft> getDrafts(MasterCipher masterCipher, long threadId) {
|
||||
public List<Draft> getDrafts(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
List<Draft> results = new LinkedList<Draft>();
|
||||
List<Draft> results = new LinkedList<>();
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = db.query(TABLE_NAME, null, THREAD_ID + " = ?", new String[] {threadId+""}, null, null, null);
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
try {
|
||||
String encryptedType = cursor.getString(cursor.getColumnIndexOrThrow(DRAFT_TYPE));
|
||||
String encryptedValue = cursor.getString(cursor.getColumnIndexOrThrow(DRAFT_VALUE));
|
||||
String type = cursor.getString(cursor.getColumnIndexOrThrow(DRAFT_TYPE));
|
||||
String value = cursor.getString(cursor.getColumnIndexOrThrow(DRAFT_VALUE));
|
||||
|
||||
results.add(new Draft(masterCipher.decryptBody(encryptedType),
|
||||
masterCipher.decryptBody(encryptedValue)));
|
||||
} catch (InvalidMessageException ime) {
|
||||
Log.w("DraftDatabase", ime);
|
||||
}
|
||||
results.add(new Draft(type, value));
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -125,7 +118,7 @@ public class DraftDatabase extends Database {
|
||||
return value;
|
||||
}
|
||||
|
||||
public String getSnippet(Context context) {
|
||||
String getSnippet(Context context) {
|
||||
switch (type) {
|
||||
case TEXT: return value;
|
||||
case IMAGE: return context.getString(R.string.DraftDatabase_Draft_image_snippet);
|
||||
@@ -158,7 +151,7 @@ public class DraftDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable Uri getUriSnippet(Context context) {
|
||||
public @Nullable Uri getUriSnippet() {
|
||||
Draft imageDraft = getDraftOfType(Draft.IMAGE);
|
||||
|
||||
if (imageDraft != null && imageDraft.getValue() != null) {
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher;
|
||||
import org.thoughtcrime.securesms.crypto.AsymmetricMasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterCipher;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUnion;
|
||||
import org.thoughtcrime.securesms.database.model.DisplayRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.LRUCache;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
public class EncryptingSmsDatabase extends SmsDatabase {
|
||||
|
||||
private final PlaintextCache plaintextCache = new PlaintextCache();
|
||||
|
||||
public EncryptingSmsDatabase(Context context, SQLiteOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
private String getAsymmetricEncryptedBody(AsymmetricMasterSecret masterSecret, String body) {
|
||||
AsymmetricMasterCipher bodyCipher = new AsymmetricMasterCipher(masterSecret);
|
||||
return bodyCipher.encryptBody(body);
|
||||
}
|
||||
|
||||
private String getEncryptedBody(MasterSecret masterSecret, String body) {
|
||||
MasterCipher bodyCipher = new MasterCipher(masterSecret);
|
||||
String ciphertext = bodyCipher.encryptBody(body);
|
||||
plaintextCache.put(ciphertext, body);
|
||||
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
public long insertMessageOutbox(MasterSecretUnion masterSecret, long threadId,
|
||||
OutgoingTextMessage message, boolean forceSms,
|
||||
long timestamp, InsertListener insertListener)
|
||||
{
|
||||
long type = Types.BASE_SENDING_TYPE;
|
||||
|
||||
if (masterSecret.getMasterSecret().isPresent()) {
|
||||
message = message.withBody(getEncryptedBody(masterSecret.getMasterSecret().get(), message.getMessageBody()));
|
||||
type |= Types.ENCRYPTION_SYMMETRIC_BIT;
|
||||
} else {
|
||||
message = message.withBody(getAsymmetricEncryptedBody(masterSecret.getAsymmetricMasterSecret().get(), message.getMessageBody()));
|
||||
type |= Types.ENCRYPTION_ASYMMETRIC_BIT;
|
||||
}
|
||||
|
||||
return insertMessageOutbox(threadId, message, type, forceSms, timestamp, insertListener);
|
||||
}
|
||||
|
||||
public Optional<InsertResult> insertMessageInbox(@NonNull MasterSecretUnion masterSecret,
|
||||
@NonNull IncomingTextMessage message)
|
||||
{
|
||||
if (masterSecret.getMasterSecret().isPresent()) {
|
||||
return insertMessageInbox(masterSecret.getMasterSecret().get(), message);
|
||||
} else {
|
||||
return insertMessageInbox(masterSecret.getAsymmetricMasterSecret().get(), message);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<InsertResult> insertMessageInbox(@NonNull MasterSecret masterSecret,
|
||||
@NonNull IncomingTextMessage message)
|
||||
{
|
||||
long type = Types.BASE_INBOX_TYPE | Types.ENCRYPTION_SYMMETRIC_BIT;
|
||||
|
||||
message = message.withMessageBody(getEncryptedBody(masterSecret, message.getMessageBody()));
|
||||
|
||||
return insertMessageInbox(message, type);
|
||||
}
|
||||
|
||||
private Optional<InsertResult> insertMessageInbox(@NonNull AsymmetricMasterSecret masterSecret,
|
||||
@NonNull IncomingTextMessage message)
|
||||
{
|
||||
long type = Types.BASE_INBOX_TYPE | Types.ENCRYPTION_ASYMMETRIC_BIT;
|
||||
|
||||
message = message.withMessageBody(getAsymmetricEncryptedBody(masterSecret, message.getMessageBody()));
|
||||
|
||||
return insertMessageInbox(message, type);
|
||||
}
|
||||
|
||||
public Pair<Long, Long> updateBundleMessageBody(MasterSecretUnion masterSecret, long messageId, String body) {
|
||||
long type = Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT;
|
||||
String encryptedBody;
|
||||
|
||||
if (masterSecret.getMasterSecret().isPresent()) {
|
||||
encryptedBody = getEncryptedBody(masterSecret.getMasterSecret().get(), body);
|
||||
type |= Types.ENCRYPTION_SYMMETRIC_BIT;
|
||||
} else {
|
||||
encryptedBody = getAsymmetricEncryptedBody(masterSecret.getAsymmetricMasterSecret().get(), body);
|
||||
type |= Types.ENCRYPTION_ASYMMETRIC_BIT;
|
||||
}
|
||||
|
||||
return updateMessageBodyAndType(messageId, encryptedBody, Types.TOTAL_MASK, type);
|
||||
}
|
||||
|
||||
public void updateMessageBody(MasterSecretUnion masterSecret, long messageId, String body) {
|
||||
long type;
|
||||
|
||||
if (masterSecret.getMasterSecret().isPresent()) {
|
||||
body = getEncryptedBody(masterSecret.getMasterSecret().get(), body);
|
||||
type = Types.ENCRYPTION_SYMMETRIC_BIT;
|
||||
} else {
|
||||
body = getAsymmetricEncryptedBody(masterSecret.getAsymmetricMasterSecret().get(), body);
|
||||
type = Types.ENCRYPTION_ASYMMETRIC_BIT;
|
||||
}
|
||||
|
||||
updateMessageBodyAndType(messageId, body, Types.ENCRYPTION_MASK, type);
|
||||
}
|
||||
|
||||
public Reader getMessages(MasterSecret masterSecret, int skip, int limit) {
|
||||
Cursor cursor = super.getMessages(skip, limit);
|
||||
return new DecryptingReader(masterSecret, cursor);
|
||||
}
|
||||
|
||||
public Reader getOutgoingMessages(MasterSecret masterSecret) {
|
||||
Cursor cursor = super.getOutgoingMessages();
|
||||
return new DecryptingReader(masterSecret, cursor);
|
||||
}
|
||||
|
||||
public SmsMessageRecord getMessage(MasterSecret masterSecret, long messageId) throws NoSuchMessageException {
|
||||
Cursor cursor = super.getMessage(messageId);
|
||||
DecryptingReader reader = new DecryptingReader(masterSecret, cursor);
|
||||
SmsMessageRecord record = reader.getNext();
|
||||
|
||||
reader.close();
|
||||
|
||||
if (record == null) throw new NoSuchMessageException("No message for ID: " + messageId);
|
||||
else return record;
|
||||
}
|
||||
|
||||
public Reader getDecryptInProgressMessages(MasterSecret masterSecret) {
|
||||
Cursor cursor = super.getDecryptInProgressMessages();
|
||||
return new DecryptingReader(masterSecret, cursor);
|
||||
}
|
||||
|
||||
public Reader readerFor(MasterSecret masterSecret, Cursor cursor) {
|
||||
return new DecryptingReader(masterSecret, cursor);
|
||||
}
|
||||
|
||||
public class DecryptingReader extends SmsDatabase.Reader {
|
||||
|
||||
private final MasterCipher masterCipher;
|
||||
|
||||
public DecryptingReader(MasterSecret masterSecret, Cursor cursor) {
|
||||
super(cursor);
|
||||
this.masterCipher = new MasterCipher(masterSecret);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DisplayRecord.Body getBody(Cursor cursor) {
|
||||
long type = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.TYPE));
|
||||
String ciphertext = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY));
|
||||
|
||||
if (ciphertext == null) {
|
||||
return new DisplayRecord.Body("", true);
|
||||
}
|
||||
|
||||
try {
|
||||
if (SmsDatabase.Types.isSymmetricEncryption(type)) {
|
||||
String plaintext = plaintextCache.get(ciphertext);
|
||||
|
||||
if (plaintext != null)
|
||||
return new DisplayRecord.Body(plaintext, true);
|
||||
|
||||
plaintext = masterCipher.decryptBody(ciphertext);
|
||||
|
||||
plaintextCache.put(ciphertext, plaintext);
|
||||
return new DisplayRecord.Body(plaintext, true);
|
||||
} else {
|
||||
return new DisplayRecord.Body(ciphertext, true);
|
||||
}
|
||||
} catch (InvalidMessageException e) {
|
||||
Log.w("EncryptingSmsDatabase", e);
|
||||
return new DisplayRecord.Body(context.getString(R.string.EncryptingSmsDatabase_error_decrypting_message), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class PlaintextCache {
|
||||
private static final int MAX_CACHE_SIZE = 2000;
|
||||
private static final Map<String, SoftReference<String>> decryptedBodyCache =
|
||||
Collections.synchronizedMap(new LRUCache<String, SoftReference<String>>(MAX_CACHE_SIZE));
|
||||
|
||||
public void put(String ciphertext, String plaintext) {
|
||||
decryptedBodyCache.put(ciphertext, new SoftReference<String>(plaintext));
|
||||
}
|
||||
|
||||
public String get(String ciphertext) {
|
||||
SoftReference<String> plaintextReference = decryptedBodyCache.get(ciphertext);
|
||||
|
||||
if (plaintextReference != null) {
|
||||
String plaintext = plaintextReference.get();
|
||||
|
||||
if (plaintext != null) {
|
||||
return plaintext;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@ import android.annotation.SuppressLint;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.graphics.Bitmap;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
@@ -14,6 +12,9 @@ import android.text.TextUtils;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
@@ -64,7 +65,7 @@ public class GroupDatabase extends Database {
|
||||
AVATAR_DIGEST + " BLOB, " +
|
||||
MMS + " INTEGER DEFAULT 0);";
|
||||
|
||||
static final String[] CREATE_INDEXS = {
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");",
|
||||
};
|
||||
|
||||
@@ -75,7 +76,7 @@ public class GroupDatabase extends Database {
|
||||
|
||||
static final List<String> TYPED_GROUP_PROJECTION = Stream.of(GROUP_PROJECTION).map(columnName -> TABLE_NAME + "." + columnName).toList();
|
||||
|
||||
public GroupDatabase(Context context, SQLiteOpenHelper databaseHelper) {
|
||||
public GroupDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ package org.thoughtcrime.securesms.database;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -33,7 +35,7 @@ public class GroupReceiptDatabase extends Database {
|
||||
"CREATE INDEX IF NOT EXISTS group_receipt_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");",
|
||||
};
|
||||
|
||||
public GroupReceiptDatabase(Context context, SQLiteOpenHelper databaseHelper) {
|
||||
public GroupReceiptDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,12 +19,13 @@ package org.thoughtcrime.securesms.database;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
@@ -72,7 +73,7 @@ public class IdentityDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
IdentityDatabase(Context context, SQLiteOpenHelper databaseHelper) {
|
||||
IdentityDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@ package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
|
||||
public class MediaDatabase extends Database {
|
||||
|
||||
@@ -44,7 +44,7 @@ public class MediaDatabase extends Database {
|
||||
private static final String GALLERY_MEDIA_QUERY = String.format(BASE_MEDIA_QUERY, AttachmentDatabase.CONTENT_TYPE + " LIKE 'image/%' OR " + AttachmentDatabase.CONTENT_TYPE + " LIKE 'video/%'");
|
||||
private static final String DOCUMENT_MEDIA_QUERY = String.format(BASE_MEDIA_QUERY, AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'image/%' AND " + AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'video/%' AND " + AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'audio/%'");
|
||||
|
||||
public MediaDatabase(Context context, SQLiteOpenHelper databaseHelper) {
|
||||
public MediaDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
@@ -76,9 +76,9 @@ public class MediaDatabase extends Database {
|
||||
this.outgoing = outgoing;
|
||||
}
|
||||
|
||||
public static MediaRecord from(@NonNull Context context, @NonNull MasterSecret masterSecret, @NonNull Cursor cursor) {
|
||||
public static MediaRecord from(@NonNull Context context, @NonNull Cursor cursor) {
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
DatabaseAttachment attachment = attachmentDatabase.getAttachment(masterSecret, cursor);
|
||||
DatabaseAttachment attachment = attachmentDatabase.getAttachment(cursor);
|
||||
String serializedAddress = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS));
|
||||
boolean outgoing = MessagingDatabase.Types.isOutgoingMessageType(cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX)));
|
||||
Address address = null;
|
||||
|
||||
@@ -3,14 +3,15 @@ package org.thoughtcrime.securesms.database;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.documents.Document;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
|
||||
@@ -23,7 +24,7 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
||||
|
||||
private static final String TAG = MessagingDatabase.class.getSimpleName();
|
||||
|
||||
public MessagingDatabase(Context context, SQLiteOpenHelper databaseHelper) {
|
||||
public MessagingDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ package org.thoughtcrime.securesms.database;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
@@ -32,19 +30,17 @@ import com.annimon.stream.Stream;
|
||||
import com.google.android.mms.pdu_alt.NotificationInd;
|
||||
import com.google.android.mms.pdu_alt.PduHeaders;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment;
|
||||
import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher;
|
||||
import org.thoughtcrime.securesms.crypto.MasterCipher;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUnion;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailureList;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.DisplayRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
@@ -63,7 +59,6 @@ import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.jobqueue.JobManager;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -153,7 +148,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
|
||||
private final JobManager jobManager;
|
||||
|
||||
public MmsDatabase(Context context, SQLiteOpenHelper databaseHelper) {
|
||||
public MmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
this.jobManager = ApplicationContext.getInstance(context).getJobManager();
|
||||
}
|
||||
@@ -289,14 +284,14 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public Reader getExpireStartedMessages(@Nullable MasterSecret masterSecret) {
|
||||
public Reader getExpireStartedMessages() {
|
||||
String where = EXPIRE_STARTED + " > 0";
|
||||
return readerFor(masterSecret, rawQuery(where, null));
|
||||
return readerFor(rawQuery(where, null));
|
||||
}
|
||||
|
||||
public Reader getDecryptInProgressMessages(MasterSecret masterSecret) {
|
||||
public Reader getDecryptInProgressMessages() {
|
||||
String where = MESSAGE_BOX + " & " + (Types.ENCRYPTION_ASYMMETRIC_BIT) + " != 0";
|
||||
return readerFor(masterSecret, rawQuery(where, null));
|
||||
return readerFor(rawQuery(where, null));
|
||||
}
|
||||
|
||||
private void updateMailboxBitmask(long id, long maskOff, long maskOn, Optional<Long> threadId) {
|
||||
@@ -494,16 +489,8 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
return expiring;
|
||||
}
|
||||
|
||||
public void updateMessageBody(MasterSecretUnion masterSecret, long messageId, String body) {
|
||||
body = getEncryptedBody(masterSecret, body);
|
||||
|
||||
long type;
|
||||
|
||||
if (masterSecret.getMasterSecret().isPresent()) {
|
||||
type = Types.ENCRYPTION_SYMMETRIC_BIT;
|
||||
} else {
|
||||
type = Types.ENCRYPTION_ASYMMETRIC_BIT;
|
||||
}
|
||||
public void updateMessageBody(long messageId, String body) {
|
||||
long type = 0;
|
||||
|
||||
updateMessageBodyAndType(messageId, body, Types.ENCRYPTION_MASK, type);
|
||||
}
|
||||
@@ -544,7 +531,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
public OutgoingMediaMessage getOutgoingMessage(MasterSecret masterSecret, long messageId)
|
||||
public OutgoingMediaMessage getOutgoingMessage(long messageId)
|
||||
throws MmsException, NoSuchMessageException
|
||||
{
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
@@ -555,13 +542,12 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX));
|
||||
String messageText = cursor.getString(cursor.getColumnIndexOrThrow(BODY));
|
||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(BODY));
|
||||
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT));
|
||||
int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID));
|
||||
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN));
|
||||
List<Attachment> attachments = new LinkedList<Attachment>(attachmentDatabase.getAttachmentsForMessage(masterSecret, messageId));
|
||||
List<Attachment> attachments = new LinkedList<>(attachmentDatabase.getAttachmentsForMessage(messageId));
|
||||
String address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS));
|
||||
String body = getDecryptedBody(masterSecret, messageText, outboxType);
|
||||
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID));
|
||||
int distributionType = DatabaseFactory.getThreadDatabase(context).getDistributionType(threadId);
|
||||
|
||||
@@ -591,9 +577,9 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
public long copyMessageInbox(MasterSecret masterSecret, long messageId) throws MmsException {
|
||||
public long copyMessageInbox(long messageId) throws MmsException {
|
||||
try {
|
||||
OutgoingMediaMessage request = getOutgoingMessage(masterSecret, messageId);
|
||||
OutgoingMediaMessage request = getOutgoingMessage(messageId);
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(ADDRESS, request.getRecipient().getAddress().serialize());
|
||||
contentValues.put(DATE_SENT, request.getSentTimeMillis());
|
||||
@@ -623,8 +609,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
databaseAttachment.isVoiceNote()));
|
||||
}
|
||||
|
||||
return insertMediaMessage(new MasterSecretUnion(masterSecret),
|
||||
request.getBody(),
|
||||
return insertMediaMessage(request.getBody(),
|
||||
attachments,
|
||||
contentValues,
|
||||
null);
|
||||
@@ -633,8 +618,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<InsertResult> insertMessageInbox(MasterSecretUnion masterSecret,
|
||||
IncomingMediaMessage retrieved,
|
||||
private Optional<InsertResult> insertMessageInbox(IncomingMediaMessage retrieved,
|
||||
String contentLocation,
|
||||
long threadId, long mailbox)
|
||||
throws MmsException
|
||||
@@ -674,7 +658,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
long messageId = insertMediaMessage(masterSecret, retrieved.getBody(), retrieved.getAttachments(), contentValues, null);
|
||||
long messageId = insertMediaMessage(retrieved.getBody(), retrieved.getAttachments(), contentValues, null);
|
||||
|
||||
if (!Types.isExpirationTimerUpdate(mailbox)) {
|
||||
DatabaseFactory.getThreadDatabase(context).incrementUnread(threadId, 1);
|
||||
@@ -687,19 +671,12 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
return Optional.of(new InsertResult(messageId, threadId));
|
||||
}
|
||||
|
||||
public Optional<InsertResult> insertMessageInbox(MasterSecretUnion masterSecret,
|
||||
IncomingMediaMessage retrieved,
|
||||
public Optional<InsertResult> insertMessageInbox(IncomingMediaMessage retrieved,
|
||||
String contentLocation, long threadId)
|
||||
throws MmsException
|
||||
{
|
||||
long type = Types.BASE_INBOX_TYPE;
|
||||
|
||||
if (masterSecret.getMasterSecret().isPresent()) {
|
||||
type |= Types.ENCRYPTION_SYMMETRIC_BIT;
|
||||
} else {
|
||||
type |= Types.ENCRYPTION_ASYMMETRIC_BIT;
|
||||
}
|
||||
|
||||
if (retrieved.isPushMessage()) {
|
||||
type |= Types.PUSH_MESSAGE_BIT;
|
||||
}
|
||||
@@ -708,22 +685,14 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
type |= Types.EXPIRATION_TIMER_UPDATE_BIT;
|
||||
}
|
||||
|
||||
return insertMessageInbox(masterSecret, retrieved, contentLocation, threadId, type);
|
||||
return insertMessageInbox(retrieved, contentLocation, threadId, type);
|
||||
}
|
||||
|
||||
public Optional<InsertResult> insertSecureDecryptedMessageInbox(MasterSecretUnion masterSecret,
|
||||
IncomingMediaMessage retrieved,
|
||||
long threadId)
|
||||
public Optional<InsertResult> insertSecureDecryptedMessageInbox(IncomingMediaMessage retrieved, long threadId)
|
||||
throws MmsException
|
||||
{
|
||||
long type = Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT;
|
||||
|
||||
if (masterSecret.getMasterSecret().isPresent()) {
|
||||
type |= Types.ENCRYPTION_SYMMETRIC_BIT;
|
||||
} else {
|
||||
type |= Types.ENCRYPTION_ASYMMETRIC_BIT;
|
||||
}
|
||||
|
||||
if (retrieved.isPushMessage()) {
|
||||
type |= Types.PUSH_MESSAGE_BIT;
|
||||
}
|
||||
@@ -732,7 +701,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
type |= Types.EXPIRATION_TIMER_UPDATE_BIT;
|
||||
}
|
||||
|
||||
return insertMessageInbox(masterSecret, retrieved, "", threadId, type);
|
||||
return insertMessageInbox(retrieved, "", threadId, type);
|
||||
}
|
||||
|
||||
public Pair<Long, Long> insertMessageInbox(@NonNull NotificationInd notification, int subscriptionId) {
|
||||
@@ -781,17 +750,13 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
jobManager.add(new TrimThreadJob(context, threadId));
|
||||
}
|
||||
|
||||
public long insertMessageOutbox(@NonNull MasterSecretUnion masterSecret,
|
||||
@NonNull OutgoingMediaMessage message,
|
||||
public long insertMessageOutbox(@NonNull OutgoingMediaMessage message,
|
||||
long threadId, boolean forceSms,
|
||||
@Nullable SmsDatabase.InsertListener insertListener)
|
||||
throws MmsException
|
||||
{
|
||||
long type = Types.BASE_SENDING_TYPE;
|
||||
|
||||
if (masterSecret.getMasterSecret().isPresent()) type |= Types.ENCRYPTION_SYMMETRIC_BIT;
|
||||
else type |= Types.ENCRYPTION_ASYMMETRIC_BIT;
|
||||
|
||||
if (message.isSecure()) type |= (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT);
|
||||
if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT;
|
||||
|
||||
@@ -821,7 +786,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
contentValues.put(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(Long::longValue).sum());
|
||||
contentValues.put(READ_RECEIPT_COUNT, Stream.of(earlyReadReceipts.values()).mapToLong(Long::longValue).sum());
|
||||
|
||||
long messageId = insertMediaMessage(masterSecret, message.getBody(), message.getAttachments(), contentValues, insertListener);
|
||||
long messageId = insertMediaMessage(message.getBody(), message.getAttachments(), contentValues, insertListener);
|
||||
|
||||
if (message.getRecipient().getAddress().isGroup()) {
|
||||
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().getAddress().toGroupString(), false);
|
||||
@@ -841,56 +806,23 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
return messageId;
|
||||
}
|
||||
|
||||
private String getEncryptedBody(MasterSecretUnion masterSecret, String body) {
|
||||
if (masterSecret.getMasterSecret().isPresent()) {
|
||||
return new MasterCipher(masterSecret.getMasterSecret().get()).encryptBody(body);
|
||||
} else {
|
||||
return new AsymmetricMasterCipher(masterSecret.getAsymmetricMasterSecret().get()).encryptBody(body);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable String getDecryptedBody(@NonNull MasterSecret masterSecret,
|
||||
@Nullable String body, long outboxType)
|
||||
{
|
||||
try {
|
||||
if (!TextUtils.isEmpty(body) && Types.isSymmetricEncryption(outboxType)) {
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
return masterCipher.decryptBody(body);
|
||||
} else {
|
||||
return body;
|
||||
}
|
||||
} catch (InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private long insertMediaMessage(@NonNull MasterSecretUnion masterSecret,
|
||||
@Nullable String body,
|
||||
private long insertMediaMessage(@Nullable String body,
|
||||
@NonNull List<Attachment> attachments,
|
||||
@NonNull ContentValues contentValues,
|
||||
@Nullable SmsDatabase.InsertListener insertListener)
|
||||
throws MmsException
|
||||
{
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
AttachmentDatabase partsDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
|
||||
if (Types.isSymmetricEncryption(contentValues.getAsLong(MESSAGE_BOX)) ||
|
||||
Types.isAsymmetricEncryption(contentValues.getAsLong(MESSAGE_BOX)))
|
||||
{
|
||||
if (!TextUtils.isEmpty(body)) {
|
||||
contentValues.put(BODY, getEncryptedBody(masterSecret, body));
|
||||
}
|
||||
}
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
AttachmentDatabase partsDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
|
||||
contentValues.put(BODY, body);
|
||||
contentValues.put(PART_COUNT, attachments.size());
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
long messageId = db.insert(TABLE_NAME, null, contentValues);
|
||||
|
||||
partsDatabase.insertAttachmentsForMessage(masterSecret, messageId, attachments);
|
||||
partsDatabase.insertAttachmentsForMessage(messageId, attachments);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
return messageId;
|
||||
@@ -1016,8 +948,8 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
public Reader readerFor(MasterSecret masterSecret, Cursor cursor) {
|
||||
return new Reader(masterSecret, cursor);
|
||||
public Reader readerFor(Cursor cursor) {
|
||||
return new Reader(cursor);
|
||||
}
|
||||
|
||||
public OutgoingMessageReader readerFor(OutgoingMediaMessage message, long threadId) {
|
||||
@@ -1097,16 +1029,10 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
|
||||
public class Reader {
|
||||
|
||||
private final Cursor cursor;
|
||||
private final MasterSecret masterSecret;
|
||||
private final MasterCipher masterCipher;
|
||||
private final Cursor cursor;
|
||||
|
||||
public Reader(MasterSecret masterSecret, Cursor cursor) {
|
||||
this.cursor = cursor;
|
||||
this.masterSecret = masterSecret;
|
||||
|
||||
if (masterSecret != null) masterCipher = new MasterCipher(masterSecret);
|
||||
else masterCipher = null;
|
||||
public Reader(Cursor cursor) {
|
||||
this.cursor = cursor;
|
||||
}
|
||||
|
||||
public MessageRecord getNext() {
|
||||
@@ -1239,27 +1165,12 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
}
|
||||
|
||||
private DisplayRecord.Body getBody(Cursor cursor) {
|
||||
try {
|
||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.BODY));
|
||||
long box = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX));
|
||||
|
||||
if (!TextUtils.isEmpty(body) && masterCipher != null && Types.isSymmetricEncryption(box)) {
|
||||
return new DisplayRecord.Body(masterCipher.decryptBody(body), true);
|
||||
} else if (!TextUtils.isEmpty(body) && masterCipher == null && Types.isSymmetricEncryption(box)) {
|
||||
return new DisplayRecord.Body(body, false);
|
||||
} else if (!TextUtils.isEmpty(body) && Types.isAsymmetricEncryption(box)) {
|
||||
return new DisplayRecord.Body(body, false);
|
||||
} else {
|
||||
return new DisplayRecord.Body(body == null ? "" : body, true);
|
||||
}
|
||||
} catch (InvalidMessageException e) {
|
||||
Log.w("MmsDatabase", e);
|
||||
return new DisplayRecord.Body(context.getString(R.string.MmsDatabase_error_decrypting_message), true);
|
||||
}
|
||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.BODY));
|
||||
return new DisplayRecord.Body(body == null ? "" : body, true);
|
||||
}
|
||||
|
||||
private SlideDeck getSlideDeck(@NonNull Cursor cursor) {
|
||||
Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(masterSecret, cursor);
|
||||
Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(cursor);
|
||||
return new SlideDeck(context, attachment);
|
||||
}
|
||||
|
||||
|
||||
@@ -71,9 +71,9 @@ public interface MmsSmsColumns {
|
||||
protected static final long GROUP_QUIT_BIT = 0x20000;
|
||||
protected static final long EXPIRATION_TIMER_UPDATE_BIT = 0x40000;
|
||||
|
||||
// Encrypted Storage Information
|
||||
protected static final long ENCRYPTION_MASK = 0xFF000000;
|
||||
protected static final long ENCRYPTION_SYMMETRIC_BIT = 0x80000000;
|
||||
// Encrypted Storage Information XXX
|
||||
public static final long ENCRYPTION_MASK = 0xFF000000;
|
||||
public static final long ENCRYPTION_SYMMETRIC_BIT = 0x80000000;
|
||||
protected static final long ENCRYPTION_ASYMMETRIC_BIT = 0x40000000;
|
||||
protected static final long ENCRYPTION_REMOTE_BIT = 0x20000000;
|
||||
protected static final long ENCRYPTION_REMOTE_FAILED_BIT = 0x10000000;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -18,23 +18,22 @@ package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.database.sqlite.SQLiteQueryBuilder;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteQueryBuilder;
|
||||
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class MmsSmsDatabase extends Database {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = MmsSmsDatabase.class.getSimpleName();
|
||||
|
||||
public static final String TRANSPORT = "transport_type";
|
||||
@@ -77,7 +76,7 @@ public class MmsSmsDatabase extends Database {
|
||||
AttachmentDatabase.NAME,
|
||||
AttachmentDatabase.TRANSFER_STATE};
|
||||
|
||||
public MmsSmsDatabase(Context context, SQLiteOpenHelper databaseHelper) {
|
||||
public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
@@ -309,34 +308,23 @@ public class MmsSmsDatabase extends Database {
|
||||
return db.rawQuery(query, null);
|
||||
}
|
||||
|
||||
public Reader readerFor(@NonNull Cursor cursor, @Nullable MasterSecret masterSecret) {
|
||||
return new Reader(cursor, masterSecret);
|
||||
}
|
||||
|
||||
public Reader readerFor(@NonNull Cursor cursor) {
|
||||
return new Reader(cursor);
|
||||
}
|
||||
|
||||
public class Reader {
|
||||
|
||||
private final Cursor cursor;
|
||||
private final Optional<MasterSecret> masterSecret;
|
||||
private EncryptingSmsDatabase.Reader smsReader;
|
||||
private MmsDatabase.Reader mmsReader;
|
||||
|
||||
public Reader(Cursor cursor, @Nullable MasterSecret masterSecret) {
|
||||
this.cursor = cursor;
|
||||
this.masterSecret = Optional.fromNullable(masterSecret);
|
||||
}
|
||||
private final Cursor cursor;
|
||||
private SmsDatabase.Reader smsReader;
|
||||
private MmsDatabase.Reader mmsReader;
|
||||
|
||||
public Reader(Cursor cursor) {
|
||||
this(cursor, null);
|
||||
this.cursor = cursor;
|
||||
}
|
||||
|
||||
private EncryptingSmsDatabase.Reader getSmsReader() {
|
||||
private SmsDatabase.Reader getSmsReader() {
|
||||
if (smsReader == null) {
|
||||
if (masterSecret.isPresent()) smsReader = DatabaseFactory.getEncryptingSmsDatabase(context).readerFor(masterSecret.get(), cursor);
|
||||
else smsReader = DatabaseFactory.getSmsDatabase(context).readerFor(cursor);
|
||||
smsReader = DatabaseFactory.getSmsDatabase(context).readerFor(cursor);
|
||||
}
|
||||
|
||||
return smsReader;
|
||||
@@ -344,7 +332,7 @@ public class MmsSmsDatabase extends Database {
|
||||
|
||||
private MmsDatabase.Reader getMmsReader() {
|
||||
if (mmsReader == null) {
|
||||
mmsReader = DatabaseFactory.getMmsDatabase(context).readerFor(masterSecret.orNull(), cursor);
|
||||
mmsReader = DatabaseFactory.getMmsDatabase(context).readerFor(cursor);
|
||||
}
|
||||
|
||||
return mmsReader;
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
|
||||
@@ -14,33 +13,35 @@ public class PlaintextBackupExporter {
|
||||
|
||||
private static final String FILENAME = "SignalPlaintextBackup.xml";
|
||||
|
||||
public static void exportPlaintextToSd(Context context, MasterSecret masterSecret)
|
||||
public static void exportPlaintextToSd(Context context)
|
||||
throws NoExternalStorageException, IOException
|
||||
{
|
||||
exportPlaintext(context, masterSecret);
|
||||
exportPlaintext(context);
|
||||
}
|
||||
|
||||
public static File getPlaintextExportFile() throws NoExternalStorageException {
|
||||
return new File(StorageUtil.getBackupDir(), FILENAME);
|
||||
}
|
||||
|
||||
private static void exportPlaintext(Context context, MasterSecret masterSecret)
|
||||
private static void exportPlaintext(Context context)
|
||||
throws NoExternalStorageException, IOException
|
||||
{
|
||||
int count = DatabaseFactory.getSmsDatabase(context).getMessageCount();
|
||||
XmlBackup.Writer writer = new XmlBackup.Writer(getPlaintextExportFile().getAbsolutePath(), count);
|
||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
int count = database.getMessageCount();
|
||||
XmlBackup.Writer writer = new XmlBackup.Writer(getPlaintextExportFile().getAbsolutePath(), count);
|
||||
|
||||
|
||||
SmsMessageRecord record;
|
||||
EncryptingSmsDatabase.Reader reader = null;
|
||||
int skip = 0;
|
||||
int ROW_LIMIT = 500;
|
||||
|
||||
SmsDatabase.Reader reader = null;
|
||||
int skip = 0;
|
||||
int ROW_LIMIT = 500;
|
||||
|
||||
do {
|
||||
if (reader != null)
|
||||
reader.close();
|
||||
|
||||
reader = DatabaseFactory.getEncryptingSmsDatabase(context).getMessages(masterSecret, skip, ROW_LIMIT);
|
||||
reader = database.readerFor(database.getMessages(skip, ROW_LIMIT));
|
||||
|
||||
while ((record = reader.getNext()) != null) {
|
||||
XmlBackup.XmlBackupItem item =
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteStatement;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterCipher;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteStatement;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
@@ -18,7 +17,7 @@ import java.util.Set;
|
||||
|
||||
public class PlaintextBackupImporter {
|
||||
|
||||
public static void importPlaintextFromSd(Context context, MasterSecret masterSecret)
|
||||
public static void importPlaintextFromSd(Context context)
|
||||
throws NoExternalStorageException, IOException
|
||||
{
|
||||
Log.w("PlaintextBackupImporter", "importPlaintext()");
|
||||
@@ -28,7 +27,6 @@ public class PlaintextBackupImporter {
|
||||
try {
|
||||
ThreadDatabase threads = DatabaseFactory.getThreadDatabase(context);
|
||||
XmlBackup backup = new XmlBackup(getPlaintextExportFile().getAbsolutePath());
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
Set<Long> modifiedThreads = new HashSet<>();
|
||||
XmlBackup.XmlBackupItem item;
|
||||
|
||||
@@ -53,7 +51,7 @@ public class PlaintextBackupImporter {
|
||||
addTranslatedTypeToStatement(statement, 8, item.getType());
|
||||
addNullToStatement(statement, 9);
|
||||
addStringToStatement(statement, 10, item.getSubject());
|
||||
addEncryptedStringToStatement(masterCipher, statement, 11, item.getBody());
|
||||
addStringToStatement(statement, 11, item.getBody());
|
||||
addStringToStatement(statement, 12, item.getServiceCenter());
|
||||
addLongToStatement(statement, 13, threadId);
|
||||
modifiedThreads.add(threadId);
|
||||
@@ -80,14 +78,7 @@ public class PlaintextBackupImporter {
|
||||
return !backup.exists() && oldBackup.exists() ? oldBackup : backup;
|
||||
}
|
||||
|
||||
private static void addEncryptedStringToStatement(MasterCipher masterCipher, SQLiteStatement statement, int index, String value) {
|
||||
if (value == null || value.equals("null")) {
|
||||
statement.bindNull(index);
|
||||
} else {
|
||||
statement.bindString(index, masterCipher.encryptBody(value));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static void addTranslatedTypeToStatement(SQLiteStatement statement, int index, int type) {
|
||||
statement.bindLong(index, SmsDatabase.Types.translateFromSystemBaseType(type) | SmsDatabase.Types.ENCRYPTION_SYMMETRIC_BIT);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ package org.thoughtcrime.securesms.database;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
@@ -31,7 +32,7 @@ public class PushDatabase extends Database {
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
TYPE + " INTEGER, " + SOURCE + " TEXT, " + DEVICE_ID + " INTEGER, " + LEGACY_MSG + " TEXT, " + CONTENT + " TEXT, " + TIMESTAMP + " INTEGER);";
|
||||
|
||||
public PushDatabase(Context context, SQLiteOpenHelper databaseHelper) {
|
||||
public PushDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ package org.thoughtcrime.securesms.database;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
@@ -12,7 +10,10 @@ import android.util.Log;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -119,7 +120,7 @@ public class RecipientDatabase extends Database {
|
||||
SIGNAL_PROFILE_AVATAR + " TEXT DEFAULT NULL, " +
|
||||
PROFILE_SHARING + " INTEGER DEFAULT 0);";
|
||||
|
||||
public RecipientDatabase(Context context, SQLiteOpenHelper databaseHelper) {
|
||||
public RecipientDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,6 @@ package org.thoughtcrime.securesms.database;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.database.sqlite.SQLiteStatement;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
@@ -30,9 +27,13 @@ import android.util.Pair;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteStatement;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.DisplayRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
@@ -107,7 +108,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
|
||||
private final JobManager jobManager;
|
||||
|
||||
public SmsDatabase(Context context, SQLiteOpenHelper databaseHelper) {
|
||||
public SmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
this.jobManager = ApplicationContext.getInstance(context).getJobManager();
|
||||
}
|
||||
@@ -410,7 +411,17 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
return results;
|
||||
}
|
||||
|
||||
protected Pair<Long, Long> updateMessageBodyAndType(long messageId, String body, long maskOff, long maskOn) {
|
||||
public Pair<Long, Long> updateBundleMessageBody(long messageId, String body) {
|
||||
long type = Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT;
|
||||
return updateMessageBodyAndType(messageId, body, Types.TOTAL_MASK, type);
|
||||
}
|
||||
|
||||
public void updateMessageBody(long messageId, String body) {
|
||||
long type = 0;
|
||||
updateMessageBodyAndType(messageId, body, Types.ENCRYPTION_MASK, type);
|
||||
}
|
||||
|
||||
private Pair<Long, Long> updateMessageBodyAndType(long messageId, String body, long maskOff, long maskOn) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.execSQL("UPDATE " + TABLE_NAME + " SET " + BODY + " = ?, " +
|
||||
TYPE + " = (" + TYPE + " & " + (Types.TOTAL_MASK - maskOff) + " | " + maskOn + ") " +
|
||||
@@ -427,31 +438,33 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
}
|
||||
|
||||
public Pair<Long, Long> copyMessageInbox(long messageId) {
|
||||
Reader reader = readerFor(getMessage(messageId));
|
||||
SmsMessageRecord record = reader.getNext();
|
||||
try {
|
||||
SmsMessageRecord record = getMessage(messageId);
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(TYPE, (record.getType() & ~Types.BASE_TYPE_MASK) | Types.BASE_INBOX_TYPE);
|
||||
contentValues.put(ADDRESS, record.getIndividualRecipient().getAddress().serialize());
|
||||
contentValues.put(ADDRESS_DEVICE_ID, record.getRecipientDeviceId());
|
||||
contentValues.put(DATE_RECEIVED, System.currentTimeMillis());
|
||||
contentValues.put(DATE_SENT, record.getDateSent());
|
||||
contentValues.put(PROTOCOL, 31337);
|
||||
contentValues.put(READ, 0);
|
||||
contentValues.put(BODY, record.getBody().getBody());
|
||||
contentValues.put(THREAD_ID, record.getThreadId());
|
||||
contentValues.put(EXPIRES_IN, record.getExpiresIn());
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(TYPE, (record.getType() & ~Types.BASE_TYPE_MASK) | Types.BASE_INBOX_TYPE);
|
||||
contentValues.put(ADDRESS, record.getIndividualRecipient().getAddress().serialize());
|
||||
contentValues.put(ADDRESS_DEVICE_ID, record.getRecipientDeviceId());
|
||||
contentValues.put(DATE_RECEIVED, System.currentTimeMillis());
|
||||
contentValues.put(DATE_SENT, record.getDateSent());
|
||||
contentValues.put(PROTOCOL, 31337);
|
||||
contentValues.put(READ, 0);
|
||||
contentValues.put(BODY, record.getBody().getBody());
|
||||
contentValues.put(THREAD_ID, record.getThreadId());
|
||||
contentValues.put(EXPIRES_IN, record.getExpiresIn());
|
||||
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
long newMessageId = db.insert(TABLE_NAME, null, contentValues);
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
long newMessageId = db.insert(TABLE_NAME, null, contentValues);
|
||||
|
||||
DatabaseFactory.getThreadDatabase(context).update(record.getThreadId(), true);
|
||||
notifyConversationListeners(record.getThreadId());
|
||||
DatabaseFactory.getThreadDatabase(context).update(record.getThreadId(), true);
|
||||
notifyConversationListeners(record.getThreadId());
|
||||
|
||||
jobManager.add(new TrimThreadJob(context, record.getThreadId()));
|
||||
reader.close();
|
||||
|
||||
return new Pair<>(newMessageId, record.getThreadId());
|
||||
jobManager.add(new TrimThreadJob(context, record.getThreadId()));
|
||||
|
||||
return new Pair<>(newMessageId, record.getThreadId());
|
||||
} catch (NoSuchMessageException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull Pair<Long, Long> insertReceivedCall(@NonNull Address address) {
|
||||
@@ -587,10 +600,11 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
return insertMessageInbox(message, Types.BASE_INBOX_TYPE);
|
||||
}
|
||||
|
||||
protected long insertMessageOutbox(long threadId, OutgoingTextMessage message,
|
||||
long type, boolean forceSms, long date,
|
||||
InsertListener insertListener)
|
||||
public long insertMessageOutbox(long threadId, OutgoingTextMessage message,
|
||||
boolean forceSms, long date, InsertListener insertListener)
|
||||
{
|
||||
long type = Types.BASE_SENDING_TYPE;
|
||||
|
||||
if (message.isKeyExchange()) type |= Types.KEY_EXCHANGE_BIT;
|
||||
else if (message.isSecureMessage()) type |= (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT);
|
||||
else if (message.isEndSession()) type |= Types.END_SESSION_BIT;
|
||||
@@ -670,12 +684,21 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
return db.query(TABLE_NAME, MESSAGE_PROJECTION, where, null, null, null, null);
|
||||
}
|
||||
|
||||
public Cursor getMessage(long messageId) {
|
||||
public SmsMessageRecord getMessage(long messageId) throws NoSuchMessageException {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[]{messageId + ""},
|
||||
null, null, null);
|
||||
setNotifyConverationListeners(cursor, getThreadIdForMessage(messageId));
|
||||
return cursor;
|
||||
Cursor cursor = db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[]{messageId + ""}, null, null, null);
|
||||
Reader reader = new Reader(cursor);
|
||||
SmsMessageRecord record = reader.getNext();
|
||||
|
||||
reader.close();
|
||||
|
||||
if (record == null) throw new NoSuchMessageException("No message for ID: " + messageId);
|
||||
else return record;
|
||||
}
|
||||
|
||||
public Cursor getMessageCursor(long messageId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
return db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[] {messageId + ""}, null, null, null);
|
||||
}
|
||||
|
||||
public boolean deleteMessage(long messageId) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -18,15 +18,14 @@ package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.database.sqlite.SQLiteStatement;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterCipher;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteStatement;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
@@ -40,19 +39,6 @@ public class SmsMigrator {
|
||||
|
||||
private static final String TAG = SmsMigrator.class.getSimpleName();
|
||||
|
||||
private static void addEncryptedStringToStatement(Context context, SQLiteStatement statement,
|
||||
Cursor cursor, MasterSecret masterSecret,
|
||||
int index, String key)
|
||||
{
|
||||
int columnIndex = cursor.getColumnIndexOrThrow(key);
|
||||
|
||||
if (cursor.isNull(columnIndex)) {
|
||||
statement.bindNull(index);
|
||||
} else {
|
||||
statement.bindString(index, encrypt(masterSecret, cursor.getString(columnIndex)));
|
||||
}
|
||||
}
|
||||
|
||||
private static void addStringToStatement(SQLiteStatement statement, Cursor cursor,
|
||||
int index, String key)
|
||||
{
|
||||
@@ -77,8 +63,8 @@ public class SmsMigrator {
|
||||
}
|
||||
}
|
||||
|
||||
private static void addTranslatedTypeToStatement(SQLiteStatement statement, Cursor cursor,
|
||||
int index, String key)
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static void addTranslatedTypeToStatement(SQLiteStatement statement, Cursor cursor, int index, String key)
|
||||
{
|
||||
int columnIndex = cursor.getColumnIndexOrThrow(key);
|
||||
|
||||
@@ -99,9 +85,8 @@ public class SmsMigrator {
|
||||
ourType == MmsSmsColumns.Types.BASE_SENT_FAILED_TYPE;
|
||||
}
|
||||
|
||||
private static void getContentValuesForRow(Context context, MasterSecret masterSecret,
|
||||
Cursor cursor, long threadId,
|
||||
SQLiteStatement statement)
|
||||
private static void getContentValuesForRow(Context context, Cursor cursor,
|
||||
long threadId, SQLiteStatement statement)
|
||||
{
|
||||
String theirAddress = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.ADDRESS));
|
||||
statement.bindString(1, Address.fromExternal(context, theirAddress).serialize());
|
||||
@@ -115,7 +100,7 @@ public class SmsMigrator {
|
||||
addTranslatedTypeToStatement(statement, cursor, 8, SmsDatabase.TYPE);
|
||||
addIntToStatement(statement, cursor, 9, SmsDatabase.REPLY_PATH_PRESENT);
|
||||
addStringToStatement(statement, cursor, 10, SmsDatabase.SUBJECT);
|
||||
addEncryptedStringToStatement(context, statement, cursor, masterSecret, 11, SmsDatabase.BODY);
|
||||
addStringToStatement(statement, cursor, 11, SmsDatabase.BODY);
|
||||
addStringToStatement(statement, cursor, 12, SmsDatabase.SERVICE_CENTER);
|
||||
|
||||
statement.bindLong(13, threadId);
|
||||
@@ -159,14 +144,7 @@ public class SmsMigrator {
|
||||
else return recipientList;
|
||||
}
|
||||
|
||||
private static String encrypt(MasterSecret masterSecret, String body)
|
||||
{
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
return masterCipher.encryptBody(body);
|
||||
}
|
||||
|
||||
private static void migrateConversation(Context context, MasterSecret masterSecret,
|
||||
SmsMigrationProgressListener listener,
|
||||
private static void migrateConversation(Context context, SmsMigrationProgressListener listener,
|
||||
ProgressDescription progress,
|
||||
long theirThreadId, long ourThreadId)
|
||||
{
|
||||
@@ -191,7 +169,7 @@ public class SmsMigrator {
|
||||
int typeColumn = cursor.getColumnIndex(SmsDatabase.TYPE);
|
||||
|
||||
if (cursor.isNull(typeColumn) || isAppropriateTypeForMigration(cursor, typeColumn)) {
|
||||
getContentValuesForRow(context, masterSecret, cursor, ourThreadId, statement);
|
||||
getContentValuesForRow(context, cursor, ourThreadId, statement);
|
||||
statement.execute();
|
||||
}
|
||||
|
||||
@@ -208,9 +186,7 @@ public class SmsMigrator {
|
||||
}
|
||||
}
|
||||
|
||||
public static void migrateDatabase(Context context,
|
||||
MasterSecret masterSecret,
|
||||
SmsMigrationProgressListener listener)
|
||||
public static void migrateDatabase(Context context, SmsMigrationProgressListener listener)
|
||||
{
|
||||
// if (context.getSharedPreferences("SecureSMS", Context.MODE_PRIVATE).getBoolean("migrated", false))
|
||||
// return;
|
||||
@@ -231,7 +207,7 @@ public class SmsMigrator {
|
||||
if (ourRecipients != null) {
|
||||
if (ourRecipients.size() == 1) {
|
||||
long ourThreadId = threadDatabase.getThreadIdFor(ourRecipients.iterator().next());
|
||||
migrateConversation(context, masterSecret, listener, progress, theirThreadId, ourThreadId);
|
||||
migrateConversation(context, listener, progress, theirThreadId, ourThreadId);
|
||||
} else if (ourRecipients.size() > 1) {
|
||||
ourRecipients.add(Recipient.from(context, Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)), true));
|
||||
|
||||
@@ -245,7 +221,7 @@ public class SmsMigrator {
|
||||
Recipient ourGroupRecipient = Recipient.from(context, Address.fromSerialized(ourGroupId), true);
|
||||
long ourThreadId = threadDatabase.getThreadIdFor(ourGroupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
|
||||
|
||||
migrateConversation(context, masterSecret, listener, progress, theirThreadId, ourThreadId);
|
||||
migrateConversation(context, listener, progress, theirThreadId, ourThreadId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,7 +238,7 @@ public class SmsMigrator {
|
||||
}
|
||||
|
||||
public interface SmsMigrationProgressListener {
|
||||
public void progressUpdate(ProgressDescription description);
|
||||
void progressUpdate(ProgressDescription description);
|
||||
}
|
||||
|
||||
public static class ProgressDescription {
|
||||
@@ -271,8 +247,8 @@ public class SmsMigrator {
|
||||
public final int secondaryTotal;
|
||||
public final int secondaryComplete;
|
||||
|
||||
public ProgressDescription(int primaryTotal, int primaryComplete,
|
||||
int secondaryTotal, int secondaryComplete)
|
||||
ProgressDescription(int primaryTotal, int primaryComplete,
|
||||
int secondaryTotal, int secondaryComplete)
|
||||
{
|
||||
this.primaryTotal = primaryTotal;
|
||||
this.primaryComplete = primaryComplete;
|
||||
@@ -280,14 +256,14 @@ public class SmsMigrator {
|
||||
this.secondaryComplete = secondaryComplete;
|
||||
}
|
||||
|
||||
public ProgressDescription(ProgressDescription that, int secondaryTotal, int secondaryComplete) {
|
||||
ProgressDescription(ProgressDescription that, int secondaryTotal, int secondaryComplete) {
|
||||
this.primaryComplete = that.primaryComplete;
|
||||
this.primaryTotal = that.primaryTotal;
|
||||
this.secondaryComplete = secondaryComplete;
|
||||
this.secondaryTotal = secondaryTotal;
|
||||
}
|
||||
|
||||
public void incrementPrimaryComplete() {
|
||||
void incrementPrimaryComplete() {
|
||||
primaryComplete += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,21 +21,19 @@ import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.MergeCursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.MasterCipher;
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.DisplayRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
@@ -46,7 +44,6 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.DelimiterUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
@@ -90,7 +87,7 @@ public class ThreadDatabase extends Database {
|
||||
LAST_SEEN + " INTEGER DEFAULT 0, " + HAS_SENT + " INTEGER DEFAULT 0, " +
|
||||
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + UNREAD_COUNT + " INTEGER DEFAULT 0);";
|
||||
|
||||
static final String[] CREATE_INDEXS = {
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + ADDRESS + ");",
|
||||
"CREATE INDEX IF NOT EXISTS archived_count_index ON " + TABLE_NAME + " (" + ARCHIVED + ", " + MESSAGE_COUNT + ");",
|
||||
};
|
||||
@@ -109,7 +106,7 @@ public class ThreadDatabase extends Database {
|
||||
Stream.of(GroupDatabase.TYPED_GROUP_PROJECTION))
|
||||
.toList();
|
||||
|
||||
public ThreadDatabase(Context context, SQLiteOpenHelper databaseHelper) {
|
||||
public ThreadDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
@@ -617,8 +614,8 @@ public class ThreadDatabase extends Database {
|
||||
void onProgress(int complete, int total);
|
||||
}
|
||||
|
||||
public Reader readerFor(Cursor cursor, MasterCipher masterCipher) {
|
||||
return new Reader(cursor, masterCipher);
|
||||
public Reader readerFor(Cursor cursor) {
|
||||
return new Reader(cursor);
|
||||
}
|
||||
|
||||
public static class DistributionTypes {
|
||||
@@ -631,12 +628,10 @@ public class ThreadDatabase extends Database {
|
||||
|
||||
public class Reader {
|
||||
|
||||
private final Cursor cursor;
|
||||
private final MasterCipher masterCipher;
|
||||
private final Cursor cursor;
|
||||
|
||||
public Reader(Cursor cursor, MasterCipher masterCipher) {
|
||||
this.cursor = cursor;
|
||||
this.masterCipher = masterCipher;
|
||||
public Reader(Cursor cursor) {
|
||||
this.cursor = cursor;
|
||||
}
|
||||
|
||||
public ThreadRecord getNext() {
|
||||
@@ -686,21 +681,7 @@ public class ThreadDatabase extends Database {
|
||||
}
|
||||
|
||||
private DisplayRecord.Body getPlaintextBody(Cursor cursor) {
|
||||
try {
|
||||
long type = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_TYPE));
|
||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(SNIPPET));
|
||||
|
||||
if (!TextUtils.isEmpty(body) && masterCipher != null && MmsSmsColumns.Types.isSymmetricEncryption(type)) {
|
||||
return new DisplayRecord.Body(masterCipher.decryptBody(body), true);
|
||||
} else if (!TextUtils.isEmpty(body) && masterCipher == null && MmsSmsColumns.Types.isSymmetricEncryption(type)) {
|
||||
return new DisplayRecord.Body(body, false);
|
||||
} else {
|
||||
return new DisplayRecord.Body(body, true);
|
||||
}
|
||||
} catch (InvalidMessageException e) {
|
||||
Log.w("ThreadDatabase", e);
|
||||
return new DisplayRecord.Body(context.getString(R.string.ThreadDatabase_error_decrypting_message), true);
|
||||
}
|
||||
return new DisplayRecord.Body(cursor.getString(cursor.getColumnIndexOrThrow(SNIPPET)), true);
|
||||
}
|
||||
|
||||
private @Nullable Uri getSnippetUri(Cursor cursor) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,197 @@
|
||||
package org.thoughtcrime.securesms.database.helpers;
|
||||
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.annimon.stream.function.Function;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||
import org.thoughtcrime.securesms.crypto.MasterCipher;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class SQLCipherMigrationHelper {
|
||||
|
||||
private static final String TAG = SQLCipherMigrationHelper.class.getSimpleName();
|
||||
|
||||
private static final long ENCRYPTION_SYMMETRIC_BIT = 0x80000000;
|
||||
private static final long ENCRYPTION_ASYMMETRIC_BIT = 0x40000000;
|
||||
|
||||
static void migratePlaintext(@NonNull android.database.sqlite.SQLiteDatabase legacyDb,
|
||||
@NonNull net.sqlcipher.database.SQLiteDatabase modernDb)
|
||||
{
|
||||
modernDb.beginTransaction();
|
||||
try {
|
||||
copyTable("identities", legacyDb, modernDb, null);
|
||||
copyTable("push", legacyDb, modernDb, null);
|
||||
copyTable("groups", legacyDb, modernDb, null);
|
||||
copyTable("recipient_preferences", legacyDb, modernDb, null);
|
||||
copyTable("group_receipts", legacyDb, modernDb, null);
|
||||
modernDb.setTransactionSuccessful();
|
||||
} finally {
|
||||
modernDb.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public static void migrateCiphertext(@NonNull Context context,
|
||||
@NonNull MasterSecret masterSecret,
|
||||
@NonNull android.database.sqlite.SQLiteDatabase legacyDb,
|
||||
@NonNull net.sqlcipher.database.SQLiteDatabase modernDb)
|
||||
{
|
||||
MasterCipher legacyCipher = new MasterCipher(masterSecret);
|
||||
AsymmetricMasterCipher legacyAsymmetricCipher = new AsymmetricMasterCipher(MasterSecretUtil.getAsymmetricMasterSecret(context, masterSecret));
|
||||
|
||||
modernDb.beginTransaction();
|
||||
|
||||
try {
|
||||
copyTable("sms", legacyDb, modernDb, (row) -> {
|
||||
Pair<Long, String> plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher,
|
||||
row.getAsLong("type"),
|
||||
row.getAsString("body"));
|
||||
|
||||
row.put("body", plaintext.second);
|
||||
row.put("type", plaintext.first);
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
copyTable("mms", legacyDb, modernDb, (row) -> {
|
||||
Pair<Long, String> plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher,
|
||||
row.getAsLong("msg_box"),
|
||||
row.getAsString("body"));
|
||||
|
||||
row.put("body", plaintext.second);
|
||||
row.put("msg_box", plaintext.first);
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
copyTable("part", legacyDb, modernDb, (row) -> {
|
||||
String fileName = row.getAsString("file_name");
|
||||
String mediaKey = row.getAsString("cd");
|
||||
|
||||
try {
|
||||
if (!TextUtils.isEmpty(fileName)) {
|
||||
row.put("file_name", legacyCipher.decryptBody(fileName));
|
||||
}
|
||||
} catch (InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!TextUtils.isEmpty(mediaKey)) {
|
||||
byte[] plaintext;
|
||||
|
||||
if (mediaKey.startsWith("?ASYNC-")) {
|
||||
plaintext = legacyAsymmetricCipher.decryptBytes(Base64.decode(mediaKey.substring("?ASYNC-".length())));
|
||||
} else {
|
||||
plaintext = legacyCipher.decryptBytes(Base64.decode(mediaKey));
|
||||
}
|
||||
|
||||
row.put("cd", Base64.encodeBytes(plaintext));
|
||||
}
|
||||
} catch (IOException | InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
copyTable("thread", legacyDb, modernDb, (row) -> {
|
||||
Pair<Long, String> plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher,
|
||||
row.getAsLong("snippet_type"),
|
||||
row.getAsString("snippet"));
|
||||
|
||||
row.put("snippet", plaintext.second);
|
||||
row.put("snippet_type", plaintext.first);
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
|
||||
copyTable("drafts", legacyDb, modernDb, (row) -> {
|
||||
String draftType = row.getAsString("type");
|
||||
String draft = row.getAsString("value");
|
||||
|
||||
try {
|
||||
if (!TextUtils.isEmpty(draftType)) row.put("type", legacyCipher.decryptBody(draftType));
|
||||
if (!TextUtils.isEmpty(draft)) row.put("value", legacyCipher.decryptBody(draft));
|
||||
} catch (InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
TextSecurePreferences.setNeedsSqlCipherMigration(context, false);
|
||||
modernDb.setTransactionSuccessful();
|
||||
} finally {
|
||||
modernDb.endTransaction();
|
||||
}
|
||||
|
||||
AttachmentSecretProvider.getInstance(context).setClassicKey(context, masterSecret.getEncryptionKey().getEncoded(), masterSecret.getMacKey().getEncoded());
|
||||
}
|
||||
|
||||
private static void copyTable(@NonNull String tableName,
|
||||
@NonNull android.database.sqlite.SQLiteDatabase legacyDb,
|
||||
@NonNull net.sqlcipher.database.SQLiteDatabase modernDb,
|
||||
@Nullable Function<ContentValues, ContentValues> transformer)
|
||||
{
|
||||
try (Cursor cursor = legacyDb.query(tableName, null, null, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
ContentValues row = new ContentValues();
|
||||
|
||||
for (int i=0;i<cursor.getColumnCount();i++) {
|
||||
String columnName = cursor.getColumnName(i);
|
||||
|
||||
switch (cursor.getType(i)) {
|
||||
case Cursor.FIELD_TYPE_STRING: row.put(columnName, cursor.getString(i)); break;
|
||||
case Cursor.FIELD_TYPE_FLOAT: row.put(columnName, cursor.getFloat(i)); break;
|
||||
case Cursor.FIELD_TYPE_INTEGER: row.put(columnName, cursor.getLong(i)); break;
|
||||
case Cursor.FIELD_TYPE_BLOB: row.put(columnName, cursor.getBlob(i)); break;
|
||||
}
|
||||
}
|
||||
|
||||
if (transformer != null) {
|
||||
row = transformer.apply(row);
|
||||
}
|
||||
|
||||
modernDb.insert(tableName, null, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Pair<Long, String> getPlaintextBody(@NonNull MasterCipher legacyCipher,
|
||||
@NonNull AsymmetricMasterCipher legacyAsymmetricCipher,
|
||||
long type,
|
||||
@Nullable String body)
|
||||
{
|
||||
try {
|
||||
if (!TextUtils.isEmpty(body)) {
|
||||
if ((type & ENCRYPTION_SYMMETRIC_BIT) != 0) body = legacyCipher.decryptBody(body);
|
||||
else if ((type & ENCRYPTION_ASYMMETRIC_BIT) != 0) body = legacyAsymmetricCipher.decryptBody(body);
|
||||
}
|
||||
} catch (InvalidMessageException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
type &= ~(ENCRYPTION_SYMMETRIC_BIT);
|
||||
type &= ~(ENCRYPTION_ASYMMETRIC_BIT);
|
||||
|
||||
return new Pair<>(type, body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package org.thoughtcrime.securesms.database.helpers;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteDatabaseHook;
|
||||
import net.sqlcipher.database.SQLiteOpenHelper;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static final String TAG = SQLCipherOpenHelper.class.getSimpleName();
|
||||
|
||||
private static final int DATABASE_VERSION = 1;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
private final DatabaseSecret databaseSecret;
|
||||
|
||||
public SQLCipherOpenHelper(@NonNull Context context, @NonNull DatabaseSecret databaseSecret) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION, new SQLiteDatabaseHook() {
|
||||
@Override
|
||||
public void preKey(SQLiteDatabase db) {
|
||||
db.rawExecSQL("PRAGMA cipher_default_kdf_iter = 1;");
|
||||
db.rawExecSQL("PRAGMA cipher_default_page_size = 4096;");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postKey(SQLiteDatabase db) {
|
||||
db.rawExecSQL("PRAGMA kdf_iter = '1';");
|
||||
db.rawExecSQL("PRAGMA cipher_page_size = 4096;");
|
||||
}
|
||||
});
|
||||
|
||||
this.context = context.getApplicationContext();
|
||||
this.databaseSecret = databaseSecret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(SmsDatabase.CREATE_TABLE);
|
||||
db.execSQL(MmsDatabase.CREATE_TABLE);
|
||||
db.execSQL(AttachmentDatabase.CREATE_TABLE);
|
||||
db.execSQL(ThreadDatabase.CREATE_TABLE);
|
||||
db.execSQL(IdentityDatabase.CREATE_TABLE);
|
||||
db.execSQL(DraftDatabase.CREATE_TABLE);
|
||||
db.execSQL(PushDatabase.CREATE_TABLE);
|
||||
db.execSQL(GroupDatabase.CREATE_TABLE);
|
||||
db.execSQL(RecipientDatabase.CREATE_TABLE);
|
||||
db.execSQL(GroupReceiptDatabase.CREATE_TABLE);
|
||||
|
||||
executeStatements(db, SmsDatabase.CREATE_INDEXS);
|
||||
executeStatements(db, MmsDatabase.CREATE_INDEXS);
|
||||
executeStatements(db, AttachmentDatabase.CREATE_INDEXS);
|
||||
executeStatements(db, ThreadDatabase.CREATE_INDEXS);
|
||||
executeStatements(db, DraftDatabase.CREATE_INDEXS);
|
||||
executeStatements(db, GroupDatabase.CREATE_INDEXS);
|
||||
executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES);
|
||||
|
||||
if (context.getDatabasePath(ClassicOpenHelper.NAME).exists()) {
|
||||
ClassicOpenHelper legacyHelper = new ClassicOpenHelper(context);
|
||||
android.database.sqlite.SQLiteDatabase legacyDb = legacyHelper.getWritableDatabase();
|
||||
|
||||
SQLCipherMigrationHelper.migratePlaintext(legacyDb, db);
|
||||
|
||||
MasterSecret masterSecret = KeyCachingService.getMasterSecret(context);
|
||||
|
||||
if (masterSecret != null) SQLCipherMigrationHelper.migrateCiphertext(context, masterSecret, legacyDb, db);
|
||||
else TextSecurePreferences.setNeedsSqlCipherMigration(context, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
|
||||
}
|
||||
|
||||
public SQLiteDatabase getReadableDatabase() {
|
||||
return getReadableDatabase(databaseSecret.asString());
|
||||
}
|
||||
|
||||
public SQLiteDatabase getWritableDatabase() {
|
||||
return getWritableDatabase(databaseSecret.asString());
|
||||
}
|
||||
|
||||
private void executeStatements(SQLiteDatabase db, String[] statements) {
|
||||
for (String statement : statements)
|
||||
db.execSQL(statement);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import android.support.v4.content.AsyncTaskLoader;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
@@ -28,15 +27,14 @@ import java.util.Map;
|
||||
|
||||
public class BucketedThreadMediaLoader extends AsyncTaskLoader<BucketedThreadMediaLoader.BucketedThreadMedia> {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = BucketedThreadMediaLoader.class.getSimpleName();
|
||||
|
||||
private final MasterSecret masterSecret;
|
||||
private final Address address;
|
||||
|
||||
public BucketedThreadMediaLoader(@NonNull Context context, @NonNull MasterSecret masterSecret, @NonNull Address address) {
|
||||
public BucketedThreadMediaLoader(@NonNull Context context, @NonNull Address address) {
|
||||
super(context);
|
||||
this.masterSecret = masterSecret;
|
||||
this.address = address;
|
||||
this.address = address;
|
||||
|
||||
onContentChanged();
|
||||
}
|
||||
@@ -60,7 +58,7 @@ public class BucketedThreadMediaLoader extends AsyncTaskLoader<BucketedThreadMed
|
||||
|
||||
try (Cursor cursor = DatabaseFactory.getMediaDatabase(getContext()).getGalleryMediaForThread(threadId)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
result.add(MediaDatabase.MediaRecord.from(getContext(), masterSecret, cursor));
|
||||
result.add(MediaDatabase.MediaRecord.from(getContext(), cursor));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ public class MessageDetailsLoader extends AbstractCursorLoader {
|
||||
public Cursor getCursor() {
|
||||
switch (type) {
|
||||
case MmsSmsDatabase.SMS_TRANSPORT:
|
||||
return DatabaseFactory.getEncryptingSmsDatabase(context).getMessage(messageId);
|
||||
return DatabaseFactory.getSmsDatabase(context).getMessageCursor(messageId);
|
||||
case MmsSmsDatabase.MMS_TRANSPORT:
|
||||
return DatabaseFactory.getMmsDatabase(context).getMessage(messageId);
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user