/* * Copyright (C) 2014-2016 Open Whisper Systems * * Licensed according to the LICENSE file in this repository. */ package org.whispersystems.signalservice.api; import org.signal.zkgroup.ServerPublicParams; import org.signal.zkgroup.VerificationFailedException; import org.signal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.FeatureFlags; import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream; import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener; 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.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.UuidUtil; 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.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; import java.util.UUID; /** * The primary interface for receiving Signal Service messages. * * @author Moxie Marlinspike */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") public class SignalServiceMessageReceiver { private final PushServiceSocket socket; private final SignalServiceConfiguration urls; private final CredentialsProvider credentialsProvider; private final String signalAgent; private final ConnectivityListener connectivityListener; private final SleepTimer sleepTimer; private final ClientZkProfileOperations clientZkProfile; /** * Construct a SignalServiceMessageReceiver. * * @param urls The URL of the Signal Service. * @param uuid The Signal Service UUID. * @param e164 The Signal Service phone number. * @param password The Signal Service user password. * @param signalingKey The 52 byte signaling key assigned to this user at registration. */ public SignalServiceMessageReceiver(SignalServiceConfiguration urls, UUID uuid, String e164, String password, String signalingKey, String signalAgent, ConnectivityListener listener, SleepTimer timer) { this(urls, new StaticCredentialsProvider(uuid, e164, password, signalingKey), signalAgent, listener, timer); } /** * Construct a SignalServiceMessageReceiver. * * @param urls The URL of the Signal Service. * @param credentials The Signal Service user's credentials. */ public SignalServiceMessageReceiver(SignalServiceConfiguration urls, CredentialsProvider credentials, String signalAgent, ConnectivityListener listener, SleepTimer timer) { this.urls = urls; this.credentialsProvider = credentials; this.socket = new PushServiceSocket(urls, credentials, signalAgent); this.signalAgent = signalAgent; this.connectivityListener = listener; this.sleepTimer = timer; this.clientZkProfile = FeatureFlags.ZK_GROUPS ? new ClientZkProfileOperations(new ServerPublicParams(urls.getZkGroupServerPublicParams())) : null; } /** * Retrieves a SignalServiceAttachment. * * @param pointer The {@link SignalServiceAttachmentPointer} * received in a {@link SignalServiceDataMessage}. * @param destination The download destination for this attachment. * * @return An InputStream that streams the plaintext attachment contents. * @throws IOException * @throws InvalidMessageException */ public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, int maxSizeBytes) throws IOException, InvalidMessageException { return retrieveAttachment(pointer, destination, maxSizeBytes, null); } public ProfileAndCredential retrieveProfile(SignalServiceAddress address, Optional profileKey, Optional unidentifiedAccess, SignalServiceProfile.RequestType requestType) throws IOException, VerificationFailedException { Optional uuid = address.getUuid(); if (FeatureFlags.VERSIONED_PROFILES && requestType == SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL && uuid.isPresent() && profileKey.isPresent()) { return socket.retrieveProfile(uuid.get(), profileKey.get(), unidentifiedAccess); } else { return new ProfileAndCredential(socket.retrieveProfile(address, unidentifiedAccess), SignalServiceProfile.RequestType.PROFILE, Optional.absent()); } } public SignalServiceProfile retrieveProfileByUsername(String username, Optional unidentifiedAccess) throws IOException { return socket.retrieveProfileByUsername(username, unidentifiedAccess); } public InputStream retrieveProfileAvatar(String path, File destination, ProfileKey profileKey, int maxSizeBytes) throws IOException { socket.retrieveProfileAvatar(path, destination, maxSizeBytes); return new ProfileCipherInputStream(new FileInputStream(destination), profileKey); } /** * Retrieves a SignalServiceAttachment. * * @param pointer The {@link SignalServiceAttachmentPointer} * received in a {@link SignalServiceDataMessage}. * @param destination The download destination for this attachment. If this file exists, it is * assumed that this is previously-downloaded content that can be resumed. * @param listener An optional listener (may be null) to receive callbacks on download progress. * * @return An InputStream that streams the plaintext attachment contents. * @throws IOException * @throws InvalidMessageException */ public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, int maxSizeBytes, ProgressListener listener) throws IOException, InvalidMessageException { if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!"); socket.retrieveAttachment(pointer.getId(), destination, maxSizeBytes, listener); 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 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); } /** * Creates a pipe for receiving SignalService messages. * * Callers must call {@link SignalServiceMessagePipe#shutdown()} when finished with the pipe. * * @return A SignalServiceMessagePipe for receiving Signal Service messages. */ public SignalServiceMessagePipe createMessagePipe() { WebSocketConnection webSocket = new WebSocketConnection(urls.getSignalServiceUrls()[0].getUrl(), urls.getSignalServiceUrls()[0].getTrustStore(), Optional.of(credentialsProvider), signalAgent, connectivityListener, sleepTimer, urls.getNetworkInterceptors()); return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfile); } public SignalServiceMessagePipe createUnidentifiedMessagePipe() { WebSocketConnection webSocket = new WebSocketConnection(urls.getSignalServiceUrls()[0].getUrl(), urls.getSignalServiceUrls()[0].getTrustStore(), Optional.absent(), signalAgent, connectivityListener, sleepTimer, urls.getNetworkInterceptors()); return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfile); } public List retrieveMessages() throws IOException { return retrieveMessages(new NullMessageReceivedCallback()); } public List retrieveMessages(MessageReceivedCallback callback) throws IOException { List results = new LinkedList<>(); List entities = socket.getMessages(); for (SignalServiceEnvelopeEntity entity : entities) { SignalServiceEnvelope envelope; if (entity.hasSource() && entity.getSourceDevice() > 0) { SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(entity.getSourceUuid()), entity.getSourceE164()); envelope = new SignalServiceEnvelope(entity.getType(), Optional.of(address), entity.getSourceDevice(), entity.getTimestamp(), entity.getMessage(), entity.getContent(), entity.getServerTimestamp(), entity.getServerUuid()); } else { envelope = new SignalServiceEnvelope(entity.getType(), entity.getTimestamp(), entity.getMessage(), entity.getContent(), entity.getServerTimestamp(), entity.getServerUuid()); } callback.onMessage(envelope); results.add(envelope); if (envelope.hasUuid()) socket.acknowledgeMessage(envelope.getUuid()); else socket.acknowledgeMessage(entity.getSourceE164(), entity.getTimestamp()); } return results; } public void setSoTimeoutMillis(long soTimeoutMillis) { socket.setSoTimeoutMillis(soTimeoutMillis); } public interface MessageReceivedCallback { public void onMessage(SignalServiceEnvelope envelope); } public static class NullMessageReceivedCallback implements MessageReceivedCallback { @Override public void onMessage(SignalServiceEnvelope envelope) {} } }