Compare commits

...

25 Commits

Author SHA1 Message Date
Moxie Marlinspike
c05108727f Bump version to 1.6.2
// FREEBIE
2015-07-30 12:55:14 -07:00
Moxie Marlinspike
e7f05eb608 Be more careful with JSON processing of server responses.
// FREEBIE
2015-07-30 12:54:58 -07:00
Moxie Marlinspike
beef6937cb Bump version to 1.6.1
// FREEBIE
2015-07-01 15:15:15 -07:00
Moxie Marlinspike
c36d13057c Add some javadoc for ProgressListener changes.
// FREEBIE
2015-06-26 10:15:41 -07:00
Moxie Marlinspike
e7f1c52eb2 Add progress listener for attachments.
// FREEBIE
2015-06-25 16:04:16 -07:00
Moxie Marlinspike
64833318da Bump version to 1.6.0
// FREEBIE
2015-06-22 14:42:59 -07:00
Moxie Marlinspike
a20818f018 Support for group sync messages and requests.
// FREEBIE
2015-06-22 14:26:38 -07:00
Moxie Marlinspike
d044a11bc0 Switch to varin32 for contact input/output stream headers.
// FREEBIE
2015-06-22 12:10:07 -07:00
Moxie Marlinspike
4731a34252 Support for device management and contact requests.
// FREEBIE
2015-06-22 12:03:33 -07:00
Moxie Marlinspike
0437bde205 Support for multi-device message and contact sync.
// FREEBIE
2015-06-22 12:03:06 -07:00
Moxie Marlinspike
1cdffebf6f Bump version to 1.5.0
// FREEBIE
2015-05-28 15:28:35 -07:00
Moxie Marlinspike
bd67150eaa Remove support for plaintext message type.
// FREEBIE
2015-05-20 15:04:21 -07:00
Moxie Marlinspike
bda2316f9b Bump version to 1.4.0
// FREEBIE
2015-05-20 12:17:27 -07:00
Moxie Marlinspike
e99129ec42 Only populate sync message context if sender == recipient.
// FREEBIE
2015-05-20 12:16:37 -07:00
Moxie Marlinspike
68a3076be4 Bump version to 1.3.1
// FREEBIE
2015-05-18 15:04:03 -07:00
Moxie Marlinspike
ecec3d27f9 Increased support for sync message contexts.
1) Surface received sync message contexts in TextSecureMessage
   objects.

2) Send a sync message context for group messages.

// FREEBIE
2015-05-18 15:00:40 -07:00
Moxie Marlinspike
a8e1160200 Bump version to 1.3.0
// FREEBIE
2015-04-15 16:47:02 -07:00
Moxie Marlinspike
807f13ddc4 Support for retrieving messages via REST.
Support for retrieving messages via HTTP rather than websockets.

// FREEBIE
2015-04-15 16:46:03 -07:00
Moxie Marlinspike
0e3ca18588 Bump version to 1.2.5 2015-04-05 15:18:30 -07:00
Moxie Marlinspike
25a38b9eea Set websocket read timeout to keepalive interaval + 10s.
// FREEBIE
2015-04-05 15:18:08 -07:00
Moxie Marlinspike
e02aea9cfb Added legal things section to README
// FREEBIE
2015-03-27 09:47:31 -07:00
Moxie Marlinspike
a59ca8da13 Bump version to 1.2.4 2015-03-25 14:03:50 -07:00
Moxie Marlinspike
72284ce5ec Don't keepalive connections on external upload.
Workaround for an Android OS bug.

Fixes #1

// FREEBIE
2015-03-25 14:02:53 -07:00
Moxie Marlinspike
c795a0119c Bump version to 1.2.3 2015-03-19 12:29:43 -07:00
lilia
9cada7e229 Fix provisioning flow
Make ProvisionMessage serializable. Previously the lack of jackson
annotations caused this object to serialize as empty string, which
elicits a 500 from the server.

Closes #3
2015-03-10 19:38:34 -07:00
45 changed files with 12641 additions and 5188 deletions

View File

@@ -96,4 +96,22 @@ try {
if (messagePipe != null)
messagePipe.close();
}
`````
`````
# Legal things
## Cryptography Notice
This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software.
BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted.
See <http://www.wassenaar.org/> for more information.
The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms.
The form and manner of this distribution makes it eligible for export under the License Exception ENC Technology Software Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Section 740.13) for both object code and source code.
## License
Copyright 2013-2015 Open Whisper Systems
Licensed under the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html

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.2.2"
ext.version_number = "1.6.2"
ext.group_info = "org.whispersystems"
ext.axolotl_version = "1.3.1"

View File

@@ -1,6 +1,6 @@
#Fri Feb 27 17:12:50 PST 2015
#Mon May 18 15:02:07 PDT 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-all.zip

View File

@@ -94,9 +94,6 @@ task packageSources(type: Jar) {
}
artifacts {
archives(packageJavadoc) {
type = 'javadoc'
}
archives packageJavadoc
archives packageSources
}

View File

