Merge branch 'dev' of https://github.com/loki-project/session-android into specific-group-updates

This commit is contained in:
Brice-W
2021-03-22 15:12:09 +11:00
167 changed files with 2487 additions and 6274 deletions

View File

@@ -1,825 +0,0 @@
/*
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.session.libsignal.service.api;
import com.google.protobuf.ByteString;
import org.jetbrains.annotations.Nullable;
import org.session.libsignal.libsignal.ecc.ECKeyPair;
import org.session.libsignal.libsignal.state.IdentityKeyStore;
import org.session.libsignal.utilities.logging.Log;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream;
import org.session.libsignal.service.api.crypto.UnidentifiedAccess;
import org.session.libsignal.service.api.crypto.UntrustedIdentityException;
import org.session.libsignal.service.api.messages.SendMessageResult;
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer;
import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream;
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
import org.session.libsignal.service.api.messages.SignalServiceGroup;
import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage;
import org.session.libsignal.service.api.messages.SignalServiceTypingMessage;
import org.session.libsignal.service.api.messages.shared.SharedContact;
import org.session.libsignal.service.api.push.SignalServiceAddress;
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
import org.session.libsignal.service.internal.crypto.PaddingInputStream;
import org.session.libsignal.service.internal.push.OutgoingPushMessage;
import org.session.libsignal.service.internal.push.OutgoingPushMessageList;
import org.session.libsignal.service.internal.push.PushAttachmentData;
import org.session.libsignal.service.internal.push.PushTransportDetails;
import org.session.libsignal.service.internal.push.SignalServiceProtos;
import org.session.libsignal.service.internal.push.SignalServiceProtos.AttachmentPointer;
import org.session.libsignal.service.internal.push.SignalServiceProtos.Content;
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage;
import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext;
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage.LokiProfile;
import org.session.libsignal.service.internal.push.SignalServiceProtos.ReceiptMessage;
import org.session.libsignal.service.internal.push.SignalServiceProtos.TypingMessage;
import org.session.libsignal.service.internal.push.http.AttachmentCipherOutputStreamFactory;
import org.session.libsignal.service.internal.push.http.OutputStreamFactory;
import org.session.libsignal.utilities.Base64;
import org.session.libsignal.service.internal.util.Util;
import org.session.libsignal.utilities.concurrent.SettableFuture;
import org.session.libsignal.service.loki.api.LokiDotNetAPI;
import org.session.libsignal.service.loki.api.PushNotificationAPI;
import org.session.libsignal.service.loki.api.SignalMessageInfo;
import org.session.libsignal.service.loki.api.SnodeAPI;
import org.session.libsignal.service.loki.api.crypto.SessionProtocol;
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI;
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI;
import org.session.libsignal.service.loki.api.opengroups.PublicChatMessage;
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol;
import org.session.libsignal.service.loki.database.LokiMessageDatabaseProtocol;
import org.session.libsignal.service.loki.database.LokiOpenGroupDatabaseProtocol;
import org.session.libsignal.service.loki.database.LokiThreadDatabaseProtocol;
import org.session.libsignal.service.loki.database.LokiUserDatabaseProtocol;
import org.session.libsignal.service.loki.utilities.TTLUtilities;
import org.session.libsignal.service.loki.utilities.Broadcaster;
import org.session.libsignal.service.loki.utilities.HexEncodingKt;
import org.session.libsignal.service.loki.utilities.PlaintextOutputStreamFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
import nl.komponents.kovenant.Promise;
/**
* The main interface for sending Signal Service messages.
*
* @author Moxie Marlinspike
*/
public class SignalServiceMessageSender {
private static final String TAG = SignalServiceMessageSender.class.getSimpleName();
private final IdentityKeyStore store;
// Loki
private final String userPublicKey;
private final LokiAPIDatabaseProtocol apiDatabase;
private final LokiThreadDatabaseProtocol threadDatabase;
private final LokiMessageDatabaseProtocol messageDatabase;
private final SessionProtocol sessionProtocolImpl;
private final LokiUserDatabaseProtocol userDatabase;
private final LokiOpenGroupDatabaseProtocol openGroupDatabase;
private final Broadcaster broadcaster;
public SignalServiceMessageSender(IdentityKeyStore store,
String userPublicKey,
LokiAPIDatabaseProtocol apiDatabase,
LokiThreadDatabaseProtocol threadDatabase,
LokiMessageDatabaseProtocol messageDatabase,
SessionProtocol sessionProtocolImpl,
LokiUserDatabaseProtocol userDatabase,
LokiOpenGroupDatabaseProtocol openGroupDatabase,
Broadcaster broadcaster)
{
this.store = store;
this.userPublicKey = userPublicKey;
this.apiDatabase = apiDatabase;
this.threadDatabase = threadDatabase;
this.messageDatabase = messageDatabase;
this.sessionProtocolImpl = sessionProtocolImpl;
this.userDatabase = userDatabase;
this.openGroupDatabase = openGroupDatabase;
this.broadcaster = broadcaster;
}
/**
* Send a read receipt for a received message.
*
* @param recipient The sender of the received message you're acknowledging.
* @param message The read receipt to deliver.
* @throws IOException
*/
public void sendReceipt(SignalServiceAddress recipient,
Optional<UnidentifiedAccess> unidentifiedAccess,
SignalServiceReceiptMessage message)
throws IOException {
byte[] content = createReceiptContent(message);
boolean useFallbackEncryption = true;
sendMessage(recipient, unidentifiedAccess, message.getWhen(), content, false, message.getTTL(), useFallbackEncryption);
}
public void sendTyping(List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccess>> unidentifiedAccess,
SignalServiceTypingMessage message)
throws IOException
{
byte[] content = createTypingContent(message);
sendMessage(0, recipients, unidentifiedAccess, message.getTimestamp(), content, true, message.getTTL(), false, false);
}
/**
* Send a message to a single recipient.
*
* @param recipient The message's destination.
* @param message The message.
* @throws IOException
*/
public SendMessageResult sendMessage(long messageID,
SignalServiceAddress recipient,
Optional<UnidentifiedAccess> unidentifiedAccess,
SignalServiceDataMessage message,
boolean isSelfSend)
throws IOException
{
byte[] content = createMessageContent(message, recipient);
long timestamp = message.getTimestamp();
boolean isClosedGroup = message.group.isPresent() && message.group.get().getGroupType() == SignalServiceGroup.GroupType.SIGNAL;
SendMessageResult result = sendMessage(messageID, recipient, unidentifiedAccess, timestamp, content, false, message.getTTL(), true, isClosedGroup, message.hasVisibleContent() && !isSelfSend, message.getSyncTarget());
return result;
}
/**
* Send a message to a group.
*
* @param recipients The group members.
* @param message The group message.
* @throws IOException
*/
public List<SendMessageResult> sendMessage(long messageID,
List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccess>> unidentifiedAccess,
SignalServiceDataMessage message)
throws IOException {
// Loki - We only need the first recipient in the line below. This is because the recipient is only used to determine
// whether an attachment is being sent to an open group or not.
byte[] content = createMessageContent(message, recipients.get(0));
long timestamp = message.getTimestamp();
boolean isClosedGroup = message.group.isPresent() && message.group.get().getGroupType() == SignalServiceGroup.GroupType.SIGNAL;
return sendMessage(messageID, recipients, unidentifiedAccess, timestamp, content, false, message.getTTL(), isClosedGroup, message.hasVisibleContent());
}
public SignalServiceAttachmentPointer uploadAttachment(SignalServiceAttachmentStream attachment, boolean usePadding, @Nullable SignalServiceAddress recipient)
throws IOException
{
boolean shouldEncrypt = true;
String server = FileServerAPI.shared.getServer();
// Loki - Check if we are sending to an open group
if (recipient != null) {
long threadID = threadDatabase.getThreadID(recipient.getNumber());
PublicChat publicChat = threadDatabase.getPublicChat(threadID);
if (publicChat != null) {
shouldEncrypt = false;
server = publicChat.getServer();
}
}
byte[] attachmentKey = Util.getSecretBytes(64);
long paddedLength = usePadding ? PaddingInputStream.getPaddedSize(attachment.getLength()) : attachment.getLength();
InputStream dataStream = usePadding ? new PaddingInputStream(attachment.getInputStream(), attachment.getLength()) : attachment.getInputStream();
long ciphertextLength = shouldEncrypt ? AttachmentCipherOutputStream.getCiphertextLength(paddedLength) : attachment.getLength();
OutputStreamFactory outputStreamFactory = shouldEncrypt ? new AttachmentCipherOutputStreamFactory(attachmentKey) : new PlaintextOutputStreamFactory();
PushAttachmentData attachmentData = new PushAttachmentData(attachment.getContentType(), dataStream, ciphertextLength, outputStreamFactory, attachment.getListener());
// Loki - Upload attachment
LokiDotNetAPI.UploadResult result = FileServerAPI.shared.uploadAttachment(server, attachmentData);
return new SignalServiceAttachmentPointer(result.getId(),
attachment.getContentType(),
attachmentKey,
Optional.of(Util.toIntExact(attachment.getLength())),
attachment.getPreview(),
attachment.getWidth(), attachment.getHeight(),
Optional.fromNullable(result.getDigest()),
attachment.getFileName(),
attachment.getVoiceNote(),
attachment.getCaption(),
result.getUrl());
}
private byte[] createTypingContent(SignalServiceTypingMessage message) {
Content.Builder container = Content.newBuilder();
TypingMessage.Builder builder = TypingMessage.newBuilder();
builder.setTimestamp(message.getTimestamp());
if (message.isTypingStarted()) builder.setAction(TypingMessage.Action.STARTED);
else if (message.isTypingStopped()) builder.setAction(TypingMessage.Action.STOPPED);
else throw new IllegalArgumentException("Unknown typing indicator");
return container.setTypingMessage(builder).build().toByteArray();
}
private byte[] createReceiptContent(SignalServiceReceiptMessage message) {
Content.Builder container = Content.newBuilder();
ReceiptMessage.Builder builder = ReceiptMessage.newBuilder();
for (long timestamp : message.getTimestamps()) {
builder.addTimestamp(timestamp);
}
if (message.isDeliveryReceipt()) builder.setType(ReceiptMessage.Type.DELIVERY);
else if (message.isReadReceipt()) builder.setType(ReceiptMessage.Type.READ);
return container.setReceiptMessage(builder).build().toByteArray();
}
private byte[] createMessageContent(SignalServiceDataMessage message, SignalServiceAddress recipient)
throws IOException
{
Content.Builder container = Content.newBuilder();
DataMessage.Builder builder = DataMessage.newBuilder();
List<AttachmentPointer> pointers = createAttachmentPointers(message.getAttachments(), recipient);
if (!pointers.isEmpty()) {
builder.addAllAttachments(pointers);
}
if (message.getBody().isPresent()) {
builder.setBody(message.getBody().get());
}
if (message.getGroupInfo().isPresent()) {
builder.setGroup(createGroupContent(message.getGroupInfo().get(), recipient));
}
if (message.isExpirationUpdate()) {
builder.setFlags(DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE);
}
if (message.getExpiresInSeconds() > 0) {
builder.setExpireTimer(message.getExpiresInSeconds());
}
if (message.getProfileKey().isPresent()) {
builder.setProfileKey(ByteString.copyFrom(message.getProfileKey().get()));
}
if (message.getSyncTarget().isPresent()) {
builder.setSyncTarget(message.getSyncTarget().get());
}
if (message.getQuote().isPresent()) {
DataMessage.Quote.Builder quoteBuilder = DataMessage.Quote.newBuilder()
.setId(message.getQuote().get().getId())
.setAuthor(message.getQuote().get().getAuthor().getNumber())
.setText(message.getQuote().get().getText());
for (SignalServiceDataMessage.Quote.QuotedAttachment attachment : message.getQuote().get().getAttachments()) {
DataMessage.Quote.QuotedAttachment.Builder quotedAttachment = DataMessage.Quote.QuotedAttachment.newBuilder();
quotedAttachment.setContentType(attachment.getContentType());
if (attachment.getFileName() != null) {
quotedAttachment.setFileName(attachment.getFileName());
}
if (attachment.getThumbnail() != null) {
quotedAttachment.setThumbnail(createAttachmentPointer(attachment.getThumbnail().asStream(), recipient));
}
quoteBuilder.addAttachments(quotedAttachment);
}
builder.setQuote(quoteBuilder);
}
if (message.getSharedContacts().isPresent()) {
builder.addAllContact(createSharedContactContent(message.getSharedContacts().get(), recipient));
}
if (message.getPreviews().isPresent()) {
for (SignalServiceDataMessage.Preview preview : message.getPreviews().get()) {
DataMessage.Preview.Builder previewBuilder = DataMessage.Preview.newBuilder();
previewBuilder.setTitle(preview.getTitle());
previewBuilder.setUrl(preview.getUrl());
if (preview.getImage().isPresent()) {
if (preview.getImage().get().isStream()) {
previewBuilder.setImage(createAttachmentPointer(preview.getImage().get().asStream(), recipient));
} else {
previewBuilder.setImage(createAttachmentPointer(preview.getImage().get().asPointer()));
}
}
builder.addPreview(previewBuilder.build());
}
}
LokiProfile.Builder lokiUserProfileBuilder = LokiProfile.newBuilder();
String displayName = userDatabase.getDisplayName(userPublicKey);
if (displayName != null) { lokiUserProfileBuilder.setDisplayName(displayName); }
String profilePictureURL = userDatabase.getProfilePictureURL(userPublicKey);
if (profilePictureURL != null) { lokiUserProfileBuilder.setProfilePicture(profilePictureURL); }
builder.setProfile(lokiUserProfileBuilder.build());
builder.setTimestamp(message.getTimestamp());
container.setDataMessage(builder);
return container.build().toByteArray();
}
private GroupContext createGroupContent(SignalServiceGroup group, SignalServiceAddress recipient)
throws IOException
{
GroupContext.Builder builder = GroupContext.newBuilder();
builder.setId(ByteString.copyFrom(group.getGroupId()));
if (group.getType() != SignalServiceGroup.Type.DELIVER) {
if (group.getType() == SignalServiceGroup.Type.UPDATE) builder.setType(GroupContext.Type.UPDATE);
else if (group.getType() == SignalServiceGroup.Type.QUIT) builder.setType(GroupContext.Type.QUIT);
else if (group.getType() == SignalServiceGroup.Type.REQUEST_INFO) builder.setType(GroupContext.Type.REQUEST_INFO);
else throw new AssertionError("Unknown type: " + group.getType());
if (group.getName().isPresent()) builder.setName(group.getName().get());
if (group.getMembers().isPresent()) builder.addAllMembers(group.getMembers().get());
if (group.getAdmins().isPresent()) builder.addAllAdmins(group.getAdmins().get());
if (group.getAvatar().isPresent()) {
if (group.getAvatar().get().isStream()) {
builder.setAvatar(createAttachmentPointer(group.getAvatar().get().asStream(), recipient));
} else {
builder.setAvatar(createAttachmentPointer(group.getAvatar().get().asPointer()));
}
}
} else {
builder.setType(GroupContext.Type.DELIVER);
}
return builder.build();
}
private List<DataMessage.Contact> createSharedContactContent(List<SharedContact> contacts, SignalServiceAddress recipient)
throws IOException
{
List<DataMessage.Contact> results = new LinkedList<>();
for (SharedContact contact : contacts) {
DataMessage.Contact.Name.Builder nameBuilder = DataMessage.Contact.Name.newBuilder();
if (contact.getName().getFamily().isPresent()) nameBuilder.setFamilyName(contact.getName().getFamily().get());
if (contact.getName().getGiven().isPresent()) nameBuilder.setGivenName(contact.getName().getGiven().get());
if (contact.getName().getMiddle().isPresent()) nameBuilder.setMiddleName(contact.getName().getMiddle().get());
if (contact.getName().getPrefix().isPresent()) nameBuilder.setPrefix(contact.getName().getPrefix().get());
if (contact.getName().getSuffix().isPresent()) nameBuilder.setSuffix(contact.getName().getSuffix().get());
if (contact.getName().getDisplay().isPresent()) nameBuilder.setDisplayName(contact.getName().getDisplay().get());
DataMessage.Contact.Builder contactBuilder = DataMessage.Contact.newBuilder()
.setName(nameBuilder);
if (contact.getAddress().isPresent()) {
for (SharedContact.PostalAddress address : contact.getAddress().get()) {
DataMessage.Contact.PostalAddress.Builder addressBuilder = DataMessage.Contact.PostalAddress.newBuilder();
switch (address.getType()) {
case HOME: addressBuilder.setType(DataMessage.Contact.PostalAddress.Type.HOME); break;
case WORK: addressBuilder.setType(DataMessage.Contact.PostalAddress.Type.WORK); break;
case CUSTOM: addressBuilder.setType(DataMessage.Contact.PostalAddress.Type.CUSTOM); break;
default: throw new AssertionError("Unknown type: " + address.getType());
}
if (address.getCity().isPresent()) addressBuilder.setCity(address.getCity().get());
if (address.getCountry().isPresent()) addressBuilder.setCountry(address.getCountry().get());
if (address.getLabel().isPresent()) addressBuilder.setLabel(address.getLabel().get());
if (address.getNeighborhood().isPresent()) addressBuilder.setNeighborhood(address.getNeighborhood().get());
if (address.getPobox().isPresent()) addressBuilder.setPobox(address.getPobox().get());
if (address.getPostcode().isPresent()) addressBuilder.setPostcode(address.getPostcode().get());
if (address.getRegion().isPresent()) addressBuilder.setRegion(address.getRegion().get());
if (address.getStreet().isPresent()) addressBuilder.setStreet(address.getStreet().get());
contactBuilder.addAddress(addressBuilder);
}
}
if (contact.getEmail().isPresent()) {
for (SharedContact.Email email : contact.getEmail().get()) {
DataMessage.Contact.Email.Builder emailBuilder = DataMessage.Contact.Email.newBuilder()
.setValue(email.getValue());
switch (email.getType()) {
case HOME: emailBuilder.setType(DataMessage.Contact.Email.Type.HOME); break;
case WORK: emailBuilder.setType(DataMessage.Contact.Email.Type.WORK); break;
case MOBILE: emailBuilder.setType(DataMessage.Contact.Email.Type.MOBILE); break;
case CUSTOM: emailBuilder.setType(DataMessage.Contact.Email.Type.CUSTOM); break;
default: throw new AssertionError("Unknown type: " + email.getType());
}
if (email.getLabel().isPresent()) emailBuilder.setLabel(email.getLabel().get());
contactBuilder.addEmail(emailBuilder);
}
}
if (contact.getPhone().isPresent()) {
for (SharedContact.Phone phone : contact.getPhone().get()) {
DataMessage.Contact.Phone.Builder phoneBuilder = DataMessage.Contact.Phone.newBuilder()
.setValue(phone.getValue());
switch (phone.getType()) {
case HOME: phoneBuilder.setType(DataMessage.Contact.Phone.Type.HOME); break;
case WORK: phoneBuilder.setType(DataMessage.Contact.Phone.Type.WORK); break;
case MOBILE: phoneBuilder.setType(DataMessage.Contact.Phone.Type.MOBILE); break;
case CUSTOM: phoneBuilder.setType(DataMessage.Contact.Phone.Type.CUSTOM); break;
default: throw new AssertionError("Unknown type: " + phone.getType());
}
if (phone.getLabel().isPresent()) phoneBuilder.setLabel(phone.getLabel().get());
contactBuilder.addNumber(phoneBuilder);
}
}
if (contact.getAvatar().isPresent()) {
AttachmentPointer pointer = contact.getAvatar().get().getAttachment().isStream() ? createAttachmentPointer(contact.getAvatar().get().getAttachment().asStream(), recipient)
: createAttachmentPointer(contact.getAvatar().get().getAttachment().asPointer());
contactBuilder.setAvatar(DataMessage.Contact.Avatar.newBuilder()
.setAvatar(pointer)
.setIsProfile(contact.getAvatar().get().isProfile()));
}
if (contact.getOrganization().isPresent()) {
contactBuilder.setOrganization(contact.getOrganization().get());
}
results.add(contactBuilder.build());
}
return results;
}
private List<SendMessageResult> sendMessage(long messageID,
List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccess>> unidentifiedAccess,
long timestamp,
byte[] content,
boolean online,
int ttl,
boolean isClosedGroup,
boolean notifyPNServer)
{
List<SendMessageResult> results = new LinkedList<>();
Iterator<SignalServiceAddress> recipientIterator = recipients.iterator();
Iterator<Optional<UnidentifiedAccess>> unidentifiedAccessIterator = unidentifiedAccess.iterator();
while (recipientIterator.hasNext()) {
SignalServiceAddress recipient = recipientIterator.next();
SendMessageResult result = sendMessage(messageID, recipient, unidentifiedAccessIterator.next(), timestamp, content, online, ttl, true, isClosedGroup, notifyPNServer, Optional.absent());
results.add(result);
}
return results;
}
private SendMessageResult sendMessage(SignalServiceAddress recipient,
Optional<UnidentifiedAccess> unidentifiedAccess,
long timestamp,
byte[] content,
boolean online,
int ttl,
boolean useFallbackEncryption)
throws IOException
{
// Loki - This method is only invoked for various types of control messages
return sendMessage(0, recipient, unidentifiedAccess, timestamp, content, online, ttl, false, useFallbackEncryption, false,Optional.absent());
}
public SendMessageResult sendMessage(final long messageID,
final SignalServiceAddress recipient,
Optional<UnidentifiedAccess> unidentifiedAccess,
long timestamp,
byte[] content,
boolean online,
int ttl,
boolean useFallbackEncryption,
boolean isClosedGroup,
boolean notifyPNServer,
Optional<String> syncTarget)
{
boolean isSelfSend = syncTarget.isPresent() && !syncTarget.get().isEmpty();
long threadID;
if (isSelfSend) {
threadID = threadDatabase.getThreadID(syncTarget.get());
} else {
threadID = threadDatabase.getThreadID(recipient.getNumber());
}
PublicChat publicChat = threadDatabase.getPublicChat(threadID);
if (publicChat != null) {
return sendMessageToPublicChat(messageID, recipient, timestamp, content, publicChat);
} else {
return sendMessageToPrivateChat(messageID, recipient, unidentifiedAccess, timestamp, content, online, ttl, useFallbackEncryption, isClosedGroup, notifyPNServer, syncTarget);
}
}
private SendMessageResult sendMessageToPublicChat(final long messageID,
final SignalServiceAddress recipient,
long timestamp,
byte[] content,
PublicChat publicChat) {
if (messageID == 0) {
Log.d("Loki", "Missing message ID.");
}
final SettableFuture<?>[] future = { new SettableFuture<Unit>() };
try {
DataMessage data = Content.parseFrom(content).getDataMessage();
String body = (data.getBody() != null && data.getBody().length() > 0) ? data.getBody() : Long.toString(data.getTimestamp());
PublicChatMessage.Quote quote = null;
if (data.hasQuote()) {
long quoteID = data.getQuote().getId();
String quoteePublicKey = data.getQuote().getAuthor();
long serverID = messageDatabase.getQuoteServerID(quoteID, quoteePublicKey);
quote = new PublicChatMessage.Quote(quoteID, quoteePublicKey, data.getQuote().getText(), serverID);
}
DataMessage.Preview linkPreview = (data.getPreviewList().size() > 0) ? data.getPreviewList().get(0) : null;
ArrayList<PublicChatMessage.Attachment> attachments = new ArrayList<>();
if (linkPreview != null && linkPreview.hasImage()) {
AttachmentPointer attachmentPointer = linkPreview.getImage();
String caption = attachmentPointer.hasCaption() ? attachmentPointer.getCaption() : null;
attachments.add(new PublicChatMessage.Attachment(
PublicChatMessage.Attachment.Kind.LinkPreview,
publicChat.getServer(),
attachmentPointer.getId(),
attachmentPointer.getContentType(),
attachmentPointer.getSize(),
attachmentPointer.getFileName(),
attachmentPointer.getFlags(),
attachmentPointer.getWidth(),
attachmentPointer.getHeight(),
caption,
attachmentPointer.getUrl(),
linkPreview.getUrl(),
linkPreview.getTitle()
));
}
for (AttachmentPointer attachmentPointer : data.getAttachmentsList()) {
String caption = attachmentPointer.hasCaption() ? attachmentPointer.getCaption() : null;
attachments.add(new PublicChatMessage.Attachment(
PublicChatMessage.Attachment.Kind.Attachment,
publicChat.getServer(),
attachmentPointer.getId(),
attachmentPointer.getContentType(),
attachmentPointer.getSize(),
attachmentPointer.getFileName(),
attachmentPointer.getFlags(),
attachmentPointer.getWidth(),
attachmentPointer.getHeight(),
caption,
attachmentPointer.getUrl(),
null,
null
));
}
PublicChatMessage message = new PublicChatMessage(userPublicKey, "", body, timestamp, PublicChatAPI.getPublicChatMessageType(), quote, attachments);
byte[] privateKey = store.getIdentityKeyPair().getPrivateKey().serialize();
new PublicChatAPI(userPublicKey, privateKey, apiDatabase, userDatabase, openGroupDatabase).sendMessage(message, publicChat.getChannel(), publicChat.getServer()).success(new Function1<PublicChatMessage, Unit>() {
@Override
public Unit invoke(PublicChatMessage message) {
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
messageDatabase.setServerID(messageID, message.getServerID());
f.set(Unit.INSTANCE);
return Unit.INSTANCE;
}
}).fail(new Function1<Exception, Unit>() {
@Override
public Unit invoke(Exception exception) {
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
f.setException(exception);
return Unit.INSTANCE;
}
});
} catch (Exception exception) {
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
f.setException(exception);
}
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
try {
f.get(1, TimeUnit.MINUTES);
return SendMessageResult.success(recipient, false, false);
} catch (Exception exception) {
return SendMessageResult.networkFailure(recipient);
}
}
private SendMessageResult sendMessageToPrivateChat(final long messageID,
final SignalServiceAddress recipient,
Optional<UnidentifiedAccess> unidentifiedAccess,
final long timestamp,
byte[] content,
boolean online,
int ttl,
boolean useFallbackEncryption,
boolean isClosedGroup,
final boolean notifyPNServer,
Optional<String> syncTarget)
{
final SettableFuture<?>[] future = { new SettableFuture<Unit>() };
OutgoingPushMessageList messages = getSessionProtocolEncryptedMessage(recipient, timestamp, content);
// Loki - Remove this when we have shared sender keys
// ========
if (messages.getMessages().isEmpty()) {
return SendMessageResult.success(recipient, false, false);
}
// ========
OutgoingPushMessage message = messages.getMessages().get(0);
final SignalServiceProtos.Envelope.Type type = SignalServiceProtos.Envelope.Type.valueOf(message.type);
final String senderID;
if (type == SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT) {
senderID = recipient.getNumber();
} else if (type == SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER) {
senderID = "";
} else {
senderID = userPublicKey;
}
final int senderDeviceID = (type == SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER) ? 0 : SignalServiceAddress.DEFAULT_DEVICE_ID;
// Make sure we have a valid ttl; otherwise default to 2 days
if (ttl <= 0) { ttl = TTLUtilities.INSTANCE.getFallbackMessageTTL(); }
final int regularMessageTTL = TTLUtilities.getTTL(TTLUtilities.MessageType.Regular);
final int __ttl = ttl;
final SignalMessageInfo messageInfo = new SignalMessageInfo(type, timestamp, senderID, senderDeviceID, message.content, recipient.getNumber(), ttl, false);
SnodeAPI.shared.sendSignalMessage(messageInfo).success(new Function1<Set<Promise<Map<?, ?>, Exception>>, Unit>() {
@Override
public Unit invoke(Set<Promise<Map<?, ?>, Exception>> promises) {
final boolean[] isSuccess = { false };
final int[] promiseCount = {promises.size()};
final int[] errorCount = { 0 };
for (Promise<Map<?, ?>, Exception> promise : promises) {
promise.success(new Function1<Map<?, ?>, Unit>() {
@Override
public Unit invoke(Map<?, ?> map) {
if (isSuccess[0]) { return Unit.INSTANCE; } // Succeed as soon as the first promise succeeds
if (__ttl == regularMessageTTL) {
broadcaster.broadcast("messageSent", timestamp);
}
isSuccess[0] = true;
if (notifyPNServer) {
PushNotificationAPI.shared.notify(messageInfo);
}
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
f.set(Unit.INSTANCE);
return Unit.INSTANCE;
}
}).fail(new Function1<Exception, Unit>() {
@Override
public Unit invoke(Exception exception) {
errorCount[0] += 1;
if (errorCount[0] != promiseCount[0]) { return Unit.INSTANCE; } // Only error out if all promises failed
if (__ttl == regularMessageTTL) {
broadcaster.broadcast("messageFailed", timestamp);
}
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
f.setException(exception);
return Unit.INSTANCE;
}
});
}
return Unit.INSTANCE;
}
}).fail(exception -> {
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
f.setException(exception);
return Unit.INSTANCE;
});
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
try {
f.get(1, TimeUnit.MINUTES);
return SendMessageResult.success(recipient, false, true);
} catch (Exception exception) {
Throwable underlyingError = exception.getCause();
if (underlyingError instanceof SnodeAPI.Error) {
return SendMessageResult.lokiAPIError(recipient, (SnodeAPI.Error)underlyingError);
} else {
return SendMessageResult.networkFailure(recipient);
}
}
}
private List<AttachmentPointer> createAttachmentPointers(Optional<List<SignalServiceAttachment>> attachments, SignalServiceAddress recipient)
throws IOException
{
List<AttachmentPointer> pointers = new LinkedList<>();
if (!attachments.isPresent() || attachments.get().isEmpty()) {
Log.w(TAG, "No attachments present...");
return pointers;
}
for (SignalServiceAttachment attachment : attachments.get()) {
if (attachment.isStream()) {
Log.w(TAG, "Found attachment, creating pointer...");
pointers.add(createAttachmentPointer(attachment.asStream(), recipient));
} else if (attachment.isPointer()) {
Log.w(TAG, "Including existing attachment pointer...");
pointers.add(createAttachmentPointer(attachment.asPointer()));
}
}
return pointers;
}
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentPointer attachment) {
AttachmentPointer.Builder builder = AttachmentPointer.newBuilder()
.setContentType(attachment.getContentType())
.setId(attachment.getId())
.setKey(ByteString.copyFrom(attachment.getKey()))
.setDigest(ByteString.copyFrom(attachment.getDigest().get()))
.setSize(attachment.getSize().get())
.setUrl(attachment.getUrl());
if (attachment.getFileName().isPresent()) {
builder.setFileName(attachment.getFileName().get());
}
if (attachment.getPreview().isPresent()) {
builder.setThumbnail(ByteString.copyFrom(attachment.getPreview().get()));
}
if (attachment.getWidth() > 0) {
builder.setWidth(attachment.getWidth());
}
if (attachment.getHeight() > 0) {
builder.setHeight(attachment.getHeight());
}
if (attachment.getVoiceNote()) {
builder.setFlags(AttachmentPointer.Flags.VOICE_MESSAGE_VALUE);
}
if (attachment.getCaption().isPresent()) {
builder.setCaption(attachment.getCaption().get());
}
return builder.build();
}
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment, SignalServiceAddress recipient)
throws IOException
{
return createAttachmentPointer(attachment, false, recipient);
}
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment, boolean usePadding, SignalServiceAddress recipient)
throws IOException
{
SignalServiceAttachmentPointer pointer = uploadAttachment(attachment, usePadding, recipient);
return createAttachmentPointer(pointer);
}
private OutgoingPushMessageList getSessionProtocolEncryptedMessage(SignalServiceAddress recipient, long timestamp, byte[] plaintext)
{
List<OutgoingPushMessage> messages = new LinkedList<>();
String publicKey = recipient.getNumber(); // Could be a contact's public key or the public key of a SSK group
boolean isClosedGroup = apiDatabase.isClosedGroup(publicKey);
String encryptionPublicKey;
if (isClosedGroup) {
ECKeyPair encryptionKeyPair = apiDatabase.getLatestClosedGroupEncryptionKeyPair(publicKey);
encryptionPublicKey = HexEncodingKt.getHexEncodedPublicKey(encryptionKeyPair);
} else {
encryptionPublicKey = publicKey;
}
byte[] ciphertext = sessionProtocolImpl.encrypt(PushTransportDetails.getPaddedMessageBody(plaintext), encryptionPublicKey);
String body = Base64.encodeBytes(ciphertext);
int type = isClosedGroup ? SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT_VALUE :
SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE;
OutgoingPushMessage message = new OutgoingPushMessage(type, body);
messages.add(message);
return new OutgoingPushMessageList(publicKey, timestamp, messages, false);
}
}

