mirror of
https://github.com/oxen-io/session-android.git
synced 2025-10-27 12:39:04 +00:00
Support encrypted transport, properly handle multiple recipients.
1) Add encryption support for the transport layer. This obscures metadata from the push messaging provider. 2) Better support the direction multiple destination messages is headed (one unique message per recipient).
This commit is contained in:
@@ -96,8 +96,8 @@ public class PreKeyBundleMessage {
|
||||
}
|
||||
}
|
||||
|
||||
public String serialize() {
|
||||
return Base64.encodeBytesWithoutPadding(this.messageBytes);
|
||||
public byte[] serialize() {
|
||||
return this.messageBytes;
|
||||
}
|
||||
|
||||
public int getSupportedVersion() {
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.InvalidVersionException;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
import org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
|
||||
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.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class IncomingEncryptedPushMessage {
|
||||
|
||||
private static final int SUPPORTED_VERSION = 1;
|
||||
private static final int CIPHER_KEY_SIZE = 32;
|
||||
private static final int MAC_KEY_SIZE = 20;
|
||||
private static final int MAC_SIZE = 10;
|
||||
|
||||
private static final int VERSION_OFFSET = 0;
|
||||
private static final int VERSION_LENGTH = 1;
|
||||
private static final int IV_OFFSET = VERSION_OFFSET + VERSION_LENGTH;
|
||||
private static final int IV_LENGTH = 16;
|
||||
private static final int CIPHERTEXT_OFFSET = IV_OFFSET + IV_LENGTH;
|
||||
|
||||
private final IncomingPushMessage incomingPushMessage;
|
||||
|
||||
public IncomingEncryptedPushMessage(String message, String signalingKey)
|
||||
throws IOException, InvalidVersionException
|
||||
{
|
||||
byte[] ciphertext = Base64.decode(message);
|
||||
|
||||
if (ciphertext.length < VERSION_LENGTH || ciphertext[VERSION_OFFSET] != SUPPORTED_VERSION)
|
||||
throw new InvalidVersionException("Unsupported version!");
|
||||
|
||||
SecretKeySpec cipherKey = getCipherKey(signalingKey);
|
||||
SecretKeySpec macKey = getMacKey(signalingKey);
|
||||
|
||||
verifyMac(ciphertext, macKey);
|
||||
|
||||
byte[] plaintext = getPlaintext(ciphertext, cipherKey);
|
||||
IncomingPushMessageSignal signal = IncomingPushMessageSignal.parseFrom(plaintext);
|
||||
|
||||
this.incomingPushMessage = new IncomingPushMessage(signal);
|
||||
}
|
||||
|
||||
public IncomingPushMessage getIncomingPushMessage() {
|
||||
return incomingPushMessage;
|
||||
}
|
||||
|
||||
private byte[] getPlaintext(byte[] ciphertext, SecretKeySpec cipherKey) throws IOException {
|
||||
try {
|
||||
byte[] ivBytes = new byte[IV_LENGTH];
|
||||
System.arraycopy(ciphertext, IV_OFFSET, ivBytes, 0, ivBytes.length);
|
||||
IvParameterSpec iv = new IvParameterSpec(ivBytes);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, cipherKey, iv);
|
||||
|
||||
return cipher.doFinal(ciphertext, CIPHERTEXT_OFFSET,
|
||||
ciphertext.length - VERSION_LENGTH - IV_LENGTH - MAC_SIZE);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (BadPaddingException e) {
|
||||
Log.w("IncomingEncryptedPushMessage", e);
|
||||
throw new IOException("Bad padding?");
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyMac(byte[] ciphertext, SecretKeySpec macKey) throws IOException {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(macKey);
|
||||
|
||||
if (ciphertext.length < MAC_SIZE + 1)
|
||||
throw new IOException("Invalid MAC!");
|
||||
|
||||
mac.update(ciphertext, 0, ciphertext.length - MAC_SIZE);
|
||||
|
||||
byte[] ourMacFull = mac.doFinal();
|
||||
byte[] ourMacBytes = new byte[MAC_SIZE];
|
||||
System.arraycopy(ourMacFull, 0, ourMacBytes, 0, ourMacBytes.length);
|
||||
|
||||
byte[] theirMacBytes = new byte[MAC_SIZE];
|
||||
System.arraycopy(ciphertext, ciphertext.length-MAC_SIZE, theirMacBytes, 0, theirMacBytes.length);
|
||||
|
||||
Log.w("IncomingEncryptedPushMessage", "Our MAC: " + Hex.toString(ourMacBytes));
|
||||
Log.w("IncomingEncryptedPushMessage", "Thr MAC: " + Hex.toString(theirMacBytes));
|
||||
|
||||
if (!Arrays.equals(ourMacBytes, theirMacBytes)) {
|
||||
throw new IOException("Invalid MAC compare!");
|
||||
}
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private SecretKeySpec getCipherKey(String signalingKey) throws IOException {
|
||||
byte[] signalingKeyBytes = Base64.decode(signalingKey);
|
||||
byte[] cipherKey = new byte[CIPHER_KEY_SIZE];
|
||||
System.arraycopy(signalingKeyBytes, 0, cipherKey, 0, cipherKey.length);
|
||||
|
||||
return new SecretKeySpec(cipherKey, "AES");
|
||||
}
|
||||
|
||||
|
||||
private SecretKeySpec getMacKey(String signalingKey) throws IOException {
|
||||
byte[] signalingKeyBytes = Base64.decode(signalingKey);
|
||||
byte[] macKey = new byte[MAC_KEY_SIZE];
|
||||
System.arraycopy(signalingKeyBytes, CIPHER_KEY_SIZE, macKey, 0, macKey.length);
|
||||
|
||||
return new SecretKeySpec(macKey, "HmacSHA256");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
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 java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class IncomingPushMessage implements Parcelable {
|
||||
public class IncomingPushMessage implements PushMessage, Parcelable {
|
||||
|
||||
public static final Parcelable.Creator<IncomingPushMessage> CREATOR = new Parcelable.Creator<IncomingPushMessage>() {
|
||||
@Override
|
||||
@@ -23,19 +27,23 @@ public class IncomingPushMessage implements Parcelable {
|
||||
private int type;
|
||||
private String source;
|
||||
private List<String> destinations;
|
||||
private String messageText;
|
||||
private byte[] message;
|
||||
private List<PushAttachmentPointer> attachments;
|
||||
private long timestamp;
|
||||
|
||||
public IncomingPushMessage(String source, List<String> destinations, String messageText,
|
||||
int type, List<PushAttachmentPointer> attachments, long timestamp)
|
||||
{
|
||||
this.type = type;
|
||||
this.source = source;
|
||||
this.destinations = destinations;
|
||||
this.messageText = messageText;
|
||||
this.attachments = attachments;
|
||||
this.timestamp = timestamp;
|
||||
public IncomingPushMessage(IncomingPushMessageSignal signal) {
|
||||
this.type = signal.getType();
|
||||
this.source = signal.getSource();
|
||||
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) {
|
||||
@@ -44,7 +52,8 @@ public class IncomingPushMessage implements Parcelable {
|
||||
|
||||
this.source = in.readString();
|
||||
in.readStringList(destinations);
|
||||
this.messageText = in.readString();
|
||||
this.message = new byte[in.readInt()];
|
||||
in.readByteArray(this.message);
|
||||
in.readList(attachments, PushAttachmentPointer.class.getClassLoader());
|
||||
this.timestamp = in.readLong();
|
||||
}
|
||||
@@ -62,7 +71,14 @@ public class IncomingPushMessage implements Parcelable {
|
||||
}
|
||||
|
||||
public String getMessageText() {
|
||||
return messageText;
|
||||
if (type == TYPE_MESSAGE_CIPHERTEXT ||
|
||||
type == TYPE_MESSAGE_KEY_EXCHANGE ||
|
||||
type == TYPE_MESSAGE_PREKEY_BUNDLE)
|
||||
{
|
||||
return Base64.encodeBytesWithoutPadding(message);
|
||||
}
|
||||
|
||||
return new String(message);
|
||||
}
|
||||
|
||||
public List<String> getDestinations() {
|
||||
@@ -82,7 +98,8 @@ public class IncomingPushMessage implements Parcelable {
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(source);
|
||||
dest.writeStringList(destinations);
|
||||
dest.writeString(messageText);
|
||||
dest.writeInt(message.length);
|
||||
dest.writeByteArray(message);
|
||||
dest.writeList(attachments);
|
||||
dest.writeLong(timestamp);
|
||||
}
|
||||
|
||||
@@ -1,50 +1,40 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class OutgoingPushMessage {
|
||||
|
||||
public static final int TYPE_MESSAGE_PLAINTEXT = 0;
|
||||
public static final int TYPE_MESSAGE_CIPHERTEXT = 1;
|
||||
public static final int TYPE_MESSAGE_KEY_EXCHANGE = 2;
|
||||
public static final int TYPE_MESSAGE_PREKEY_BUNDLE = 3;
|
||||
public class OutgoingPushMessage implements PushMessage {
|
||||
|
||||
private int type;
|
||||
private List<String> destinations;
|
||||
private String messageText;
|
||||
private String destination;
|
||||
private String body;
|
||||
private List<PushAttachmentPointer> attachments;
|
||||
|
||||
public OutgoingPushMessage(String destination, String messageText, int type) {
|
||||
this.destinations = new LinkedList<String>();
|
||||
this.attachments = new LinkedList<PushAttachmentPointer>();
|
||||
this.messageText = messageText;
|
||||
this.destinations.add(destination);
|
||||
this.type = type;
|
||||
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(List<String> destinations, String messageText, int type) {
|
||||
this.destinations = destinations;
|
||||
this.messageText = messageText;
|
||||
this.attachments = new LinkedList<PushAttachmentPointer>();
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public OutgoingPushMessage(List<String> destinations, String messageText,
|
||||
List<PushAttachmentPointer> attachments, int type)
|
||||
public OutgoingPushMessage(String destination, byte[] body,
|
||||
List<PushAttachmentPointer> attachments,
|
||||
int type)
|
||||
{
|
||||
this.destinations = destinations;
|
||||
this.messageText = messageText;
|
||||
this.attachments = attachments;
|
||||
this.type = type;
|
||||
this.destination = destination;
|
||||
this.body = Base64.encodeBytes(body);
|
||||
this.attachments = attachments;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public List<String> getDestinations() {
|
||||
return destinations;
|
||||
public String getDestination() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public String getMessageText() {
|
||||
return messageText;
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public List<PushAttachmentPointer> getAttachments() {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class OutgoingPushMessageList {
|
||||
|
||||
private List<OutgoingPushMessage> messages;
|
||||
|
||||
public OutgoingPushMessageList(OutgoingPushMessage message) {
|
||||
this.messages = new LinkedList<OutgoingPushMessage>();
|
||||
this.messages.add(message);
|
||||
}
|
||||
|
||||
public OutgoingPushMessageList(List<OutgoingPushMessage> messages) {
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
public List<OutgoingPushMessage> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
|
||||
public interface PushMessage {
|
||||
public static final int TYPE_MESSAGE_PLAINTEXT = 0;
|
||||
public static final int TYPE_MESSAGE_CIPHERTEXT = 1;
|
||||
public static final int TYPE_MESSAGE_KEY_EXCHANGE = 2;
|
||||
public static final int TYPE_MESSAGE_PREKEY_BUNDLE = 3;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,13 +4,10 @@ import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.thoughtcrimegson.Gson;
|
||||
import org.whispersystems.textsecure.R;
|
||||
import org.whispersystems.textsecure.Release;
|
||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
||||
import org.whispersystems.textsecure.crypto.PreKeyPair;
|
||||
import org.whispersystems.textsecure.crypto.PreKeyPublic;
|
||||
import org.whispersystems.textsecure.directory.DirectoryDescriptor;
|
||||
import org.whispersystems.textsecure.storage.PreKeyRecord;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
@@ -33,6 +30,7 @@ import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -79,31 +77,45 @@ public class PushServiceSocket {
|
||||
makeRequest(REGISTER_GCM_PATH, "DELETE", null);
|
||||
}
|
||||
|
||||
public void sendMessage(String recipient, String messageText, int type)
|
||||
public void sendMessage(String recipient, byte[] body, int type)
|
||||
throws IOException
|
||||
{
|
||||
OutgoingPushMessage message = new OutgoingPushMessage(recipient, messageText, type);
|
||||
sendMessage(message);
|
||||
OutgoingPushMessage message = new OutgoingPushMessage(recipient, body, type);
|
||||
sendMessage(new OutgoingPushMessageList(message));
|
||||
}
|
||||
|
||||
public void sendMessage(List<String> recipients, String messageText, int type)
|
||||
public void sendMessage(List<String> recipients, List<byte[]> bodies,
|
||||
List<List<PushAttachmentData>> attachmentsList, int type)
|
||||
throws IOException
|
||||
{
|
||||
OutgoingPushMessage message = new OutgoingPushMessage(recipients, messageText, type);
|
||||
sendMessage(message);
|
||||
List<OutgoingPushMessage> messages = new LinkedList<OutgoingPushMessage>();
|
||||
|
||||
Iterator<String> recipientsIterator = recipients.iterator();
|
||||
Iterator<byte[]> bodiesIterator = bodies.iterator();
|
||||
Iterator<List<PushAttachmentData>> attachmentsIterator = attachmentsList.iterator();
|
||||
|
||||
while (recipientsIterator.hasNext()) {
|
||||
String recipient = recipientsIterator.next();
|
||||
byte[] body = bodiesIterator.next();
|
||||
List<PushAttachmentData> attachments = attachmentsIterator.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);
|
||||
}
|
||||
|
||||
sendMessage(new OutgoingPushMessageList(messages));
|
||||
}
|
||||
|
||||
public void sendMessage(List<String> recipients, String messageText,
|
||||
List<PushAttachmentData> attachments, int type)
|
||||
throws IOException
|
||||
{
|
||||
List<PushAttachmentPointer> attachmentIds = sendAttachments(attachments);
|
||||
OutgoingPushMessage message = new OutgoingPushMessage(recipients, messageText, attachmentIds, type);
|
||||
sendMessage(message);
|
||||
}
|
||||
|
||||
private void sendMessage(OutgoingPushMessage message) throws IOException {
|
||||
String responseText = makeRequest(MESSAGE_PATH, "POST", new Gson().toJson(message));
|
||||
private void sendMessage(OutgoingPushMessageList messages) throws IOException {
|
||||
String responseText = makeRequest(MESSAGE_PATH, "POST", new Gson().toJson(messages));
|
||||
PushMessageResponse response = new Gson().fromJson(responseText, PushMessageResponse.class);
|
||||
|
||||
if (response.getFailure().size() != 0)
|
||||
|
||||
@@ -34,11 +34,11 @@ public class PushTransportDetails implements TransportDetails {
|
||||
|
||||
@Override
|
||||
public byte[] getEncodedMessage(byte[] messageWithMac) {
|
||||
return Base64.encodeBytesWithoutPadding(messageWithMac).getBytes();
|
||||
return messageWithMac;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getDecodedMessage(byte[] encodedMessageBytes) throws IOException {
|
||||
return Base64.decodeWithoutPadding(new String(encodedMessageBytes));
|
||||
return encodedMessageBytes;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user