@@ -26,6 +26,7 @@ import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.messages.multidevice.DeviceInfo;
import org.whispersystems.textsecure.api.push.ContactTokenDetails;
import org.whispersystems.textsecure.api.push.SignedPreKeyEntity;
import org.whispersystems.textsecure.api.push.TrustStore;
@@ -234,6 +235,14 @@ public class TextSecureAccountManager {
this.pushServiceSocket.sendProvisioningMessage(deviceIdentifier, ciphertext);
}
public List<DeviceInfo> getDevices() throws IOException {
return this.pushServiceSocket.getDevices();
}
public void removeDevice(long deviceId) throws IOException {
this.pushServiceSocket.removeDevice(deviceId);
}
private String createDirectoryServerToken(String e164number, boolean urlSafe) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA1");

View File

@@ -18,16 +18,23 @@ package org.whispersystems.textsecure.api;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
import org.whispersystems.textsecure.api.messages.TextSecureAttachment.ProgressListener;
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;
import org.whispersystems.textsecure.internal.push.PushServiceSocket;
import org.whispersystems.textsecure.internal.push.TextSecureEnvelopeEntity;
import org.whispersystems.textsecure.internal.util.StaticCredentialsProvider;
import org.whispersystems.textsecure.internal.websocket.WebSocketConnection;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList;
import java.util.List;
/**
* The primary interface for receiving TextSecure messages.
@@ -76,7 +83,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.
@@ -86,7 +93,26 @@ public class TextSecureMessageReceiver {
public InputStream retrieveAttachment(TextSecureAttachmentPointer pointer, File destination)
throws IOException, InvalidMessageException
{
socket.retrieveAttachment(pointer.getRelay().orNull(), pointer.getId(), destination);
return retrieveAttachment(pointer, destination, null);
}
/**
* Retrieves a TextSecure attachment.
*
* @param pointer The {@link org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer}
* received in a {@link TextSecureDataMessage}.
* @param destination The download destination for this attachment.
* @param listener An optional listener (may be null) to receive callbacks on download progress.
*
* @return An InputStream that streams the plaintext attachment contents.
* @throws IOException
* @throws InvalidMessageException
*/
public InputStream retrieveAttachment(TextSecureAttachmentPointer pointer, File destination, ProgressListener listener)
throws IOException, InvalidMessageException
{
socket.retrieveAttachment(pointer.getRelay().orNull(), pointer.getId(), destination, listener);
return new AttachmentCipherInputStream(destination, pointer.getKey());
}
@@ -102,4 +128,39 @@ public class TextSecureMessageReceiver {
return new TextSecureMessagePipe(webSocket, credentialsProvider);
}
public List<TextSecureEnvelope> retrieveMessages() throws IOException {
return retrieveMessages(new NullMessageReceivedCallback());
}
public List<TextSecureEnvelope> retrieveMessages(MessageReceivedCallback callback)
throws IOException
{
List<TextSecureEnvelope> results = new LinkedList<>();
List<TextSecureEnvelopeEntity> entities = socket.getMessages();
for (TextSecureEnvelopeEntity entity : entities) {
TextSecureEnvelope envelope = new TextSecureEnvelope(entity.getType(), entity.getSource(),
entity.getSourceDevice(), entity.getRelay(),
entity.getTimestamp(), entity.getMessage(),
entity.getContent());
callback.onMessage(envelope);
results.add(envelope);
socket.acknowledgeMessage(entity.getSource(), entity.getTimestamp());
}
return results;
}
public interface MessageReceivedCallback {
public void onMessage(TextSecureEnvelope envelope);
}
public static class NullMessageReceivedCallback implements MessageReceivedCallback {
@Override
public void onMessage(TextSecureEnvelope envelope) {}
}
}

View File

