package org.thoughtcrime.SMP.crypto.SMP;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.whispersystems.libaxolotl.AxolotlAddress;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.SessionBuilder;
import org.whispersystems.libaxolotl.logging.Log;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.libaxolotl.state.PreKeyBundle;
import org.whispersystems.libaxolotl.util.guava.Optional;
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.TextSecureGroup;
import org.whispersystems.textsecure.api.push.TextSecureAddress;
import org.whispersystems.textsecure.api.push.TrustStore;
import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.textsecure.api.push.exceptions.NetworkFailureException;
import org.whispersystems.textsecure.api.push.exceptions.PushNetworkException;
import org.whispersystems.textsecure.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.textsecure.internal.push.MismatchedDevices;
import org.whispersystems.textsecure.internal.push.OutgoingPushMessage;
import org.whispersystems.textsecure.internal.push.OutgoingPushMessageList;
import org.whispersystems.textsecure.internal.push.PushAttachmentData;
import org.whispersystems.textsecure.internal.push.PushMessageProtos;
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.exceptions.MismatchedDevicesException;
import org.whispersystems.textsecure.internal.push.exceptions.StaleDevicesException;
import org.whispersystems.textsecure.internal.util.StaticCredentialsProvider;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
/**
* Created by ludwig on 19/07/15.
*/
public class TextSecureSMPMessageSender {
private static final String TAG = TextSecureSMPMessageSender.class.getSimpleName();
private final PushServiceSocket socket;
private final AxolotlStore store;
private final TextSecureAddress localAddress;
private final Optional<TextSecureSMPMessageSender.EventListener> eventListener;
public TextSecureSMPMessageSender(String url, TrustStore trustStore, String user, String
password, AxolotlStore store, Optional<EventListener> eventListener) {
this.socket = new PushServiceSocket(url, trustStore, new StaticCredentialsProvider(user, password, (String)null));
this.store = store;
this.localAddress = new TextSecureAddress(user);
this.eventListener = eventListener;
}
public void sendMessage(TextSecureAddress recipient, TextSecureSMPMessage message)
throws
UntrustedIdentityException, IOException {
Log.d(TAG, "sendMessage() check for SMPSync true: " + message.isSMPSyncMessage());
byte[] content = this.createMessageContent(message);
long timestamp = message.getTimestamp();
/*
//TODO: figured this out ... still what triggers the true response?!!?
SendMessageResponse response = new SendMessageResponse(true); // this.sendMessage(recipient,
// timestamp, content);
Log.d(TAG, "SendMessageResponse.needsSYC: " + response.getNeedsSync());
// TODO: adapt to SMP here & change to "isSMPSyncMessage()"
Log.d(TAG, "isSMPMessage: " + message.isSMPMessage());
if(response != null && response.getNeedsSync()) {
byte[] syncSMPMessage = this.createSyncSMPMessageContent(content, Optional.of(recipient),
timestamp);
this.sendMessage(this.localAddress, timestamp, syncSMPMessage);
Log.d(TAG, "sendSyncMessage");
}
if(message.isEndSession()) {
this.store.deleteAllSessions(recipient.getNumber());
if(this.eventListener.isPresent()) {
(this.eventListener.get()).onSecurityEvent(recipient);
}
}
*/
// TODO: adapt to SMP here & change to "isSMPSyncMessage()"
Log.d(TAG, "isSMPMessage: " + message.isSMPMessage());
Log.d(TAG, "isSMPSyncMessage: " + message.isSMPSyncMessage());
if(message.isSMPSyncMessage()) {
byte[] syncSMPMessage = this.createSyncSMPMessageContent(content, Optional.of(recipient),
timestamp);
Log.d(TAG, "sendsSyncMessage");
this.sendMessage(recipient, timestamp, syncSMPMessage);
} else {
Log.d(TAG, "sendsSMPMessage");
this.sendMessage(recipient, timestamp, content);
}
}
private byte[] createMessageContent(TextSecureSMPMessage message) throws IOException {
PushMessageProtos.PushMessageContent.Builder builder = PushMessageProtos.PushMessageContent.newBuilder();
List pointers = this.createAttachmentPointers(message.getAttachments());
if(!pointers.isEmpty()) {
builder.addAllAttachments(pointers);
}
if(message.getBody().isPresent()) {
Log.d(TAG, "createMessageContent getBody.isPresent()");
builder.setBody((String)message.getBody().get());
}
if(message.getGroupInfo().isPresent()) {
builder.setGroup(this.createGroupContent(message.getGroupInfo().get()));
}
if(message.isEndSession()) {
builder.setFlags(1);
}
if(message.isSMPSyncMessage()){
Log.d(TAG, "createMessageContent.isSMPSyncMessage: " + message.isSMPSyncMessage());
builder.setBody((String)message.getBody().get());
}
return builder.build().toByteArray();
}
private SendMessageResponse sendMessage(TextSecureAddress recipient, long timestamp, byte[] content) throws UntrustedIdentityException, IOException {
for(int i = 0; i < 3; ++i) {
try {
OutgoingPushMessageList ste = this.getEncryptedMessages(this.socket, recipient, timestamp, content);
return this.socket.sendMessage(ste);
} catch (MismatchedDevicesException var7) {
Log.w(TAG, var7);
this.handleMismatchedDevices(this.socket, recipient, var7.getMismatchedDevices());
} catch (StaleDevicesException var8) {
Log.w(TAG, var8);
this.handleStaleDevices(recipient, var8.getStaleDevices());
}
}
throw new IOException("Failed to resolve conflicts after 3 attempts!");
}
private byte[] createSyncSMPMessageContent(byte[] content, Optional<TextSecureAddress> recipient,
long timestamp) {
try {
org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.SyncMessageContext.Builder e = PushMessageProtos.PushMessageContent.SyncMessageContext.newBuilder();
e.setTimestamp(timestamp);
if(recipient.isPresent()) {
Log.d(TAG, "createSynSMPMessageContent recipient: " + recipient.get().getNumber());
e.setDestination((recipient.get()).getNumber());
}
Log.d(TAG, "createSyncSMPMessageContent SyncMessageContext.Builder: " + e
.getDescriptorForType().getFullName());
PushMessageProtos.PushMessageContent.Builder builder = PushMessageProtos.PushMessageContent.parseFrom(content).toBuilder();
builder.setSync(e.build());
Log.d(TAG, "createSyncSMPMessageContent: " + builder.getSync());
return builder.build().toByteArray();
} catch (InvalidProtocolBufferException var7) {
throw new AssertionError(var7);
}
}
private List<PushMessageProtos.PushMessageContent.AttachmentPointer> createAttachmentPointers(Optional<List<TextSecureAttachment>> attachments) throws IOException {
LinkedList pointers = new LinkedList();
if(attachments.isPresent() && !((List)attachments.get()).isEmpty()) {
Iterator var3 = ((List)attachments.get()).iterator();
while(var3.hasNext()) {
TextSecureAttachment attachment = (TextSecureAttachment)var3.next();
if(attachment.isStream()) {
Log.w(TAG, "Found attachment, creating pointer...");
pointers.add(this.createAttachmentPointer(attachment.asStream()));
}
}
return pointers;
} else {
Log.w(TAG, "No attachments present...");
return pointers;
}
}
private PushMessageProtos.PushMessageContent.AttachmentPointer createAttachmentPointer(TextSecureAttachmentStream attachment) throws IOException {
byte[] attachmentKey = org.whispersystems.textsecure.internal.util.Util.getSecretBytes(64);
PushAttachmentData attachmentData = new PushAttachmentData(attachment.getContentType(), attachment.getInputStream(), attachment.getLength(), attachmentKey);
long attachmentId = this.socket.sendAttachment(attachmentData);
return PushMessageProtos.PushMessageContent.AttachmentPointer.newBuilder().setContentType(attachment.getContentType()).setId(attachmentId).setKey(ByteString.copyFrom(attachmentKey)).build();
}
private PushMessageProtos.PushMessageContent.GroupContext createGroupContent(TextSecureGroup group) throws IOException {
org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.GroupContext.Builder builder = PushMessageProtos.PushMessageContent.GroupContext.newBuilder();
builder.setId(ByteString.copyFrom(group.getGroupId()));
if(group.getType() != TextSecureGroup.Type.DELIVER) {
if(group.getType() == TextSecureGroup.Type.UPDATE) {
builder.setType(org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.GroupContext.Type.UPDATE);
} else {
if(group.getType() != TextSecureGroup.Type.QUIT) {
throw new AssertionError("Unknown type: " + group.getType());
}
builder.setType(org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.GroupContext.Type.QUIT);
}
if(group.getName().isPresent()) {
builder.setName((String)group.getName().get());
}
if(group.getMembers().isPresent()) {
builder.addAllMembers((Iterable)group.getMembers().get());
}
if(group.getAvatar().isPresent() && ((TextSecureAttachment)group.getAvatar().get()).isStream()) {
PushMessageProtos.PushMessageContent.AttachmentPointer pointer = this.createAttachmentPointer(((TextSecureAttachment)group.getAvatar().get()).asStream());
builder.setAvatar(pointer);
}
} else {
builder.setType(org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.GroupContext.Type.DELIVER);
}
return builder.build();
}
private SendMessageResponse sendMessage(List<TextSecureAddress> recipients, long timestamp, byte[] content) throws IOException, EncapsulatedExceptions {
LinkedList untrustedIdentities = new LinkedList();
LinkedList unregisteredUsers = new LinkedList();
LinkedList networkExceptions = new LinkedList();
SendMessageResponse response = null;
Iterator var9 = recipients.iterator();
while(var9.hasNext()) {
TextSecureAddress recipient = (TextSecureAddress)var9.next();
try {
response = this.sendMessage(recipient, timestamp, content);
} catch (UntrustedIdentityException var12) {
Log.w(TAG, var12);
untrustedIdentities.add(var12);
} catch (UnregisteredUserException var13) {
Log.w(TAG, var13);
unregisteredUsers.add(var13);
} catch (PushNetworkException var14) {
Log.w(TAG, var14);
networkExceptions.add(new NetworkFailureException(recipient.getNumber(), var14));
}
}
if(untrustedIdentities.isEmpty() && unregisteredUsers.isEmpty() && networkExceptions.isEmpty()) {
return response;
} else {
throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions);
}
}
private OutgoingPushMessageList getEncryptedMessages(PushServiceSocket socket, TextSecureAddress recipient, long timestamp, byte[] plaintext) throws IOException, UntrustedIdentityException {
LinkedList messages = new LinkedList();
if(!recipient.equals(this.localAddress)) {
Log.d(TAG, "recipient != localAddress");
messages.add(this.getEncryptedMessage(socket, recipient, 1, plaintext));
}
Log.d(TAG, "recipient == localAddress");
Iterator var7 = this.store.getSubDeviceSessions(recipient.getNumber()).iterator();
while(var7.hasNext()) {
int deviceId = ((Integer)var7.next()).intValue();
messages.add(this.getEncryptedMessage(socket, recipient, deviceId, plaintext));
}
return new OutgoingPushMessageList(recipient.getNumber(), timestamp, (String)recipient.getRelay().orNull(), messages);
}
private OutgoingPushMessage getEncryptedMessage(PushServiceSocket socket, TextSecureAddress recipient, int deviceId, byte[] plaintext) throws IOException, UntrustedIdentityException {
AxolotlAddress axolotlAddress = new AxolotlAddress(recipient.getNumber(), deviceId);
TextSecureCipher cipher = new TextSecureCipher(this.localAddress, this.store);
if(!this.store.containsSession(axolotlAddress)) {
try {
List e = socket.getPreKeys(recipient, deviceId);
Iterator var8 = e.iterator();
while(var8.hasNext()) {
PreKeyBundle preKey = (PreKeyBundle)var8.next();
try {
AxolotlAddress e1 = new AxolotlAddress(recipient.getNumber(), preKey.getDeviceId());
SessionBuilder sessionBuilder = new SessionBuilder(this.store, e1);
sessionBuilder.process(preKey);
} catch (org.whispersystems.libaxolotl.UntrustedIdentityException var12) {
throw new UntrustedIdentityException("Untrusted identity key!", recipient.getNumber(), preKey.getIdentityKey());
}
}
if(this.eventListener.isPresent()) {
(this.eventListener.get()).onSecurityEvent(recipient);
}
} catch (InvalidKeyException var13) {
throw new IOException(var13);
}
}
return cipher.encrypt(axolotlAddress, plaintext);
}
private void handleMismatchedDevices(PushServiceSocket socket, TextSecureAddress recipient, MismatchedDevices mismatchedDevices) throws IOException, UntrustedIdentityException {
try {
Iterator e = mismatchedDevices.getExtraDevices().iterator();
int missingDeviceId;
while(e.hasNext()) {
missingDeviceId = ((Integer)e.next()).intValue();
this.store.deleteSession(new AxolotlAddress(recipient.getNumber(), missingDeviceId));
}
e = mismatchedDevices.getMissingDevices().iterator();
while(e.hasNext()) {
missingDeviceId = ((Integer)e.next()).intValue();
PreKeyBundle preKey = socket.getPreKey(recipient, missingDeviceId);
try {
SessionBuilder e1 = new SessionBuilder(this.store, new AxolotlAddress(recipient.getNumber(), missingDeviceId));
e1.process(preKey);
} catch (org.whispersystems.libaxolotl.UntrustedIdentityException var8) {
throw new UntrustedIdentityException("Untrusted identity key!", recipient.getNumber(), preKey.getIdentityKey());
}
}
} catch (InvalidKeyException var9) {
throw new IOException(var9);
}
}
private void handleStaleDevices(TextSecureAddress recipient, StaleDevices staleDevices) {
Iterator var3 = staleDevices.getStaleDevices().iterator();
while(var3.hasNext()) {
int staleDeviceId = ((Integer)var3.next()).intValue();
this.store.deleteSession(new AxolotlAddress(recipient.getNumber(), staleDeviceId));
}
}
public interface EventListener {
void onSecurityEvent(TextSecureAddress var1);
}
}