Basic support for encrypted push-based attachments.

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

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

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

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

View File

@ -7,13 +7,18 @@ message IncomingPushMessageSignal {
optional uint32 type = 1; optional uint32 type = 1;
optional string source = 2; optional string source = 2;
repeated string destinations = 3; repeated string destinations = 3;
optional bytes message = 4; optional uint64 timestamp = 4;
optional bytes message = 5; // Contains an encrypted IncomingPushMessageContent
}
message PushMessageContent {
optional string body = 1;
message AttachmentPointer { message AttachmentPointer {
optional string contentType = 1; optional fixed64 id = 1;
optional string key = 2; optional string contentType = 2;
optional bytes key = 3;
} }
repeated AttachmentPointer attachments = 5; repeated AttachmentPointer attachments = 2;
optional uint64 timestamp = 6;
} }

View File

@ -0,0 +1,156 @@
/**
* Copyright (C) 2013 Open 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.whispersystems.textsecure.crypto;
import android.util.Log;
import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
/**
* Encrypts push attachments.
*
* @author Moxie Marlinspike
*/
public class AttachmentCipher {
static final int CIPHER_KEY_SIZE = 32;
static final int MAC_KEY_SIZE = 20;
private final SecretKeySpec cipherKey;
private final SecretKeySpec macKey;
private final Cipher cipher;
private final Mac mac;
public AttachmentCipher() {
this.cipherKey = initializeRandomCipherKey();
this.macKey = initializeRandomMacKey();
this.cipher = initializeCipher();
this.mac = initializeMac();
}
public AttachmentCipher(byte[] combinedKeyMaterial) {
byte[][] parts = Util.split(combinedKeyMaterial, CIPHER_KEY_SIZE, MAC_KEY_SIZE);
this.cipherKey = new SecretKeySpec(parts[0], "AES");
this.macKey = new SecretKeySpec(parts[1], "HmacSHA1");
this.cipher = initializeCipher();
this.mac = initializeMac();
}
public byte[] getCombinedKeyMaterial() {
return Util.combine(this.cipherKey.getEncoded(), this.macKey.getEncoded());
}
public byte[] encrypt(byte[] plaintext) {
try {
this.cipher.init(Cipher.ENCRYPT_MODE, this.cipherKey);
this.mac.init(this.macKey);
byte[] ciphertext = this.cipher.doFinal(plaintext);
byte[] iv = this.cipher.getIV();
byte[] mac = this.mac.doFinal(Util.combine(iv, ciphertext));
return Util.combine(iv, ciphertext, mac);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
public byte[] decrypt(byte[] ciphertext)
throws InvalidMacException, InvalidMessageException
{
try {
if (ciphertext.length <= cipher.getBlockSize() + mac.getMacLength()) {
throw new InvalidMessageException("Message too short!");
}
byte[][] ciphertextParts = Util.split(ciphertext,
this.cipher.getBlockSize(),
ciphertext.length - this.cipher.getBlockSize() - this.mac.getMacLength(),
this.mac.getMacLength());
this.mac.update(ciphertext, 0, ciphertext.length - mac.getMacLength());
byte[] ourMac = this.mac.doFinal();
if (!Arrays.equals(ourMac, ciphertextParts[2])) {
throw new InvalidMacException("Mac doesn't match!");
}
this.cipher.init(Cipher.DECRYPT_MODE, this.cipherKey,
new IvParameterSpec(ciphertextParts[0]));
return cipher.doFinal(ciphertextParts[1]);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
} catch (InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new InvalidMessageException(e);
}
}
private Mac initializeMac() {
try {
Mac mac = Mac.getInstance("HmacSHA1");
return mac;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private Cipher initializeCipher() {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
return cipher;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (NoSuchPaddingException e) {
throw new AssertionError(e);
}
}
private SecretKeySpec initializeRandomCipherKey() {
byte[] key = new byte[CIPHER_KEY_SIZE];
Util.getSecureRandom().nextBytes(key);
return new SecretKeySpec(key, "AES");
}
private SecretKeySpec initializeRandomMacKey() {
byte[] key = new byte[MAC_KEY_SIZE];
Util.getSecureRandom().nextBytes(key);
return new SecretKeySpec(key, "HmacSHA1");
}
}

View File

@ -0,0 +1,178 @@
/**
* Copyright (C) 2013 Open 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.whispersystems.textsecure.crypto;
import android.util.Log;
import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
/**
* Class for streaming an encrypted push attachment off disk.
*
* @author Moxie Marlinspike
*/
public class AttachmentCipherInputStream extends FileInputStream {
private static final int BLOCK_SIZE = 16;
private Cipher cipher;
private boolean done;
private long totalDataSize;
private long totalRead;
public AttachmentCipherInputStream(File file, byte[] combinedKeyMaterial)
throws IOException, InvalidMessageException
{
super(file);
try {
byte[][] parts = Util.split(combinedKeyMaterial,
AttachmentCipher.CIPHER_KEY_SIZE,
AttachmentCipher.MAC_KEY_SIZE);
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(new SecretKeySpec(parts[1], "HmacSHA1"));
if (file.length() <= BLOCK_SIZE + mac.getMacLength()) {
throw new InvalidMessageException("Message shorter than crypto overhead!");
}
verifyMac(file, mac);
byte[] iv = new byte[BLOCK_SIZE];
readFully(iv);
this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(parts[0], "AES"), new IvParameterSpec(iv));
this.done = false;
this.totalRead = 0;
this.totalDataSize = file.length() - cipher.getBlockSize() - mac.getMacLength();
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
} catch (InvalidMacException e) {
throw new InvalidMessageException(e);
} catch (NoSuchPaddingException e) {
throw new AssertionError(e);
} catch (InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
}
}
@Override
public int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
@Override
public int read(byte[] buffer, int offset, int length) throws IOException {
if (totalRead != totalDataSize) return readIncremental(buffer, offset, length);
else if (!done) return readFinal(buffer, offset, length);
else return -1;
}
private int readFinal(byte[] buffer, int offset, int length) throws IOException {
try {
int flourish = cipher.doFinal(buffer, offset);
done = true;
return flourish;
} catch (IllegalBlockSizeException e) {
Log.w("EncryptingPartInputStream", e);
throw new IOException("Illegal block size exception!");
} catch (ShortBufferException e) {
Log.w("EncryptingPartInputStream", e);
throw new IOException("Short buffer exception!");
} catch (BadPaddingException e) {
Log.w("EncryptingPartInputStream", e);
throw new IOException("Bad padding exception!");
}
}
private int readIncremental(byte[] buffer, int offset, int length) throws IOException {
if (length + totalRead > totalDataSize)
length = (int)(totalDataSize - totalRead);
byte[] internalBuffer = new byte[length];
int read = super.read(internalBuffer, 0, internalBuffer.length <= cipher.getBlockSize() ? internalBuffer.length : internalBuffer.length - cipher.getBlockSize());
totalRead += read;
try {
return cipher.update(internalBuffer, 0, read, buffer, offset);
} catch (ShortBufferException e) {
throw new AssertionError(e);
}
}
private void verifyMac(File file, Mac mac) throws FileNotFoundException, InvalidMacException {
try {
FileInputStream fin = new FileInputStream(file);
int remainingData = (int) file.length() - mac.getMacLength();
byte[] buffer = new byte[4096];
while (remainingData > 0) {
int read = fin.read(buffer, 0, Math.min(buffer.length, remainingData));
mac.update(buffer, 0, read);
remainingData -= read;
}
byte[] ourMac = mac.doFinal();
byte[] theirMac = new byte[mac.getMacLength()];
Util.readFully(fin, theirMac);
if (!Arrays.equals(ourMac, theirMac)) {
throw new InvalidMacException("MAC doesn't match!");
}
} catch (IOException e1) {
throw new InvalidMacException(e1);
}
}
private void readFully(byte[] buffer) throws IOException {
int offset = 0;
for (;;) {
int read = super.read(buffer, offset, buffer.length - offset);
if (read + offset < buffer.length) offset += read;
else return;
}
}
}

View File

@ -1,12 +1,26 @@
/**
* Copyright (C) 2013 Open 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.whispersystems.textsecure.push; package org.whispersystems.textsecure.push;
import org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal;
import org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal.AttachmentPointer;
import org.whispersystems.textsecure.util.Base64;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -24,12 +38,20 @@ public class IncomingPushMessage implements PushMessage, Parcelable {
} }
}; };
private int type; private int type;
private String source; private String source;
private List<String> destinations; private List<String> destinations;
private byte[] message; private byte[] message;
private List<PushAttachmentPointer> attachments; private long timestamp;
private long timestamp;
private IncomingPushMessage(IncomingPushMessage message, byte[] body) {
this.type = message.type;
this.source = message.source;
this.destinations = new LinkedList<String>();
this.destinations.addAll(message.destinations);
this.message = body;
this.timestamp = message.timestamp;
}
public IncomingPushMessage(IncomingPushMessageSignal signal) { public IncomingPushMessage(IncomingPushMessageSignal signal) {
this.type = signal.getType(); this.type = signal.getType();
@ -37,25 +59,15 @@ public class IncomingPushMessage implements PushMessage, Parcelable {
this.destinations = signal.getDestinationsList(); this.destinations = signal.getDestinationsList();
this.message = signal.getMessage().toByteArray(); this.message = signal.getMessage().toByteArray();
this.timestamp = signal.getTimestamp(); this.timestamp = signal.getTimestamp();
this.attachments = new LinkedList<PushAttachmentPointer>();
List<AttachmentPointer> attachmentPointers = signal.getAttachmentsList();
for (AttachmentPointer pointer : attachmentPointers) {
this.attachments.add(new PushAttachmentPointer(pointer.getContentType(), pointer.getKey()));
}
} }
public IncomingPushMessage(Parcel in) { public IncomingPushMessage(Parcel in) {
this.destinations = new LinkedList<String>(); this.destinations = new LinkedList<String>();
this.attachments = new LinkedList<PushAttachmentPointer>();
this.type = in.readInt(); this.type = in.readInt();
this.source = in.readString(); this.source = in.readString();
in.readStringList(destinations); in.readStringList(destinations);
this.message = new byte[in.readInt()]; this.message = new byte[in.readInt()];
in.readByteArray(this.message); in.readByteArray(this.message);
in.readList(attachments, PushAttachmentPointer.class.getClassLoader());
this.timestamp = in.readLong(); this.timestamp = in.readLong();
} }
@ -67,10 +79,6 @@ public class IncomingPushMessage implements PushMessage, Parcelable {
return source; return source;
} }
public List<PushAttachmentPointer> getAttachments() {
return attachments;
}
public byte[] getBody() { public byte[] getBody() {
return message; return message;
} }
@ -79,10 +87,6 @@ public class IncomingPushMessage implements PushMessage, Parcelable {
return destinations; return destinations;
} }
public boolean hasAttachments() {
return getAttachments() != null && !getAttachments().isEmpty();
}
@Override @Override
public int describeContents() { public int describeContents() {
return 0; return 0;
@ -95,11 +99,22 @@ public class IncomingPushMessage implements PushMessage, Parcelable {
dest.writeStringList(destinations); dest.writeStringList(destinations);
dest.writeInt(message.length); dest.writeInt(message.length);
dest.writeByteArray(message); dest.writeByteArray(message);
dest.writeList(attachments);
dest.writeLong(timestamp); dest.writeLong(timestamp);
} }
public IncomingPushMessage withBody(byte[] body) {
return new IncomingPushMessage(this, body);
}
public int getType() { public int getType() {
return type; return type;
} }
public boolean isSecureMessage() {
return getType() == PushMessage.TYPE_MESSAGE_CIPHERTEXT;
}
public boolean isPreKeyBundle() {
return getType() == PushMessage.TYPE_MESSAGE_PREKEY_BUNDLE;
}
} }

View File

@ -1,34 +1,35 @@
/**
* Copyright (C) 2013 Open 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.whispersystems.textsecure.push; package org.whispersystems.textsecure.push;
import org.whispersystems.textsecure.util.Base64; import org.whispersystems.textsecure.util.Base64;
import java.util.LinkedList;
import java.util.List;
public class OutgoingPushMessage implements PushMessage { public class OutgoingPushMessage implements PushMessage {
private int type; private int type;
private String destination; private String destination;
private String body; private String body;
private List<PushAttachmentPointer> attachments;
public OutgoingPushMessage(String destination, byte[] body, int type) { public OutgoingPushMessage(String destination, byte[] body, int type) {
this.attachments = new LinkedList<PushAttachmentPointer>();
this.destination = destination; this.destination = destination;
this.body = Base64.encodeBytes(body); this.body = Base64.encodeBytes(body);
this.type = type; this.type = type;
} }
public OutgoingPushMessage(String destination, byte[] body,
List<PushAttachmentPointer> attachments,
int type)
{
this.destination = destination;
this.body = Base64.encodeBytes(body);
this.attachments = attachments;
this.type = type;
}
public String getDestination() { public String getDestination() {
return destination; return destination;
} }
@ -37,10 +38,6 @@ public class OutgoingPushMessage implements PushMessage {
return body; return body;
} }
public List<PushAttachmentPointer> getAttachments() {
return attachments;
}
public int getType() { public int getType() {
return type; return type;
} }

View File

@ -18,23 +18,33 @@ public class PushAttachmentPointer implements Parcelable {
}; };
private final String contentType; private final String contentType;
private final String key; private final long id;
private final byte[] key;
public PushAttachmentPointer(String contentType, String key) { public PushAttachmentPointer(String contentType, long id, byte[] key) {
this.contentType = contentType; this.contentType = contentType;
this.id = id;
this.key = key; this.key = key;
} }
public PushAttachmentPointer(Parcel in) { public PushAttachmentPointer(Parcel in) {
this.contentType = in.readString(); this.contentType = in.readString();
this.key = in.readString(); this.id = in.readLong();
int keyLength = in.readInt();
this.key = new byte[keyLength];
in.readByteArray(this.key);
} }
public String getContentType() { public String getContentType() {
return contentType; return contentType;
} }
public String getKey() { public long getId() {
return id;
}
public byte[] getKey() {
return key; return key;
} }
@ -46,6 +56,8 @@ public class PushAttachmentPointer implements Parcelable {
@Override @Override
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
dest.writeString(contentType); dest.writeString(contentType);
dest.writeString(key); dest.writeLong(id);
dest.writeInt(this.key.length);
dest.writeByteArray(this.key);
} }
} }

View File

@ -84,31 +84,21 @@ public class PushServiceSocket {
sendMessage(new OutgoingPushMessageList(message)); sendMessage(new OutgoingPushMessageList(message));
} }
public void sendMessage(List<String> recipients, List<byte[]> bodies, public void sendMessage(List<String> recipients, List<byte[]> bodies, List<Integer> types)
List<List<PushAttachmentData>> attachmentsList, int type)
throws IOException throws IOException
{ {
List<OutgoingPushMessage> messages = new LinkedList<OutgoingPushMessage>(); List<OutgoingPushMessage> messages = new LinkedList<OutgoingPushMessage>();
Iterator<String> recipientsIterator = recipients.iterator(); Iterator<String> recipientsIterator = recipients.iterator();
Iterator<byte[]> bodiesIterator = bodies.iterator(); Iterator<byte[]> bodiesIterator = bodies.iterator();
Iterator<List<PushAttachmentData>> attachmentsIterator = attachmentsList.iterator(); Iterator<Integer> typesIterator = types.iterator();
while (recipientsIterator.hasNext()) { while (recipientsIterator.hasNext()) {
String recipient = recipientsIterator.next(); String recipient = recipientsIterator.next();
byte[] body = bodiesIterator.next(); byte[] body = bodiesIterator.next();
List<PushAttachmentData> attachments = attachmentsIterator.next(); int type = typesIterator.next();
OutgoingPushMessage message; messages.add(new OutgoingPushMessage(recipient, body, type));
if (!attachments.isEmpty()) {
List<PushAttachmentPointer> attachmentIds = sendAttachments(attachments);
message = new OutgoingPushMessage(recipient, body, attachmentIds, type);
} else {
message = new OutgoingPushMessage(recipient, body, type);
}
messages.add(message);
} }
sendMessage(new OutgoingPushMessageList(messages)); sendMessage(new OutgoingPushMessageList(messages));
@ -149,20 +139,7 @@ public class PushServiceSocket {
return PreKeyEntity.fromJson(responseText); return PreKeyEntity.fromJson(responseText);
} }
private List<PushAttachmentPointer> sendAttachments(List<PushAttachmentData> attachments) public long sendAttachment(PushAttachmentData attachment) throws IOException {
throws IOException
{
List<PushAttachmentPointer> attachmentIds = new LinkedList<PushAttachmentPointer>();
for (PushAttachmentData attachment : attachments) {
attachmentIds.add(new PushAttachmentPointer(attachment.getContentType(),
sendAttachment(attachment)));
}
return attachmentIds;
}
private String sendAttachment(PushAttachmentData attachment) throws IOException {
Pair<String, String> response = makeRequestForResponseHeader(String.format(ATTACHMENT_PATH, ""), Pair<String, String> response = makeRequestForResponseHeader(String.format(ATTACHMENT_PATH, ""),
"GET", null, "Content-Location"); "GET", null, "Content-Location");
@ -178,25 +155,18 @@ public class PushServiceSocket {
return new Gson().fromJson(response.second, AttachmentKey.class).getId(); return new Gson().fromJson(response.second, AttachmentKey.class).getId();
} }
public List<Pair<File,String>> retrieveAttachments(List<PushAttachmentPointer> attachmentIds) public File retrieveAttachment(long attachmentId) throws IOException {
throws IOException Pair<String, String> response = makeRequestForResponseHeader(String.format(ATTACHMENT_PATH, String.valueOf(attachmentId)),
{ "GET", null, "Content-Location");
List<Pair<File,String>> attachments = new LinkedList<Pair<File,String>>();
for (PushAttachmentPointer attachmentId : attachmentIds) { Log.w("PushServiceSocket", "Attachment: " + attachmentId + " is at: " + response.first);
Pair<String, String> response = makeRequestForResponseHeader(String.format(ATTACHMENT_PATH, attachmentId.getKey()),
"GET", null, "Content-Location");
Log.w("PushServiceSocket", "Attachment: " + attachmentId.getKey() + " is at: " + response.first); File attachment = File.createTempFile("attachment", ".tmp", context.getFilesDir());
attachment.deleteOnExit();
File attachment = File.createTempFile("attachment", ".tmp", context.getFilesDir()); downloadExternalFile(response.first, attachment);
attachment.deleteOnExit();
downloadExternalFile(response.first, attachment); return attachment;
attachments.add(new Pair<File, String>(attachment, attachmentId.getContentType()));
}
return attachments;
} }
public Pair<DirectoryDescriptor, File> retrieveDirectory() { public Pair<DirectoryDescriptor, File> retrieveDirectory() {
@ -394,13 +364,13 @@ public class PushServiceSocket {
} }
private static class AttachmentKey { private static class AttachmentKey {
private String id; private long id;
public AttachmentKey(String id) { public AttachmentKey(long id) {
this.id = id; this.id = id;
} }
public String getId() { public long getId() {
return id; return id;
} }
} }

View File

@ -45,6 +45,33 @@ public class Util {
} }
public static byte[][] split(byte[] input, int firstLength, int secondLength) {
byte[][] parts = new byte[2][];
parts[0] = new byte[firstLength];
System.arraycopy(input, 0, parts[0], 0, firstLength);
parts[1] = new byte[secondLength];
System.arraycopy(input, firstLength, parts[1], 0, secondLength);
return parts;
}
public static byte[][] split(byte[] input, int firstLength, int secondLength, int thirdLength) {
byte[][] parts = new byte[3][];
parts[0] = new byte[firstLength];
System.arraycopy(input, 0, parts[0], 0, firstLength);
parts[1] = new byte[secondLength];
System.arraycopy(input, firstLength, parts[1], 0, secondLength);
parts[2] = new byte[thirdLength];
System.arraycopy(input, firstLength + secondLength, parts[2], 0, thirdLength);
return parts;
}
public static boolean isEmpty(String value) { public static boolean isEmpty(String value) {
return value == null || value.trim().length() == 0; return value == null || value.trim().length() == 0;
} }
@ -94,6 +121,18 @@ public class Util {
return new String(bout.toByteArray()); return new String(bout.toByteArray());
} }
public static void readFully(InputStream in, byte[] buffer) throws IOException {
int offset = 0;
for (;;) {
int read = in.read(buffer, offset, buffer.length - offset);
if (read + offset < buffer.length) offset += read;
else return;
}
}
public static void copy(InputStream in, OutputStream out) throws IOException { public static void copy(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[4096]; byte[] buffer = new byte[4096];
int read; int read;

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/* //device/apps/common/res/drawable/status_icon_background.xml
**
** Copyright 2008, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
-->
<animation-list
xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item android:drawable="@drawable/stat_sys_download_anim0" android:duration="200" />
<item android:drawable="@drawable/stat_sys_download_anim1" android:duration="200" />
<item android:drawable="@drawable/stat_sys_download_anim2" android:duration="200" />
<item android:drawable="@drawable/stat_sys_download_anim3" android:duration="200" />
<item android:drawable="@drawable/stat_sys_download_anim4" android:duration="200" />
<item android:drawable="@drawable/stat_sys_download_anim5" android:duration="200" />
</animation-list>

View File

@ -17,6 +17,7 @@
package org.thoughtcrime.securesms.crypto; package org.thoughtcrime.securesms.crypto;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.util.Log; import android.util.Log;
@ -33,22 +34,26 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.PushDownloader;
import org.thoughtcrime.securesms.service.PushReceiver;
import org.thoughtcrime.securesms.service.SendReceiveService;
import org.thoughtcrime.securesms.sms.SmsTransportDetails; import org.thoughtcrime.securesms.sms.SmsTransportDetails;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.IdentityKeyPair; import org.whispersystems.textsecure.crypto.IdentityKeyPair;
import org.whispersystems.textsecure.crypto.InvalidKeyException; import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidMessageException; import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.InvalidVersionException; import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.crypto.MessageCipher;
import org.whispersystems.textsecure.crypto.SessionCipher;
import org.whispersystems.textsecure.util.Hex;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.WorkerThread;
import org.whispersystems.textsecure.crypto.KeyUtil; import org.whispersystems.textsecure.crypto.KeyUtil;
import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.MessageCipher;
import org.whispersystems.textsecure.crypto.SessionCipher;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.push.PushTransportDetails;
import org.whispersystems.textsecure.util.Hex;
import java.io.IOException; import java.io.IOException;
import java.util.LinkedList; import java.util.concurrent.Executor;
import java.util.List; import java.util.concurrent.Executors;
import ws.com.google.android.mms.ContentType; import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.MmsException; import ws.com.google.android.mms.MmsException;
@ -64,21 +69,13 @@ import ws.com.google.android.mms.pdu.RetrieveConf;
public class DecryptingQueue { public class DecryptingQueue {
private static final List<Runnable> workQueue = new LinkedList<Runnable>(); private static final Executor executor = Executors.newSingleThreadExecutor();
static {
Thread workerThread = new WorkerThread(workQueue, "Async Decryption Thread");
workerThread.start();
}
public static void scheduleDecryption(Context context, MasterSecret masterSecret, public static void scheduleDecryption(Context context, MasterSecret masterSecret,
long messageId, long threadId, MultimediaMessagePdu mms) long messageId, long threadId, MultimediaMessagePdu mms)
{ {
MmsDecryptionItem runnable = new MmsDecryptionItem(context, masterSecret, messageId, threadId, mms); MmsDecryptionItem runnable = new MmsDecryptionItem(context, masterSecret, messageId, threadId, mms);
synchronized (workQueue) { executor.execute(runnable);
workQueue.add(runnable);
workQueue.notifyAll();
}
} }
public static void scheduleDecryption(Context context, MasterSecret masterSecret, public static void scheduleDecryption(Context context, MasterSecret masterSecret,
@ -87,10 +84,15 @@ public class DecryptingQueue {
{ {
DecryptionWorkItem runnable = new DecryptionWorkItem(context, masterSecret, messageId, threadId, DecryptionWorkItem runnable = new DecryptionWorkItem(context, masterSecret, messageId, threadId,
originator, body, isSecureMessage, isKeyExchange); originator, body, isSecureMessage, isKeyExchange);
synchronized (workQueue) { executor.execute(runnable);
workQueue.add(runnable); }
workQueue.notifyAll();
} public static void scheduleDecryption(Context context, MasterSecret masterSecret,
long messageId, IncomingPushMessage message)
{
PushDecryptionWorkItem runnable = new PushDecryptionWorkItem(context, masterSecret,
messageId, message);
executor.execute(runnable);
} }
public static void schedulePendingDecrypts(Context context, MasterSecret masterSecret) { public static void schedulePendingDecrypts(Context context, MasterSecret masterSecret) {
@ -143,6 +145,59 @@ public class DecryptingQueue {
originator, body, isSecureMessage, isKeyExchange); originator, body, isSecureMessage, isKeyExchange);
} }
private static class PushDecryptionWorkItem implements Runnable {
private Context context;
private MasterSecret masterSecret;
private long messageId;
private IncomingPushMessage message;
public PushDecryptionWorkItem(Context context, MasterSecret masterSecret,
long messageId, IncomingPushMessage message)
{
this.context = context;
this.masterSecret = masterSecret;
this.messageId = messageId;
this.message = message;
}
public void run() {
synchronized (SessionCipher.CIPHER_LOCK) {
try {
Recipients recipients = RecipientFactory.getRecipientsFromString(context, message.getSource(), false);
Recipient recipient = recipients.getPrimaryRecipient();
if (!KeyUtil.isSessionFor(context, recipient)) {
sendResult(PushReceiver.RESULT_NO_SESSION);
return;
}
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
MessageCipher messageCipher = new MessageCipher(context, masterSecret, identityKey, new PushTransportDetails());
byte[] plaintextBody = messageCipher.decrypt(recipient, message.getBody());
message = message.withBody(plaintextBody);
sendResult(PushReceiver.RESULT_OK);
} catch (InvalidMessageException e) {
Log.w("DecryptionQueue", e);
sendResult(PushReceiver.RESULT_DECRYPT_FAILED);
} catch (RecipientFormattingException e) {
Log.w("DecryptionQueue", e);
sendResult(PushReceiver.RESULT_DECRYPT_FAILED);
}
}
}
private void sendResult(int result) {
Intent intent = new Intent(context, SendReceiveService.class);
intent.setAction(SendReceiveService.DECRYPTED_PUSH_ACTION);
intent.putExtra("message", message);
intent.putExtra("message_id", messageId);
intent.putExtra("result", result);
context.startService(intent);
}
}
private static class MmsDecryptionItem implements Runnable { private static class MmsDecryptionItem implements Runnable {
private long messageId; private long messageId;
private long threadId; private long threadId;
@ -267,13 +322,10 @@ public class DecryptingQueue {
synchronized (SessionCipher.CIPHER_LOCK) { synchronized (SessionCipher.CIPHER_LOCK) {
try { try {
Log.w("DecryptingQueue", "Parsing recipient for originator: " + originator);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, originator, false); Recipients recipients = RecipientFactory.getRecipientsFromString(context, originator, false);
Recipient recipient = recipients.getPrimaryRecipient(); Recipient recipient = recipients.getPrimaryRecipient();
Log.w("DecryptingQueue", "Parsed Recipient: " + recipient.getNumber());
if (!KeyUtil.isSessionFor(context, recipient)) { if (!KeyUtil.isSessionFor(context, recipient)) {
Log.w("DecryptingQueue", "No such recipient session...");
database.markAsNoSession(messageId); database.markAsNoSession(messageId);
return; return;
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -7,8 +7,6 @@ import android.util.Log;
import com.google.android.gcm.GCMBaseIntentService; import com.google.android.gcm.GCMBaseIntentService;
import org.thoughtcrime.securesms.service.RegistrationService; import org.thoughtcrime.securesms.service.RegistrationService;
import org.thoughtcrime.securesms.service.SendReceiveService; import org.thoughtcrime.securesms.service.SendReceiveService;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.SmsTransportDetails;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.InvalidVersionException; import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.push.IncomingEncryptedPushMessage; import org.whispersystems.textsecure.push.IncomingEncryptedPushMessage;
@ -17,7 +15,6 @@ import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.util.Util; import org.whispersystems.textsecure.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
public class GcmIntentService extends GCMBaseIntentService { public class GcmIntentService extends GCMBaseIntentService {
@ -61,8 +58,11 @@ public class GcmIntentService extends GCMBaseIntentService {
IncomingEncryptedPushMessage encryptedMessage = new IncomingEncryptedPushMessage(data, sessionKey); IncomingEncryptedPushMessage encryptedMessage = new IncomingEncryptedPushMessage(data, sessionKey);
IncomingPushMessage message = encryptedMessage.getIncomingPushMessage(); IncomingPushMessage message = encryptedMessage.getIncomingPushMessage();
if (!message.hasAttachments()) handleIncomingTextMessage(context, message); Intent service = new Intent(context, SendReceiveService.class);
else handleIncomingMediaMessage(context, message); service.setAction(SendReceiveService.RECEIVE_PUSH_ACTION);
service.putExtra("message", message);
context.startService(service);
} catch (IOException e) { } catch (IOException e) {
Log.w("GcmIntentService", e); Log.w("GcmIntentService", e);
} catch (InvalidVersionException e) { } catch (InvalidVersionException e) {
@ -75,25 +75,6 @@ public class GcmIntentService extends GCMBaseIntentService {
Log.w("GcmIntentService", "GCM Error: " + s); Log.w("GcmIntentService", "GCM Error: " + s);
} }
private void handleIncomingTextMessage(Context context, IncomingPushMessage message) {
ArrayList<IncomingTextMessage> messages = new ArrayList<IncomingTextMessage>();
String encodedBody = new String(new SmsTransportDetails().getEncodedMessage(message.getBody()));
messages.add(new IncomingTextMessage(message, encodedBody));
Intent receivedIntent = new Intent(context, SendReceiveService.class);
receivedIntent.setAction(SendReceiveService.RECEIVE_SMS_ACTION);
receivedIntent.putParcelableArrayListExtra("text_messages", messages);
receivedIntent.putExtra("push_type", message.getType());
context.startService(receivedIntent);
}
private void handleIncomingMediaMessage(Context context, IncomingPushMessage message) {
Intent receivedIntent = new Intent(context, SendReceiveService.class);
receivedIntent.setAction(SendReceiveService.RECEIVE_PUSH_MMS_ACTION);
receivedIntent.putExtra("media_message", message);
context.startService(receivedIntent);
}
private PushServiceSocket getGcmSocket(Context context) { private PushServiceSocket getGcmSocket(Context context) {
String localNumber = TextSecurePreferences.getLocalNumber(context); String localNumber = TextSecurePreferences.getLocalNumber(context);
String password = TextSecurePreferences.getPushServerPassword(context); String password = TextSecurePreferences.getPushServerPassword(context);

View File

@ -55,21 +55,21 @@ public class AttachmentManager {
public void setImage(Uri image) throws IOException, BitmapDecodingException { public void setImage(Uri image) throws IOException, BitmapDecodingException {
ImageSlide slide = new ImageSlide(context, image); ImageSlide slide = new ImageSlide(context, image);
slideDeck.addSlide(slide); slideDeck.addSlide(slide);
thumbnail.setImageBitmap(slide.getThumbnail(345, 261)); thumbnail.setImageDrawable(slide.getThumbnail(345, 261));
attachmentView.setVisibility(View.VISIBLE); attachmentView.setVisibility(View.VISIBLE);
} }
public void setVideo(Uri video) throws IOException, MediaTooLargeException { public void setVideo(Uri video) throws IOException, MediaTooLargeException {
VideoSlide slide = new VideoSlide(context, video); VideoSlide slide = new VideoSlide(context, video);
slideDeck.addSlide(slide); slideDeck.addSlide(slide);
thumbnail.setImageBitmap(slide.getThumbnail(thumbnail.getWidth(), thumbnail.getHeight())); thumbnail.setImageDrawable(slide.getThumbnail(thumbnail.getWidth(), thumbnail.getHeight()));
attachmentView.setVisibility(View.VISIBLE); attachmentView.setVisibility(View.VISIBLE);
} }
public void setAudio(Uri audio)throws IOException, MediaTooLargeException { public void setAudio(Uri audio)throws IOException, MediaTooLargeException {
AudioSlide slide = new AudioSlide(context, audio); AudioSlide slide = new AudioSlide(context, audio);
slideDeck.addSlide(slide); slideDeck.addSlide(slide);
thumbnail.setImageBitmap(slide.getThumbnail(thumbnail.getWidth(), thumbnail.getHeight())); thumbnail.setImageDrawable(slide.getThumbnail(thumbnail.getWidth(), thumbnail.getHeight()));
attachmentView.setVisibility(View.VISIBLE); attachmentView.setVisibility(View.VISIBLE);
} }

View File

@ -25,6 +25,7 @@ import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.provider.MediaStore.Audio; import android.provider.MediaStore.Audio;
import android.widget.ImageView; import android.widget.ImageView;
@ -50,8 +51,8 @@ public class AudioSlide extends Slide {
} }
@Override @Override
public Bitmap getThumbnail(int maxWidth, int maxHeight) { public Drawable getThumbnail(int maxWidth, int maxHeight) {
return BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_menu_add_sound); return context.getResources().getDrawable(R.drawable.ic_menu_add_sound);
} }
public static PduPart constructPartFromUri(Context context, Uri uri) throws IOException, MediaTooLargeException { public static PduPart constructPartFromUri(Context context, Uri uri) throws IOException, MediaTooLargeException {

View File

@ -17,9 +17,8 @@
package org.thoughtcrime.securesms.mms; package org.thoughtcrime.securesms.mms;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
@ -30,11 +29,11 @@ import android.util.Log;
import android.widget.ImageView; import android.widget.ImageView;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.LRUCache; import org.thoughtcrime.securesms.util.LRUCache;
import org.whispersystems.textsecure.crypto.MasterSecret;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
@ -50,8 +49,8 @@ import ws.com.google.android.mms.pdu.PduPart;
public class ImageSlide extends Slide { public class ImageSlide extends Slide {
private static final int MAX_CACHE_SIZE = 10; private static final int MAX_CACHE_SIZE = 10;
private static final Map<Uri, SoftReference<Bitmap>> thumbnailCache = private static final Map<Uri, SoftReference<Drawable>> thumbnailCache =
Collections.synchronizedMap(new LRUCache<Uri, SoftReference<Bitmap>>(MAX_CACHE_SIZE)); Collections.synchronizedMap(new LRUCache<Uri, SoftReference<Drawable>>(MAX_CACHE_SIZE));
public ImageSlide(Context context, MasterSecret masterSecret, PduPart part) { public ImageSlide(Context context, MasterSecret masterSecret, PduPart part) {
super(context, masterSecret, part); super(context, masterSecret, part);
@ -62,32 +61,37 @@ public class ImageSlide extends Slide {
} }
@Override @Override
public Bitmap getThumbnail(int maxWidth, int maxHeight) { public Drawable getThumbnail(int maxWidth, int maxHeight) {
Bitmap thumbnail = getCachedThumbnail(); Drawable thumbnail = getCachedThumbnail();
if (thumbnail != null) if (thumbnail != null) {
return thumbnail; return thumbnail;
}
if (part.isPendingPush()) {
return context.getResources().getDrawable(R.drawable.stat_sys_download);
}
try { try {
InputStream measureStream = getPartDataInputStream(); InputStream measureStream = getPartDataInputStream();
InputStream dataStream = getPartDataInputStream(); InputStream dataStream = getPartDataInputStream();
thumbnail = BitmapUtil.createScaledBitmap(measureStream, dataStream, maxWidth, maxHeight); thumbnail = new BitmapDrawable(context.getResources(), BitmapUtil.createScaledBitmap(measureStream, dataStream, maxWidth, maxHeight));
thumbnailCache.put(part.getDataUri(), new SoftReference<Bitmap>(thumbnail)); thumbnailCache.put(part.getDataUri(), new SoftReference<Drawable>(thumbnail));
return thumbnail; return thumbnail;
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
Log.w("ImageSlide", e); Log.w("ImageSlide", e);
return BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_missing_thumbnail_picture); return context.getResources().getDrawable(R.drawable.ic_missing_thumbnail_picture);
} catch (BitmapDecodingException e) { } catch (BitmapDecodingException e) {
Log.w("ImageSlide", e); Log.w("ImageSlide", e);
return BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_missing_thumbnail_picture); return context.getResources().getDrawable(R.drawable.ic_missing_thumbnail_picture);
} }
} }
@Override @Override
public void setThumbnailOn(ImageView imageView) { public void setThumbnailOn(ImageView imageView) {
Bitmap thumbnail = getCachedThumbnail(); Drawable thumbnail = getCachedThumbnail();
if (thumbnail != null) { if (thumbnail != null) {
Log.w("ImageSlide", "Setting cached thumbnail..."); Log.w("ImageSlide", "Setting cached thumbnail...");
@ -109,8 +113,9 @@ public class ImageSlide extends Slide {
MmsDatabase.slideResolver.execute(new Runnable() { MmsDatabase.slideResolver.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
final Bitmap bitmap = getThumbnail(maxWidth, maxHeight); final Drawable bitmap = getThumbnail(maxWidth, maxHeight);
final ImageView destination = weakImageView.get(); final ImageView destination = weakImageView.get();
if (destination != null && destination.getDrawable() == temporaryDrawable) { if (destination != null && destination.getDrawable() == temporaryDrawable) {
handler.post(new Runnable() { handler.post(new Runnable() {
@Override @Override
@ -123,24 +128,26 @@ public class ImageSlide extends Slide {
}); });
} }
private void setThumbnailOn(ImageView imageView, Bitmap thumbnail, boolean fromMemory) { private void setThumbnailOn(ImageView imageView, Drawable thumbnail, boolean fromMemory) {
if (fromMemory) { if (fromMemory) {
imageView.setImageBitmap(thumbnail); imageView.setImageDrawable(thumbnail);
} else if (thumbnail instanceof AnimationDrawable) {
imageView.setImageDrawable(thumbnail);
((AnimationDrawable)imageView.getDrawable()).start();
} else { } else {
BitmapDrawable result = new BitmapDrawable(context.getResources(), thumbnail); TransitionDrawable fadingResult = new TransitionDrawable(new Drawable[]{new ColorDrawable(Color.TRANSPARENT), thumbnail});
TransitionDrawable fadingResult = new TransitionDrawable(new Drawable[]{new ColorDrawable(Color.TRANSPARENT), result});
imageView.setImageDrawable(fadingResult); imageView.setImageDrawable(fadingResult);
fadingResult.startTransition(300); fadingResult.startTransition(300);
} }
} }
private Bitmap getCachedThumbnail() { private Drawable getCachedThumbnail() {
synchronized (thumbnailCache) { synchronized (thumbnailCache) {
SoftReference<Bitmap> bitmapReference = thumbnailCache.get(part.getDataUri()); SoftReference<Drawable> bitmapReference = thumbnailCache.get(part.getDataUri());
Log.w("ImageSlide", "Got soft reference: " + bitmapReference); Log.w("ImageSlide", "Got soft reference: " + bitmapReference);
if (bitmapReference != null) { if (bitmapReference != null) {
Bitmap bitmap = bitmapReference.get(); Drawable bitmap = bitmapReference.get();
Log.w("ImageSlide", "Got cached bitmap: " + bitmap); Log.w("ImageSlide", "Got cached bitmap: " + bitmap);
if (bitmap != null) return bitmap; if (bitmap != null) return bitmap;
else thumbnailCache.remove(part.getDataUri()); else thumbnailCache.remove(part.getDataUri());

View File

@ -1,15 +1,13 @@
package org.thoughtcrime.securesms.mms; package org.thoughtcrime.securesms.mms;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.push.IncomingPushMessage; import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
import org.whispersystems.textsecure.util.Base64;
import java.io.File; import java.io.UnsupportedEncodingException;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.List;
import ws.com.google.android.mms.pdu.CharacterSets; import ws.com.google.android.mms.pdu.CharacterSets;
import ws.com.google.android.mms.pdu.EncodedStringValue; import ws.com.google.android.mms.pdu.EncodedStringValue;
@ -28,9 +26,9 @@ public class IncomingMediaMessage {
this.body = retreived.getBody(); this.body = retreived.getBody();
} }
public IncomingMediaMessage(String localNumber, IncomingPushMessage message, public IncomingMediaMessage(MasterSecret masterSecret, String localNumber,
List<Pair<File, String>> attachments) IncomingPushMessage message,
throws IOException PushMessageContent messageContent)
{ {
this.headers = new PduHeaders(); this.headers = new PduHeaders();
this.body = new PduBody(); this.body = new PduBody();
@ -39,32 +37,29 @@ public class IncomingMediaMessage {
this.headers.appendEncodedStringValue(new EncodedStringValue(localNumber), PduHeaders.TO); this.headers.appendEncodedStringValue(new EncodedStringValue(localNumber), PduHeaders.TO);
for (String destination : message.getDestinations()) { for (String destination : message.getDestinations()) {
if (!destination.equals(localNumber)) { this.headers.appendEncodedStringValue(new EncodedStringValue(destination), PduHeaders.CC);
this.headers.appendEncodedStringValue(new EncodedStringValue(destination), PduHeaders.CC);
}
} }
this.headers.setLongInteger(message.getTimestampMillis() / 1000, PduHeaders.DATE); this.headers.setLongInteger(message.getTimestampMillis() / 1000, PduHeaders.DATE);
if (message.getBody() != null && message.getBody().length > 0) { if (messageContent.getBody() != null && messageContent.getBody().length() > 0) {
PduPart text = new PduPart(); PduPart text = new PduPart();
text.setData(message.getBody()); text.setData(Util.toIsoBytes(messageContent.getBody()));
text.setContentType("text/plain".getBytes(CharacterSets.MIMENAME_ISO_8859_1)); text.setContentType(Util.toIsoBytes("text/plain"));
body.addPart(text); body.addPart(text);
} }
if (attachments != null) { if (messageContent.getAttachmentsCount() > 0) {
for (Pair<File, String> attachment : attachments) { for (PushMessageContent.AttachmentPointer attachment : messageContent.getAttachmentsList()) {
PduPart media = new PduPart(); PduPart media = new PduPart();
FileInputStream fin = new FileInputStream(attachment.first); byte[] encryptedKey = new MasterCipher(masterSecret).encryptBytes(attachment.getKey().toByteArray());
byte[] data = Util.readFully(fin);
Log.w("IncomingMediaMessage", "Adding part: " + attachment.second + " with length: " + data.length); media.setContentType(Util.toIsoBytes(attachment.getContentType()));
media.setContentLocation(Util.toIsoBytes(String.valueOf(attachment.getId())));
media.setContentDisposition(Util.toIsoBytes(Base64.encodeBytes(encryptedKey)));
media.setPendingPush(true);
media.setContentType(attachment.second.getBytes(CharacterSets.MIMENAME_ISO_8859_1));
media.setData(data);
body.addPart(media); body.addPart(media);
attachment.first.delete();
} }
} }
} }

View File

@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.providers.PartProvider;
import android.content.ContentUris; import android.content.ContentUris;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
import android.widget.ImageView; import android.widget.ImageView;
@ -90,12 +91,12 @@ public abstract class Slide {
return part.getDataUri(); return part.getDataUri();
} }
public Bitmap getThumbnail(int maxWidth, int maxHeight) { public Drawable getThumbnail(int maxWidth, int maxHeight) {
throw new AssertionError("getThumbnail() called on non-thumbnail producing slide!"); throw new AssertionError("getThumbnail() called on non-thumbnail producing slide!");
} }
public void setThumbnailOn(ImageView imageView) { public void setThumbnailOn(ImageView imageView) {
imageView.setImageBitmap(getThumbnail(imageView.getWidth(), imageView.getHeight())); imageView.setImageDrawable(getThumbnail(imageView.getWidth(), imageView.getHeight()));
} }
public boolean hasImage() { public boolean hasImage() {

View File

@ -26,6 +26,7 @@ import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.util.Log; import android.util.Log;
@ -42,8 +43,8 @@ public class VideoSlide extends Slide {
} }
@Override @Override
public Bitmap getThumbnail(int width, int height) { public Drawable getThumbnail(int width, int height) {
return BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_launcher_video_player); return context.getResources().getDrawable(R.drawable.ic_launcher_video_player);
} }
@Override @Override

View File

@ -48,14 +48,8 @@ public class MmsReceiver {
} }
public void process(MasterSecret masterSecret, Intent intent) { public void process(MasterSecret masterSecret, Intent intent) {
try { if (intent.getAction().equals(SendReceiveService.RECEIVE_MMS_ACTION)) {
if (intent.getAction().equals(SendReceiveService.RECEIVE_MMS_ACTION)) { handleMmsNotification(intent);
handleMmsNotification(intent);
} else if (intent.getAction().equals(SendReceiveService.RECEIVE_PUSH_MMS_ACTION)) {
handlePushMedia(masterSecret, intent);
}
} catch (MmsException e) {
Log.w("MmsReceiver", e);
} }
} }
@ -73,28 +67,6 @@ public class MmsReceiver {
} }
} }
private void handlePushMedia(MasterSecret masterSecret, Intent intent) throws MmsException {
IncomingPushMessage pushMessage = intent.getParcelableExtra("media_message");
String localNumber = TextSecurePreferences.getLocalNumber(context);
String password = TextSecurePreferences.getPushServerPassword(context);
PushServiceSocket socket = new PushServiceSocket(context, localNumber, password);
try {
List<Pair<File, String>> attachments = socket.retrieveAttachments(pushMessage.getAttachments());
IncomingMediaMessage message = new IncomingMediaMessage(localNumber, pushMessage, attachments);
DatabaseFactory.getMmsDatabase(context).insertMessageInbox(masterSecret, message, "", -1);
} catch (IOException e) {
Log.w("MmsReceiver", e);
try {
IncomingMediaMessage message = new IncomingMediaMessage(localNumber, pushMessage, null);
DatabaseFactory.getMmsDatabase(context).insertMessageInbox(masterSecret, message, "", -1);
} catch (IOException e1) {
throw new MmsException(e1);
}
}
}
private void scheduleDownload(NotificationInd pdu, long messageId, long threadId) { private void scheduleDownload(NotificationInd pdu, long messageId, long threadId) {
Intent intent = new Intent(SendReceiveService.DOWNLOAD_MMS_ACTION, null, context, SendReceiveService.class); Intent intent = new Intent(SendReceiveService.DOWNLOAD_MMS_ACTION, null, context, SendReceiveService.class);
intent.putExtra("content_location", new String(pdu.getContentLocation())); intent.putExtra("content_location", new String(pdu.getContentLocation()));

View File

@ -0,0 +1,107 @@
package org.thoughtcrime.securesms.service;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingPartDatabase;
import org.thoughtcrime.securesms.database.PartDatabase;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.crypto.AttachmentCipherInputStream;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.util.Base64;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.PduPart;
public class PushDownloader {
private final Context context;
public PushDownloader(Context context) {
this.context = context.getApplicationContext();
}
public void process(MasterSecret masterSecret, Intent intent) {
if (!intent.getAction().equals(SendReceiveService.DOWNLOAD_PUSH_ACTION))
return;
long messageId = intent.getLongExtra("message_id", -1);
PartDatabase database = DatabaseFactory.getEncryptingPartDatabase(context, masterSecret);
Log.w("PushDownloader", "Downloading push parts for: " + messageId);
if (messageId != -1) {
List<Pair<Long, PduPart>> parts = database.getParts(messageId, false);
for (Pair<Long, PduPart> partPair : parts) {
retrievePart(masterSecret, partPair.second, messageId, partPair.first);
Log.w("PushDownloader", "Got part: " + partPair.first);
}
} else {
List<Pair<Long, Pair<Long, PduPart>>> parts = database.getPushPendingParts();
for (Pair<Long, Pair<Long, PduPart>> partPair : parts) {
retrievePart(masterSecret, partPair.second.second, partPair.first, partPair.second.first);
Log.w("PushDownloader", "Got part: " + partPair.second.first);
}
}
}
private void retrievePart(MasterSecret masterSecret, PduPart part, long messageId, long partId) {
EncryptingPartDatabase database = DatabaseFactory.getEncryptingPartDatabase(context, masterSecret);
File attachmentFile = null;
try {
MasterCipher masterCipher = new MasterCipher(masterSecret);
long contentLocation = Long.parseLong(Util.toIsoString(part.getContentLocation()));
byte[] key = masterCipher.decryptBytes(Base64.decode(Util.toIsoString(part.getContentDisposition())));
attachmentFile = downloadAttachment(contentLocation);
InputStream attachmentInput = new AttachmentCipherInputStream(attachmentFile, key);
database.updateDownloadedPart(messageId, partId, part, attachmentInput);
} catch (InvalidMessageException e) {
Log.w("PushDownloader", e);
try {
database.updateFailedDownloadedPart(messageId, partId, part);
} catch (MmsException mme) {
Log.w("PushDownloader", mme);
}
} catch (MmsException e) {
Log.w("PushDownloader", e);
try {
database.updateFailedDownloadedPart(messageId, partId, part);
} catch (MmsException mme) {
Log.w("PushDownloader", mme);
}
} catch (IOException e) {
Log.w("PushDownloader", e);
/// XXX schedule some kind of soft failure retry action
} finally {
if (attachmentFile != null)
attachmentFile.delete();
}
}
private File downloadAttachment(long contentLocation) throws IOException {
String localNumber = TextSecurePreferences.getLocalNumber(context);
String password = TextSecurePreferences.getPushServerPassword(context);
PushServiceSocket socket = new PushServiceSocket(context, localNumber, password);
return socket.retrieveAttachment(contentLocation);
}
}

View File

@ -0,0 +1,211 @@
package org.thoughtcrime.securesms.service;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.util.Pair;
import com.google.protobuf.InvalidProtocolBufferException;
import org.thoughtcrime.securesms.crypto.DecryptingQueue;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.protocol.PreKeyBundleMessage;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
import org.whispersystems.textsecure.storage.InvalidKeyIdException;
import ws.com.google.android.mms.MmsException;
public class PushReceiver {
public static final int RESULT_OK = 0;
public static final int RESULT_NO_SESSION = 1;
public static final int RESULT_DECRYPT_FAILED = 2;
private final Context context;
public PushReceiver(Context context) {
this.context = context.getApplicationContext();
}
public void process(MasterSecret masterSecret, Intent intent) {
if (intent.getAction().equals(SendReceiveService.RECEIVE_PUSH_ACTION)) {
handleMessage(masterSecret, intent);
} else if (intent.getAction().equals(SendReceiveService.DECRYPTED_PUSH_ACTION)) {
handleDecrypt(masterSecret, intent);
}
}
private void handleDecrypt(MasterSecret masterSecret, Intent intent) {
IncomingPushMessage message = intent.getParcelableExtra("message");
long messageId = intent.getLongExtra("message_id", -1);
int result = intent.getIntExtra("result", 0);
if (result == RESULT_OK) handleReceivedMessage(masterSecret, message, true);
else if (result == RESULT_NO_SESSION) handleReceivedMessageForNoSession(masterSecret, message);
else if (result == RESULT_DECRYPT_FAILED) handleReceivedCorruptedMessage(masterSecret, message, true);
DatabaseFactory.getPushDatabase(context).delete(messageId);
}
private void handleMessage(MasterSecret masterSecret, Intent intent) {
IncomingPushMessage message = intent.getExtras().getParcelable("message");
if (message.isSecureMessage()) handleReceivedSecureMessage(masterSecret, message);
else if (message.isPreKeyBundle()) handleReceivedPreKeyBundle(masterSecret, message);
else handleReceivedMessage(masterSecret, message, false);
}
private void handleReceivedSecureMessage(MasterSecret masterSecret, IncomingPushMessage message) {
long id = DatabaseFactory.getPushDatabase(context).insert(message);
DecryptingQueue.scheduleDecryption(context, masterSecret, id, message);
}
private void handleReceivedPreKeyBundle(MasterSecret masterSecret, IncomingPushMessage message) {
try {
Recipient recipient = new Recipient(null, message.getSource(), null, null);
KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, recipient);
PreKeyBundleMessage preKeyExchange = new PreKeyBundleMessage(message.getBody());
if (processor.isTrusted(preKeyExchange)) {
processor.processKeyExchangeMessage(preKeyExchange);
IncomingPushMessage bundledMessage = message.withBody(preKeyExchange.getBundledMessage());
handleReceivedSecureMessage(masterSecret, bundledMessage);
} else {
/// XXX
}
} catch (InvalidKeyException e) {
Log.w("SmsReceiver", e);
handleReceivedCorruptedKey(masterSecret, message, false);
} catch (InvalidVersionException e) {
Log.w("SmsReceiver", e);
handleReceivedCorruptedKey(masterSecret, message, true);
} catch (InvalidKeyIdException e) {
Log.w("SmsReceiver", e);
handleReceivedCorruptedKey(masterSecret, message, false);
}
}
private void handleReceivedMessage(MasterSecret masterSecret,
IncomingPushMessage message,
boolean secure)
{
try {
PushMessageContent messageContent = PushMessageContent.parseFrom(message.getBody());
if (messageContent.getAttachmentsCount() > 0) {
Log.w("PushReceiver", "Received push media message...");
handleReceivedMediaMessage(masterSecret, message, messageContent, secure);
} else {
Log.w("PushReceiver", "Received push text message...");
handleReceivedTextMessage(masterSecret, message, messageContent, secure);
}
} catch (InvalidProtocolBufferException e) {
Log.w("PushReceiver", e);
handleReceivedCorruptedMessage(masterSecret, message, secure);
}
}
private void handleReceivedMediaMessage(MasterSecret masterSecret,
IncomingPushMessage message,
PushMessageContent messageContent,
boolean secure)
{
try {
String localNumber = TextSecurePreferences.getLocalNumber(context);
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterSecret, localNumber,
message, messageContent);
Pair<Long, Long> messageAndThreadId;
if (secure) {
messageAndThreadId = database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1);
} else {
messageAndThreadId = database.insertMessageInbox(masterSecret, mediaMessage, null, -1);
}
Intent intent = new Intent(context, SendReceiveService.class);
intent.setAction(SendReceiveService.DOWNLOAD_PUSH_ACTION);
intent.putExtra("message_id", messageAndThreadId.first);
context.startService(intent);
} catch (MmsException e) {
Log.w("PushReceiver", e);
// XXX
}
}
private void handleReceivedTextMessage(MasterSecret masterSecret,
IncomingPushMessage message,
PushMessageContent messageContent,
boolean secure)
{
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
IncomingTextMessage textMessage = new IncomingTextMessage(message, "");
if (secure) {
textMessage = new IncomingEncryptedMessage(textMessage, "");
}
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox(masterSecret, textMessage);
database.updateMessageBody(masterSecret, messageAndThreadId.first, messageContent.getBody());
}
private void handleReceivedCorruptedMessage(MasterSecret masterSecret,
IncomingPushMessage message,
boolean secure)
{
long messageId = insertMessagePlaceholder(masterSecret, message, secure);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsDecryptFailed(messageId);
}
private void handleReceivedCorruptedKey(MasterSecret masterSecret,
IncomingPushMessage message,
boolean invalidVersion)
{
IncomingTextMessage corruptedMessage = new IncomingTextMessage(message, "");
IncomingKeyExchangeMessage corruptedKeyMessage = new IncomingKeyExchangeMessage(corruptedMessage, "");
if (!invalidVersion) corruptedKeyMessage.setCorrupted(true);
else corruptedKeyMessage.setInvalidVersion(true);
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, corruptedKeyMessage);
}
private void handleReceivedMessageForNoSession(MasterSecret masterSecret,
IncomingPushMessage message)
{
long messageId = insertMessagePlaceholder(masterSecret, message, true);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsNoSession(messageId);
}
private long insertMessagePlaceholder(MasterSecret masterSecret,
IncomingPushMessage message,
boolean secure)
{
IncomingTextMessage placeholder = new IncomingTextMessage(message, "");
if (secure) {
placeholder = new IncomingEncryptedMessage(placeholder, "");
}
Pair<Long, Long> messageAndThreadId = DatabaseFactory.getEncryptingSmsDatabase(context)
.insertMessageInbox(masterSecret,
placeholder);
return messageAndThreadId.first;
}
}

View File

@ -51,10 +51,12 @@ public class SendReceiveService extends Service {
public static final String RECEIVE_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_SMS_ACTION"; public static final String RECEIVE_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_SMS_ACTION";
public static final String SEND_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SEND_MMS_ACTION"; public static final String SEND_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SEND_MMS_ACTION";
public static final String RECEIVE_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_MMS_ACTION"; public static final String RECEIVE_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_MMS_ACTION";
public static final String RECEIVE_PUSH_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_PUSH_MMS_ACTION";
public static final String DOWNLOAD_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_ACTION"; public static final String DOWNLOAD_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_ACTION";
public static final String DOWNLOAD_MMS_CONNECTIVITY_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_CONNECTIVITY_ACTION"; public static final String DOWNLOAD_MMS_CONNECTIVITY_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_CONNECTIVITY_ACTION";
public static final String DOWNLOAD_MMS_PENDING_APN_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_PENDING_APN_ACTION"; public static final String DOWNLOAD_MMS_PENDING_APN_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_PENDING_APN_ACTION";
public static final String RECEIVE_PUSH_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_PUSH_ACTION";
public static final String DECRYPTED_PUSH_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DECRYPTED_PUSH_ACTION";
public static final String DOWNLOAD_PUSH_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_PUSH_ACTION";
private static final int SEND_SMS = 0; private static final int SEND_SMS = 0;
private static final int RECEIVE_SMS = 1; private static final int RECEIVE_SMS = 1;
@ -62,14 +64,18 @@ public class SendReceiveService extends Service {
private static final int RECEIVE_MMS = 3; private static final int RECEIVE_MMS = 3;
private static final int DOWNLOAD_MMS = 4; private static final int DOWNLOAD_MMS = 4;
private static final int DOWNLOAD_MMS_PENDING = 5; private static final int DOWNLOAD_MMS_PENDING = 5;
private static final int RECEIVE_PUSH = 6;
private static final int DOWNLOAD_PUSH = 7;
private ToastHandler toastHandler; private ToastHandler toastHandler;
private SmsReceiver smsReceiver; private SmsReceiver smsReceiver;
private SmsSender smsSender; private SmsSender smsSender;
private MmsReceiver mmsReceiver; private MmsReceiver mmsReceiver;
private MmsSender mmsSender; private MmsSender mmsSender;
private MmsDownloader mmsDownloader; private MmsDownloader mmsDownloader;
private PushReceiver pushReceiver;
private PushDownloader pushDownloader;
private MasterSecret masterSecret; private MasterSecret masterSecret;
private boolean hasSecret; private boolean hasSecret;
@ -78,7 +84,6 @@ public class SendReceiveService extends Service {
private ClearKeyReceiver clearKeyReceiver; private ClearKeyReceiver clearKeyReceiver;
private List<Runnable> workQueue; private List<Runnable> workQueue;
private List<Runnable> pendingSecretList; private List<Runnable> pendingSecretList;
private Thread workerThread;
@Override @Override
public void onCreate() { public void onCreate() {
@ -105,12 +110,18 @@ public class SendReceiveService extends Service {
scheduleIntent(SEND_SMS, intent); scheduleIntent(SEND_SMS, intent);
else if (action.equals(SEND_MMS_ACTION)) else if (action.equals(SEND_MMS_ACTION))
scheduleSecretRequiredIntent(SEND_MMS, intent); scheduleSecretRequiredIntent(SEND_MMS, intent);
else if (action.equals(RECEIVE_MMS_ACTION) || action.equals(RECEIVE_PUSH_MMS_ACTION)) else if (action.equals(RECEIVE_MMS_ACTION))
scheduleIntent(RECEIVE_MMS, intent); scheduleIntent(RECEIVE_MMS, intent);
else if (action.equals(DOWNLOAD_MMS_ACTION)) else if (action.equals(DOWNLOAD_MMS_ACTION))
scheduleSecretRequiredIntent(DOWNLOAD_MMS, intent); scheduleSecretRequiredIntent(DOWNLOAD_MMS, intent);
else if (intent.getAction().equals(DOWNLOAD_MMS_PENDING_APN_ACTION)) else if (intent.getAction().equals(DOWNLOAD_MMS_PENDING_APN_ACTION))
scheduleSecretRequiredIntent(DOWNLOAD_MMS_PENDING, intent); scheduleSecretRequiredIntent(DOWNLOAD_MMS_PENDING, intent);
else if (action.equals(RECEIVE_PUSH_ACTION))
scheduleIntent(RECEIVE_PUSH, intent);
else if (action.equals(DECRYPTED_PUSH_ACTION))
scheduleSecretRequiredIntent(RECEIVE_PUSH, intent);
else if (action.equals(DOWNLOAD_PUSH_ACTION))
scheduleSecretRequiredIntent(DOWNLOAD_PUSH, intent);
else else
Log.w("SendReceiveService", "Received intent with unknown action: " + intent.getAction()); Log.w("SendReceiveService", "Received intent with unknown action: " + intent.getAction());
} }
@ -142,13 +153,15 @@ public class SendReceiveService extends Service {
mmsReceiver = new MmsReceiver(this); mmsReceiver = new MmsReceiver(this);
mmsSender = new MmsSender(this, toastHandler); mmsSender = new MmsSender(this, toastHandler);
mmsDownloader = new MmsDownloader(this, toastHandler); mmsDownloader = new MmsDownloader(this, toastHandler);
pushReceiver = new PushReceiver(this);
pushDownloader = new PushDownloader(this);
} }
private void initializeWorkQueue() { private void initializeWorkQueue() {
pendingSecretList = new LinkedList<Runnable>(); pendingSecretList = new LinkedList<Runnable>();
workQueue = new LinkedList<Runnable>(); workQueue = new LinkedList<Runnable>();
workerThread = new WorkerThread(workQueue, "SendReceveService-WorkerThread");
Thread workerThread = new WorkerThread(workQueue, "SendReceveService-WorkerThread");
workerThread.start(); workerThread.start();
} }
@ -222,12 +235,14 @@ public class SendReceiveService extends Service {
@Override @Override
public void run() { public void run() {
switch (what) { switch (what) {
case RECEIVE_SMS: smsReceiver.process(masterSecret, intent); return; case RECEIVE_SMS: smsReceiver.process(masterSecret, intent); return;
case SEND_SMS: smsSender.process(masterSecret, intent); return; case SEND_SMS: smsSender.process(masterSecret, intent); return;
case RECEIVE_MMS: mmsReceiver.process(masterSecret, intent); return; case RECEIVE_MMS: mmsReceiver.process(masterSecret, intent); return;
case SEND_MMS: mmsSender.process(masterSecret, intent); return; case SEND_MMS: mmsSender.process(masterSecret, intent); return;
case DOWNLOAD_MMS: mmsDownloader.process(masterSecret, intent); return; case DOWNLOAD_MMS: mmsDownloader.process(masterSecret, intent); return;
case DOWNLOAD_MMS_PENDING: mmsDownloader.process(masterSecret, intent); return; case DOWNLOAD_MMS_PENDING: mmsDownloader.process(masterSecret, intent); return;
case RECEIVE_PUSH: pushReceiver.process(masterSecret, intent); return;
case DOWNLOAD_PUSH: pushDownloader.process(masterSecret, intent); return;
} }
} }
} }

View File

@ -4,11 +4,18 @@ import android.content.Context;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.mms.PartParser; import org.thoughtcrime.securesms.mms.PartParser;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.whispersystems.textsecure.crypto.AttachmentCipher;
import org.whispersystems.textsecure.push.PushAttachmentPointer;
import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
import org.whispersystems.textsecure.push.RawTransportDetails; import org.whispersystems.textsecure.push.RawTransportDetails;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
@ -24,6 +31,7 @@ import org.whispersystems.textsecure.push.PushAttachmentData;
import org.whispersystems.textsecure.push.PushServiceSocket; import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.PushTransportDetails; import org.whispersystems.textsecure.push.PushTransportDetails;
import org.whispersystems.textsecure.push.RateLimitException; import org.whispersystems.textsecure.push.RateLimitException;
import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.PhoneNumberFormatter; import org.whispersystems.textsecure.util.PhoneNumberFormatter;
import java.io.IOException; import java.io.IOException;
@ -50,10 +58,12 @@ public class PushTransport extends BaseTransport {
String password = TextSecurePreferences.getPushServerPassword(context); String password = TextSecurePreferences.getPushServerPassword(context);
PushServiceSocket socket = new PushServiceSocket(context, localNumber, password); PushServiceSocket socket = new PushServiceSocket(context, localNumber, password);
Recipient recipient = message.getIndividualRecipient(); Recipient recipient = message.getIndividualRecipient();
String plaintext = message.getBody().getBody(); String plaintextBody = message.getBody().getBody();
String recipientCanonicalNumber = PhoneNumberFormatter.formatNumber(recipient.getNumber(), PushMessageContent.Builder builder = PushMessageContent.newBuilder();
localNumber); byte[] plaintext = builder.setBody(plaintextBody).build().toByteArray();
String recipientCanonicalNumber = PhoneNumberFormatter.formatNumber(recipient.getNumber(),
localNumber);
Pair<Integer, byte[]> typeAndCiphertext = getEncryptedMessage(socket, recipient, recipientCanonicalNumber, plaintext); Pair<Integer, byte[]> typeAndCiphertext = getEncryptedMessage(socket, recipient, recipientCanonicalNumber, plaintext);
@ -68,39 +78,70 @@ public class PushTransport extends BaseTransport {
public void deliver(SendReq message, List<String> destinations) throws IOException { public void deliver(SendReq message, List<String> destinations) throws IOException {
try { try {
String localNumber = TextSecurePreferences.getLocalNumber(context); String localNumber = TextSecurePreferences.getLocalNumber(context);
String password = TextSecurePreferences.getPushServerPassword(context); String password = TextSecurePreferences.getPushServerPassword(context);
PushServiceSocket socket = new PushServiceSocket(context, localNumber, password); PushServiceSocket socket = new PushServiceSocket(context, localNumber, password);
byte[] messageText = PartParser.getMessageText(message.getBody()).getBytes(); String messageBody = PartParser.getMessageText(message.getBody());
List<PushAttachmentData> attachments = getAttachmentsFromBody(message.getBody()); List<byte[]> ciphertext = new LinkedList<byte[]> ();
List<Integer> types = new LinkedList<Integer>();
List<byte[]> messagesList = new LinkedList<byte[]>(); for (String destination : destinations) {
List<List<PushAttachmentData>> attachmentsList = new LinkedList<List<PushAttachmentData>>(); Recipients recipients = RecipientFactory.getRecipientsFromString(context, destination, false);
List<PushAttachmentPointer> attachments = getPushAttachmentPointers(socket, message.getBody());
PushMessageContent.Builder builder = PushMessageContent.newBuilder();
for (String recipient : destinations) { if (messageBody != null) {
messagesList.add(messageText); builder.setBody(messageBody);
attachmentsList.add(attachments); }
for (PushAttachmentPointer attachment : attachments) {
PushMessageContent.AttachmentPointer.Builder attachmentBuilder =
PushMessageContent.AttachmentPointer.newBuilder();
attachmentBuilder.setId(attachment.getId());
attachmentBuilder.setContentType(attachment.getContentType());
attachmentBuilder.setKey(ByteString.copyFrom(attachment.getKey()));
builder.addAttachments(attachmentBuilder.build());
}
byte[] plaintext = builder.build().toByteArray();
Pair<Integer, byte[]> typeAndCiphertext = getEncryptedMessage(socket, recipients.getPrimaryRecipient(),
destination, plaintext);
types.add(typeAndCiphertext.first);
ciphertext.add(typeAndCiphertext.second);
} }
socket.sendMessage(destinations, messagesList, attachmentsList, socket.sendMessage(destinations, ciphertext, types);
OutgoingPushMessage.TYPE_MESSAGE_PLAINTEXT);
} catch (RateLimitException e) { } catch (RateLimitException e) {
Log.w("PushTransport", e); Log.w("PushTransport", e);
throw new IOException("Rate limit exceeded."); throw new IOException("Rate limit exceeded.");
} catch (RecipientFormattingException e) {
Log.w("PushTransport", e);
throw new IOException("Bad destination!");
} }
} }
private List<PushAttachmentData> getAttachmentsFromBody(PduBody body) { private List<PushAttachmentPointer> getPushAttachmentPointers(PushServiceSocket socket, PduBody body)
List<PushAttachmentData> attachments = new LinkedList<PushAttachmentData>(); throws IOException
{
List<PushAttachmentPointer> attachments = new LinkedList<PushAttachmentPointer>();
for (int i=0;i<body.getPartsNum();i++) { for (int i=0;i<body.getPartsNum();i++) {
String contentType = Util.toIsoString(body.getPart(i).getContentType()); String contentType = Util.toIsoString(body.getPart(i).getContentType());
if (ContentType.isImageType(contentType) || if (ContentType.isImageType(contentType) ||
ContentType.isAudioType(contentType) || ContentType.isAudioType(contentType) ||
ContentType.isVideoType(contentType)) ContentType.isVideoType(contentType))
{ {
attachments.add(new PushAttachmentData(contentType, body.getPart(i).getData())); AttachmentCipher cipher = new AttachmentCipher();
byte[] key = cipher.getCombinedKeyMaterial();
byte[] ciphertextAttachment = cipher.encrypt(body.getPart(i).getData());
PushAttachmentData attachmentData = new PushAttachmentData(contentType, ciphertextAttachment);
long attachmentId = socket.sendAttachment(attachmentData);
attachments.add(new PushAttachmentPointer(contentType, attachmentId, key));
} }
} }
@ -108,7 +149,7 @@ public class PushTransport extends BaseTransport {
} }
private Pair<Integer, byte[]> getEncryptedMessage(PushServiceSocket socket, Recipient recipient, private Pair<Integer, byte[]> getEncryptedMessage(PushServiceSocket socket, Recipient recipient,
String canonicalRecipientNumber, String plaintext) String canonicalRecipientNumber, byte[] plaintext)
throws IOException throws IOException
{ {
if (KeyUtil.isNonPrekeySessionFor(context, masterSecret, recipient)) { if (KeyUtil.isNonPrekeySessionFor(context, masterSecret, recipient)) {
@ -127,13 +168,13 @@ public class PushTransport extends BaseTransport {
} }
private byte[] getEncryptedPrekeyBundleMessageForExistingSession(Recipient recipient, private byte[] getEncryptedPrekeyBundleMessageForExistingSession(Recipient recipient,
String plaintext) byte[] plaintext)
{ {
IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret); IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
IdentityKey identityKey = identityKeyPair.getPublicKey(); IdentityKey identityKey = identityKeyPair.getPublicKey();
MessageCipher message = new MessageCipher(context, masterSecret, identityKeyPair, new RawTransportDetails()); MessageCipher message = new MessageCipher(context, masterSecret, identityKeyPair, new RawTransportDetails());
byte[] bundledMessage = message.encrypt(recipient, plaintext.getBytes()); byte[] bundledMessage = message.encrypt(recipient, plaintext);
PreKeyBundleMessage preKeyBundleMessage = new PreKeyBundleMessage(identityKey, bundledMessage); PreKeyBundleMessage preKeyBundleMessage = new PreKeyBundleMessage(identityKey, bundledMessage);
return preKeyBundleMessage.serialize(); return preKeyBundleMessage.serialize();
@ -142,7 +183,7 @@ public class PushTransport extends BaseTransport {
private byte[] getEncryptedPrekeyBundleMessageForNewSession(PushServiceSocket socket, private byte[] getEncryptedPrekeyBundleMessageForNewSession(PushServiceSocket socket,
Recipient recipient, Recipient recipient,
String canonicalRecipientNumber, String canonicalRecipientNumber,
String plaintext) byte[] plaintext)
throws IOException throws IOException
{ {
IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret); IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
@ -153,20 +194,20 @@ public class PushTransport extends BaseTransport {
processor.processKeyExchangeMessage(preKey); processor.processKeyExchangeMessage(preKey);
MessageCipher message = new MessageCipher(context, masterSecret, identityKeyPair, new RawTransportDetails()); MessageCipher message = new MessageCipher(context, masterSecret, identityKeyPair, new RawTransportDetails());
byte[] bundledMessage = message.encrypt(recipient, plaintext.getBytes()); byte[] bundledMessage = message.encrypt(recipient, plaintext);
PreKeyBundleMessage preKeyBundleMessage = new PreKeyBundleMessage(identityKey, bundledMessage); PreKeyBundleMessage preKeyBundleMessage = new PreKeyBundleMessage(identityKey, bundledMessage);
return preKeyBundleMessage.serialize(); return preKeyBundleMessage.serialize();
} }
private byte[] getEncryptedMessageForExistingSession(Recipient recipient, String plaintext) private byte[] getEncryptedMessageForExistingSession(Recipient recipient, byte[] plaintext)
throws IOException throws IOException
{ {
IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret); IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
MessageCipher messageCipher = new MessageCipher(context, masterSecret, identityKeyPair, MessageCipher messageCipher = new MessageCipher(context, masterSecret, identityKeyPair,
new PushTransportDetails()); new PushTransportDetails());
return messageCipher.encrypt(recipient, plaintext.getBytes()); return messageCipher.encrypt(recipient, plaintext);
} }
} }

View File

@ -41,6 +41,16 @@ public class PduBody {
mPartMapByFileName = new HashMap<String, PduPart>(); mPartMapByFileName = new HashMap<String, PduPart>();
} }
public boolean containsPushInProgress() {
for (int i=0;i<getPartsNum();i++) {
if (getPart(i).isPendingPush()) {
return true;
}
}
return false;
}
private void putPartToMaps(PduPart part) { private void putPartToMaps(PduPart part) {
// Put part to mPartMapByContentId. // Put part to mPartMapByContentId.
byte[] contentId = part.getContentId(); byte[] contentId = part.getContentId();

View File

@ -122,6 +122,7 @@ public class PduPart {
private static final String TAG = "PduPart"; private static final String TAG = "PduPart";
private boolean isEncrypted; private boolean isEncrypted;
private boolean isPendingPush;
/** /**
* Empty Constructor. * Empty Constructor.
@ -138,6 +139,14 @@ public class PduPart {
return isEncrypted; return isEncrypted;
} }
public void setPendingPush(boolean isPendingPush) {
this.isPendingPush = isPendingPush;
}
public boolean isPendingPush() {
return isPendingPush;
}
/** /**
* Set part data. The data are stored as byte array. * Set part data. The data are stored as byte array.
* *