View File

@@ -1,11 +0,0 @@
package org.session.libsignal.service.api.crypto;
public class InvalidCiphertextException extends Exception {
public InvalidCiphertextException(Exception nested) {
super(nested);
}
public InvalidCiphertextException(String s) {
super(s);
}
}

View File

@@ -1,45 +0,0 @@
package org.session.libsignal.service.api.crypto;
import org.session.libsignal.libsignal.util.ByteUtil;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class UnidentifiedAccess {
private final byte[] unidentifiedAccessKey;
public UnidentifiedAccess(byte[] unidentifiedAccessKey)
{
this.unidentifiedAccessKey = unidentifiedAccessKey;
}
public byte[] getUnidentifiedAccessKey() {
return unidentifiedAccessKey;
}
public static byte[] deriveAccessKeyFrom(byte[] profileKey) {
try {
byte[] nonce = new byte[12];
byte[] input = new byte[16];
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(profileKey, "AES"), new GCMParameterSpec(128, nonce));
byte[] ciphertext = cipher.doFinal(input);
return ByteUtil.trim(ciphertext, 16);
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException | BadPaddingException | IllegalBlockSizeException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -1,30 +0,0 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.session.libsignal.service.api.crypto;
import org.session.libsignal.libsignal.IdentityKey;
public class UntrustedIdentityException extends Exception {
private final IdentityKey identityKey;
private final String e164number;
public UntrustedIdentityException(String s, String e164number, IdentityKey identityKey) {
super(s);
this.e164number = e164number;
this.identityKey = identityKey;
}
public IdentityKey getIdentityKey() {
return identityKey;
}
public String getE164Number() {
return e164number;
}
}

View File

@@ -1,96 +0,0 @@
package org.session.libsignal.service.api.messages;
import org.session.libsignal.libsignal.IdentityKey;
import org.session.libsignal.service.api.push.SignalServiceAddress;
import org.session.libsignal.service.loki.api.SnodeAPI;
public class SendMessageResult {
private final SignalServiceAddress address;
private final Success success;
private final boolean networkFailure;
private final boolean unregisteredFailure;
private final IdentityFailure identityFailure;
private final SnodeAPI.Error lokiAPIError;
public static SendMessageResult success(SignalServiceAddress address, boolean unidentified, boolean needsSync) {
return new SendMessageResult(address, new Success(unidentified, needsSync), false, false, null, null);
}
public static SendMessageResult lokiAPIError(SignalServiceAddress address, SnodeAPI.Error lokiAPIError) {
return new SendMessageResult(address, null, false, false, null, lokiAPIError);
}
public static SendMessageResult networkFailure(SignalServiceAddress address) {
return new SendMessageResult(address, null, true, false, null, null);
}
public static SendMessageResult unregisteredFailure(SignalServiceAddress address) {
return new SendMessageResult(address, null, false, true, null, null);
}
public static SendMessageResult identityFailure(SignalServiceAddress address, IdentityKey identityKey) {
return new SendMessageResult(address, null, false, false, new IdentityFailure(identityKey), null);
}
public SignalServiceAddress getAddress() {
return address;
}
public Success getSuccess() {
return success;
}
public boolean isNetworkFailure() {
return networkFailure;
}
public IdentityFailure getIdentityFailure() {
return identityFailure;
}
public SnodeAPI.Error getLokiAPIError() { return lokiAPIError; }
private SendMessageResult(SignalServiceAddress address, Success success, boolean networkFailure, boolean unregisteredFailure, IdentityFailure identityFailure, SnodeAPI.Error lokiAPIError) {
this.address = address;
this.success = success;
this.networkFailure = networkFailure;
this.unregisteredFailure = unregisteredFailure;
this.identityFailure = identityFailure;
this.lokiAPIError = lokiAPIError;
}
public static class Success {
private final boolean unidentified;
private final boolean needsSync;
private Success(boolean unidentified, boolean needsSync) {
this.unidentified = unidentified;
this.needsSync = needsSync;
}
public boolean isUnidentified() {
return unidentified;
}
public boolean isNeedsSync() {
return needsSync;
}
}
public static class IdentityFailure {
private final IdentityKey identityKey;
private IdentityFailure(IdentityKey identityKey) {
this.identityKey = identityKey;
}
public IdentityKey getIdentityKey() {
return identityKey;
}
}
}

View File

@@ -1,24 +0,0 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.session.libsignal.service.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
public class OutgoingPushMessage {
@JsonProperty
public int type;
@JsonProperty
public String content;
public OutgoingPushMessage(int type, String content)
{
this.type = type;
this.content = content;
}
}

View File

@@ -1,53 +0,0 @@
/*
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.session.libsignal.service.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class OutgoingPushMessageList {
@JsonProperty
private String destination;
@JsonProperty
private long timestamp;
@JsonProperty
private List<OutgoingPushMessage> messages;
@JsonProperty
private boolean online;
public OutgoingPushMessageList(String destination,
long timestamp,
List<OutgoingPushMessage> messages,
boolean online)
{
this.timestamp = timestamp;
this.destination = destination;
this.messages = messages;
this.online = online;
}
public String getDestination() {
return destination;
}
public List<OutgoingPushMessage> getMessages() {
return messages;
}
public long getTimestamp() {
return timestamp;
}
public boolean isOnline() {
return online;
}
}

View File

@@ -4,7 +4,7 @@ public class Snode(val address: String, val port: Int, val publicKeySet: KeySet?
val ip: String get() = address.removePrefix("https://")
internal enum class Method(val rawValue: String) {
enum class Method(val rawValue: String) {
/**
* Only supported by snode targets.
*/

View File

@@ -17,17 +17,6 @@ interface SessionProtocol {
object DecryptionFailed : Exception("Couldn't decrypt message.")
object InvalidSignature : Exception("Invalid message signature.")
}
/**
* Encrypts `plaintext` using the Session protocol for `hexEncodedX25519PublicKey`.
*
* @param plaintext the plaintext to encrypt. Must already be padded.
* @param recipientHexEncodedX25519PublicKey the X25519 public key to encrypt for. Could be the Session ID of a user, or the public key of a closed group.
*
* @return the encrypted message.
*/
fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray
/**
* Decrypts `ciphertext` using the Session protocol and `x25519KeyPair`.
*