@@ -30,8 +30,9 @@ 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.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;
@@ -45,6 +46,11 @@ import org.whispersystems.textsecure.internal.push.PushAttachmentData;
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;
@@ -54,10 +60,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.
*
@@ -69,7 +71,7 @@ public class TextSecureMessageSender {
private final PushServiceSocket socket;
private final AxolotlStore store;
private final TextSecureAddress syncAddress;
private final TextSecureAddress localAddress;
private final Optional<EventListener> eventListener;
/**
@@ -90,7 +92,7 @@ public class TextSecureMessageSender {
{
this.socket = new PushServiceSocket(url, trustStore, new StaticCredentialsProvider(user, password, null));
this.store = store;
this.syncAddress = new TextSecureAddress(user);
this.localAddress = new TextSecureAddress(user);
this.eventListener = eventListener;
}
@@ -113,16 +115,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, recipient, timestamp);
sendMessage(syncAddress, timestamp, syncMessage);
byte[] syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.of(recipient), timestamp);
sendMessage(localAddress, timestamp, syncMessage, false);
}
if (message.isEndSession()) {
@@ -142,16 +144,42 @@ 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);
sendMessage(recipients, message.getTimestamp(), content);
byte[] content = createMessageContent(message);
long timestamp = message.getTimestamp();
SendMessageResponse response = sendMessage(recipients, timestamp, content, true);
try {
if (response != null && response.getNeedsSync()) {
byte[] syncMessage = createMultiDeviceSentTranscriptContent(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 sendMessage(TextSecureSyncMessage message)
throws IOException, UntrustedIdentityException
{
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);
}
private byte[] createMessageContent(TextSecureDataMessage message) throws IOException {
DataMessage.Builder builder = DataMessage.newBuilder();
List<AttachmentPointer> pointers = createAttachmentPointers(message.getAttachments());
if (!pointers.isEmpty()) {
builder.addAllAttachments(pointers);
@@ -166,21 +194,44 @@ 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, TextSecureAddress recipient, long timestamp) {
try {
PushMessageContent.Builder builder = PushMessageContent.parseFrom(content).toBuilder();
builder.setSync(PushMessageContent.SyncMessageContext.newBuilder()
.setDestination(recipient.getNumber())
.setTimestamp(timestamp)
.build());
private byte[] createMultiDeviceContactsContent(TextSecureAttachmentStream contacts) throws IOException {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder builder = SyncMessage.newBuilder();
builder.setContacts(SyncMessage.Contacts.newBuilder()
.setBlob(createAttachmentPointer(contacts)));
return builder.build().toByteArray();
return container.setSyncMessage(builder).build().toByteArray();
}
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();
SyncMessage.Sent.Builder sentMessage = SyncMessage.Sent.newBuilder();
sentMessage.setTimestamp(timestamp);
sentMessage.setMessage(DataMessage.parseFrom(content));
if (recipient.isPresent()) {
sentMessage.setDestination(recipient.get().getNumber());
}
return container.setSyncMessage(syncMessage.setSent(sentMessage)).build().toByteArray();
} catch (InvalidProtocolBufferException e) {
throw new AssertionError(e);
}
@@ -209,16 +260,18 @@ public class TextSecureMessageSender {
return builder.build();
}
private void 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<>();
List<UnregisteredUserException> unregisteredUsers = new LinkedList<>();
List<NetworkFailureException> networkExceptions = new LinkedList<>();
SendMessageResponse response = null;
for (TextSecureAddress recipient : recipients) {
try {
sendMessage(recipient, timestamp, content);
response = sendMessage(recipient, timestamp, content, legacy);
} catch (UntrustedIdentityException e) {
Log.w(TAG, e);
untrustedIdentities.add(e);
@@ -234,14 +287,16 @@ public class TextSecureMessageSender {
if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) {
throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions);
}
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);
@@ -280,6 +335,7 @@ public class TextSecureMessageSender {
PushAttachmentData attachmentData = new PushAttachmentData(attachment.getContentType(),
attachment.getInputStream(),
attachment.getLength(),
attachment.getListener(),
attachmentKey);
long attachmentId = socket.sendAttachment(attachmentData);
@@ -295,27 +351,28 @@ 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(syncAddress)) {
messages.add(getEncryptedMessage(socket, recipient, TextSecureAddress.DEFAULT_DEVICE_ID, plaintext));
if (!recipient.equals(localAddress)) {
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);
TextSecureCipher cipher = new TextSecureCipher(store);
TextSecureCipher cipher = new TextSecureCipher(localAddress, store);
if (!store.containsSession(axolotlAddress)) {
try {
@@ -339,7 +396,7 @@ public class TextSecureMessageSender {
}
}
return cipher.encrypt(axolotlAddress, plaintext);
return cipher.encrypt(axolotlAddress, plaintext, legacy);
}
private void handleMismatchedDevices(PushServiceSocket socket, TextSecureAddress recipient,

View File

@@ -34,20 +34,27 @@ 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.TextSecureDataMessage;
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.multidevice.RequestMessage;
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.PushMessageProtos;
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.
@@ -56,13 +63,17 @@ import static org.whispersystems.textsecure.internal.push.PushMessageProtos.Push
*/
public class TextSecureCipher {
private final AxolotlStore axolotlStore;
private static final String TAG = TextSecureCipher.class.getSimpleName();
public TextSecureCipher(AxolotlStore axolotlStore) {
private final AxolotlStore axolotlStore;
private final TextSecureAddress localAddress;
public TextSecureCipher(TextSecureAddress localAddress, AxolotlStore axolotlStore) {
this.axolotlStore = axolotlStore;
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));
@@ -77,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);
}
/**
@@ -94,54 +106,87 @@ 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 if (envelope.isPlaintext()) {
paddedMessage = 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) {
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);
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() & PushMessageContent.Flags.END_SESSION_VALUE) != 0);
boolean secure = envelope.isWhisperMessage() || envelope.isPreKeyWhisperMessage();
boolean endSession = ((content.getFlags() & DataMessage.Flags.END_SESSION_VALUE) != 0);
for (PushMessageContent.AttachmentPointer pointer : content.getAttachmentsList()) {
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(), secure, endSession);
return new TextSecureDataMessage(envelope.getTimestamp(), groupInfo, attachments,
content.getBody(), endSession);
}
private TextSecureGroup createGroupInfo(TextSecureEnvelope envelope, PushMessageContent content) {
private TextSecureSyncMessage createSynchronizeMessage(TextSecureEnvelope envelope, SyncMessage content) {
if (content.hasSent()) {
SyncMessage.Sent sentContent = content.getSent();
return TextSecureSyncMessage.forSentTranscript(new SentTranscriptMessage(sentContent.getDestination(),
sentContent.getTimestamp(),
createTextSecureMessage(envelope, sentContent.getMessage())));
}
if (content.hasRequest()) {
return TextSecureSyncMessage.forRequest(new RequestMessage(content.getRequest()));
}
return TextSecureSyncMessage.empty();
}
private TextSecureGroup createGroupInfo(TextSecureEnvelope envelope, DataMessage content) {
if (!content.hasGroup()) return null;
TextSecureGroup.Type type;

View File

@@ -47,9 +47,10 @@ public abstract class TextSecureAttachment {
public static class Builder {
private InputStream inputStream;
private String contentType;
private long length;
private InputStream inputStream;
private String contentType;
private long length;
private ProgressListener listener;
private Builder() {}
@@ -68,12 +69,31 @@ public abstract class TextSecureAttachment {
return this;
}
public Builder withListener(ProgressListener listener) {
this.listener = listener;
return this;
}
public TextSecureAttachmentStream build() {
if (inputStream == null) throw new IllegalArgumentException("Must specify stream!");
if (contentType == null) throw new IllegalArgumentException("No content type specified!");
if (length == 0) throw new IllegalArgumentException("No length specified!");
return new TextSecureAttachmentStream(inputStream, contentType, length);
return new TextSecureAttachmentStream(inputStream, contentType, length, listener);
}
}
/**
* An interface to receive progress information on upload/download of
* an attachment.
*/
public interface ProgressListener {
/**
* Called on a progress change event.
*
* @param total The total amount to transmit/receive in bytes.
* @param progress The amount that has been transmitted/received in bytes thus far
*/
public void onAttachmentProgress(long total, long progress);
}
}

