*/
package org.asamk.signal.manager;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
import org.asamk.Signal;
import org.asamk.signal.AttachmentInvalidException;
import org.asamk.signal.GroupNotFoundException;
+import org.asamk.signal.JsonStickerPack;
import org.asamk.signal.NotAGroupMemberException;
+import org.asamk.signal.StickerPackInvalidException;
import org.asamk.signal.TrustLevel;
import org.asamk.signal.UserAlreadyExists;
import org.asamk.signal.storage.SignalAccount;
import org.signal.libsignal.metadata.ProtocolNoSessionException;
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
import org.signal.libsignal.metadata.SelfSendException;
+import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.signal.zkgroup.InvalidInputException;
+import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
+import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
+import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
+import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifestUpload;
+import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifestUpload.StickerInfo;
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
+import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.util.Base64;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
public class Manager implements Signal {
return username;
}
- private SignalServiceAddress getSelfAddress() {
- return new SignalServiceAddress(null, username);
- }
-
private SignalServiceAccountManager getSignalServiceAccountManager() {
return new SignalServiceAccountManager(BaseConfig.serviceConfiguration, null, account.getUsername(), account.getPassword(), account.getDeviceId(), BaseConfig.USER_AGENT, timer);
}
}
private SignalServiceMessageReceiver getMessageReceiver() {
- return new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, null, username, account.getPassword(), account.getDeviceId(), account.getSignalingKey(), BaseConfig.USER_AGENT, null, timer);
+ return new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), account.getSignalingKey(), BaseConfig.USER_AGENT, null, timer);
}
private SignalServiceMessageSender getMessageSender() {
- return new SignalServiceMessageSender(BaseConfig.serviceConfiguration, null, username, account.getPassword(),
+ return new SignalServiceMessageSender(BaseConfig.serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(),
account.getDeviceId(), account.getSignalProtocolStore(), BaseConfig.USER_AGENT, account.isMultiDevice(), Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent());
}
+ private SignalServiceProfile getRecipientProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess) throws IOException {
+ SignalServiceMessagePipe pipe = unidentifiedMessagePipe != null && unidentifiedAccess.isPresent() ? unidentifiedMessagePipe
+ : messagePipe;
+
+ if (pipe != null) {
+ try {
+ return pipe.getProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).getProfile();
+ } catch (IOException ignored) {
+ }
+ }
+
+ SignalServiceMessageReceiver receiver = getMessageReceiver();
+ try {
+ return receiver.retrieveProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).getProfile();
+ } catch (VerificationFailedException e) {
+ throw new AssertionError(e);
+ }
+ }
+
private Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(byte[] groupId) throws IOException {
File file = getGroupAvatarFile(groupId);
if (!file.exists()) {
throw new GroupNotFoundException(groupId);
}
for (String member : g.members) {
- if (member.equals(this.username)) {
+ if (member.equals(account.getUsername())) {
return g;
}
}
@Override
public void sendGroupMessage(String messageText, List<String> attachments,
byte[] groupId)
- throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
+ throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException {
final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
if (attachments != null) {
messageBuilder.withAttachments(Utils.getSignalServiceAttachments(attachments));
final GroupInfo g = getGroupForSending(groupId);
+ final Collection<SignalServiceAddress> membersSend = getSignalServiceAddresses(g.members);
// Don't send group message to ourself
- final List<String> membersSend = new ArrayList<>(g.members);
- membersSend.remove(this.username);
+ membersSend.remove(account.getSelfAddress());
sendMessageLegacy(messageBuilder, membersSend);
}
public void sendGroupMessageReaction(String emoji, boolean remove, SignalServiceAddress targetAuthor,
long targetSentTimestamp, byte[] groupId)
- throws IOException, EncapsulatedExceptions, AttachmentInvalidException {
+ throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException {
SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, targetAuthor, targetSentTimestamp);
final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.withReaction(reaction)
messageBuilder.asGroupMessage(group);
}
final GroupInfo g = getGroupForSending(groupId);
+ final Collection<SignalServiceAddress> membersSend = getSignalServiceAddresses(g.members);
// Don't send group message to ourself
- final List<String> membersSend = new ArrayList<>(g.members);
- membersSend.remove(this.username);
+ membersSend.remove(account.getSelfAddress());
sendMessageLegacy(messageBuilder, membersSend);
}
- public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions {
+ public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, InvalidNumberException {
SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
.withId(groupId)
.build();
.asGroupMessage(group);
final GroupInfo g = getGroupForSending(groupId);
- g.members.remove(this.username);
+ g.members.remove(account.getUsername());
account.getGroupStore().updateGroup(g);
- sendMessageLegacy(messageBuilder, g.members);
+ sendMessageLegacy(messageBuilder, getSignalServiceAddresses(g.members));
}
- private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
+ private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException {
GroupInfo g;
if (groupId == null) {
// Create new group
g = new GroupInfo(KeyUtils.createGroupId());
- g.members.add(username);
+ g.members.add(account.getUsername());
} else {
g = getGroupForSending(groupId);
}
if (members != null) {
Set<String> newMembers = new HashSet<>();
for (String member : members) {
- try {
- member = Utils.canonicalizeNumber(member, username);
- } catch (InvalidNumberException e) {
- System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage());
- System.err.println("Aborting…");
- System.exit(1);
- }
+ member = Utils.canonicalizeNumber(member, account.getUsername());
if (g.members.contains(member)) {
continue;
}
SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
+ final Collection<SignalServiceAddress> membersSend = getSignalServiceAddresses(g.members);
// Don't send group message to ourself
- final List<String> membersSend = new ArrayList<>(g.members);
- membersSend.remove(this.username);
+ membersSend.remove(account.getSelfAddress());
sendMessageLegacy(messageBuilder, membersSend);
return g.groupId;
}
- private void sendUpdateGroupMessage(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions {
+ private void sendUpdateGroupMessage(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions {
if (groupId == null) {
return;
}
GroupInfo g = getGroupForSending(groupId);
- if (!g.members.contains(recipient)) {
+ if (!g.members.contains(recipient.getNumber().get())) {
return;
}
SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
// Send group message only to the recipient who requested it
- final List<String> membersSend = new ArrayList<>();
- membersSend.add(recipient);
- sendMessageLegacy(messageBuilder, membersSend);
+ sendMessageLegacy(messageBuilder, Collections.singleton(recipient));
}
private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) {
return messageBuilder;
}
- private void sendGroupInfoRequest(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions {
+ private void sendGroupInfoRequest(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions {
if (groupId == null) {
return;
}
}
// Send group info request message to the recipient who sent us a message with this groupId
- final List<String> membersSend = new ArrayList<>();
- membersSend.add(recipient);
- sendMessageLegacy(messageBuilder, membersSend);
+ sendMessageLegacy(messageBuilder, Collections.singleton(recipient));
}
@Override
public void sendMessage(String message, List<String> attachments, String recipient)
- throws EncapsulatedExceptions, AttachmentInvalidException, IOException {
+ throws EncapsulatedExceptions, AttachmentInvalidException, IOException, InvalidNumberException {
List<String> recipients = new ArrayList<>(1);
recipients.add(recipient);
sendMessage(message, attachments, recipients);
@Override
public void sendMessage(String messageText, List<String> attachments,
List<String> recipients)
- throws IOException, EncapsulatedExceptions, AttachmentInvalidException {
+ throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException {
final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
if (attachments != null) {
List<SignalServiceAttachment> attachmentStreams = Utils.getSignalServiceAttachments(attachments);
messageBuilder.withAttachments(attachmentPointers);
}
messageBuilder.withProfileKey(account.getProfileKey().serialize());
- sendMessageLegacy(messageBuilder, recipients);
+ sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients));
}
public void sendMessageReaction(String emoji, boolean remove, SignalServiceAddress targetAuthor,
long targetSentTimestamp, List<String> recipients)
- throws IOException, EncapsulatedExceptions, AttachmentInvalidException {
+ throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException {
SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, targetAuthor, targetSentTimestamp);
final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.withReaction(reaction)
.withProfileKey(account.getProfileKey().serialize());
- sendMessageLegacy(messageBuilder, recipients);
+ sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients));
}
@Override
- public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions {
+ public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions, InvalidNumberException {
SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.asEndSessionMessage();
- sendMessageLegacy(messageBuilder, recipients);
+ sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients));
}
@Override
public String getContactName(String number) throws InvalidNumberException {
- String canonicalizedNumber = Utils.canonicalizeNumber(number, username);
- ContactInfo contact = account.getContactStore().getContact(canonicalizedNumber);
+ String canonicalizedNumber = Utils.canonicalizeNumber(number, account.getUsername());
+ ContactInfo contact = account.getContactStore().getContact(new SignalServiceAddress(null, canonicalizedNumber));
if (contact == null) {
return "";
} else {
@Override
public void setContactName(String number, String name) throws InvalidNumberException {
- String canonicalizedNumber = Utils.canonicalizeNumber(number, username);
- ContactInfo contact = account.getContactStore().getContact(canonicalizedNumber);
+ String canonicalizedNumber = Utils.canonicalizeNumber(number, account.getUsername());
+ final SignalServiceAddress address = new SignalServiceAddress(null, canonicalizedNumber);
+ ContactInfo contact = account.getContactStore().getContact(address);
if (contact == null) {
- contact = new ContactInfo();
- contact.number = canonicalizedNumber;
+ contact = new ContactInfo(address);
System.err.println("Add contact " + canonicalizedNumber + " named " + name);
} else {
System.err.println("Updating contact " + canonicalizedNumber + " name " + contact.name + " -> " + name);
@Override
public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException {
- number = Utils.canonicalizeNumber(number, username);
- ContactInfo contact = account.getContactStore().getContact(number);
+ number = Utils.canonicalizeNumber(number, account.getUsername());
+ final SignalServiceAddress address = new SignalServiceAddress(null, number);
+ ContactInfo contact = account.getContactStore().getContact(address);
if (contact == null) {
- contact = new ContactInfo();
- contact.number = number;
+ contact = new ContactInfo(address);
System.err.println("Adding and " + (blocked ? "blocking" : "unblocking") + " contact " + number);
} else {
System.err.println((blocked ? "Blocking" : "Unblocking") + " contact " + number);
}
@Override
- public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
+ public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException {
if (groupId.length == 0) {
groupId = null;
}
/**
* Change the expiration timer for a thread (number of groupId)
- *
- * @param numberOrGroupId
- * @param messageExpirationTimer
*/
public void setExpirationTimer(String numberOrGroupId, int messageExpirationTimer) {
ThreadInfo thread = account.getThreadStore().getThread(numberOrGroupId);
account.getThreadStore().updateThread(thread);
}
+ /**
+ * Upload the sticker pack from path.
+ *
+ * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
+ * @return if successful, returns the URL to install the sticker pack in the signal app
+ */
+ public String uploadStickerPack(String path) throws IOException, StickerPackInvalidException {
+ SignalServiceStickerManifestUpload manifest = getSignalServiceStickerManifestUpload(path);
+
+ SignalServiceMessageSender messageSender = getMessageSender();
+
+ byte[] packKey = KeyUtils.createStickerUploadKey();
+ String packId = messageSender.uploadStickerManifest(manifest, packKey);
+
+ try {
+ return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder.encode(packId, "utf-8") + "&pack_key=" + URLEncoder.encode(Hex.toStringCondensed(packKey), "utf-8"))
+ .toString();
+ } catch (URISyntaxException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private SignalServiceStickerManifestUpload getSignalServiceStickerManifestUpload(final String path) throws IOException, StickerPackInvalidException {
+ ZipFile zip = null;
+ String rootPath = null;
+
+ final File file = new File(path);
+ if (file.getName().endsWith(".zip")) {
+ zip = new ZipFile(file);
+ } else if (file.getName().equals("manifest.json")) {
+ rootPath = file.getParent();
+ } else {
+ throw new StickerPackInvalidException("Could not find manifest.json");
+ }
+
+ JsonStickerPack pack = parseStickerPack(rootPath, zip);
+
+ if (pack.stickers == null) {
+ throw new StickerPackInvalidException("Must set a 'stickers' field.");
+ }
+
+ if (pack.stickers.isEmpty()) {
+ throw new StickerPackInvalidException("Must include stickers.");
+ }
+
+ List<StickerInfo> stickers = new ArrayList<>(pack.stickers.size());
+ for (JsonStickerPack.JsonSticker sticker : pack.stickers) {
+ if (sticker.file == null) {
+ throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
+ }
+
+ Pair<InputStream, Long> data;
+ try {
+ data = getInputStreamAndLength(rootPath, zip, sticker.file);
+ } catch (IOException ignored) {
+ throw new StickerPackInvalidException("Could not find find " + sticker.file);
+ }
+
+ StickerInfo stickerInfo = new StickerInfo(data.first(), data.second(), Optional.fromNullable(sticker.emoji).or(""));
+ stickers.add(stickerInfo);
+ }
+
+ StickerInfo cover = null;
+ if (pack.cover != null) {
+ if (pack.cover.file == null) {
+ throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
+ }
+
+ Pair<InputStream, Long> data;
+ try {
+ data = getInputStreamAndLength(rootPath, zip, pack.cover.file);
+ } catch (IOException ignored) {
+ throw new StickerPackInvalidException("Could not find find " + pack.cover.file);
+ }
+
+ cover = new StickerInfo(data.first(), data.second(), Optional.fromNullable(pack.cover.emoji).or(""));
+ }
+
+ return new SignalServiceStickerManifestUpload(
+ pack.title,
+ pack.author,
+ cover,
+ stickers);
+ }
+
+ private static JsonStickerPack parseStickerPack(String rootPath, ZipFile zip) throws IOException {
+ InputStream inputStream;
+ if (zip != null) {
+ inputStream = zip.getInputStream(zip.getEntry("manifest.json"));
+ } else {
+ inputStream = new FileInputStream((new File(rootPath, "manifest.json")));
+ }
+ return new ObjectMapper().readValue(inputStream, JsonStickerPack.class);
+ }
+
+ private static Pair<InputStream, Long> getInputStreamAndLength(final String rootPath, final ZipFile zip, final String subfile) throws IOException {
+ if (zip != null) {
+ final ZipEntry entry = zip.getEntry(subfile);
+ return new Pair<>(zip.getInputStream(entry), entry.getSize());
+ } else {
+ final File file = new File(rootPath, subfile);
+ return new Pair<>(new FileInputStream(file), file.length());
+ }
+ }
+
private void requestSyncGroups() throws IOException {
SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build();
SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
}
}
+ private byte[] getSenderCertificate() throws IOException {
+ byte[] certificate = accountManager.getSenderCertificate();
+ // TODO cache for a day
+ return certificate;
+ }
+
private byte[] getSelfUnidentifiedAccessKey() {
return UnidentifiedAccess.deriveAccessKeyFrom(account.getProfileKey());
}
- private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) {
- // TODO implement
- return null;
+ private static SignalProfile decryptProfile(SignalServiceProfile encryptedProfile, ProfileKey profileKey) throws IOException {
+ ProfileCipher profileCipher = new ProfileCipher(profileKey);
+ try {
+ return new SignalProfile(
+ encryptedProfile.getIdentityKey(),
+ encryptedProfile.getName() == null ? null : new String(profileCipher.decryptName(Base64.decode(encryptedProfile.getName()))),
+ encryptedProfile.getAvatar(),
+ encryptedProfile.getUnidentifiedAccess() == null || !profileCipher.verifyUnidentifiedAccess(Base64.decode(encryptedProfile.getUnidentifiedAccess())) ? null : encryptedProfile.getUnidentifiedAccess(),
+ encryptedProfile.isUnrestrictedUnidentifiedAccess()
+ );
+ } catch (InvalidCiphertextException e) {
+ return null;
+ }
}
- private Optional<UnidentifiedAccessPair> getAccessForSync() {
- // TODO implement
- return Optional.absent();
+ private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) throws IOException {
+ ContactInfo contact = account.getContactStore().getContact(recipient);
+ if (contact == null || contact.profileKey == null) {
+ return null;
+ }
+ ProfileKey theirProfileKey;
+ try {
+ theirProfileKey = new ProfileKey(Base64.decode(contact.profileKey));
+ } catch (InvalidInputException e) {
+ throw new AssertionError(e);
+ }
+ SignalProfile targetProfile = decryptProfile(getRecipientProfile(recipient, Optional.absent()), theirProfileKey);
+
+ if (targetProfile == null || targetProfile.getUnidentifiedAccess() == null) {
+ return null;
+ }
+
+ if (targetProfile.isUnrestrictedUnidentifiedAccess()) {
+ return KeyUtils.createUnrestrictedUnidentifiedAccess();
+ }
+
+ return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
}
- private List<Optional<UnidentifiedAccessPair>> getAccessFor(Collection<SignalServiceAddress> recipients) {
+ private Optional<UnidentifiedAccessPair> getAccessForSync() throws IOException {
+ byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
+ byte[] selfUnidentifiedAccessCertificate = getSenderCertificate();
+
+ if (selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
+ return Optional.absent();
+ }
+
+ try {
+ return Optional.of(new UnidentifiedAccessPair(
+ new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate),
+ new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate)
+ ));
+ } catch (InvalidCertificateException e) {
+ return Optional.absent();
+ }
+ }
+
+ private List<Optional<UnidentifiedAccessPair>> getAccessFor(Collection<SignalServiceAddress> recipients) throws IOException {
List<Optional<UnidentifiedAccessPair>> result = new ArrayList<>(recipients.size());
for (SignalServiceAddress recipient : recipients) {
- result.add(Optional.absent());
+ result.add(getAccessFor(recipient));
}
return result;
}
- private Optional<UnidentifiedAccessPair> getAccessFor(SignalServiceAddress recipient) {
- // TODO implement
- return Optional.absent();
+ private Optional<UnidentifiedAccessPair> getAccessFor(SignalServiceAddress recipient) throws IOException {
+ byte[] recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
+ byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
+ byte[] selfUnidentifiedAccessCertificate = getSenderCertificate();
+
+ if (recipientUnidentifiedAccessKey == null || selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
+ return Optional.absent();
+ }
+
+ try {
+ return Optional.of(new UnidentifiedAccessPair(
+ new UnidentifiedAccess(recipientUnidentifiedAccessKey, selfUnidentifiedAccessCertificate),
+ new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate)
+ ));
+ } catch (InvalidCertificateException e) {
+ return Optional.absent();
+ }
}
private void sendSyncMessage(SignalServiceSyncMessage message)
/**
* This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult.
*/
- private void sendMessageLegacy(SignalServiceDataMessage.Builder messageBuilder, Collection<String> recipients)
+ private void sendMessageLegacy(SignalServiceDataMessage.Builder messageBuilder, Collection<SignalServiceAddress> recipients)
throws EncapsulatedExceptions, IOException {
List<SendMessageResult> results = sendMessage(messageBuilder, recipients);
}
}
- private List<SendMessageResult> sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection<String> recipients)
- throws IOException {
- Set<SignalServiceAddress> recipientsTS = Utils.getSignalServiceAddresses(recipients, username);
- if (recipientsTS == null) {
- account.save();
- return Collections.emptyList();
+ private Collection<SignalServiceAddress> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException {
+ final Set<SignalServiceAddress> signalServiceAddresses = new HashSet<>(numbers.size());
+ final String username = account.getUsername();
+
+ for (String number : numbers) {
+ String canonicalizedNumber = Utils.canonicalizeNumber(number, username);
+ if (canonicalizedNumber.equals(username)) {
+ signalServiceAddresses.add(account.getSelfAddress());
+ } else {
+ // TODO get corresponding uuid
+ signalServiceAddresses.add(new SignalServiceAddress(null, canonicalizedNumber));
+ }
}
+ return signalServiceAddresses;
+ }
+ private List<SendMessageResult> sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection<SignalServiceAddress> recipients)
+ throws IOException {
+ if (messagePipe == null) {
+ messagePipe = getMessageReceiver().createMessagePipe();
+ }
+ if (unidentifiedMessagePipe == null) {
+ unidentifiedMessagePipe = getMessageReceiver().createUnidentifiedMessagePipe();
+ }
SignalServiceDataMessage message = null;
try {
SignalServiceMessageSender messageSender = getMessageSender();
if (message.getGroupInfo().isPresent()) {
try {
final boolean isRecipientUpdate = false;
- List<SendMessageResult> result = messageSender.sendMessage(new ArrayList<>(recipientsTS), getAccessFor(recipientsTS), isRecipientUpdate, message);
+ List<SendMessageResult> result = messageSender.sendMessage(new ArrayList<>(recipients), getAccessFor(recipients), isRecipientUpdate, message);
for (SendMessageResult r : result) {
if (r.getIdentityFailure() != null) {
account.getSignalProtocolStore().saveIdentity(r.getAddress().getNumber().get(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED);
account.getSignalProtocolStore().saveIdentity(e.getIdentifier(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
return Collections.emptyList();
}
- } else if (recipientsTS.size() == 1 && recipientsTS.contains(getSelfAddress())) {
- SignalServiceAddress recipient = getSelfAddress();
+ } else if (recipients.size() == 1 && recipients.contains(account.getSelfAddress())) {
+ SignalServiceAddress recipient = account.getSelfAddress();
final Optional<UnidentifiedAccessPair> unidentifiedAccess = getAccessFor(recipient);
SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(recipient),
message.getTimestamp(),
false);
SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
- List<SendMessageResult> results = new ArrayList<>(recipientsTS.size());
+ List<SendMessageResult> results = new ArrayList<>(recipients.size());
try {
messageSender.sendMessage(syncMessage, unidentifiedAccess);
} catch (UntrustedIdentityException e) {
return results;
} else {
// Send to all individually, so sync messages are sent correctly
- List<SendMessageResult> results = new ArrayList<>(recipientsTS.size());
- for (SignalServiceAddress address : recipientsTS) {
+ List<SendMessageResult> results = new ArrayList<>(recipients.size());
+ for (SignalServiceAddress address : recipients) {
ThreadInfo thread = account.getThreadStore().getThread(address.getNumber().get());
if (thread != null) {
messageBuilder.withExpiration(thread.messageExpirationTime);
}
} finally {
if (message != null && message.isEndSession()) {
- for (SignalServiceAddress recipient : recipientsTS) {
+ for (SignalServiceAddress recipient : recipients) {
handleEndSession(recipient.getNumber().get());
}
}
}
private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, ProtocolUntrustedIdentityException, SelfSendException, UnsupportedDataMessageException {
- SignalServiceCipher cipher = new SignalServiceCipher(getSelfAddress(), account.getSignalProtocolStore(), Utils.getCertificateValidator());
+ SignalServiceCipher cipher = new SignalServiceCipher(account.getSelfAddress(), account.getSignalProtocolStore(), Utils.getCertificateValidator());
try {
return cipher.decrypt(envelope);
} catch (ProtocolUntrustedIdentityException e) {
account.getSignalProtocolStore().deleteAllSessions(source);
}
- private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, SignalServiceAddress destination, boolean ignoreAttachments) {
+ private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, SignalServiceAddress source, SignalServiceAddress destination, boolean ignoreAttachments) {
String threadId;
if (message.getGroupInfo().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupInfo().get();
e.printStackTrace();
}
} else {
- group.members.remove(source);
+ group.members.remove(source.getNumber().get());
account.getGroupStore().updateGroup(group);
}
break;
if (isSync) {
threadId = destination.getNumber().get();
} else {
- threadId = source;
+ threadId = source.getNumber().get();
}
}
if (message.isEndSession()) {
- handleEndSession(isSync ? destination.getNumber().get() : source);
+ handleEndSession(isSync ? destination.getNumber().get() : source.getNumber().get());
}
if (message.isExpirationUpdate() || message.getBody().isPresent()) {
ThreadInfo thread = account.getThreadStore().getThread(threadId);
}
}
if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
- if (source.equals(username)) {
+ if (source.equals(account.getSelfAddress())) {
try {
this.account.setProfileKey(new ProfileKey(message.getProfileKey().get()));
} catch (InvalidInputException ignored) {
}
ContactInfo contact = account.getContactStore().getContact(source);
if (contact == null) {
- contact = new ContactInfo();
- contact.number = source;
+ contact = new ContactInfo(source);
}
contact.profileKey = Base64.encodeBytes(message.getProfileKey().get());
+ account.getContactStore().updateContact(contact);
}
if (message.getPreviews().isPresent()) {
final List<SignalServiceDataMessage.Preview> previews = message.getPreviews().get();
}
if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
- handleSignalServiceDataMessage(message, false, sender.getNumber().get(), getSelfAddress(), ignoreAttachments);
+ handleSignalServiceDataMessage(message, false, sender, account.getSelfAddress(), ignoreAttachments);
}
if (content.getSyncMessage().isPresent()) {
account.setMultiDevice(true);
SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
if (syncMessage.getSent().isPresent()) {
SentTranscriptMessage message = syncMessage.getSent().get();
- handleSignalServiceDataMessage(message.getMessage(), true, sender.getNumber().get(), message.getDestination().orNull(), ignoreAttachments);
+ handleSignalServiceDataMessage(message.getMessage(), true, sender, message.getDestination().orNull(), ignoreAttachments);
}
if (syncMessage.getRequest().isPresent()) {
RequestMessage rm = syncMessage.getRequest().get();
syncGroup.name = g.getName().get();
}
syncGroup.addMembers(g.getMembers());
- syncGroup.active = g.isActive();
+ if (!g.isActive()) {
+ syncGroup.members.remove(account.getUsername());
+ } else {
+ // Add ourself to the member set as it's marked as active
+ syncGroup.members.add(account.getUsername());
+ }
syncGroup.blocked = g.isBlocked();
if (g.getColor().isPresent()) {
syncGroup.color = g.getColor().get();
if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) {
account.setProfileKey(c.getProfileKey().get());
}
- ContactInfo contact = account.getContactStore().getContact(c.getAddress().getNumber().get());
+ ContactInfo contact = account.getContactStore().getContact(c.getAddress());
if (contact == null) {
- contact = new ContactInfo();
- contact.number = c.getAddress().getNumber().get();
+ contact = new ContactInfo(c.getAddress());
}
if (c.getName().isPresent()) {
contact.name = c.getName().get();
ThreadInfo info = account.getThreadStore().getThread(Base64.encodeBytes(record.groupId));
out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
new ArrayList<>(record.getMembers()), createGroupAvatarAttachment(record.groupId),
- record.active, Optional.fromNullable(info != null ? info.messageExpirationTime : null),
+ record.members.contains(account.getUsername()), Optional.fromNullable(info != null ? info.messageExpirationTime : null),
Optional.fromNullable(record.color), record.blocked, Optional.fromNullable(record.inboxPosition), record.archived));
}
}
}
public ContactInfo getContact(String number) {
- return account.getContactStore().getContact(number);
+ return account.getContactStore().getContact(new SignalServiceAddress(null, number));
}
public GroupInfo getGroup(byte[] groupId) {
}
public Pair<String, List<JsonIdentityKeyStore.Identity>> getIdentities(String number) throws InvalidNumberException {
- String canonicalizedNumber = Utils.canonicalizeNumber(number, username);
+ String canonicalizedNumber = Utils.canonicalizeNumber(number, account.getUsername());
return new Pair<>(canonicalizedNumber, account.getSignalProtocolStore().getIdentities(canonicalizedNumber));
}
}
public String computeSafetyNumber(String theirUsername, IdentityKey theirIdentityKey) {
- return Utils.computeSafetyNumber(username, getIdentity(), theirUsername, theirIdentityKey);
+ return Utils.computeSafetyNumber(account.getUsername(), getIdentity(), theirUsername, theirIdentityKey);
}
public interface ReceiveMessageHandler {