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 string source = 2;
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 {
optional string contentType = 1;
optional string key = 2;
optional fixed64 id = 1;
optional string contentType = 2;
optional bytes key = 3;
}
repeated AttachmentPointer attachments = 5;
optional uint64 timestamp = 6;
repeated AttachmentPointer attachments = 2;
}

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;
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.Parcelable;
import org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal;
import java.util.LinkedList;
import java.util.List;
@ -24,12 +38,20 @@ public class IncomingPushMessage implements PushMessage, Parcelable {
}
};
private int type;
private String source;
private List<String> destinations;
private byte[] message;
private List<PushAttachmentPointer> attachments;
private long timestamp;
private int type;
private String source;
private List<String> destinations;
private byte[] message;
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) {
this.type = signal.getType();
@ -37,25 +59,15 @@ public class IncomingPushMessage implements PushMessage, Parcelable {
this.destinations = signal.getDestinationsList();
this.message = signal.getMessage().toByteArray();
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) {
this.destinations = new LinkedList<String>();
this.attachments = new LinkedList<PushAttachmentPointer>();
this.type = in.readInt();
this.source = in.readString();
in.readStringList(destinations);
this.message = new byte[in.readInt()];
in.readByteArray(this.message);
in.readList(attachments, PushAttachmentPointer.class.getClassLoader());
this.timestamp = in.readLong();
}
@ -67,10 +79,6 @@ public class IncomingPushMessage implements PushMessage, Parcelable {
return source;
}
public List<PushAttachmentPointer> getAttachments() {
return attachments;
}
public byte[] getBody() {
return message;
}
@ -79,10 +87,6 @@ public class IncomingPushMessage implements PushMessage, Parcelable {
return destinations;
}
public boolean hasAttachments() {
return getAttachments() != null && !getAttachments().isEmpty();
}
@Override
public int describeContents() {
return 0;
@ -95,11 +99,22 @@ public class IncomingPushMessage implements PushMessage, Parcelable {
dest.writeStringList(destinations);
dest.writeInt(message.length);
dest.writeByteArray(message);
dest.writeList(attachments);
dest.writeLong(timestamp);
}
public IncomingPushMessage withBody(byte[] body) {
return new IncomingPushMessage(this, body);
}
public int getType() {
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;
import org.whispersystems.textsecure.util.Base64;
import java.util.LinkedList;
import java.util.List;
public class OutgoingPushMessage implements PushMessage {
private int type;
private String destination;
private String body;
private List<PushAttachmentPointer> attachments;
private int type;
private String destination;
private String body;
public OutgoingPushMessage(String destination, byte[] body, int type) {
this.attachments = new LinkedList<PushAttachmentPointer>();
this.destination = destination;
this.body = Base64.encodeBytes(body);
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() {
return destination;
}
@ -37,10 +38,6 @@ public class OutgoingPushMessage implements PushMessage {
return body;
}
public List<PushAttachmentPointer> getAttachments() {
return attachments;
}
public int getType() {
return type;
}

View File

@ -18,23 +18,33 @@ public class PushAttachmentPointer implements Parcelable {
};
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.id = id;
this.key = key;
}
public PushAttachmentPointer(Parcel in) {
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() {
return contentType;
}
public String getKey() {
public long getId() {
return id;
}
public byte[] getKey() {
return key;
}
@ -46,6 +56,8 @@ public class PushAttachmentPointer implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
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));
}
public void sendMessage(List<String> recipients, List<byte[]> bodies,
List<List<PushAttachmentData>> attachmentsList, int type)
public void sendMessage(List<String> recipients, List<byte[]> bodies, List<Integer> types)
throws IOException
{
List<OutgoingPushMessage> messages = new LinkedList<OutgoingPushMessage>();
Iterator<String> recipientsIterator = recipients.iterator();
Iterator<byte[]> bodiesIterator = bodies.iterator();
Iterator<List<PushAttachmentData>> attachmentsIterator = attachmentsList.iterator();
Iterator<String> recipientsIterator = recipients.iterator();
Iterator<byte[]> bodiesIterator = bodies.iterator();
Iterator<Integer> typesIterator = types.iterator();
while (recipientsIterator.hasNext()) {
String recipient = recipientsIterator.next();
byte[] body = bodiesIterator.next();
List<PushAttachmentData> attachments = attachmentsIterator.next();
String recipient = recipientsIterator.next();
byte[] body = bodiesIterator.next();
int type = typesIterator.next();
OutgoingPushMessage message;
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);
messages.add(new OutgoingPushMessage(recipient, body, type));
}
sendMessage(new OutgoingPushMessageList(messages));
@ -149,20 +139,7 @@ public class PushServiceSocket {
return PreKeyEntity.fromJson(responseText);
}
private List<PushAttachmentPointer> sendAttachments(List<PushAttachmentData> attachments)
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 {
public long sendAttachment(PushAttachmentData attachment) throws IOException {
Pair<String, String> response = makeRequestForResponseHeader(String.format(ATTACHMENT_PATH, ""),
"GET", null, "Content-Location");
@ -178,25 +155,18 @@ public class PushServiceSocket {
return new Gson().fromJson(response.second, AttachmentKey.class).getId();
}
public List<Pair<File,String>> retrieveAttachments(List<PushAttachmentPointer> attachmentIds)
throws IOException
{
List<Pair<File,String>> attachments = new LinkedList<Pair<File,String>>();
public File retrieveAttachment(long attachmentId) throws IOException {
Pair<String, String> response = makeRequestForResponseHeader(String.format(ATTACHMENT_PATH, String.valueOf(attachmentId)),
"GET", null, "Content-Location");
for (PushAttachmentPointer attachmentId : attachmentIds) {
Pair<String, String> response = makeRequestForResponseHeader(String.format(ATTACHMENT_PATH, attachmentId.getKey()),
"GET", null, "Content-Location");
Log.w("PushServiceSocket", "Attachment: " + attachmentId + " is at: " + response.first);
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());
attachment.deleteOnExit();
downloadExternalFile(response.first, attachment);
downloadExternalFile(response.first, attachment);
attachments.add(new Pair<File, String>(attachment, attachmentId.getContentType()));
}
return attachments;
return attachment;
}
public Pair<DirectoryDescriptor, File> retrieveDirectory() {
@ -394,13 +364,13 @@ public class PushServiceSocket {
}
private static class AttachmentKey {
private String id;
private long id;
public AttachmentKey(String id) {
public AttachmentKey(long id) {
this.id = id;
}
public String getId() {
public long getId() {
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) {
return value == null || value.trim().length() == 0;
}
@ -94,6 +121,18 @@ public class Util {
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 {
byte[] buffer = new byte[4096];
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;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
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.RecipientFormattingException;
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.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.IdentityKeyPair;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
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.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.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.MmsException;
@ -64,21 +69,13 @@ import ws.com.google.android.mms.pdu.RetrieveConf;
public class DecryptingQueue {
private static final List<Runnable> workQueue = new LinkedList<Runnable>();
static {
Thread workerThread = new WorkerThread(workQueue, "Async Decryption Thread");
workerThread.start();
}
private static final Executor executor = Executors.newSingleThreadExecutor();
public static void scheduleDecryption(Context context, MasterSecret masterSecret,
long messageId, long threadId, MultimediaMessagePdu mms)
{
MmsDecryptionItem runnable = new MmsDecryptionItem(context, masterSecret, messageId, threadId, mms);
synchronized (workQueue) {
workQueue.add(runnable);
workQueue.notifyAll();
}
executor.execute(runnable);
}
public static void scheduleDecryption(Context context, MasterSecret masterSecret,
@ -87,10 +84,15 @@ public class DecryptingQueue {
{
DecryptionWorkItem runnable = new DecryptionWorkItem(context, masterSecret, messageId, threadId,
originator, body, isSecureMessage, isKeyExchange);
synchronized (workQueue) {
workQueue.add(runnable);
workQueue.notifyAll();
}
executor.execute(runnable);
}
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) {
@ -143,6 +145,59 @@ public class DecryptingQueue {
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 long messageId;
private long threadId;
@ -267,13 +322,10 @@ public class DecryptingQueue {
synchronized (SessionCipher.CIPHER_LOCK) {
try {
Log.w("DecryptingQueue", "Parsing recipient for originator: " + originator);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, originator, false);
Recipient recipient = recipients.getPrimaryRecipient();
Log.w("DecryptingQueue", "Parsed Recipient: " + recipient.getNumber());
if (!KeyUtil.isSessionFor(context, recipient)) {
Log.w("DecryptingQueue", "No such recipient session...");
database.markAsNoSession(messageId);
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_FROM_VERSION = 8;
private static final int INTRODUCED_TOFU_IDENTITY_VERSION = 9;
private static final int DATABASE_VERSION = 9;
private static final int INTRODUCED_PUSH_DATABASE_VERSION = 10;
private static final int DATABASE_VERSION = 10;
private static final String DATABASE_NAME = "messages.db";
private static final Object lock = new Object();
@ -71,6 +72,7 @@ public class DatabaseFactory {
private final MmsSmsDatabase mmsSmsDatabase;
private final IdentityDatabase identityDatabase;
private final DraftDatabase draftDatabase;
private final PushDatabase pushDatabase;
public static DatabaseFactory getInstance(Context context) {
synchronized (lock) {
@ -132,6 +134,10 @@ public class DatabaseFactory {
return getInstance(context).draftDatabase;
}
public static PushDatabase getPushDatabase(Context context) {
return getInstance(context).pushDatabase;
}
private DatabaseFactory(Context context) {
this.databaseHelper = new DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
this.sms = new SmsDatabase(context, databaseHelper);
@ -144,6 +150,7 @@ public class DatabaseFactory {
this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper);
this.identityDatabase = new IdentityDatabase(context, databaseHelper);
this.draftDatabase = new DraftDatabase(context, databaseHelper);
this.pushDatabase = new PushDatabase(context, databaseHelper);
}
public void reset(Context context) {
@ -425,6 +432,7 @@ public class DatabaseFactory {
db.execSQL(MmsAddressDatabase.CREATE_TABLE);
db.execSQL(IdentityDatabase.CREATE_TABLE);
db.execSQL(DraftDatabase.CREATE_TABLE);
db.execSQL(PushDatabase.CREATE_TABLE);
executeStatements(db, SmsDatabase.CREATE_INDEXS);
executeStatements(db, MmsDatabase.CREATE_INDEXS);
@ -617,6 +625,12 @@ public class DatabaseFactory {
db.execSQL("CREATE TABLE identities (_id INTEGER PRIMARY KEY, recipient INTEGER UNIQUE, key TEXT, mac TEXT);");
}
if (oldVersion < INTRODUCED_PUSH_DATABASE_VERSION) {
db.execSQL("CREATE TABLE push (_id INTEGER PRIMARY KEY, type INTEGER, source TEXT, destinations TEXT, body TEXT, TIMESTAMP INTEGER);");
db.execSQL("ALTER TABLE part ADD COLUMN pending_push INTEGER;");
db.execSQL("CREATE INDEX IF NOT EXISTS pending_push_index ON parts (pending_push);");
}
db.setTransactionSuccessful();
db.endTransaction();
}

View File

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

View File

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

View File

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

View File

@ -7,8 +7,6 @@ import android.util.Log;
import com.google.android.gcm.GCMBaseIntentService;
import org.thoughtcrime.securesms.service.RegistrationService;
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.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.push.IncomingEncryptedPushMessage;
@ -17,7 +15,6 @@ import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.util.Util;
import java.io.IOException;
import java.util.ArrayList;
public class GcmIntentService extends GCMBaseIntentService {
@ -61,8 +58,11 @@ public class GcmIntentService extends GCMBaseIntentService {
IncomingEncryptedPushMessage encryptedMessage = new IncomingEncryptedPushMessage(data, sessionKey);
IncomingPushMessage message = encryptedMessage.getIncomingPushMessage();
if (!message.hasAttachments()) handleIncomingTextMessage(context, message);
else handleIncomingMediaMessage(context, message);
Intent service = new Intent(context, SendReceiveService.class);
service.setAction(SendReceiveService.RECEIVE_PUSH_ACTION);
service.putExtra("message", message);
context.startService(service);
} catch (IOException e) {
Log.w("GcmIntentService", e);
} catch (InvalidVersionException e) {
@ -75,25 +75,6 @@ public class GcmIntentService extends GCMBaseIntentService {
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) {
String localNumber = TextSecurePreferences.getLocalNumber(context);
String password = TextSecurePreferences.getPushServerPassword(context);

View File

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

View File

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

View File

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

View File

@ -1,15 +1,13 @@
package org.thoughtcrime.securesms.mms;
import android.util.Log;
import android.util.Pair;
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.PushMessageProtos.PushMessageContent;
import org.whispersystems.textsecure.util.Base64;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.List;
import java.io.UnsupportedEncodingException;
import ws.com.google.android.mms.pdu.CharacterSets;
import ws.com.google.android.mms.pdu.EncodedStringValue;
@ -28,9 +26,9 @@ public class IncomingMediaMessage {
this.body = retreived.getBody();
}
public IncomingMediaMessage(String localNumber, IncomingPushMessage message,
List<Pair<File, String>> attachments)
throws IOException
public IncomingMediaMessage(MasterSecret masterSecret, String localNumber,
IncomingPushMessage message,
PushMessageContent messageContent)
{
this.headers = new PduHeaders();
this.body = new PduBody();
@ -39,32 +37,29 @@ public class IncomingMediaMessage {
this.headers.appendEncodedStringValue(new EncodedStringValue(localNumber), PduHeaders.TO);
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);
if (message.getBody() != null && message.getBody().length > 0) {
if (messageContent.getBody() != null && messageContent.getBody().length() > 0) {
PduPart text = new PduPart();
text.setData(message.getBody());
text.setContentType("text/plain".getBytes(CharacterSets.MIMENAME_ISO_8859_1));
text.setData(Util.toIsoBytes(messageContent.getBody()));
text.setContentType(Util.toIsoBytes("text/plain"));
body.addPart(text);
}
if (attachments != null) {
for (Pair<File, String> attachment : attachments) {
PduPart media = new PduPart();
FileInputStream fin = new FileInputStream(attachment.first);
byte[] data = Util.readFully(fin);
if (messageContent.getAttachmentsCount() > 0) {
for (PushMessageContent.AttachmentPointer attachment : messageContent.getAttachmentsList()) {
PduPart media = new PduPart();
byte[] encryptedKey = new MasterCipher(masterSecret).encryptBytes(attachment.getKey().toByteArray());
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);
attachment.first.delete();
}
}
}

View File

@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.providers.PartProvider;
import android.content.ContentUris;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.Log;
import android.widget.ImageView;
@ -90,12 +91,12 @@ public abstract class Slide {
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!");
}
public void setThumbnailOn(ImageView imageView) {
imageView.setImageBitmap(getThumbnail(imageView.getWidth(), imageView.getHeight()));
imageView.setImageDrawable(getThumbnail(imageView.getWidth(), imageView.getHeight()));
}
public boolean hasImage() {

View File

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

View File

@ -48,14 +48,8 @@ public class MmsReceiver {
}
public void process(MasterSecret masterSecret, Intent intent) {
try {
if (intent.getAction().equals(SendReceiveService.RECEIVE_MMS_ACTION)) {
handleMmsNotification(intent);
} else if (intent.getAction().equals(SendReceiveService.RECEIVE_PUSH_MMS_ACTION)) {
handlePushMedia(masterSecret, intent);
}
} catch (MmsException e) {
Log.w("MmsReceiver", e);
if (intent.getAction().equals(SendReceiveService.RECEIVE_MMS_ACTION)) {
handleMmsNotification(intent);
}
}
@ -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) {
Intent intent = new Intent(SendReceiveService.DOWNLOAD_MMS_ACTION, null, context, SendReceiveService.class);
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 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_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_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 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 RECEIVE_SMS = 1;
@ -62,14 +64,18 @@ public class SendReceiveService extends Service {
private static final int RECEIVE_MMS = 3;
private static final int DOWNLOAD_MMS = 4;
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 SmsReceiver smsReceiver;
private SmsSender smsSender;
private MmsReceiver mmsReceiver;
private MmsSender mmsSender;
private MmsDownloader mmsDownloader;
private SmsReceiver smsReceiver;
private SmsSender smsSender;
private MmsReceiver mmsReceiver;
private MmsSender mmsSender;
private MmsDownloader mmsDownloader;
private PushReceiver pushReceiver;
private PushDownloader pushDownloader;
private MasterSecret masterSecret;
private boolean hasSecret;
@ -78,7 +84,6 @@ public class SendReceiveService extends Service {
private ClearKeyReceiver clearKeyReceiver;
private List<Runnable> workQueue;
private List<Runnable> pendingSecretList;
private Thread workerThread;
@Override
public void onCreate() {
@ -105,12 +110,18 @@ public class SendReceiveService extends Service {
scheduleIntent(SEND_SMS, intent);
else if (action.equals(SEND_MMS_ACTION))
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);
else if (action.equals(DOWNLOAD_MMS_ACTION))
scheduleSecretRequiredIntent(DOWNLOAD_MMS, intent);
else if (intent.getAction().equals(DOWNLOAD_MMS_PENDING_APN_ACTION))
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
Log.w("SendReceiveService", "Received intent with unknown action: " + intent.getAction());
}
@ -142,13 +153,15 @@ public class SendReceiveService extends Service {
mmsReceiver = new MmsReceiver(this);
mmsSender = new MmsSender(this, toastHandler);
mmsDownloader = new MmsDownloader(this, toastHandler);
pushReceiver = new PushReceiver(this);
pushDownloader = new PushDownloader(this);
}
private void initializeWorkQueue() {
pendingSecretList = new LinkedList<Runnable>();
workQueue = new LinkedList<Runnable>();
workerThread = new WorkerThread(workQueue, "SendReceveService-WorkerThread");
Thread workerThread = new WorkerThread(workQueue, "SendReceveService-WorkerThread");
workerThread.start();
}
@ -222,12 +235,14 @@ public class SendReceiveService extends Service {
@Override
public void run() {
switch (what) {
case RECEIVE_SMS: smsReceiver.process(masterSecret, intent); return;
case SEND_SMS: smsSender.process(masterSecret, intent); return;
case RECEIVE_MMS: mmsReceiver.process(masterSecret, intent); return;
case SEND_MMS: mmsSender.process(masterSecret, intent); return;
case DOWNLOAD_MMS: mmsDownloader.process(masterSecret, intent); return;
case RECEIVE_SMS: smsReceiver.process(masterSecret, intent); return;
case SEND_SMS: smsSender.process(masterSecret, intent); return;
case RECEIVE_MMS: mmsReceiver.process(masterSecret, intent); return;
case SEND_MMS: mmsSender.process(masterSecret, intent); return;
case DOWNLOAD_MMS: 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.Pair;
import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.mms.PartParser;
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.thoughtcrime.securesms.util.TextSecurePreferences;
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.PushTransportDetails;
import org.whispersystems.textsecure.push.RateLimitException;
import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.PhoneNumberFormatter;
import java.io.IOException;
@ -50,10 +58,12 @@ public class PushTransport extends BaseTransport {
String password = TextSecurePreferences.getPushServerPassword(context);
PushServiceSocket socket = new PushServiceSocket(context, localNumber, password);
Recipient recipient = message.getIndividualRecipient();
String plaintext = message.getBody().getBody();
String recipientCanonicalNumber = PhoneNumberFormatter.formatNumber(recipient.getNumber(),
localNumber);
Recipient recipient = message.getIndividualRecipient();
String plaintextBody = message.getBody().getBody();
PushMessageContent.Builder builder = PushMessageContent.newBuilder();
byte[] plaintext = builder.setBody(plaintextBody).build().toByteArray();
String recipientCanonicalNumber = PhoneNumberFormatter.formatNumber(recipient.getNumber(),
localNumber);
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 {
try {
String localNumber = TextSecurePreferences.getLocalNumber(context);
String password = TextSecurePreferences.getPushServerPassword(context);
PushServiceSocket socket = new PushServiceSocket(context, localNumber, password);
byte[] messageText = PartParser.getMessageText(message.getBody()).getBytes();
List<PushAttachmentData> attachments = getAttachmentsFromBody(message.getBody());
String localNumber = TextSecurePreferences.getLocalNumber(context);
String password = TextSecurePreferences.getPushServerPassword(context);
PushServiceSocket socket = new PushServiceSocket(context, localNumber, password);
String messageBody = PartParser.getMessageText(message.getBody());
List<byte[]> ciphertext = new LinkedList<byte[]> ();
List<Integer> types = new LinkedList<Integer>();
List<byte[]> messagesList = new LinkedList<byte[]>();
List<List<PushAttachmentData>> attachmentsList = new LinkedList<List<PushAttachmentData>>();
for (String destination : destinations) {
Recipients recipients = RecipientFactory.getRecipientsFromString(context, destination, false);
List<PushAttachmentPointer> attachments = getPushAttachmentPointers(socket, message.getBody());
PushMessageContent.Builder builder = PushMessageContent.newBuilder();
for (String recipient : destinations) {
messagesList.add(messageText);
attachmentsList.add(attachments);
if (messageBody != null) {
builder.setBody(messageBody);
}
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,
OutgoingPushMessage.TYPE_MESSAGE_PLAINTEXT);
socket.sendMessage(destinations, ciphertext, types);
} catch (RateLimitException e) {
Log.w("PushTransport", e);
throw new IOException("Rate limit exceeded.");
} catch (RecipientFormattingException e) {
Log.w("PushTransport", e);
throw new IOException("Bad destination!");
}
}
private List<PushAttachmentData> getAttachmentsFromBody(PduBody body) {
List<PushAttachmentData> attachments = new LinkedList<PushAttachmentData>();
private List<PushAttachmentPointer> getPushAttachmentPointers(PushServiceSocket socket, PduBody body)
throws IOException
{
List<PushAttachmentPointer> attachments = new LinkedList<PushAttachmentPointer>();
for (int i=0;i<body.getPartsNum();i++) {
String contentType = Util.toIsoString(body.getPart(i).getContentType());
if (ContentType.isImageType(contentType) ||
ContentType.isAudioType(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,
String canonicalRecipientNumber, String plaintext)
String canonicalRecipientNumber, byte[] plaintext)
throws IOException
{
if (KeyUtil.isNonPrekeySessionFor(context, masterSecret, recipient)) {
@ -127,13 +168,13 @@ public class PushTransport extends BaseTransport {
}
private byte[] getEncryptedPrekeyBundleMessageForExistingSession(Recipient recipient,
String plaintext)
byte[] plaintext)
{
IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
IdentityKey identityKey = identityKeyPair.getPublicKey();
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);
return preKeyBundleMessage.serialize();
@ -142,7 +183,7 @@ public class PushTransport extends BaseTransport {
private byte[] getEncryptedPrekeyBundleMessageForNewSession(PushServiceSocket socket,
Recipient recipient,
String canonicalRecipientNumber,
String plaintext)
byte[] plaintext)
throws IOException
{
IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
@ -153,20 +194,20 @@ public class PushTransport extends BaseTransport {
processor.processKeyExchangeMessage(preKey);
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);
return preKeyBundleMessage.serialize();
}
private byte[] getEncryptedMessageForExistingSession(Recipient recipient, String plaintext)
private byte[] getEncryptedMessageForExistingSession(Recipient recipient, byte[] plaintext)
throws IOException
{
IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
MessageCipher messageCipher = new MessageCipher(context, masterSecret, identityKeyPair,
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>();
}
public boolean containsPushInProgress() {
for (int i=0;i<getPartsNum();i++) {
if (getPart(i).isPendingPush()) {
return true;
}
}
return false;
}
private void putPartToMaps(PduPart part) {
// Put part to mPartMapByContentId.
byte[] contentId = part.getContentId();

View File

@ -122,6 +122,7 @@ public class PduPart {
private static final String TAG = "PduPart";
private boolean isEncrypted;
private boolean isPendingPush;
/**
* Empty Constructor.
@ -137,6 +138,14 @@ public class PduPart {
public boolean getEncrypted() {
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.