View File

@@ -23,13 +23,15 @@ import java.io.InputStream;
*/
public class TextSecureAttachmentStream extends TextSecureAttachment {
private final InputStream inputStream;
private final long length;
private final InputStream inputStream;
private final long length;
private final ProgressListener listener;
public TextSecureAttachmentStream(InputStream inputStream, String contentType, long length) {
public TextSecureAttachmentStream(InputStream inputStream, String contentType, long length, ProgressListener listener) {
super(contentType);
this.inputStream = inputStream;
this.length = length;
this.listener = listener;
}
@Override
@@ -49,4 +51,8 @@ public class TextSecureAttachmentStream extends TextSecureAttachment {
public long getLength() {
return length;
}
public ProgressListener getListener() {
return listener;
}
}

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 boolean secure;
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, true, false);
public TextSecureDataMessage(long timestamp, TextSecureGroup group, List<TextSecureAttachment> attachments, String body) {
this(timestamp, group, attachments, body, false);
}
/**
@@ -77,14 +76,12 @@ public class TextSecureMessage {
* @param group The group information (or null if none).
* @param attachments The attachments (or null if none).
* @param body The message contents.
* @param secure Flag indicating whether this message is to be encrypted.
* @param endSession Flag indicating whether this message should close a session.
*/
public TextSecureMessage(long timestamp, TextSecureGroup group, List<TextSecureAttachment> attachments, String body, boolean secure, 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.secure = secure;
this.endSession = endSession;
if (attachments != null && !attachments.isEmpty()) {
@@ -126,10 +123,6 @@ public class TextSecureMessage {
return group;
}
public boolean isSecure() {
return secure;
}
public boolean isEndSession() {
return endSession;
}
@@ -183,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, true, 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,56 +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 true if the containing message is plaintext.
*/
public boolean isPlaintext() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.PLAINTEXT_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

@@ -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

@@ -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,38 @@
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;
public class DeviceContactsInputStream extends ChunkedInputStream {
public DeviceContactsInputStream(InputStream in) {
super(in);
}
public DeviceContact read() throws IOException {
long detailsLength = readRawVarint32();
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, null));
}
return new DeviceContact(number, name, avatar);
}
}

View File

@@ -0,0 +1,50 @@
package org.whispersystems.textsecure.api.messages.multidevice;
import org.whispersystems.textsecure.internal.push.TextSecureProtos;
import java.io.IOException;
import java.io.OutputStream;
public class DeviceContactsOutputStream extends ChunkedOutputStream {
public DeviceContactsOutputStream(OutputStream out) {
super(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()) {
writeStream(contact.getAvatar().get().getInputStream());
}
}
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((int)contact.getAvatar().get().getLength());
contactDetails.setAvatar(avatarBuilder);
}
byte[] serializedContactDetails = contactDetails.build().toByteArray();
writeVarint32(serializedContactDetails.length);
out.write(serializedContactDetails);
}
}

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, null));
}
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

@@ -0,0 +1,36 @@
package org.whispersystems.textsecure.api.messages.multidevice;
import com.fasterxml.jackson.annotation.JsonProperty;
public class DeviceInfo {
@JsonProperty
private long id;
@JsonProperty
private String name;
@JsonProperty
private long created;
@JsonProperty
private long lastSeen;
public DeviceInfo() {}
public long getId() {
return id;
}
public String getName() {
return name;
}
public long getCreated() {
return created;
}
public long getLastSeen() {
return lastSeen;
}
}

