Support for group sync messages and requests.

// FREEBIE
This commit is contained in:
Moxie Marlinspike 2015-06-22 14:26:38 -07:00
parent d044a11bc0
commit a20818f018
14 changed files with 2186 additions and 458 deletions

View File

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

View File

@ -32,6 +32,7 @@ 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.multidevice.TextSecureSyncMessage;
import org.whispersystems.textsecure.api.push.TextSecureAddress;
import org.whispersystems.textsecure.api.push.TrustStore;
import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions;
@ -122,7 +123,7 @@ public class TextSecureMessageSender {
SendMessageResponse response = sendMessage(recipient, timestamp, content, true);
if (response != null && response.getNeedsSync()) {
byte[] syncMessage = createSentTranscriptMessage(content, Optional.of(recipient), timestamp);
byte[] syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.of(recipient), timestamp);
sendMessage(localAddress, timestamp, syncMessage, false);
}
@ -152,7 +153,7 @@ public class TextSecureMessageSender {
try {
if (response != null && response.getNeedsSync()) {
byte[] syncMessage = createSentTranscriptMessage(content, Optional.<TextSecureAddress>absent(), timestamp);
byte[] syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.<TextSecureAddress>absent(), timestamp);
sendMessage(localAddress, timestamp, syncMessage, false);
}
} catch (UntrustedIdentityException e) {
@ -160,10 +161,19 @@ public class TextSecureMessageSender {
}
}
public void sendMultiDeviceContactsUpdate(TextSecureAttachmentStream contacts)
public void sendMessage(TextSecureSyncMessage message)
throws IOException, UntrustedIdentityException
{
byte[] content = createMultiDeviceContactsContent(contacts);
byte[] content;
if (message.getContacts().isPresent()) {
content = createMultiDeviceContactsContent(message.getContacts().get().asStream());
} else if (message.getGroups().isPresent()) {
content = createMultiDeviceGroupsContent(message.getGroups().get().asStream());
} else {
throw new IOException("Unsupported sync message!");
}
sendMessage(localAddress, System.currentTimeMillis(), content, false);
}
@ -199,7 +209,16 @@ public class TextSecureMessageSender {
return container.setSyncMessage(builder).build().toByteArray();
}
private byte[] createSentTranscriptMessage(byte[] content, Optional<TextSecureAddress> recipient, long timestamp) {
private byte[] createMultiDeviceGroupsContent(TextSecureAttachmentStream groups) throws IOException {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder builder = SyncMessage.newBuilder();
builder.setGroups(SyncMessage.Groups.newBuilder()
.setBlob(createAttachmentPointer(groups)));
return container.setSyncMessage(builder).build().toByteArray();
}
private byte[] createMultiDeviceSentTranscriptContent(byte[] content, Optional<TextSecureAddress> recipient, long timestamp) {
try {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder syncMessage = SyncMessage.newBuilder();

View File

@ -28,7 +28,6 @@ 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;
@ -36,9 +35,9 @@ 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.TextSecureDataMessage;
import org.whispersystems.textsecure.api.messages.TextSecureEnvelope;
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
import org.whispersystems.textsecure.api.messages.TextSecureDataMessage;
import org.whispersystems.textsecure.api.messages.multidevice.RequestMessage;
import org.whispersystems.textsecure.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.textsecure.api.messages.multidevice.TextSecureSyncMessage;
@ -175,16 +174,16 @@ public class TextSecureCipher {
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 TextSecureSyncMessage.forSentTranscript(new SentTranscriptMessage(sentContent.getDestination(),
sentContent.getTimestamp(),
createTextSecureMessage(envelope, sentContent.getMessage())));
}
if (content.hasRequest()) {
return new TextSecureSyncMessage(new RequestMessage(content.getRequest()));
return TextSecureSyncMessage.forRequest(new RequestMessage(content.getRequest()));
}
return new TextSecureSyncMessage();
return TextSecureSyncMessage.empty();
}
private TextSecureGroup createGroupInfo(TextSecureEnvelope envelope, DataMessage content) {

View File

@ -0,0 +1,116 @@
package org.whispersystems.textsecure.api.messages.multidevice;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
public class ChunkedInputStream {
protected final InputStream in;
public ChunkedInputStream(InputStream in) {
this.in = in;
}
protected int readRawVarint32() throws IOException {
byte tmp = (byte)in.read();
if (tmp >= 0) {
return tmp;
}
int result = tmp & 0x7f;
if ((tmp = (byte)in.read()) >= 0) {
result |= tmp << 7;
} else {
result |= (tmp & 0x7f) << 7;
if ((tmp = (byte)in.read()) >= 0) {
result |= tmp << 14;
} else {
result |= (tmp & 0x7f) << 14;
if ((tmp = (byte)in.read()) >= 0) {
result |= tmp << 21;
} else {
result |= (tmp & 0x7f) << 21;
result |= (tmp = (byte)in.read()) << 28;
if (tmp < 0) {
// Discard upper 32 bits.
for (int i = 0; i < 5; i++) {
if ((byte)in.read() >= 0) {
return result;
}
}
throw new IOException("Malformed varint!");
}
}
}
}
return result;
}
protected 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,38 @@
package org.whispersystems.textsecure.api.messages.multidevice;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class ChunkedOutputStream {
protected final OutputStream out;
public ChunkedOutputStream(OutputStream out) {
this.out = out;
}
protected void writeVarint32(int value) throws IOException {
while (true) {
if ((value & ~0x7F) == 0) {
out.write(value);
return;
} else {
out.write((value & 0x7F) | 0x80);
value >>>= 7;
}
}
}
protected void writeStream(InputStream in) throws IOException {
byte[] buffer = new byte[4096];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
in.close();
}
}

View File

@ -5,16 +5,13 @@ 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 class DeviceContactsInputStream extends ChunkedInputStream {
public DeviceContactsInputStream(InputStream in) {
this.in = in;
super(in);
}
public DeviceContact read() throws IOException {
@ -38,105 +35,4 @@ public class DeviceContactsInputStream {
return new DeviceContact(number, name, avatar);
}
public int readRawVarint32() throws IOException {
byte tmp = (byte)in.read();
if (tmp >= 0) {
return tmp;
}
int result = tmp & 0x7f;
if ((tmp = (byte)in.read()) >= 0) {
result |= tmp << 7;
} else {
result |= (tmp & 0x7f) << 7;
if ((tmp = (byte)in.read()) >= 0) {
result |= tmp << 14;
} else {
result |= (tmp & 0x7f) << 14;
if ((tmp = (byte)in.read()) >= 0) {
result |= tmp << 21;
} else {
result |= (tmp & 0x7f) << 21;
result |= (tmp = (byte)in.read()) << 28;
if (tmp < 0) {
// Discard upper 32 bits.
for (int i = 0; i < 5; i++) {
if ((byte)in.read() >= 0) {
return result;
}
}
throw new IOException("Malformed varint!");
}
}
}
}
return result;
}
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

@ -1,18 +1,14 @@
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.InputStream;
import java.io.OutputStream;
public class DeviceContactsOutputStream {
private final OutputStream out;
public class DeviceContactsOutputStream extends ChunkedOutputStream {
public DeviceContactsOutputStream(OutputStream out) {
this.out = out;
super(out);
}
public void write(DeviceContact contact) throws IOException {
@ -26,16 +22,7 @@ public class DeviceContactsOutputStream {
private void writeAvatarImage(DeviceContact contact) throws IOException {
if (contact.getAvatar().isPresent()) {
InputStream in = contact.getAvatar().get().getInputStream();
byte[] buffer = new byte[4096];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
in.close();
writeStream(contact.getAvatar().get().getInputStream());
}
}
@ -50,7 +37,7 @@ public class DeviceContactsOutputStream {
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());
avatarBuilder.setLength((int)contact.getAvatar().get().getLength());
contactDetails.setAvatar(avatarBuilder);
}
@ -60,16 +47,4 @@ public class DeviceContactsOutputStream {
out.write(serializedContactDetails);
}
private void writeVarint32(int value) throws IOException {
while (true) {
if ((value & ~0x7F) == 0) {
out.write(value);
return;
} else {
out.write((value & 0x7F) | 0x80);
value >>>= 7;
}
}
}
}

View File

@ -0,0 +1,38 @@
package org.whispersystems.textsecure.api.messages.multidevice;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentStream;
import java.util.List;
public class DeviceGroup {
private final byte[] id;
private final Optional<String> name;
private final List<String> members;
private final Optional<TextSecureAttachmentStream> avatar;
public DeviceGroup(byte[] id, Optional<String> name, List<String> members, Optional<TextSecureAttachmentStream> avatar) {
this.id = id;
this.name = name;
this.members = members;
this.avatar = avatar;
}
public Optional<TextSecureAttachmentStream> getAvatar() {
return avatar;
}
public Optional<String> getName() {
return name;
}
public byte[] getId() {
return id;
}
public List<String> getMembers() {
return members;
}
}

View File

@ -0,0 +1,45 @@
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.IOException;
import java.io.InputStream;
import java.util.List;
public class DeviceGroupsInputStream extends ChunkedInputStream{
public DeviceGroupsInputStream(InputStream in) {
super(in);
}
public DeviceGroup read() throws IOException {
long detailsLength = readRawVarint32();
byte[] detailsSerialized = new byte[(int)detailsLength];
Util.readFully(in, detailsSerialized);
TextSecureProtos.GroupDetails details = TextSecureProtos.GroupDetails.parseFrom(detailsSerialized);
if (!details.hasId()) {
throw new IOException("ID missing on group record!");
}
byte[] id = details.getId().toByteArray();
Optional<String> name = Optional.fromNullable(details.getName());
List<String> members = details.getMembersList();
Optional<TextSecureAttachmentStream> avatar = Optional.absent();
if (details.hasAvatar()) {
long avatarLength = details.getAvatar().getLength();
InputStream avatarStream = new ChunkedInputStream.LimitedInputStream(in, avatarLength);
String avatarContentType = details.getAvatar().getContentType();
avatar = Optional.of(new TextSecureAttachmentStream(avatarStream, avatarContentType, avatarLength));
}
return new DeviceGroup(id, name, members, avatar);
}
}

View File

@ -0,0 +1,55 @@
package org.whispersystems.textsecure.api.messages.multidevice;
import com.google.protobuf.ByteString;
import org.whispersystems.textsecure.internal.push.TextSecureProtos;
import java.io.IOException;
import java.io.OutputStream;
public class DeviceGroupsOutputStream extends ChunkedOutputStream {
public DeviceGroupsOutputStream(OutputStream out) {
super(out);
}
public void write(DeviceGroup group) throws IOException {
writeGroupDetails(group);
writeAvatarImage(group);
}
public void close() throws IOException {
out.close();
}
private void writeAvatarImage(DeviceGroup contact) throws IOException {
if (contact.getAvatar().isPresent()) {
writeStream(contact.getAvatar().get().getInputStream());
}
}
private void writeGroupDetails(DeviceGroup group) throws IOException {
TextSecureProtos.GroupDetails.Builder groupDetails = TextSecureProtos.GroupDetails.newBuilder();
groupDetails.setId(ByteString.copyFrom(group.getId()));
if (group.getName().isPresent()) {
groupDetails.setName(group.getName().get());
}
if (group.getAvatar().isPresent()) {
TextSecureProtos.GroupDetails.Avatar.Builder avatarBuilder = TextSecureProtos.GroupDetails.Avatar.newBuilder();
avatarBuilder.setContentType(group.getAvatar().get().getContentType());
avatarBuilder.setLength((int)group.getAvatar().get().getLength());
groupDetails.setAvatar(avatarBuilder);
}
groupDetails.addAllMembers(group.getMembers());
byte[] serializedContactDetails = groupDetails.build().toByteArray();
writeVarint32(serializedContactDetails.length);
out.write(serializedContactDetails);
}
}

View File

@ -13,4 +13,8 @@ public class RequestMessage {
public boolean isContactsRequest() {
return request.getType() == Request.Type.CONTACTS;
}
public boolean isGroupsRequest() {
return request.getType() == Request.Type.GROUPS;
}
}

View File

@ -8,50 +8,61 @@ public class TextSecureSyncMessage {
private final Optional<SentTranscriptMessage> sent;
private final Optional<TextSecureAttachment> contacts;
private final Optional<TextSecureGroup> group;
private final Optional<TextSecureAttachment> groups;
private final Optional<RequestMessage> request;
public TextSecureSyncMessage() {
this.sent = Optional.absent();
this.contacts = Optional.absent();
this.group = Optional.absent();
this.request = Optional.absent();
private TextSecureSyncMessage(Optional<SentTranscriptMessage> sent,
Optional<TextSecureAttachment> contacts,
Optional<TextSecureAttachment> groups,
Optional<RequestMessage> request)
{
this.sent = sent;
this.contacts = contacts;
this.groups = groups;
this.request = request;
}
public TextSecureSyncMessage(SentTranscriptMessage sent) {
this.sent = Optional.of(sent);
this.contacts = Optional.absent();
this.group = Optional.absent();
this.request = Optional.absent();
public static TextSecureSyncMessage forSentTranscript(SentTranscriptMessage sent) {
return new TextSecureSyncMessage(Optional.of(sent),
Optional.<TextSecureAttachment>absent(),
Optional.<TextSecureAttachment>absent(),
Optional.<RequestMessage>absent());
}
public TextSecureSyncMessage(TextSecureAttachment contacts) {
this.contacts = Optional.of(contacts);
this.sent = Optional.absent();
this.group = Optional.absent();
this.request = Optional.absent();
public static TextSecureSyncMessage forContacts(TextSecureAttachment contacts) {
return new TextSecureSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.of(contacts),
Optional.<TextSecureAttachment>absent(),
Optional.<RequestMessage>absent());
}
public TextSecureSyncMessage(TextSecureGroup group) {
this.group = Optional.of(group);
this.sent = Optional.absent();
this.contacts = Optional.absent();
this.request = Optional.absent();
public static TextSecureSyncMessage forGroups(TextSecureAttachment groups) {
return new TextSecureSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<TextSecureAttachment>absent(),
Optional.of(groups),
Optional.<RequestMessage>absent());
}
public TextSecureSyncMessage(RequestMessage contactsRequest) {
this.request = Optional.of(contactsRequest);
this.sent = Optional.absent();
this.contacts = Optional.absent();
this.group = Optional.absent();
public static TextSecureSyncMessage forRequest(RequestMessage request) {
return new TextSecureSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<TextSecureAttachment>absent(),
Optional.<TextSecureAttachment>absent(),
Optional.of(request));
}
public static TextSecureSyncMessage empty() {
return new TextSecureSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<TextSecureAttachment>absent(),
Optional.<TextSecureAttachment>absent(),
Optional.<RequestMessage>absent());
}
public Optional<SentTranscriptMessage> getSent() {
return sent;
}
public Optional<TextSecureGroup> getGroup() {
return group;
public Optional<TextSecureAttachment> getGroups() {
return groups;
}
public Optional<TextSecureAttachment> getContacts() {

View File

@ -48,14 +48,15 @@ message SyncMessage {
optional AttachmentPointer blob = 1;
}
message Group {
optional GroupContext group = 1;
message Groups {
optional AttachmentPointer blob = 1;
}
message Request {
enum Type {
UNKNOWN = 0;
CONTACTS = 1;
GROUPS = 2;
}
optional Type type = 1;
@ -63,7 +64,7 @@ message SyncMessage {
optional Sent sent = 1;
optional Contacts contacts = 2;
optional Group group = 3;
optional Groups groups = 3;
optional Request request = 4;
}
@ -90,10 +91,22 @@ message GroupContext {
message ContactDetails {
message Avatar {
optional string contentType = 1;
optional uint64 length = 2;
optional uint32 length = 2;
}
optional string number = 1;
optional string name = 2;
optional Avatar avatar = 3;
}
message GroupDetails {
message Avatar {
optional string contentType = 1;
optional uint32 length = 2;
}
optional bytes id = 1;
optional string name = 2;
repeated string members = 3;
optional Avatar avatar = 4;
}