Support for multi-device message and contact sync.

// FREEBIE
This commit is contained in:
Moxie Marlinspike 2015-06-15 12:21:52 -07:00
parent 1cdffebf6f
commit 0437bde205
22 changed files with 9700 additions and 5122 deletions

View File

@ -4,7 +4,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:1.1.0'
classpath 'com.android.tools.build:gradle:1.2.3'
}
}

View File

@ -1,5 +1,5 @@
subprojects {
ext.version_number = "1.5.0"
ext.version_number = "1.6.0-RC4"
ext.group_info = "org.whispersystems"
ext.axolotl_version = "1.3.1"

View File

@ -19,6 +19,7 @@ package org.whispersystems.textsecure.api;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer;
import org.whispersystems.textsecure.api.messages.TextSecureDataMessage;
import org.whispersystems.textsecure.api.messages.TextSecureEnvelope;
import org.whispersystems.textsecure.api.push.TrustStore;
import org.whispersystems.textsecure.api.util.CredentialsProvider;
@ -80,7 +81,7 @@ public class TextSecureMessageReceiver {
* Retrieves a TextSecure attachment.
*
* @param pointer The {@link org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer}
* received in a {@link org.whispersystems.textsecure.api.messages.TextSecureMessage}.
* received in a {@link TextSecureDataMessage}.
* @param destination The download destination for this attachment.
*
* @return An InputStream that streams the plaintext attachment contents.
@ -119,7 +120,8 @@ public class TextSecureMessageReceiver {
for (TextSecureEnvelopeEntity entity : entities) {
TextSecureEnvelope envelope = new TextSecureEnvelope(entity.getType(), entity.getSource(),
entity.getSourceDevice(), entity.getRelay(),
entity.getTimestamp(), entity.getMessage());
entity.getTimestamp(), entity.getMessage(),
entity.getContent());
callback.onMessage(envelope);
results.add(envelope);

View File

@ -30,8 +30,8 @@ import org.whispersystems.textsecure.api.crypto.TextSecureCipher;
import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException;
import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentStream;
import org.whispersystems.textsecure.api.messages.TextSecureDataMessage;
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
import org.whispersystems.textsecure.api.push.TextSecureAddress;
import org.whispersystems.textsecure.api.push.TrustStore;
import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions;
@ -42,10 +42,14 @@ import org.whispersystems.textsecure.internal.push.MismatchedDevices;
import org.whispersystems.textsecure.internal.push.OutgoingPushMessage;
import org.whispersystems.textsecure.internal.push.OutgoingPushMessageList;
import org.whispersystems.textsecure.internal.push.PushAttachmentData;
import org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.SyncMessageContext;
import org.whispersystems.textsecure.internal.push.PushServiceSocket;
import org.whispersystems.textsecure.internal.push.SendMessageResponse;
import org.whispersystems.textsecure.internal.push.StaleDevices;
import org.whispersystems.textsecure.internal.push.TextSecureProtos.AttachmentPointer;
import org.whispersystems.textsecure.internal.push.TextSecureProtos.Content;
import org.whispersystems.textsecure.internal.push.TextSecureProtos.DataMessage;
import org.whispersystems.textsecure.internal.push.TextSecureProtos.GroupContext;
import org.whispersystems.textsecure.internal.push.TextSecureProtos.SyncMessage;
import org.whispersystems.textsecure.internal.push.exceptions.MismatchedDevicesException;
import org.whispersystems.textsecure.internal.push.exceptions.StaleDevicesException;
import org.whispersystems.textsecure.internal.util.StaticCredentialsProvider;
@ -55,10 +59,6 @@ import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent;
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.AttachmentPointer;
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.GroupContext;
/**
* The main interface for sending TextSecure messages.
*
@ -114,16 +114,16 @@ public class TextSecureMessageSender {
* @throws UntrustedIdentityException
* @throws IOException
*/
public void sendMessage(TextSecureAddress recipient, TextSecureMessage message)
public void sendMessage(TextSecureAddress recipient, TextSecureDataMessage message)
throws UntrustedIdentityException, IOException
{
byte[] content = createMessageContent(message);
long timestamp = message.getTimestamp();
SendMessageResponse response = sendMessage(recipient, timestamp, content);
SendMessageResponse response = sendMessage(recipient, timestamp, content, true);
if (response != null && response.getNeedsSync()) {
byte[] syncMessage = createSyncMessageContent(content, Optional.of(recipient), timestamp);
sendMessage(localAddress, timestamp, syncMessage);
byte[] syncMessage = createSentTranscriptMessage(content, Optional.of(recipient), timestamp);
sendMessage(localAddress, timestamp, syncMessage, false);
}
if (message.isEndSession()) {
@ -143,26 +143,33 @@ public class TextSecureMessageSender {
* @throws IOException
* @throws EncapsulatedExceptions
*/
public void sendMessage(List<TextSecureAddress> recipients, TextSecureMessage message)
public void sendMessage(List<TextSecureAddress> recipients, TextSecureDataMessage message)
throws IOException, EncapsulatedExceptions
{
byte[] content = createMessageContent(message);
long timestamp = message.getTimestamp();
SendMessageResponse response = sendMessage(recipients, timestamp, content);
SendMessageResponse response = sendMessage(recipients, timestamp, content, true);
try {
if (response != null && response.getNeedsSync()) {
byte[] syncMessage = createSyncMessageContent(content, Optional.<TextSecureAddress>absent(), timestamp);
sendMessage(localAddress, timestamp, syncMessage);
byte[] syncMessage = createSentTranscriptMessage(content, Optional.<TextSecureAddress>absent(), timestamp);
sendMessage(localAddress, timestamp, syncMessage, false);
}
} catch (UntrustedIdentityException e) {
throw new EncapsulatedExceptions(e);
}
}
private byte[] createMessageContent(TextSecureMessage message) throws IOException {
PushMessageContent.Builder builder = PushMessageContent.newBuilder();
List<AttachmentPointer> pointers = createAttachmentPointers(message.getAttachments());
public void sendMultiDeviceContactsUpdate(TextSecureAttachmentStream contacts)
throws IOException, UntrustedIdentityException
{
byte[] content = createMultiDeviceContactsContent(contacts);
sendMessage(localAddress, System.currentTimeMillis(), content, false);
}
private byte[] createMessageContent(TextSecureDataMessage message) throws IOException {
DataMessage.Builder builder = DataMessage.newBuilder();
List<AttachmentPointer> pointers = createAttachmentPointers(message.getAttachments());
if (!pointers.isEmpty()) {
builder.addAllAttachments(pointers);
@ -177,25 +184,34 @@ public class TextSecureMessageSender {
}
if (message.isEndSession()) {
builder.setFlags(PushMessageContent.Flags.END_SESSION_VALUE);
builder.setFlags(DataMessage.Flags.END_SESSION_VALUE);
}
return builder.build().toByteArray();
}
private byte[] createSyncMessageContent(byte[] content, Optional<TextSecureAddress> recipient, long timestamp) {
private byte[] createMultiDeviceContactsContent(TextSecureAttachmentStream contacts) throws IOException {
SyncMessage.Builder builder = SyncMessage.newBuilder();
builder.setContacts(SyncMessage.Contacts.newBuilder()
.setBlob(createAttachmentPointer(contacts)));
return builder.build().toByteArray();
}
private byte[] createSentTranscriptMessage(byte[] content, Optional<TextSecureAddress> recipient, long timestamp) {
try {
SyncMessageContext.Builder syncMessageContext = SyncMessageContext.newBuilder();
syncMessageContext.setTimestamp(timestamp);
Content.Builder container = Content.newBuilder();
SyncMessage.Builder syncMessage = SyncMessage.newBuilder();
SyncMessage.Sent.Builder sentMessage = SyncMessage.Sent.newBuilder();
sentMessage.setTimestamp(timestamp);
sentMessage.setMessage(DataMessage.parseFrom(content));
if (recipient.isPresent()) {
syncMessageContext.setDestination(recipient.get().getNumber());
sentMessage.setDestination(recipient.get().getNumber());
}
PushMessageContent.Builder builder = PushMessageContent.parseFrom(content).toBuilder();
builder.setSync(syncMessageContext.build());
return builder.build().toByteArray();
return container.setSyncMessage(syncMessage.setSent(sentMessage)).build().toByteArray();
} catch (InvalidProtocolBufferException e) {
throw new AssertionError(e);
}
@ -224,7 +240,7 @@ public class TextSecureMessageSender {
return builder.build();
}
private SendMessageResponse sendMessage(List<TextSecureAddress> recipients, long timestamp, byte[] content)
private SendMessageResponse sendMessage(List<TextSecureAddress> recipients, long timestamp, byte[] content, boolean legacy)
throws IOException, EncapsulatedExceptions
{
List<UntrustedIdentityException> untrustedIdentities = new LinkedList<>();
@ -235,7 +251,7 @@ public class TextSecureMessageSender {
for (TextSecureAddress recipient : recipients) {
try {
response = sendMessage(recipient, timestamp, content);
response = sendMessage(recipient, timestamp, content, legacy);
} catch (UntrustedIdentityException e) {
Log.w(TAG, e);
untrustedIdentities.add(e);
@ -255,12 +271,12 @@ public class TextSecureMessageSender {
return response;
}
private SendMessageResponse sendMessage(TextSecureAddress recipient, long timestamp, byte[] content)
private SendMessageResponse sendMessage(TextSecureAddress recipient, long timestamp, byte[] content, boolean legacy)
throws UntrustedIdentityException, IOException
{
for (int i=0;i<3;i++) {
try {
OutgoingPushMessageList messages = getEncryptedMessages(socket, recipient, timestamp, content);
OutgoingPushMessageList messages = getEncryptedMessages(socket, recipient, timestamp, content, legacy);
return socket.sendMessage(messages);
} catch (MismatchedDevicesException mde) {
Log.w(TAG, mde);
@ -314,23 +330,24 @@ public class TextSecureMessageSender {
private OutgoingPushMessageList getEncryptedMessages(PushServiceSocket socket,
TextSecureAddress recipient,
long timestamp,
byte[] plaintext)
byte[] plaintext,
boolean legacy)
throws IOException, UntrustedIdentityException
{
List<OutgoingPushMessage> messages = new LinkedList<>();
if (!recipient.equals(localAddress)) {
messages.add(getEncryptedMessage(socket, recipient, TextSecureAddress.DEFAULT_DEVICE_ID, plaintext));
messages.add(getEncryptedMessage(socket, recipient, TextSecureAddress.DEFAULT_DEVICE_ID, plaintext, legacy));
}
for (int deviceId : store.getSubDeviceSessions(recipient.getNumber())) {
messages.add(getEncryptedMessage(socket, recipient, deviceId, plaintext));
messages.add(getEncryptedMessage(socket, recipient, deviceId, plaintext, legacy));
}
return new OutgoingPushMessageList(recipient.getNumber(), timestamp, recipient.getRelay().orNull(), messages);
}
private OutgoingPushMessage getEncryptedMessage(PushServiceSocket socket, TextSecureAddress recipient, int deviceId, byte[] plaintext)
private OutgoingPushMessage getEncryptedMessage(PushServiceSocket socket, TextSecureAddress recipient, int deviceId, byte[] plaintext, boolean legacy)
throws IOException, UntrustedIdentityException
{
AxolotlAddress axolotlAddress = new AxolotlAddress(recipient.getNumber(), deviceId);
@ -358,7 +375,7 @@ public class TextSecureMessageSender {
}
}
return cipher.encrypt(axolotlAddress, plaintext);
return cipher.encrypt(axolotlAddress, plaintext, legacy);
}
private void handleMismatchedDevices(PushServiceSocket socket, TextSecureAddress recipient,

View File

@ -28,27 +28,33 @@ import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.NoSessionException;
import org.whispersystems.libaxolotl.SessionCipher;
import org.whispersystems.libaxolotl.UntrustedIdentityException;
import org.whispersystems.libaxolotl.logging.Log;
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
import org.whispersystems.libaxolotl.protocol.WhisperMessage;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer;
import org.whispersystems.textsecure.api.messages.TextSecureContent;
import org.whispersystems.textsecure.api.messages.TextSecureEnvelope;
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
import org.whispersystems.textsecure.api.messages.TextSecureSyncContext;
import org.whispersystems.textsecure.api.messages.TextSecureDataMessage;
import org.whispersystems.textsecure.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.textsecure.api.messages.multidevice.TextSecureSyncMessage;
import org.whispersystems.textsecure.api.push.TextSecureAddress;
import org.whispersystems.textsecure.internal.push.OutgoingPushMessage;
import org.whispersystems.textsecure.internal.push.PushTransportDetails;
import org.whispersystems.textsecure.internal.push.TextSecureProtos.AttachmentPointer;
import org.whispersystems.textsecure.internal.push.TextSecureProtos.Content;
import org.whispersystems.textsecure.internal.push.TextSecureProtos.DataMessage;
import org.whispersystems.textsecure.internal.push.TextSecureProtos.Envelope.Type;
import org.whispersystems.textsecure.internal.push.TextSecureProtos.SyncMessage;
import org.whispersystems.textsecure.internal.util.Base64;
import java.util.LinkedList;
import java.util.List;
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.IncomingPushMessageSignal.Type;
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent;
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.GroupContext.Type.DELIVER;
import static org.whispersystems.textsecure.internal.push.TextSecureProtos.GroupContext.Type.DELIVER;
/**
* This is used to decrypt received {@link org.whispersystems.textsecure.api.messages.TextSecureEnvelope}s.
@ -57,6 +63,8 @@ import static org.whispersystems.textsecure.internal.push.PushMessageProtos.Push
*/
public class TextSecureCipher {
private static final String TAG = TextSecureCipher.class.getSimpleName();
private final AxolotlStore axolotlStore;
private final TextSecureAddress localAddress;
@ -65,7 +73,7 @@ public class TextSecureCipher {
this.localAddress = localAddress;
}
public OutgoingPushMessage encrypt(AxolotlAddress destination, byte[] unpaddedMessage) {
public OutgoingPushMessage encrypt(AxolotlAddress destination, byte[] unpaddedMessage, boolean legacy) {
SessionCipher sessionCipher = new SessionCipher(axolotlStore, destination);
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion());
CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
@ -80,7 +88,8 @@ public class TextSecureCipher {
default: throw new AssertionError("Bad type: " + message.getType());
}
return new OutgoingPushMessage(type, destination.getDeviceId(), remoteRegistrationId, body);
return new OutgoingPushMessage(type, destination.getDeviceId(), remoteRegistrationId,
legacy ? body : null, legacy ? null : body);
}
/**
@ -97,60 +106,83 @@ public class TextSecureCipher {
* @throws LegacyMessageException
* @throws NoSessionException
*/
public TextSecureMessage decrypt(TextSecureEnvelope envelope)
public TextSecureContent decrypt(TextSecureEnvelope envelope)
throws InvalidVersionException, InvalidMessageException, InvalidKeyException,
DuplicateMessageException, InvalidKeyIdException, UntrustedIdentityException,
LegacyMessageException, NoSessionException
{
try {
AxolotlAddress sourceAddress = new AxolotlAddress(envelope.getSource(), envelope.getSourceDevice());
SessionCipher sessionCipher = new SessionCipher(axolotlStore, sourceAddress);
TextSecureContent content = new TextSecureContent();
byte[] paddedMessage;
if (envelope.hasLegacyMessage()) {
DataMessage message = DataMessage.parseFrom(decrypt(envelope, envelope.getLegacyMessage()));
content = new TextSecureContent(createTextSecureMessage(envelope, message));
} else if (envelope.hasContent()) {
Content message = Content.parseFrom(decrypt(envelope, envelope.getContent()));
if (envelope.isPreKeyWhisperMessage()) {
paddedMessage = sessionCipher.decrypt(new PreKeyWhisperMessage(envelope.getMessage()));
} else if (envelope.isWhisperMessage()) {
paddedMessage = sessionCipher.decrypt(new WhisperMessage(envelope.getMessage()));
} else {
throw new InvalidMessageException("Unknown type: " + envelope.getType());
if (message.hasDataMessage()) {
content = new TextSecureContent(createTextSecureMessage(envelope, message.getDataMessage()));
} else if (message.hasSyncMessage() && localAddress.getNumber().equals(envelope.getSource())) {
content = new TextSecureContent(createSynchronizeMessage(envelope, message.getSyncMessage()));
}
}
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion());
PushMessageContent content = PushMessageContent.parseFrom(transportDetails.getStrippedPaddingMessageBody(paddedMessage));
return createTextSecureMessage(envelope, content);
return content;
} catch (InvalidProtocolBufferException e) {
throw new InvalidMessageException(e);
}
}
private TextSecureMessage createTextSecureMessage(TextSecureEnvelope envelope, PushMessageContent content) {
TextSecureGroup groupInfo = createGroupInfo(envelope, content);
TextSecureSyncContext syncContext = createSyncContext(envelope, content);
List<TextSecureAttachment> attachments = new LinkedList<>();
boolean endSession = ((content.getFlags() & PushMessageContent.Flags.END_SESSION_VALUE) != 0);
private byte[] decrypt(TextSecureEnvelope envelope, byte[] ciphertext)
throws InvalidVersionException, InvalidMessageException, InvalidKeyException,
DuplicateMessageException, InvalidKeyIdException, UntrustedIdentityException,
LegacyMessageException, NoSessionException
{
AxolotlAddress sourceAddress = new AxolotlAddress(envelope.getSource(), envelope.getSourceDevice());
SessionCipher sessionCipher = new SessionCipher(axolotlStore, sourceAddress);
for (PushMessageContent.AttachmentPointer pointer : content.getAttachmentsList()) {
byte[] paddedMessage;
if (envelope.isPreKeyWhisperMessage()) {
paddedMessage = sessionCipher.decrypt(new PreKeyWhisperMessage(ciphertext));
} else if (envelope.isWhisperMessage()) {
paddedMessage = sessionCipher.decrypt(new WhisperMessage(ciphertext));
} else {
throw new InvalidMessageException("Unknown type: " + envelope.getType());
}
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion());
return transportDetails.getStrippedPaddingMessageBody(paddedMessage);
}
private TextSecureDataMessage createTextSecureMessage(TextSecureEnvelope envelope, DataMessage content) {
TextSecureGroup groupInfo = createGroupInfo(envelope, content);
List<TextSecureAttachment> attachments = new LinkedList<>();
boolean endSession = ((content.getFlags() & DataMessage.Flags.END_SESSION_VALUE) != 0);
for (AttachmentPointer pointer : content.getAttachmentsList()) {
attachments.add(new TextSecureAttachmentPointer(pointer.getId(),
pointer.getContentType(),
pointer.getKey().toByteArray(),
envelope.getRelay()));
}
return new TextSecureMessage(envelope.getTimestamp(), groupInfo, attachments,
content.getBody(), syncContext, endSession);
return new TextSecureDataMessage(envelope.getTimestamp(), groupInfo, attachments,
content.getBody(), endSession);
}
private TextSecureSyncContext createSyncContext(TextSecureEnvelope envelope, PushMessageContent content) {
if (!content.hasSync()) return null;
if (!envelope.getSource().equals(localAddress.getNumber())) return null;
private TextSecureSyncMessage createSynchronizeMessage(TextSecureEnvelope envelope, SyncMessage content) {
if (content.hasSent()) {
SyncMessage.Sent sentContent = content.getSent();
return new TextSecureSyncMessage(new SentTranscriptMessage(sentContent.getDestination(),
sentContent.getTimestamp(),
createTextSecureMessage(envelope, sentContent.getMessage())));
}
return new TextSecureSyncContext(content.getSync().getDestination(),
content.getSync().getTimestamp());
return new TextSecureSyncMessage();
}
private TextSecureGroup createGroupInfo(TextSecureEnvelope envelope, PushMessageContent content) {
private TextSecureGroup createGroupInfo(TextSecureEnvelope envelope, DataMessage content) {
if (!content.hasGroup()) return null;
TextSecureGroup.Type type;

View File

@ -0,0 +1,33 @@
package org.whispersystems.textsecure.api.messages;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.messages.multidevice.TextSecureSyncMessage;
public class TextSecureContent {
private final Optional<TextSecureDataMessage> message;
private final Optional<TextSecureSyncMessage> synchronizeMessage;
public TextSecureContent() {
this.message = Optional.absent();
this.synchronizeMessage = Optional.absent();
}
public TextSecureContent(TextSecureDataMessage message) {
this.message = Optional.fromNullable(message);
this.synchronizeMessage = Optional.absent();
}
public TextSecureContent(TextSecureSyncMessage synchronizeMessage) {
this.message = Optional.absent();
this.synchronizeMessage = Optional.fromNullable(synchronizeMessage);
}
public Optional<TextSecureDataMessage> getDataMessage() {
return message;
}
public Optional<TextSecureSyncMessage> getSyncMessage() {
return synchronizeMessage;
}
}

View File

@ -24,13 +24,12 @@ import java.util.List;
/**
* Represents a decrypted text secure message.
*/
public class TextSecureMessage {
public class TextSecureDataMessage {
private final long timestamp;
private final Optional<List<TextSecureAttachment>> attachments;
private final Optional<String> body;
private final Optional<TextSecureGroup> group;
private final Optional<TextSecureSyncContext> syncContext;
private final boolean endSession;
/**
@ -39,11 +38,11 @@ public class TextSecureMessage {
* @param timestamp The sent timestamp.
* @param body The message contents.
*/
public TextSecureMessage(long timestamp, String body) {
public TextSecureDataMessage(long timestamp, String body) {
this(timestamp, (List<TextSecureAttachment>)null, body);
}
public TextSecureMessage(final long timestamp, final TextSecureAttachment attachment, final String body) {
public TextSecureDataMessage(final long timestamp, final TextSecureAttachment attachment, final String body) {
this(timestamp, new LinkedList<TextSecureAttachment>() {{add(attachment);}}, body);
}
@ -54,7 +53,7 @@ public class TextSecureMessage {
* @param attachments The attachments.
* @param body The message contents.
*/
public TextSecureMessage(long timestamp, List<TextSecureAttachment> attachments, String body) {
public TextSecureDataMessage(long timestamp, List<TextSecureAttachment> attachments, String body) {
this(timestamp, null, attachments, body);
}
@ -66,8 +65,8 @@ public class TextSecureMessage {
* @param attachments The attachments.
* @param body The message contents.
*/
public TextSecureMessage(long timestamp, TextSecureGroup group, List<TextSecureAttachment> attachments, String body) {
this(timestamp, group, attachments, body, null, false);
public TextSecureDataMessage(long timestamp, TextSecureGroup group, List<TextSecureAttachment> attachments, String body) {
this(timestamp, group, attachments, body, false);
}
/**
@ -79,11 +78,10 @@ public class TextSecureMessage {
* @param body The message contents.
* @param endSession Flag indicating whether this message should close a session.
*/
public TextSecureMessage(long timestamp, TextSecureGroup group, List<TextSecureAttachment> attachments, String body, TextSecureSyncContext syncContext, boolean endSession) {
public TextSecureDataMessage(long timestamp, TextSecureGroup group, List<TextSecureAttachment> attachments, String body, boolean endSession) {
this.timestamp = timestamp;
this.body = Optional.fromNullable(body);
this.group = Optional.fromNullable(group);
this.syncContext = Optional.fromNullable(syncContext);
this.endSession = endSession;
if (attachments != null && !attachments.isEmpty()) {
@ -125,10 +123,6 @@ public class TextSecureMessage {
return group;
}
public Optional<TextSecureSyncContext> getSyncContext() {
return syncContext;
}
public boolean isEndSession() {
return endSession;
}
@ -182,9 +176,9 @@ public class TextSecureMessage {
return this;
}
public TextSecureMessage build() {
public TextSecureDataMessage build() {
if (timestamp == 0) timestamp = System.currentTimeMillis();
return new TextSecureMessage(timestamp, group, attachments, body, null, endSession);
return new TextSecureDataMessage(timestamp, group, attachments, body, endSession);
}
}
}

View File

@ -22,7 +22,7 @@ import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.libaxolotl.logging.Log;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.push.TextSecureAddress;
import org.whispersystems.textsecure.internal.push.PushMessageProtos.IncomingPushMessageSignal;
import org.whispersystems.textsecure.internal.push.TextSecureProtos.Envelope;
import org.whispersystems.textsecure.internal.util.Base64;
import org.whispersystems.textsecure.internal.util.Hex;
@ -63,7 +63,7 @@ public class TextSecureEnvelope {
private static final int IV_LENGTH = 16;
private static final int CIPHERTEXT_OFFSET = IV_OFFSET + IV_LENGTH;
private final IncomingPushMessageSignal signal;
private final Envelope envelope;
/**
* Construct an envelope from a serialized, Base64 encoded TextSecureEnvelope, encrypted
@ -99,42 +99,46 @@ public class TextSecureEnvelope {
verifyMac(ciphertext, macKey);
this.signal = IncomingPushMessageSignal.parseFrom(getPlaintext(ciphertext, cipherKey));
this.envelope = Envelope.parseFrom(getPlaintext(ciphertext, cipherKey));
}
public TextSecureEnvelope(int type, String source, int sourceDevice,
String relay, long timestamp, byte[] message)
String relay, long timestamp,
byte[] legacyMessage, byte[] content)
{
this.signal = IncomingPushMessageSignal.newBuilder()
.setType(IncomingPushMessageSignal.Type.valueOf(type))
.setSource(source)
.setSourceDevice(sourceDevice)
.setRelay(relay)
.setTimestamp(timestamp)
.setMessage(ByteString.copyFrom(message))
.build();
Envelope.Builder builder = Envelope.newBuilder()
.setType(Envelope.Type.valueOf(type))
.setSource(source)
.setSourceDevice(sourceDevice)
.setRelay(relay)
.setTimestamp(timestamp);
if (legacyMessage != null) builder.setLegacyMessage(ByteString.copyFrom(legacyMessage));
if (content != null) builder.setContent(ByteString.copyFrom(content));
this.envelope = builder.build();
}
/**
* @return The envelope's sender.
*/
public String getSource() {
return signal.getSource();
return envelope.getSource();
}
/**
* @return The envelope's sender device ID.
*/
public int getSourceDevice() {
return signal.getSourceDevice();
return envelope.getSourceDevice();
}
/**
* @return The envelope's sender as a TextSecureAddress.
*/
public TextSecureAddress getSourceAddress() {
return new TextSecureAddress(signal.getSource(),
signal.hasRelay() ? Optional.fromNullable(signal.getRelay()) :
return new TextSecureAddress(envelope.getSource(),
envelope.hasRelay() ? Optional.fromNullable(envelope.getRelay()) :
Optional.<String>absent());
}
@ -142,49 +146,70 @@ public class TextSecureEnvelope {
* @return The envelope content type.
*/
public int getType() {
return signal.getType().getNumber();
return envelope.getType().getNumber();
}
/**
* @return The federated server this envelope came from.
*/
public String getRelay() {
return signal.getRelay();
return envelope.getRelay();
}
/**
* @return The timestamp this envelope was sent.
*/
public long getTimestamp() {
return signal.getTimestamp();
return envelope.getTimestamp();
}
/**
* @return The envelope's containing message.
* @return Whether the envelope contains a TextSecureDataMessage
*/
public byte[] getMessage() {
return signal.getMessage().toByteArray();
public boolean hasLegacyMessage() {
return envelope.hasLegacyMessage();
}
/**
* @return The envelope's containing TextSecure message.
*/
public byte[] getLegacyMessage() {
return envelope.getLegacyMessage().toByteArray();
}
/**
* @return Whether the envelope contains an encrypted TextSecureContent
*/
public boolean hasContent() {
return envelope.hasContent();
}
/**
* @return The envelope's encrypted TextSecureContent.
*/
public byte[] getContent() {
return envelope.getContent().toByteArray();
}
/**
* @return true if the containing message is a {@link org.whispersystems.libaxolotl.protocol.WhisperMessage}
*/
public boolean isWhisperMessage() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.CIPHERTEXT_VALUE;
return envelope.getType().getNumber() == Envelope.Type.CIPHERTEXT_VALUE;
}
/**
* @return true if the containing message is a {@link org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage}
*/
public boolean isPreKeyWhisperMessage() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.PREKEY_BUNDLE_VALUE;
return envelope.getType().getNumber() == Envelope.Type.PREKEY_BUNDLE_VALUE;
}
/**
* @return true if the containing message is a delivery receipt.
*/
public boolean isReceipt() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.RECEIPT_VALUE;
return envelope.getType().getNumber() == Envelope.Type.RECEIPT_VALUE;
}
private byte[] getPlaintext(byte[] ciphertext, SecretKeySpec cipherKey) throws IOException {

View File

@ -1,20 +0,0 @@
package org.whispersystems.textsecure.api.messages;
public class TextSecureSyncContext {
private final String destination;
private final long timestamp;
public TextSecureSyncContext(String destination, long timestamp) {
this.destination = destination;
this.timestamp = timestamp;
}
public String getDestination() {
return destination;
}
public long getTimestamp() {
return timestamp;
}
}

View File

@ -0,0 +1,30 @@
package org.whispersystems.textsecure.api.messages.multidevice;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentStream;
public class DeviceContact {
private final String number;
private final Optional<String> name;
private final Optional<TextSecureAttachmentStream> avatar;
public DeviceContact(String number, Optional<String> name, Optional<TextSecureAttachmentStream> avatar) {
this.number = number;
this.name = name;
this.avatar = avatar;
}
public Optional<TextSecureAttachmentStream> getAvatar() {
return avatar;
}
public Optional<String> getName() {
return name;
}
public String getNumber() {
return number;
}
}

View File

@ -0,0 +1,121 @@
package org.whispersystems.textsecure.api.messages.multidevice;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentStream;
import org.whispersystems.textsecure.internal.push.TextSecureProtos;
import org.whispersystems.textsecure.internal.util.Util;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
public class DeviceContactsInputStream {
private final InputStream in;
public DeviceContactsInputStream(InputStream in) {
this.in = in;
}
public DeviceContact read() throws IOException {
long detailsLength = readRawVarint64();
byte[] detailsSerialized = new byte[(int)detailsLength];
Util.readFully(in, detailsSerialized);
TextSecureProtos.ContactDetails details = TextSecureProtos.ContactDetails.parseFrom(detailsSerialized);
String number = details.getNumber();
Optional<String> name = Optional.fromNullable(details.getName());
Optional<TextSecureAttachmentStream> avatar = Optional.absent();
if (details.hasAvatar()) {
long avatarLength = details.getAvatar().getLength();
InputStream avatarStream = new LimitedInputStream(in, avatarLength);
String avatarContentType = details.getAvatar().getContentType();
avatar = Optional.of(new TextSecureAttachmentStream(avatarStream, avatarContentType, avatarLength));
}
return new DeviceContact(number, name, avatar);
}
private long readRawVarint64() throws IOException {
int shift = 0;
long result = 0;
while (shift < 64) {
final byte b = (byte)in.read();
result |= (long)(b & 0x7F) << shift;
if ((b & 0x80) == 0) {
return result;
}
shift += 7;
}
throw new IOException("Malformed varint!");
}
private static final class LimitedInputStream extends FilterInputStream {
private long left;
private long mark = -1;
LimitedInputStream(InputStream in, long limit) {
super(in);
left = limit;
}
@Override public int available() throws IOException {
return (int) Math.min(in.available(), left);
}
// it's okay to mark even if mark isn't supported, as reset won't work
@Override public synchronized void mark(int readLimit) {
in.mark(readLimit);
mark = left;
}
@Override public int read() throws IOException {
if (left == 0) {
return -1;
}
int result = in.read();
if (result != -1) {
--left;
}
return result;
}
@Override public int read(byte[] b, int off, int len) throws IOException {
if (left == 0) {
return -1;
}
len = (int) Math.min(len, left);
int result = in.read(b, off, len);
if (result != -1) {
left -= result;
}
return result;
}
@Override public synchronized void reset() throws IOException {
if (!in.markSupported()) {
throw new IOException("Mark not supported");
}
if (mark == -1) {
throw new IOException("Mark not set");
}
in.reset();
left = mark;
}
@Override public long skip(long n) throws IOException {
n = Math.min(n, left);
long skipped = in.skip(n);
left -= skipped;
return skipped;
}
}
}

View File

@ -0,0 +1,64 @@
package org.whispersystems.textsecure.api.messages.multidevice;
import org.whispersystems.textsecure.internal.push.TextSecureProtos;
import org.whispersystems.textsecure.internal.util.Util;
import java.io.IOException;
import java.io.OutputStream;
public class DeviceContactsOutputStream {
private final OutputStream out;
public DeviceContactsOutputStream(OutputStream out) {
this.out = out;
}
public void write(DeviceContact contact) throws IOException {
writeContactDetails(contact);
writeAvatarImage(contact);
}
public void close() throws IOException {
out.close();
}
private void writeAvatarImage(DeviceContact contact) throws IOException {
if (contact.getAvatar().isPresent()) {
Util.copy(contact.getAvatar().get().getInputStream(), out);
}
}
private void writeContactDetails(DeviceContact contact) throws IOException {
TextSecureProtos.ContactDetails.Builder contactDetails = TextSecureProtos.ContactDetails.newBuilder();
contactDetails.setNumber(contact.getNumber());
if (contact.getName().isPresent()) {
contactDetails.setName(contact.getName().get());
}
if (contact.getAvatar().isPresent()) {
TextSecureProtos.ContactDetails.Avatar.Builder avatarBuilder = TextSecureProtos.ContactDetails.Avatar.newBuilder();
avatarBuilder.setContentType(contact.getAvatar().get().getContentType());
avatarBuilder.setLength(contact.getAvatar().get().getLength());
}
byte[] serializedContactDetails = contactDetails.build().toByteArray();
writeVarint64(serializedContactDetails.length);
out.write(serializedContactDetails);
}
public void writeVarint64(long value) throws IOException {
while (true) {
if ((value & ~0x7FL) == 0) {
out.write((int) value);
return;
} else {
out.write(((int) value & 0x7F) | 0x80);
value >>>= 7;
}
}
}
}

View File

@ -0,0 +1,35 @@
package org.whispersystems.textsecure.api.messages.multidevice;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.messages.TextSecureDataMessage;
public class SentTranscriptMessage {
private final Optional<String> destination;
private final long timestamp;
private final TextSecureDataMessage message;
public SentTranscriptMessage(String destination, long timestamp, TextSecureDataMessage message) {
this.destination = Optional.of(destination);
this.timestamp = timestamp;
this.message = message;
}
public SentTranscriptMessage(long timestamp, TextSecureDataMessage message) {
this.destination = Optional.absent();
this.timestamp = timestamp;
this.message = message;
}
public Optional<String> getDestination() {
return destination;
}
public long getTimestamp() {
return timestamp;
}
public TextSecureDataMessage getMessage() {
return message;
}
}

View File

@ -0,0 +1,49 @@
package org.whispersystems.textsecure.api.messages.multidevice;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
public class TextSecureSyncMessage {
private final Optional<SentTranscriptMessage> sent;
private final Optional<TextSecureAttachment> contacts;
private final Optional<TextSecureGroup> group;
public TextSecureSyncMessage() {
this.sent = Optional.absent();
this.contacts = Optional.absent();
this.group = Optional.absent();
}
public TextSecureSyncMessage(SentTranscriptMessage sent) {
this.sent = Optional.of(sent);
this.contacts = Optional.absent();
this.group = Optional.absent();
}
public TextSecureSyncMessage(TextSecureAttachment contacts) {
this.contacts = Optional.of(contacts);
this.sent = Optional.absent();
this.group = Optional.absent();
}
public TextSecureSyncMessage(TextSecureGroup group) {
this.group = Optional.of(group);
this.sent = Optional.absent();
this.contacts = Optional.absent();
}
public Optional<SentTranscriptMessage> getSent() {
return sent;
}
public Optional<TextSecureGroup> getGroup() {
return group;
}
public Optional<TextSecureAttachment> getContacts() {
return contacts;
}
}

View File

@ -32,31 +32,18 @@ public class OutgoingPushMessage {
private int destinationRegistrationId;
@JsonProperty
private String body;
@JsonProperty
private String content;
public OutgoingPushMessage(int type,
int destinationDeviceId,
int destinationRegistrationId,
String body)
String legacyMessage, String content)
{
this.type = type;
this.destinationDeviceId = destinationDeviceId;
this.destinationRegistrationId = destinationRegistrationId;
this.body = body;
}
public int getDestinationDeviceId() {
return destinationDeviceId;
}
public String getBody() {
return body;
}
public int getType() {
return type;
}
public int getDestinationRegistrationId() {
return destinationRegistrationId;
this.body = legacyMessage;
this.content = content;
}
}

View File

@ -22,6 +22,9 @@ public class TextSecureEnvelopeEntity {
@JsonProperty
private byte[] message;
@JsonProperty
private byte[] content;
public TextSecureEnvelopeEntity() {}
public int getType() {
@ -47,4 +50,8 @@ public class TextSecureEnvelopeEntity {
public byte[] getMessage() {
return message;
}
public byte[] getContent() {
return content;
}
}

View File

@ -125,4 +125,18 @@ public class Util {
}
}
public static byte[] toVarint64(long value) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
while (true) {
if ((value & ~0x7FL) == 0) {
out.write((int) value);
return out.toByteArray();
} else {
out.write(((int) value & 0x7F) | 0x80);
value >>>= 7;
}
}
}
}

View File

@ -1,59 +0,0 @@
package textsecure;
option java_package = "org.whispersystems.textsecure.internal.push";
option java_outer_classname = "PushMessageProtos";
message IncomingPushMessageSignal {
enum Type {
UNKNOWN = 0;
CIPHERTEXT = 1;
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
// PLAINTEXT = 4; // No longer supported
RECEIPT = 5;
}
optional Type type = 1;
optional string source = 2;
optional uint32 sourceDevice = 7;
optional string relay = 3;
optional uint64 timestamp = 5;
optional bytes message = 6; // Contains an encrypted PushMessageContent
// repeated string destinations = 4; // No longer supported
}
message PushMessageContent {
message AttachmentPointer {
optional fixed64 id = 1;
optional string contentType = 2;
optional bytes key = 3;
}
message GroupContext {
enum Type {
UNKNOWN = 0;
UPDATE = 1;
DELIVER = 2;
QUIT = 3;
}
optional bytes id = 1;
optional Type type = 2;
optional string name = 3;
repeated string members = 4;
optional AttachmentPointer avatar = 5;
}
message SyncMessageContext {
optional string destination = 1;
optional uint64 timestamp = 2;
}
enum Flags {
END_SESSION = 1;
}
optional string body = 1;
repeated AttachmentPointer attachments = 2;
optional GroupContext group = 3;
optional uint32 flags = 4;
optional SyncMessageContext sync = 5;
}

View File

@ -1,3 +1,3 @@
all:
protoc --java_out=../java/src/main/java/ IncomingPushMessageSignal.proto Provisioning.proto WebSocketResources.proto
protoc --java_out=../java/src/main/java/ TextSecure.proto Provisioning.proto WebSocketResources.proto

89
protobuf/TextSecure.proto Normal file
View File

@ -0,0 +1,89 @@
package textsecure;
option java_package = "org.whispersystems.textsecure.internal.push";
option java_outer_classname = "TextSecureProtos";
message Envelope {
enum Type {
UNKNOWN = 0;
CIPHERTEXT = 1;
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
RECEIPT = 5;
}
optional Type type = 1;
optional string source = 2;
optional uint32 sourceDevice = 7;
optional string relay = 3;
optional uint64 timestamp = 5;
optional bytes legacyMessage = 6; // Contains an encrypted DataMessage
optional bytes content = 8; // Contains an encrypted Content
}
message Content {
optional DataMessage dataMessage = 1;
optional SyncMessage syncMessage = 2;
}
message DataMessage {
enum Flags {
END_SESSION = 1;
}
optional string body = 1;
repeated AttachmentPointer attachments = 2;
optional GroupContext group = 3;
optional uint32 flags = 4;
}
message SyncMessage {
message Sent {
optional string destination = 1;
optional uint64 timestamp = 2;
optional DataMessage message = 3;
}
message Contacts {
optional AttachmentPointer blob = 1;
}
message Group {
optional GroupContext group = 1;
}
optional Sent sent = 1;
optional Contacts contacts = 2;
optional Group group = 3;
}
message AttachmentPointer {
optional fixed64 id = 1;
optional string contentType = 2;
optional bytes key = 3;
}
message GroupContext {
enum Type {
UNKNOWN = 0;
UPDATE = 1;
DELIVER = 2;
QUIT = 3;
}
optional bytes id = 1;
optional Type type = 2;
optional string name = 3;
repeated string members = 4;
optional AttachmentPointer avatar = 5;
}
message ContactDetails {
message Avatar {
optional string contentType = 1;
optional uint64 length = 2;
}
optional string number = 1;
optional string name = 2;
optional Avatar avatar = 3;
}