Add support for stickers

This commit is contained in:
Greyson Parrelli 2019-04-19 10:52:05 -04:00
parent 67ecd91eaf
commit 66e1ee6e7c
15 changed files with 4625 additions and 173 deletions

View File

@ -15,6 +15,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.Pro
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
@ -23,13 +24,18 @@ import org.whispersystems.signalservice.api.websocket.ConnectivityListener;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.SignalServiceEnvelopeEntity;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.sticker.StickerProtos;
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@ -133,7 +139,45 @@ public class SignalServiceMessageReceiver {
if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!");
socket.retrieveAttachment(pointer.getId(), destination, maxSizeBytes, listener);
return AttachmentCipherInputStream.createFor(destination, pointer.getSize().or(0), pointer.getKey(), pointer.getDigest().get());
return AttachmentCipherInputStream.createForAttachment(destination, pointer.getSize().or(0), pointer.getKey(), pointer.getDigest().get());
}
public InputStream retrieveSticker(byte[] packId, byte[] packKey, int stickerId)
throws IOException, InvalidMessageException
{
byte[] data = socket.retrieveSticker(packId, stickerId);
return AttachmentCipherInputStream.createForStickerData(data, packKey);
}
/**
* Retrieves a {@link SignalServiceStickerManifest}.
*
* @param packId The 16-byte packId that identifies the sticker pack.
* @param packKey The 32-byte packKey that decrypts the sticker pack.
* @return The {@link SignalServiceStickerManifest} representing the sticker pack.
* @throws IOException
* @throws InvalidMessageException
*/
public SignalServiceStickerManifest retrieveStickerManifest(byte[] packId, byte[] packKey)
throws IOException, InvalidMessageException
{
byte[] manifestBytes = socket.retrieveStickerManifest(packId);
InputStream cipherStream = AttachmentCipherInputStream.createForStickerData(manifestBytes, packKey);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Util.copy(cipherStream, outputStream);
StickerProtos.Pack pack = StickerProtos.Pack.parseFrom(outputStream.toByteArray());
List<SignalServiceStickerManifest.StickerInfo> stickers = new ArrayList<>(pack.getStickersCount());
SignalServiceStickerManifest.StickerInfo cover = pack.hasCover() ? new SignalServiceStickerManifest.StickerInfo(pack.getCover().getId(), pack.getCover().getEmoji())
: null;
for (StickerProtos.Pack.Sticker sticker : pack.getStickersList()) {
stickers.add(new SignalServiceStickerManifest.StickerInfo(sticker.getId(), sticker.getEmoji()));
}
return new SignalServiceStickerManifest(pack.getTitle(), pack.getAuthor(), cover, stickers);
}
/**

View File

@ -38,6 +38,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMe
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@ -284,6 +285,8 @@ public class SignalServiceMessageSender {
content = createMultiDeviceConfigurationContent(message.getConfiguration().get());
} else if (message.getSent().isPresent()) {
content = createMultiDeviceSentTranscriptContent(message.getSent().get(), unidentifiedAccess);
} else if (message.getStickerPackOperations().isPresent()) {
content = createMultiDeviceStickerPackOperationContent(message.getStickerPackOperations().get());
} else if (message.getVerified().isPresent()) {
sendMessage(message.getVerified().get(), unidentifiedAccess);
return;
@ -486,6 +489,22 @@ public class SignalServiceMessageSender {
}
}
if (message.getSticker().isPresent()) {
DataMessage.Sticker.Builder stickerBuilder = DataMessage.Sticker.newBuilder();
stickerBuilder.setPackId(ByteString.copyFrom(message.getSticker().get().getPackId()));
stickerBuilder.setPackKey(ByteString.copyFrom(message.getSticker().get().getPackKey()));
stickerBuilder.setStickerId(message.getSticker().get().getStickerId());
if (message.getSticker().get().getAttachment().isStream()) {
stickerBuilder.setData(createAttachmentPointer(message.getSticker().get().getAttachment().asStream()));
} else {
stickerBuilder.setData(createAttachmentPointer(message.getSticker().get().getAttachment().asPointer()));
}
builder.setSticker(stickerBuilder.build());
}
builder.setTimestamp(message.getTimestamp());
return container.setDataMessage(builder).build().toByteArray();
@ -639,6 +658,34 @@ public class SignalServiceMessageSender {
return container.setSyncMessage(syncMessage.setConfiguration(configurationMessage)).build().toByteArray();
}
private byte[] createMultiDeviceStickerPackOperationContent(List<StickerPackOperationMessage> stickerPackOperations) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
for (StickerPackOperationMessage stickerPackOperation : stickerPackOperations) {
SyncMessage.StickerPackOperation.Builder builder = SyncMessage.StickerPackOperation.newBuilder();
if (stickerPackOperation.getPackId().isPresent()) {
builder.setPackId(ByteString.copyFrom(stickerPackOperation.getPackId().get()));
}
if (stickerPackOperation.getPackKey().isPresent()) {
builder.setPackKey(ByteString.copyFrom(stickerPackOperation.getPackKey().get()));
}
if (stickerPackOperation.getType().isPresent()) {
switch (stickerPackOperation.getType().get()) {
case INSTALL: builder.setType(SyncMessage.StickerPackOperation.Type.INSTALL); break;
case REMOVE: builder.setType(SyncMessage.StickerPackOperation.Type.REMOVE); break;
}
}
syncMessage.addStickerPackOperation(builder);
}
return container.setSyncMessage(syncMessage).build().toByteArray();
}
private byte[] createMultiDeviceVerifiedContent(VerifiedMessage verifiedMessage, byte[] nullMessage) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder syncMessage = createSyncMessageBuilder();

View File

@ -8,12 +8,13 @@ package org.whispersystems.signalservice.api.crypto;
import org.whispersystems.libsignal.InvalidMacException;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.kdf.HKDFv3;
import org.whispersystems.signalservice.internal.util.ContentLengthInputStream;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
@ -50,7 +51,7 @@ public class AttachmentCipherInputStream extends FilterInputStream {
private long totalRead;
private byte[] overflowBuffer;
public static InputStream createFor(File file, long plaintextLength, byte[] combinedKeyMaterial, byte[] digest)
public static InputStream createForAttachment(File file, long plaintextLength, byte[] combinedKeyMaterial, byte[] digest)
throws InvalidMessageException, IOException
{
try {
@ -62,7 +63,13 @@ public class AttachmentCipherInputStream extends FilterInputStream {
throw new InvalidMessageException("Message shorter than crypto overhead!");
}
verifyMac(file, mac, digest);
if (digest == null) {
throw new InvalidMacException("Missing digest!");
}
try (FileInputStream fin = new FileInputStream(file)) {
verifyMac(fin, file.length(), mac, digest);
}
InputStream inputStream = new AttachmentCipherInputStream(new FileInputStream(file), parts[0], file.length() - BLOCK_SIZE - mac.getMacLength());
@ -78,6 +85,31 @@ public class AttachmentCipherInputStream extends FilterInputStream {
}
}
public static InputStream createForStickerData(byte[] data, byte[] packKey)
throws InvalidMessageException, IOException
{
try {
byte[] combinedKeyMaterial = new HKDFv3().deriveSecrets(packKey, "Sticker Pack".getBytes(), 64);
byte[][] parts = Util.split(combinedKeyMaterial, CIPHER_KEY_SIZE, MAC_KEY_SIZE);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(parts[1], "HmacSHA256"));
if (data.length <= BLOCK_SIZE + mac.getMacLength()) {
throw new InvalidMessageException("Message shorter than crypto overhead!");
}
try (InputStream inputStream = new ByteArrayInputStream(data)) {
verifyMac(inputStream, data.length, mac, null);
}
return new AttachmentCipherInputStream(new ByteArrayInputStream(data), parts[0], data.length - BLOCK_SIZE - mac.getMacLength());
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
} catch (InvalidMacException e) {
throw new InvalidMessageException(e);
}
}
private AttachmentCipherInputStream(InputStream inputStream, byte[] cipherKey, long totalDataSize)
throws IOException
{
@ -190,17 +222,16 @@ public class AttachmentCipherInputStream extends FilterInputStream {
}
}
private static void verifyMac(File file, Mac mac, byte[] theirDigest)
throws FileNotFoundException, InvalidMacException
private static void verifyMac(InputStream inputStream, long length, Mac mac, byte[] theirDigest)
throws InvalidMacException
{
try {
MessageDigest digest = MessageDigest.getInstance("SHA256");
FileInputStream fin = new FileInputStream(file);
int remainingData = Util.toIntExact(file.length()) - mac.getMacLength();
int remainingData = Util.toIntExact(length) - mac.getMacLength();
byte[] buffer = new byte[4096];
while (remainingData > 0) {
int read = fin.read(buffer, 0, Math.min(buffer.length, remainingData));
int read = inputStream.read(buffer, 0, Math.min(buffer.length, remainingData));
mac.update(buffer, 0, read);
digest.update(buffer, 0, read);
remainingData -= read;
@ -208,7 +239,7 @@ public class AttachmentCipherInputStream extends FilterInputStream {
byte[] ourMac = mac.doFinal();
byte[] theirMac = new byte[mac.getMacLength()];
Util.readFully(fin, theirMac);
Util.readFully(inputStream, theirMac);
if (!MessageDigest.isEqual(ourMac, theirMac)) {
throw new InvalidMacException("MAC doesn't match!");
@ -216,7 +247,7 @@ public class AttachmentCipherInputStream extends FilterInputStream {
byte[] ourDigest = digest.digest(theirMac);
if (!MessageDigest.isEqual(ourDigest, theirDigest)) {
if (theirDigest != null && !MessageDigest.isEqual(ourDigest, theirDigest)) {
throw new InvalidMacException("Digest doesn't match!");
}
@ -237,6 +268,4 @@ public class AttachmentCipherInputStream extends FilterInputStream {
else return;
}
}
}

View File

@ -43,6 +43,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Sticker;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
@ -57,6 +58,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage.VerifiedState;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
@ -270,6 +272,7 @@ public class SignalServiceCipher {
SignalServiceDataMessage.Quote quote = createQuote(content);
List<SharedContact> sharedContacts = createSharedContacts(content);
List<Preview> previews = createPreviews(content);
Sticker sticker = createSticker(content);
for (AttachmentPointer pointer : content.getAttachmentsList()) {
attachments.add(createAttachmentPointer(pointer));
@ -292,7 +295,8 @@ public class SignalServiceCipher {
profileKeyUpdate,
quote,
sharedContacts,
previews);
previews,
sticker);
}
private SignalServiceSyncMessage createSynchronizeMessage(Metadata metadata, SyncMessage content)
@ -352,6 +356,26 @@ public class SignalServiceCipher {
}
}
if (content.getStickerPackOperationList().size() > 0) {
List<StickerPackOperationMessage> operations = new LinkedList<>();
for (SyncMessage.StickerPackOperation operation : content.getStickerPackOperationList()) {
byte[] packId = operation.hasPackId() ? operation.getPackId().toByteArray() : null;
byte[] packKey = operation.hasPackKey() ? operation.getPackKey().toByteArray() : null;
StickerPackOperationMessage.Type type = null;
if (operation.hasType()) {
switch (operation.getType()) {
case INSTALL: type = StickerPackOperationMessage.Type.INSTALL; break;
case REMOVE: type = StickerPackOperationMessage.Type.REMOVE; break;
}
}
operations.add(new StickerPackOperationMessage(packId, packKey, type));
}
return SignalServiceSyncMessage.forStickerPackOperations(operations);
}
return SignalServiceSyncMessage.empty();
}
@ -446,6 +470,24 @@ public class SignalServiceCipher {
return results;
}
private Sticker createSticker(DataMessage content) {
if (!content.hasSticker() ||
!content.getSticker().hasPackId() ||
!content.getSticker().hasPackKey() ||
!content.getSticker().hasStickerId() ||
!content.getSticker().hasData())
{
return null;
}
DataMessage.Sticker sticker = content.getSticker();
return new Sticker(sticker.getPackId().toByteArray(),
sticker.getPackKey().toByteArray(),
sticker.getStickerId(),
createAttachmentPointer(sticker.getData()));
}
private List<SharedContact> createSharedContacts(DataMessage content) {
if (content.getContactCount() <= 0) return null;

View File

@ -30,6 +30,7 @@ public class SignalServiceDataMessage {
private final Optional<Quote> quote;
private final Optional<List<SharedContact>> contacts;
private final Optional<List<Preview>> previews;
private final Optional<Sticker> sticker;
/**
* Construct a SignalServiceDataMessage with a body and no attachments.
@ -103,7 +104,7 @@ public class SignalServiceDataMessage {
* @param expiresInSeconds The number of seconds in which a message should disappear after having been seen.
*/
public SignalServiceDataMessage(long timestamp, SignalServiceGroup group, List<SignalServiceAttachment> attachments, String body, int expiresInSeconds) {
this(timestamp, group, attachments, body, false, expiresInSeconds, false, null, false, null, null, null);
this(timestamp, group, attachments, body, false, expiresInSeconds, false, null, false, null, null, null, null);
}
/**
@ -120,7 +121,8 @@ public class SignalServiceDataMessage {
List<SignalServiceAttachment> attachments,
String body, boolean endSession, int expiresInSeconds,
boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate,
Quote quote, List<SharedContact> sharedContacts, List<Preview> previews)
Quote quote, List<SharedContact> sharedContacts, List<Preview> previews,
Sticker sticker)
{
this.timestamp = timestamp;
this.body = Optional.fromNullable(body);
@ -131,6 +133,7 @@ public class SignalServiceDataMessage {
this.profileKey = Optional.fromNullable(profileKey);
this.profileKeyUpdate = profileKeyUpdate;
this.quote = Optional.fromNullable(quote);
this.sticker = Optional.fromNullable(sticker);
if (attachments != null && !attachments.isEmpty()) {
this.attachments = Optional.of(attachments);
@ -219,6 +222,10 @@ public class SignalServiceDataMessage {
return previews;
}
public Optional<Sticker> getSticker() {
return sticker;
}
public static class Builder {
private List<SignalServiceAttachment> attachments = new LinkedList<>();
@ -234,6 +241,7 @@ public class SignalServiceDataMessage {
private byte[] profileKey;
private boolean profileKeyUpdate;
private Quote quote;
private Sticker sticker;
private Builder() {}
@ -315,11 +323,17 @@ public class SignalServiceDataMessage {
return this;
}
public Builder withSticker(Sticker sticker) {
this.sticker = sticker;
return this;
}
public SignalServiceDataMessage build() {
if (timestamp == 0) timestamp = System.currentTimeMillis();
return new SignalServiceDataMessage(timestamp, group, attachments, body, endSession,
expiresInSeconds, expirationUpdate, profileKey,
profileKeyUpdate, quote, sharedContacts, previews);
profileKeyUpdate, quote, sharedContacts, previews,
sticker);
}
}
@ -401,4 +415,33 @@ public class SignalServiceDataMessage {
}
}
public static class Sticker {
private final byte[] packId;
private final byte[] packKey;
private final int stickerId;
private final SignalServiceAttachment attachment;
public Sticker(byte[] packId, byte[] packKey, int stickerId, SignalServiceAttachment attachment) {
this.packId = packId;
this.packKey = packKey;
this.stickerId = stickerId;
this.attachment = attachment;
}
public byte[] getPackId() {
return packId;
}
public byte[] getPackKey() {
return packKey;
}
public int getStickerId() {
return stickerId;
}
public SignalServiceAttachment getAttachment() {
return attachment;
}
}
}

View File

@ -0,0 +1,56 @@
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SignalServiceStickerManifest {
private final Optional<String> title;
private final Optional<String> author;
private final Optional<StickerInfo> cover;
private final List<StickerInfo> stickers;
public SignalServiceStickerManifest(String title, String author, StickerInfo cover, List<StickerInfo> stickers) {
this.title = Optional.of(title);
this.author = Optional.of(author);
this.cover = Optional.of(cover);
this.stickers = (stickers == null) ? Collections.<StickerInfo>emptyList() : new ArrayList<>(stickers);
}
public Optional<String> getTitle() {
return title;
}
public Optional<String> getAuthor() {
return author;
}
public Optional<StickerInfo> getCover() {
return cover;
}
public List<StickerInfo> getStickers() {
return stickers;
}
public static final class StickerInfo {
private final int id;
private final String emoji;
public StickerInfo(int id, String emoji) {
this.id = id;
this.emoji = emoji;
}
public int getId() {
return id;
}
public String getEmoji() {
return emoji;
}
}
}

View File

@ -9,37 +9,42 @@ package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class SignalServiceSyncMessage {
private final Optional<SentTranscriptMessage> sent;
private final Optional<ContactsMessage> contacts;
private final Optional<SignalServiceAttachment> groups;
private final Optional<BlockedListMessage> blockedList;
private final Optional<RequestMessage> request;
private final Optional<List<ReadMessage>> reads;
private final Optional<VerifiedMessage> verified;
private final Optional<ConfigurationMessage> configuration;
private final Optional<SentTranscriptMessage> sent;
private final Optional<ContactsMessage> contacts;
private final Optional<SignalServiceAttachment> groups;
private final Optional<BlockedListMessage> blockedList;
private final Optional<RequestMessage> request;
private final Optional<List<ReadMessage>> reads;
private final Optional<VerifiedMessage> verified;
private final Optional<ConfigurationMessage> configuration;
private final Optional<List<StickerPackOperationMessage>> stickerPackOperations;
private SignalServiceSyncMessage(Optional<SentTranscriptMessage> sent,
Optional<ContactsMessage> contacts,
Optional<SignalServiceAttachment> groups,
Optional<BlockedListMessage> blockedList,
Optional<RequestMessage> request,
Optional<List<ReadMessage>> reads,
Optional<VerifiedMessage> verified,
Optional<ConfigurationMessage> configuration)
private SignalServiceSyncMessage(Optional<SentTranscriptMessage> sent,
Optional<ContactsMessage> contacts,
Optional<SignalServiceAttachment> groups,
Optional<BlockedListMessage> blockedList,
Optional<RequestMessage> request,
Optional<List<ReadMessage>> reads,
Optional<VerifiedMessage> verified,
Optional<ConfigurationMessage> configuration,
Optional<List<StickerPackOperationMessage>> stickerPackOperations)
{
this.sent = sent;
this.contacts = contacts;
this.groups = groups;
this.blockedList = blockedList;
this.request = request;
this.reads = reads;
this.verified = verified;
this.configuration = configuration;
this.sent = sent;
this.contacts = contacts;
this.groups = groups;
this.blockedList = blockedList;
this.request = request;
this.reads = reads;
this.verified = verified;
this.configuration = configuration;
this.stickerPackOperations = stickerPackOperations;
}
public static SignalServiceSyncMessage forSentTranscript(SentTranscriptMessage sent) {
@ -50,7 +55,8 @@ public class SignalServiceSyncMessage {
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent());
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent());
}
public static SignalServiceSyncMessage forContacts(ContactsMessage contacts) {
@ -61,7 +67,8 @@ public class SignalServiceSyncMessage {
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent());
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent());
}
public static SignalServiceSyncMessage forGroups(SignalServiceAttachment groups) {
@ -72,7 +79,8 @@ public class SignalServiceSyncMessage {
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent());
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent());
}
public static SignalServiceSyncMessage forRequest(RequestMessage request) {
@ -83,7 +91,8 @@ public class SignalServiceSyncMessage {
Optional.of(request),
Optional.<List<ReadMessage>>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent());
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent());
}
public static SignalServiceSyncMessage forRead(List<ReadMessage> reads) {
@ -94,7 +103,8 @@ public class SignalServiceSyncMessage {
Optional.<RequestMessage>absent(),
Optional.of(reads),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent());
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent());
}
public static SignalServiceSyncMessage forRead(ReadMessage read) {
@ -108,7 +118,8 @@ public class SignalServiceSyncMessage {
Optional.<RequestMessage>absent(),
Optional.of(reads),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent());
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent());
}
public static SignalServiceSyncMessage forVerified(VerifiedMessage verifiedMessage) {
@ -119,7 +130,8 @@ public class SignalServiceSyncMessage {
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.of(verifiedMessage),
Optional.<ConfigurationMessage>absent());
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent());
}
public static SignalServiceSyncMessage forBlocked(BlockedListMessage blocked) {
@ -130,7 +142,8 @@ public class SignalServiceSyncMessage {
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent());
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent());
}
public static SignalServiceSyncMessage forConfiguration(ConfigurationMessage configuration) {
@ -141,7 +154,20 @@ public class SignalServiceSyncMessage {
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<VerifiedMessage>absent(),
Optional.of(configuration));
Optional.of(configuration),
Optional.<List<StickerPackOperationMessage>>absent());
}
public static SignalServiceSyncMessage forStickerPackOperations(List<StickerPackOperationMessage> stickerPackOperations) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.of(stickerPackOperations));
}
public static SignalServiceSyncMessage empty() {
@ -152,7 +178,8 @@ public class SignalServiceSyncMessage {
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent());
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent());
}
public Optional<SentTranscriptMessage> getSent() {
@ -187,4 +214,8 @@ public class SignalServiceSyncMessage {
return configuration;
}
public Optional<List<StickerPackOperationMessage>> getStickerPackOperations() {
return stickerPackOperations;
}
}

View File

@ -0,0 +1,32 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.util.guava.Optional;
public class StickerPackOperationMessage {
private final Optional<byte[]> packId;
private final Optional<byte[]> packKey;
private final Optional<Type> type;
public StickerPackOperationMessage(byte[] packId, byte[] packKey, Type type) {
this.packId = Optional.fromNullable(packId);
this.packKey = Optional.fromNullable(packKey);
this.type = Optional.fromNullable(type);
}
public Optional<byte[]> getPackId() {
return packId;
}
public Optional<byte[]> getPackKey() {
return packKey;
}
public Optional<Type> getType() {
return type;
}
public enum Type {
INSTALL, REMOVE
}
}

View File

@ -47,9 +47,11 @@ import org.whispersystems.signalservice.internal.push.http.DigestingRequestBody;
import org.whispersystems.signalservice.internal.push.http.OutputStreamFactory;
import org.whispersystems.signalservice.internal.util.Base64;
import org.whispersystems.signalservice.internal.util.BlacklistingTrustManager;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
@ -72,7 +74,9 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
@ -131,6 +135,9 @@ public class PushServiceSocket {
private static final String ATTACHMENT_DOWNLOAD_PATH = "attachments/%d";
private static final String ATTACHMENT_UPLOAD_PATH = "attachments/";
private static final String STICKER_MANIFEST_PATH = "stickers/%s/manifest.proto";
private static final String STICKER_PATH = "stickers/%s/full/%d";
private static final Map<String, String> NO_HEADERS = Collections.emptyMap();
private static final ResponseCodeHandler NO_HANDLER = new EmptyResponseCodeHandler();
@ -420,6 +427,35 @@ public class PushServiceSocket {
downloadFromCdn(destination, String.format(ATTACHMENT_DOWNLOAD_PATH, attachmentId), maxSizeBytes, listener);
}
public void retrieveSticker(File destination, byte[] packId, int stickerId)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
String hexPackId = Hex.toStringCondensed(packId);
downloadFromCdn(destination, String.format(STICKER_PATH, hexPackId, stickerId), 1024 * 1024, null);
}
public byte[] retrieveSticker(byte[] packId, int stickerId)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
String hexPackId = Hex.toStringCondensed(packId);
ByteArrayOutputStream output = new ByteArrayOutputStream();
downloadFromCdn(output, String.format(STICKER_PATH, hexPackId, stickerId), 1024 * 1024, null);
return output.toByteArray();
}
public byte[] retrieveStickerManifest(byte[] packId)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
String hexPackId = Hex.toStringCondensed(packId);
ByteArrayOutputStream output = new ByteArrayOutputStream();
downloadFromCdn(output, String.format(STICKER_MANIFEST_PATH, hexPackId), 1024 * 1024, null);
return output.toByteArray();
}
public SignalServiceProfile retrieveProfile(SignalServiceAddress target, Optional<UnidentifiedAccess> unidentifiedAccess)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
@ -599,6 +635,16 @@ public class PushServiceSocket {
private void downloadFromCdn(File destination, String path, int maxSizeBytes, ProgressListener listener)
throws PushNetworkException, NonSuccessfulResponseCodeException
{
try (FileOutputStream outputStream = new FileOutputStream(destination)) {
downloadFromCdn(outputStream, path, maxSizeBytes, listener);
} catch (IOException e) {
throw new PushNetworkException(e);
}
}
private void downloadFromCdn(OutputStream outputStream, String path, int maxSizeBytes, ProgressListener listener)
throws PushNetworkException, NonSuccessfulResponseCodeException
{
ConnectionHolder connectionHolder = getRandom(cdnClients, random);
OkHttpClient okHttpClient = connectionHolder.getClient()
@ -631,13 +677,12 @@ public class PushServiceSocket {
if (body.contentLength() > maxSizeBytes) throw new PushNetworkException("Response exceeds max size!");
InputStream in = body.byteStream();
OutputStream out = new FileOutputStream(destination);
byte[] buffer = new byte[32768];
int read, totalRead = 0;
while ((read = in.read(buffer, 0, buffer.length)) != -1) {
out.write(buffer, 0, read);
outputStream.write(buffer, 0, read);
if ((totalRead += read) > maxSizeBytes) throw new PushNetworkException("Response exceeded max size!");
if (listener != null) {

View File

@ -0,0 +1,244 @@
package org.whispersystems.signalservice.api.crypto;
import junit.framework.TestCase;
import org.conscrypt.Conscrypt;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.kdf.HKDFv3;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.Security;
import java.util.Arrays;
public class AttachmentCipherTest extends TestCase {
static {
Security.insertProviderAt(Conscrypt.newProvider(), 1);
}
public void test_attachment_encryptDecrypt() throws IOException, InvalidMessageException {
byte[] key = Util.getSecretBytes(64);
byte[] plaintextInput = "Peter Parker".getBytes();
EncryptResult encryptResult = encryptData(plaintextInput, key);
File cipherFile = writeToFile(encryptResult.ciphertext);
InputStream inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest);
byte[] plaintextOutput = readInputStreamFully(inputStream);
assertTrue(Arrays.equals(plaintextInput, plaintextOutput));
cipherFile.delete();
}
public void test_attachment_encryptDecryptEmpty() throws IOException, InvalidMessageException {
byte[] key = Util.getSecretBytes(64);
byte[] plaintextInput = "".getBytes();
EncryptResult encryptResult = encryptData(plaintextInput, key);
File cipherFile = writeToFile(encryptResult.ciphertext);
InputStream inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest);
byte[] plaintextOutput = readInputStreamFully(inputStream);
assertTrue(Arrays.equals(plaintextInput, plaintextOutput));
cipherFile.delete();
}
public void test_attachment_decryptFailOnBadKey() throws IOException{
File cipherFile = null;
boolean hitCorrectException = false;
try {
byte[] key = Util.getSecretBytes(64);
byte[] plaintextInput = "Gwen Stacy".getBytes();
EncryptResult encryptResult = encryptData(plaintextInput, key);
byte[] badKey = new byte[64];
cipherFile = writeToFile(encryptResult.ciphertext);
AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, badKey, encryptResult.digest);
} catch (InvalidMessageException e) {
hitCorrectException = true;
} finally {
if (cipherFile != null) {
cipherFile.delete();
}
}
assertTrue(hitCorrectException);
}
public void test_attachment_decryptFailOnBadDigest() throws IOException{
File cipherFile = null;
boolean hitCorrectException = false;
try {
byte[] key = Util.getSecretBytes(64);
byte[] plaintextInput = "Mary Jane Watson".getBytes();
EncryptResult encryptResult = encryptData(plaintextInput, key);
byte[] badDigest = new byte[32];
cipherFile = writeToFile(encryptResult.ciphertext);
AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, badDigest);
} catch (InvalidMessageException e) {
hitCorrectException = true;
} finally {
if (cipherFile != null) {
cipherFile.delete();
}
}
assertTrue(hitCorrectException);
}
public void test_attachment_decryptFailOnNullDigest() throws IOException{
File cipherFile = null;
boolean hitCorrectException = false;
try {
byte[] key = Util.getSecretBytes(64);
byte[] plaintextInput = "Aunt May".getBytes();
EncryptResult encryptResult = encryptData(plaintextInput, key);
cipherFile = writeToFile(encryptResult.ciphertext);
AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, null);
} catch (InvalidMessageException e) {
hitCorrectException = true;
} finally {
if (cipherFile != null) {
cipherFile.delete();
}
}
assertTrue(hitCorrectException);
}
public void test_attachment_decryptFailOnBadMac() throws IOException {
File cipherFile = null;
boolean hitCorrectException = false;
try {
byte[] key = Util.getSecretBytes(64);
byte[] plaintextInput = "Uncle Ben".getBytes();
EncryptResult encryptResult = encryptData(plaintextInput, key);
byte[] badMacCiphertext = Arrays.copyOf(encryptResult.ciphertext, encryptResult.ciphertext.length);
badMacCiphertext[badMacCiphertext.length - 1] = 0;
cipherFile = writeToFile(badMacCiphertext);
AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest);
} catch (InvalidMessageException e) {
hitCorrectException = true;
} finally {
if (cipherFile != null) {
cipherFile.delete();
}
}
assertTrue(hitCorrectException);
}
public void test_sticker_encryptDecrypt() throws IOException, InvalidMessageException {
byte[] packKey = Util.getSecretBytes(32);
byte[] plaintextInput = "Peter Parker".getBytes();
EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey));
InputStream inputStream = AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, packKey);
byte[] plaintextOutput = readInputStreamFully(inputStream);
assertTrue(Arrays.equals(plaintextInput, plaintextOutput));
}
public void test_sticker_encryptDecryptEmpty() throws IOException, InvalidMessageException {
byte[] packKey = Util.getSecretBytes(32);
byte[] plaintextInput = "".getBytes();
EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey));
InputStream inputStream = AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, packKey);
byte[] plaintextOutput = readInputStreamFully(inputStream);
assertTrue(Arrays.equals(plaintextInput, plaintextOutput));
}
public void test_sticker_decryptFailOnBadKey() throws IOException{
boolean hitCorrectException = false;
try {
byte[] packKey = Util.getSecretBytes(32);
byte[] plaintextInput = "Gwen Stacy".getBytes();
EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey));
byte[] badPackKey = new byte[32];
AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, badPackKey);
} catch (InvalidMessageException e) {
hitCorrectException = true;
}
assertTrue(hitCorrectException);
}
public void test_sticker_decryptFailOnBadMac() throws IOException {
boolean hitCorrectException = false;
try {
byte[] packKey = Util.getSecretBytes(32);
byte[] plaintextInput = "Uncle Ben".getBytes();
EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey));
byte[] badMacCiphertext = Arrays.copyOf(encryptResult.ciphertext, encryptResult.ciphertext.length);
badMacCiphertext[badMacCiphertext.length - 1] = 0;
AttachmentCipherInputStream.createForStickerData(badMacCiphertext, packKey);
} catch (InvalidMessageException e) {
hitCorrectException = true;
}
assertTrue(hitCorrectException);
}
private static EncryptResult encryptData(byte[] data, byte[] keyMaterial) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
AttachmentCipherOutputStream encryptStream = new AttachmentCipherOutputStream(keyMaterial, outputStream);
encryptStream.write(data);
encryptStream.flush();
encryptStream.close();
return new EncryptResult(outputStream.toByteArray(), encryptStream.getTransmittedDigest());
}
private static File writeToFile(byte[] data) throws IOException {
File file = File.createTempFile("temp", ".data");
OutputStream outputStream = new FileOutputStream(file);
outputStream.write(data);
outputStream.close();
return file;
}
private static byte[] readInputStreamFully(InputStream inputStream) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Util.copy(inputStream, outputStream);
return outputStream.toByteArray();
}
private static byte[] expandPackKey(byte[] shortKey) {
return new HKDFv3().deriveSecrets(shortKey, "Sticker Pack".getBytes(), 64);
}
private static class EncryptResult {
final byte[] ciphertext;
final byte[] digest;
private EncryptResult(byte[] ciphertext, byte[] digest) {
this.ciphertext = ciphertext;
this.digest = digest;
}
}
}

View File

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

View File

@ -166,6 +166,13 @@ message DataMessage {
optional AttachmentPointer image = 3;
}
message Sticker {
optional bytes packId = 1;
optional bytes packKey = 2;
optional uint32 stickerId = 3;
optional AttachmentPointer data = 4;
}
optional string body = 1;
repeated AttachmentPointer attachments = 2;
optional GroupContext group = 3;
@ -176,6 +183,7 @@ message DataMessage {
optional Quote quote = 8;
repeated Contact contact = 9;
repeated Preview preview = 10;
optional Sticker sticker = 11;
}
message NullMessage {
@ -268,15 +276,28 @@ message SyncMessage {
optional bool linkPreviews = 4;
}
optional Sent sent = 1;
optional Contacts contacts = 2;
optional Groups groups = 3;
optional Request request = 4;
repeated Read read = 5;
optional Blocked blocked = 6;
optional Verified verified = 7;
optional Configuration configuration = 9;
optional bytes padding = 8;
message StickerPackOperation {
enum Type {
INSTALL = 0;
REMOVE = 1;
}
optional bytes packId = 1;
optional bytes packKey = 2;
optional Type type = 3;
}
optional Sent sent = 1;
optional Contacts contacts = 2;
optional Groups groups = 3;
optional Request request = 4;
repeated Read read = 5;
optional Blocked blocked = 6;
optional Verified verified = 7;
optional Configuration configuration = 9;
optional bytes padding = 8;
repeated StickerPackOperation stickerPackOperation = 10;
}
message AttachmentPointer {

View File

@ -0,0 +1,23 @@
/**
* Copyright (C) 2019 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package signalservice;
option java_package = "org.whispersystems.signalservice.internal.sticker";
option java_outer_classname = "StickerProtos";
message Pack {
message Sticker {
optional uint32 id = 1;
optional string emoji = 2;
}
optional string title = 1;
optional string author = 2;
optional Sticker cover = 3;
repeated Sticker stickers = 4;
}