View File

@@ -0,0 +1,20 @@
package org.whispersystems.textsecure.api.messages.multidevice;
import org.whispersystems.textsecure.internal.push.TextSecureProtos.SyncMessage.Request;
public class RequestMessage {
private final Request request;
public RequestMessage(Request request) {
this.request = request;
}
public boolean isContactsRequest() {
return request.getType() == Request.Type.CONTACTS;
}
public boolean isGroupsRequest() {
return request.getType() == Request.Type.GROUPS;
}
}

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,76 @@
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<TextSecureAttachment> groups;
private final Optional<RequestMessage> request;
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 static TextSecureSyncMessage forSentTranscript(SentTranscriptMessage sent) {
return new TextSecureSyncMessage(Optional.of(sent),
Optional.<TextSecureAttachment>absent(),
Optional.<TextSecureAttachment>absent(),
Optional.<RequestMessage>absent());
}
public static TextSecureSyncMessage forContacts(TextSecureAttachment contacts) {
return new TextSecureSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.of(contacts),
Optional.<TextSecureAttachment>absent(),
Optional.<RequestMessage>absent());
}
public static TextSecureSyncMessage forGroups(TextSecureAttachment groups) {
return new TextSecureSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<TextSecureAttachment>absent(),
Optional.of(groups),
Optional.<RequestMessage>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<TextSecureAttachment> getGroups() {
return groups;
}
public Optional<TextSecureAttachment> getContacts() {
return contacts;
}
public Optional<RequestMessage> getRequest() {
return request;
}
}

View File

