Add support for stickers
This commit is contained in:
parent
67ecd91eaf
commit
66e1ee6e7c
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
23
protobuf/StickerResources.proto
Normal file
23
protobuf/StickerResources.proto
Normal 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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user