@@ -18,6 +18,7 @@ package org.whispersystems.textsecure.api.push.exceptions;
import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException;
import java.util.LinkedList;
import java.util.List;
public class EncapsulatedExceptions extends Throwable {
@@ -35,6 +36,14 @@ public class EncapsulatedExceptions extends Throwable {
this.networkExceptions = networkExceptions;
}
public EncapsulatedExceptions(UntrustedIdentityException e) {
this.untrustedIdentityExceptions = new LinkedList<>();
this.unregisteredUserExceptions = new LinkedList<>();
this.networkExceptions = new LinkedList<>();
this.untrustedIdentityExceptions.add(e);
}
public List<UntrustedIdentityException> getUntrustedIdentityExceptions() {
return untrustedIdentityExceptions;
}

View File

@@ -0,0 +1,19 @@
package org.whispersystems.textsecure.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecure.api.messages.multidevice.DeviceInfo;
import java.util.List;
public class DeviceInfoList {
@JsonProperty
private List<DeviceInfo> devices;
public DeviceInfoList() {}
public List<DeviceInfo> getDevices() {
return devices;
}
}

View File

@@ -0,0 +1,20 @@
package org.whispersystems.textsecure.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
public class DeviceLimit {
@JsonProperty
private int current;
@JsonProperty
private int max;
public int getCurrent() {
return current;
}
public int getMax() {
return max;
}
}

View File

@@ -0,0 +1,20 @@
package org.whispersystems.textsecure.internal.push;
import org.whispersystems.textsecure.api.push.exceptions.NonSuccessfulResponseCodeException;
public class DeviceLimitExceededException extends NonSuccessfulResponseCodeException {
private final DeviceLimit deviceLimit;
public DeviceLimitExceededException(DeviceLimit deviceLimit) {
this.deviceLimit = deviceLimit;
}
public int getCurrent() {
return deviceLimit.getCurrent();
}
public int getMax() {
return deviceLimit.getMax();
}
}

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

@@ -1,7 +1,10 @@
package org.whispersystems.textsecure.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
public class ProvisioningMessage {
@JsonProperty
private String body;
public ProvisioningMessage(String body) {

View File

@@ -16,20 +16,26 @@
*/
package org.whispersystems.textsecure.internal.push;
import org.whispersystems.textsecure.api.messages.TextSecureAttachment.ProgressListener;
import java.io.InputStream;
public class PushAttachmentData {
private final String contentType;
private final InputStream data;
private final long dataSize;
private final byte[] key;
private final String contentType;
private final InputStream data;
private final long dataSize;
private final byte[] key;
private final ProgressListener listener;
public PushAttachmentData(String contentType, InputStream data, long dataSize, byte[] key) {
public PushAttachmentData(String contentType, InputStream data, long dataSize,
ProgressListener listener, byte[] key)
{
this.contentType = contentType;
this.data = data;
this.dataSize = dataSize;
this.key = key;
this.listener = listener;
}
public String getContentType() {
@@ -47,4 +53,8 @@ public class PushAttachmentData {
public byte[] getKey() {
return key;
}
public ProgressListener getListener() {
return listener;
}
}

View File

@@ -17,6 +17,7 @@
package org.whispersystems.textsecure.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.apache.http.conn.ssl.StrictHostnameVerifier;
import org.whispersystems.libaxolotl.IdentityKey;
@@ -27,9 +28,11 @@ import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.crypto.AttachmentCipherOutputStream;
import org.whispersystems.textsecure.api.messages.TextSecureAttachment.ProgressListener;
import org.whispersystems.textsecure.api.messages.multidevice.DeviceInfo;
import org.whispersystems.textsecure.api.push.ContactTokenDetails;
import org.whispersystems.textsecure.api.push.TextSecureAddress;
import org.whispersystems.textsecure.api.push.SignedPreKeyEntity;
import org.whispersystems.textsecure.api.push.TextSecureAddress;
import org.whispersystems.textsecure.api.push.TrustStore;
import org.whispersystems.textsecure.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.textsecure.api.push.exceptions.ExpectationFailedException;
@@ -86,10 +89,12 @@ public class PushServiceSocket {
private static final String PROVISIONING_CODE_PATH = "/v1/devices/provisioning/code";
private static final String PROVISIONING_MESSAGE_PATH = "/v1/provisioning/%s";
private static final String DEVICE_PATH = "/v1/devices/%s";
private static final String DIRECTORY_TOKENS_PATH = "/v1/directory/tokens";
private static final String DIRECTORY_VERIFY_PATH = "/v1/directory/%s";
private static final String MESSAGE_PATH = "/v1/messages/%s";
private static final String ACKNOWLEDGE_MESSAGE_PATH = "/v1/messages/%s/%d";
private static final String RECEIPT_PATH = "/v1/receipt/%s/%d";
private static final String ATTACHMENT_PATH = "/v1/attachments/%s";
@@ -125,6 +130,15 @@ public class PushServiceSocket {
return JsonUtil.fromJson(responseText, DeviceCode.class).getVerificationCode();
}
public List<DeviceInfo> getDevices() throws IOException {
String responseText = makeRequest(String.format(DEVICE_PATH, ""), "GET", null);
return JsonUtil.fromJson(responseText, DeviceInfoList.class).getDevices();
}
public void removeDevice(long deviceId) throws IOException {
makeRequest(String.format(DEVICE_PATH, String.valueOf(deviceId)), "DELETE", null);
}
public void sendProvisioningMessage(String destination, byte[] body) throws IOException {
makeRequest(String.format(PROVISIONING_MESSAGE_PATH, destination), "PUT",
JsonUtil.toJson(new ProvisioningMessage(Base64.encodeBytes(body))));
@@ -162,6 +176,15 @@ public class PushServiceSocket {
}
}
public List<TextSecureEnvelopeEntity> getMessages() throws IOException {
String responseText = makeRequest(String.format(MESSAGE_PATH, ""), "GET", null);
return JsonUtil.fromJson(responseText, TextSecureEnvelopeEntityList.class).getMessages();
}
public void acknowledgeMessage(String sender, long timestamp) throws IOException {
makeRequest(String.format(ACKNOWLEDGE_MESSAGE_PATH, sender, timestamp), "DELETE", null);
}
public void registerPreKeys(IdentityKey identityKey,
PreKeyRecord lastResortKey,
SignedPreKeyRecord signedPreKey,
@@ -237,8 +260,6 @@ public class PushServiceSocket {
}
return bundles;
} catch (JsonUtil.JsonParseException e) {
throw new IOException(e);
} catch (NotFoundException nfe) {
throw new UnregisteredUserException(destination.getNumber(), nfe);
}
@@ -279,8 +300,6 @@ public class PushServiceSocket {
return new PreKeyBundle(device.getRegistrationId(), device.getDeviceId(), preKeyId, preKey,
signedPreKeyId, signedPreKey, signedPreKeySignature, response.getIdentityKey());
} catch (JsonUtil.JsonParseException e) {
throw new IOException(e);
} catch (NotFoundException nfe) {
throw new UnregisteredUserException(destination.getNumber(), nfe);
}
@@ -314,12 +333,12 @@ public class PushServiceSocket {
Log.w(TAG, "Got attachment content location: " + attachmentKey.getLocation());
uploadAttachment("PUT", attachmentKey.getLocation(), attachment.getData(),
attachment.getDataSize(), attachment.getKey());
attachment.getDataSize(), attachment.getKey(), attachment.getListener());
return attachmentKey.getId();
}
public void retrieveAttachment(String relay, long attachmentId, File destination) throws IOException {
public void retrieveAttachment(String relay, long attachmentId, File destination, ProgressListener listener) throws IOException {
String path = String.format(ATTACHMENT_PATH, String.valueOf(attachmentId));
if (!Util.isEmpty(relay)) {
@@ -331,17 +350,22 @@ public class PushServiceSocket {
Log.w(TAG, "Attachment: " + attachmentId + " is at: " + descriptor.getLocation());
downloadExternalFile(descriptor.getLocation(), destination);
downloadExternalFile(descriptor.getLocation(), destination, listener);
}
public List<ContactTokenDetails> retrieveDirectory(Set<String> contactTokens)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
ContactTokenList contactTokenList = new ContactTokenList(new LinkedList<>(contactTokens));
String response = makeRequest(DIRECTORY_TOKENS_PATH, "PUT", JsonUtil.toJson(contactTokenList));
ContactTokenDetailsList activeTokens = JsonUtil.fromJson(response, ContactTokenDetailsList.class);
try {
ContactTokenList contactTokenList = new ContactTokenList(new LinkedList<>(contactTokens));
String response = makeRequest(DIRECTORY_TOKENS_PATH, "PUT", JsonUtil.toJson(contactTokenList));
ContactTokenDetailsList activeTokens = JsonUtil.fromJson(response, ContactTokenDetailsList.class);
return activeTokens.getContacts();
return activeTokens.getContacts();
} catch (IOException e) {
Log.w(TAG, e);
throw new NonSuccessfulResponseCodeException("Unable to parse entity");
}
}
public ContactTokenDetails getContactTokenDetails(String contactToken) throws IOException {
@@ -353,7 +377,7 @@ public class PushServiceSocket {
}
}
private void downloadExternalFile(String url, File localDestination)
private void downloadExternalFile(String url, File localDestination, ProgressListener listener)
throws IOException
{
URL downloadUrl = new URL(url);
@@ -367,13 +391,19 @@ public class PushServiceSocket {
throw new NonSuccessfulResponseCodeException("Bad response: " + connection.getResponseCode());
}
OutputStream output = new FileOutputStream(localDestination);
InputStream input = connection.getInputStream();
byte[] buffer = new byte[4096];
int read;
OutputStream output = new FileOutputStream(localDestination);
InputStream input = connection.getInputStream();
byte[] buffer = new byte[4096];
int contentLength = connection.getContentLength();
int read,totalRead = 0;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
totalRead += read;
if (listener != null) {
listener.onAttachmentProgress(contentLength, totalRead);
}
}
output.close();
@@ -385,7 +415,8 @@ public class PushServiceSocket {
}
}
private void uploadAttachment(String method, String url, InputStream data, long dataSize, byte[] key)
private void uploadAttachment(String method, String url, InputStream data,
long dataSize, byte[] key, ProgressListener listener)
throws IOException
{
URL uploadUrl = new URL(url);
@@ -400,14 +431,27 @@ public class PushServiceSocket {
connection.setRequestMethod(method);
connection.setRequestProperty("Content-Type", "application/octet-stream");
connection.setRequestProperty("Connection", "close");
connection.connect();
try {
OutputStream stream = connection.getOutputStream();
AttachmentCipherOutputStream out = new AttachmentCipherOutputStream(key, stream);
byte[] buffer = new byte[4096];
int read, written = 0;
Util.copy(data, out);
while ((read = data.read(buffer)) != -1) {
out.write(buffer, 0, read);
written += read;
if (listener != null) {
listener.onAttachmentProgress(dataSize, written);
}
}
data.close();
out.flush();
out.close();
if (connection.getResponseCode() != 200) {
throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage());
@@ -459,19 +503,45 @@ public class PushServiceSocket {
connection.disconnect();
throw new NotFoundException("Not found");
case 409:
MismatchedDevices mismatchedDevices;
try {
response = Util.readFully(connection.getErrorStream());
response = Util.readFully(connection.getErrorStream());
mismatchedDevices = JsonUtil.fromJson(response, MismatchedDevices.class);
} catch (JsonProcessingException e) {
Log.w(TAG, e);
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
} catch (IOException e) {
throw new PushNetworkException(e);
}
throw new MismatchedDevicesException(JsonUtil.fromJson(response, MismatchedDevices.class));
throw new MismatchedDevicesException(mismatchedDevices);
case 410:
StaleDevices staleDevices;
try {
response = Util.readFully(connection.getErrorStream());
response = Util.readFully(connection.getErrorStream());
staleDevices = JsonUtil.fromJson(response, StaleDevices.class);
} catch (JsonProcessingException e) {
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
} catch (IOException e) {
throw new PushNetworkException(e);
}
throw new StaleDevicesException(JsonUtil.fromJson(response, StaleDevices.class));
throw new StaleDevicesException(staleDevices);
case 411:
DeviceLimit deviceLimit;
try {
response = Util.readFully(connection.getErrorStream());
deviceLimit = JsonUtil.fromJson(response, DeviceLimit.class);
} catch (JsonProcessingException e) {
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
} catch (IOException e) {
throw new PushNetworkException(e);
}
throw new DeviceLimitExceededException(deviceLimit);
case 417:
throw new ExpectationFailedException();
}

View File

@@ -0,0 +1,57 @@
package org.whispersystems.textsecure.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
public class TextSecureEnvelopeEntity {
@JsonProperty
private int type;
@JsonProperty
private String relay;
@JsonProperty
private long timestamp;
@JsonProperty
private String source;
@JsonProperty
private int sourceDevice;
@JsonProperty
private byte[] message;
@JsonProperty
private byte[] content;
public TextSecureEnvelopeEntity() {}
public int getType() {
return type;
}
public String getRelay() {
return relay;
}
public long getTimestamp() {
return timestamp;
}
public String getSource() {
return source;
}
public int getSourceDevice() {
return sourceDevice;
}
public byte[] getMessage() {
return message;
}
public byte[] getContent() {
return content;
}
}

View File

@@ -0,0 +1,16 @@
package org.whispersystems.textsecure.internal.push;
import org.whispersystems.textsecure.api.messages.TextSecureEnvelope;
import java.util.List;
public class TextSecureEnvelopeEntityList {
private List<TextSecureEnvelopeEntity> messages;
public TextSecureEnvelopeEntityList() {}
public List<TextSecureEnvelopeEntity> getMessages() {
return messages;
}
}

View File

@@ -2,6 +2,7 @@ package org.whispersystems.textsecure.internal.util;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
@@ -36,21 +37,12 @@ public class JsonUtil {
}
}
public static <T> T fromJson(String json, Class<T> clazz) {
try {
return objectMapper.readValue(json, clazz);
} catch (IOException e) {
Log.w(TAG, e);
throw new JsonParseException(e);
}
public static <T> T fromJson(String json, Class<T> clazz)
throws IOException
{
return objectMapper.readValue(json, clazz);
}
public static class JsonParseException extends RuntimeException {
public JsonParseException(Exception e) {
super(e);
}
}
public static class IdentityKeySerializer extends JsonSerializer<IdentityKey> {
@Override
public void serialize(IdentityKey value, JsonGenerator gen, SerializerProvider serializers)

View File

@@ -96,7 +96,6 @@ public class Util {
}
}
public static void copy(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[4096];
int read;
@@ -124,5 +123,4 @@ public class Util {
throw new AssertionError(e);
}
}
}

View File

@@ -48,13 +48,13 @@ public class OkHttpClientWrapper implements WebSocketListener {
this.listener = listener;
}
public void connect() {
public void connect(final int timeout, final TimeUnit timeUnit) {
new Thread() {
@Override
public void run() {
int attempt = 0;
while ((webSocket = newSocket()) != null) {
while ((webSocket = newSocket(timeout, timeUnit)) != null) {
try {
Response response = webSocket.connect(OkHttpClientWrapper.this);
@@ -117,14 +117,17 @@ public class OkHttpClientWrapper implements WebSocketListener {
listener.onClose();
}
private synchronized WebSocket newSocket() {
private synchronized WebSocket newSocket(int timeout, TimeUnit unit) {
if (closed) return null;
String filledUri = String.format(uri, credentialsProvider.getUser(), credentialsProvider.getPassword());
SSLSocketFactory socketFactory = createTlsSocketFactory(trustStore);
String filledUri = String.format(uri, credentialsProvider.getUser(), credentialsProvider.getPassword());
OkHttpClient okHttpClient = new OkHttpClient();
return WebSocket.newWebSocket(new OkHttpClient().setSslSocketFactory(socketFactory),
new Request.Builder().url(filledUri).build());
okHttpClient.setSslSocketFactory(createTlsSocketFactory(trustStore));
okHttpClient.setReadTimeout(timeout, unit);
okHttpClient.setConnectTimeout(timeout, unit);
return WebSocket.newWebSocket(okHttpClient, new Request.Builder().url(filledUri).build());
}
private SSLSocketFactory createTlsSocketFactory(TrustStore trustStore) {

View File

@@ -19,7 +19,8 @@ import static org.whispersystems.textsecure.internal.websocket.WebSocketProtos.W
public class WebSocketConnection implements WebSocketEventListener {
private static final String TAG = WebSocketConnection.class.getSimpleName();
private static final String TAG = WebSocketConnection.class.getSimpleName();
private static final int KEEPALIVE_TIMEOUT_SECONDS = 55;
private final LinkedList<WebSocketRequestMessage> incomingRequests = new LinkedList<>();
@@ -42,7 +43,7 @@ public class WebSocketConnection implements WebSocketEventListener {
if (client == null) {
client = new OkHttpClientWrapper(wsUri, trustStore, credentialsProvider, this);
client.connect();
client.connect(KEEPALIVE_TIMEOUT_SECONDS + 10, TimeUnit.SECONDS);
}
}
@@ -140,6 +141,7 @@ public class WebSocketConnection implements WebSocketEventListener {
public synchronized void onConnected() {
if (client != null && keepAliveSender == null) {
Log.w(TAG, "onConnected()");
keepAliveSender = new KeepAliveSender();
keepAliveSender.start();
}
@@ -156,7 +158,7 @@ public class WebSocketConnection implements WebSocketEventListener {
public void run() {
while (!stop.get()) {
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(55));
Thread.sleep(TimeUnit.SECONDS.toMillis(KEEPALIVE_TIMEOUT_SECONDS));
Log.w(TAG, "Sending keep alive...");
sendKeepAlive();

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;
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=../src/main/java/ IncomingPushMessageSignal.proto Provisioning.proto WebSocketResources.proto
protoc --java_out=../java/src/main/java/ TextSecure.proto Provisioning.proto WebSocketResources.proto

112
protobuf/TextSecure.proto Normal file
View File

@@ -0,0 +1,112 @@
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 Groups {
optional AttachmentPointer blob = 1;
}
message Request {
enum Type {
UNKNOWN = 0;
CONTACTS = 1;
GROUPS = 2;
}
optional Type type = 1;
}
optional Sent sent = 1;
optional Contacts contacts = 2;
optional Groups groups = 3;
optional Request request = 4;
}
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 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;
}