X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/b1ec1e65878a9cc5bceb83d1c0b30306559aea8c..e36a2f862c996825944927c6e24bf8521edd6edb:/src/main/java/org/asamk/signal/Manager.java diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 8b2477e7..adc31f70 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -16,49 +16,15 @@ */ package org.asamk.signal; -import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; -import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; -import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InvalidObjectException; -import java.io.OutputStream; -import java.io.RandomAccessFile; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.PosixFilePermissions; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Date; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.http.util.TextUtils; import org.asamk.Signal; import org.asamk.signal.storage.contacts.ContactInfo; @@ -69,17 +35,8 @@ import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; import org.asamk.signal.storage.threads.JsonThreadStore; import org.asamk.signal.storage.threads.ThreadInfo; -import org.asamk.signal.util.Base64; import org.asamk.signal.util.Util; -import org.whispersystems.libsignal.DuplicateMessageException; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.IdentityKeyPair; -import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.InvalidKeyIdException; -import org.whispersystems.libsignal.InvalidMessageException; -import org.whispersystems.libsignal.InvalidVersionException; -import org.whispersystems.libsignal.LegacyMessageException; -import org.whispersystems.libsignal.NoSessionException; +import org.whispersystems.libsignal.*; import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.ECKeyPair; import org.whispersystems.libsignal.ecc.ECPublicKey; @@ -96,49 +53,48 @@ import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; -import org.whispersystems.signalservice.api.messages.SignalServiceContent; -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.multidevice.DeviceContact; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; -import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; -import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.messages.*; +import org.whispersystems.signalservice.api.messages.multidevice.*; import org.whispersystems.signalservice.api.push.ContactTokenDetails; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.TrustStore; -import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; -import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; -import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; -import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; -import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; +import org.whispersystems.signalservice.api.push.exceptions.*; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; +import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; +import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; -import org.whispersystems.signalservice.internal.push.SignalServiceUrl; +import org.whispersystems.signalservice.internal.util.Base64; -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.node.ObjectNode; +import java.io.*; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static java.nio.file.attribute.PosixFilePermission.*; class Manager implements Signal { private final static String URL = "https://textsecure-service.whispersystems.org"; + private final static String CDN_URL = "https://cdn.signal.org"; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); - private final static SignalServiceUrl[] serviceUrls = new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)}; + private final static SignalServiceConfiguration serviceConfiguration = new SignalServiceConfiguration( + new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)}, + new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)} + ); public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle(); public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion(); @@ -146,6 +102,7 @@ class Manager implements Signal { private final static int PREKEY_MINIMUM_COUNT = 20; private static final int PREKEY_BATCH_SIZE = 100; + private static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; private final String settingsPath; private final String dataPath; @@ -280,7 +237,7 @@ class Manager implements Signal { migrateLegacyConfigs(); - accountManager = new SignalServiceAccountManager(serviceUrls, username, password, deviceId, USER_AGENT); + accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, deviceId, USER_AGENT); try { if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) { refreshPreKeys(); @@ -405,7 +362,7 @@ class Manager implements Signal { public void register(boolean voiceVerification) throws IOException { password = Util.getSecret(18); - accountManager = new SignalServiceAccountManager(serviceUrls, username, password, USER_AGENT); + accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, USER_AGENT); if (voiceVerification) accountManager.requestVoiceVerificationCode(); @@ -417,7 +374,7 @@ class Manager implements Signal { } public void updateAccountAttributes() throws IOException { - accountManager.setAccountAttributes(signalingKey, signalProtocolStore.getLocalRegistrationId(), false, false,true); + accountManager.setAccountAttributes(signalingKey, signalProtocolStore.getLocalRegistrationId(), true); } public void unregister() throws IOException { @@ -430,7 +387,7 @@ class Manager implements Signal { public URI getDeviceLinkUri() throws TimeoutException, IOException { password = Util.getSecret(18); - accountManager = new SignalServiceAccountManager(serviceUrls, username, password, USER_AGENT); + accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, USER_AGENT); String uuid = accountManager.getNewDeviceUuid(); registered = false; @@ -509,7 +466,8 @@ class Manager implements Signal { IdentityKeyPair identityKeyPair = signalProtocolStore.getIdentityKeyPair(); String verificationCode = accountManager.getNewDeviceVerificationCode(); - accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, verificationCode); + // TODO send profile key + accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.absent(), verificationCode); } private List generatePreKeys() { @@ -530,24 +488,6 @@ class Manager implements Signal { return records; } - private PreKeyRecord getOrGenerateLastResortPreKey() { - if (signalProtocolStore.containsPreKey(Medium.MAX_VALUE)) { - try { - return signalProtocolStore.loadPreKey(Medium.MAX_VALUE); - } catch (InvalidKeyIdException e) { - signalProtocolStore.removePreKey(Medium.MAX_VALUE); - } - } - - ECKeyPair keyPair = Curve.generateKeyPair(); - PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair); - - signalProtocolStore.storePreKey(Medium.MAX_VALUE, record); - save(); - - return record; - } - private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) { try { ECKeyPair keyPair = Curve.generateKeyPair(); @@ -567,7 +507,7 @@ class Manager implements Signal { public void verifyAccount(String verificationCode) throws IOException { verificationCode = verificationCode.replace("-", ""); signalingKey = Util.getSecret(52); - accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), false, false,true); + accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), true); //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); registered = true; @@ -578,10 +518,9 @@ class Manager implements Signal { private void refreshPreKeys() throws IOException { List oneTimePreKeys = generatePreKeys(); - PreKeyRecord lastResortKey = getOrGenerateLastResortPreKey(); SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(signalProtocolStore.getIdentityKeyPair()); - accountManager.setPreKeys(signalProtocolStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys); + accountManager.setPreKeys(signalProtocolStore.getIdentityKeyPair().getPublicKey(), signedPreKeyRecord, oneTimePreKeys); } @@ -607,7 +546,8 @@ class Manager implements Signal { if (mime == null) { mime = "application/octet-stream"; } - return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, null); + // TODO mabybe add a parameter to set the voiceNote and preview option + return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, Optional.absent(), null); } private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { @@ -640,7 +580,7 @@ class Manager implements Signal { } throw new NotAGroupMemberException(groupId, g.name); } - + public List getGroups() { return groupStore.getGroups(); } @@ -855,9 +795,9 @@ class Manager implements Signal { if (contact == null) { contact = new ContactInfo(); contact.number = number; - System.out.println("Add contact " + number + " named " + name); + System.err.println("Add contact " + number + " named " + name); } else { - System.out.println("Updating contact " + number + " name " + contact.name + " -> " + name); + System.err.println("Updating contact " + number + " name " + contact.name + " -> " + name); } contact.name = name; contactStore.updateContact(contact); @@ -885,7 +825,10 @@ class Manager implements Signal { } @Override - public void updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + if (groupId.length == 0) { + groupId = null; + } if (name.isEmpty()) { name = null; } @@ -895,7 +838,7 @@ class Manager implements Signal { if (avatar.isEmpty()) { avatar = null; } - sendUpdateGroupMessage(groupId, name, members, avatar); + return sendUpdateGroupMessage(groupId, name, members, avatar); } private void requestSyncGroups() throws IOException { @@ -920,7 +863,7 @@ class Manager implements Signal { private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException { - SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceUrls, username, password, + SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceConfiguration, username, password, deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.absent()); try { messageSender.sendMessage(message); @@ -937,7 +880,7 @@ class Manager implements Signal { SignalServiceDataMessage message = null; try { - SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceUrls, username, password, + SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceConfiguration, username, password, deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.absent()); message = messageBuilder.build(); @@ -1158,15 +1101,17 @@ class Manager implements Signal { try { Files.delete(fileEntry.toPath()); } catch (IOException e) { - System.out.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage()); + System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage()); } } + // Try to delete directory if empty + dir.delete(); } } public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException { retryFailedReceivedMessages(handler, ignoreAttachments); - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT); try { if (messagePipe == null) { @@ -1214,8 +1159,10 @@ class Manager implements Signal { try { cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp()); Files.delete(cacheFile.toPath()); + // Try to delete directory if empty + new File(getMessageCachePath()).delete(); } catch (IOException e) { - System.out.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage()); + System.err.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage()); } } } @@ -1260,23 +1207,25 @@ class Manager implements Signal { File tmpFile = null; try { tmpFile = Util.createTempFile(); - DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer(), tmpFile)); - DeviceGroup g; - while ((g = s.read()) != null) { - GroupInfo syncGroup = groupStore.getGroup(g.getId()); - if (syncGroup == null) { - syncGroup = new GroupInfo(g.getId()); - } - if (g.getName().isPresent()) { - syncGroup.name = g.getName().get(); - } - syncGroup.members.addAll(g.getMembers()); - syncGroup.active = g.isActive(); - - if (g.getAvatar().isPresent()) { - retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId); + try (InputStream attachmentAsStream = retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer(), tmpFile)) { + DeviceGroupsInputStream s = new DeviceGroupsInputStream(attachmentAsStream); + DeviceGroup g; + while ((g = s.read()) != null) { + GroupInfo syncGroup = groupStore.getGroup(g.getId()); + if (syncGroup == null) { + syncGroup = new GroupInfo(g.getId()); + } + if (g.getName().isPresent()) { + syncGroup.name = g.getName().get(); + } + syncGroup.members.addAll(g.getMembers()); + syncGroup.active = g.isActive(); + + if (g.getAvatar().isPresent()) { + retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId); + } + groupStore.updateGroup(syncGroup); } - groupStore.updateGroup(syncGroup); } } catch (Exception e) { e.printStackTrace(); @@ -1285,7 +1234,7 @@ class Manager implements Signal { try { Files.delete(tmpFile.toPath()); } catch (IOException e) { - System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage()); + System.err.println("Failed to delete received groups temp file “" + tmpFile + "”: " + e.getMessage()); } } } @@ -1297,24 +1246,30 @@ class Manager implements Signal { File tmpFile = null; try { tmpFile = Util.createTempFile(); - DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer(), tmpFile)); - DeviceContact c; - while ((c = s.read()) != null) { - ContactInfo contact = contactStore.getContact(c.getNumber()); - if (contact == null) { - contact = new ContactInfo(); - contact.number = c.getNumber(); - } - if (c.getName().isPresent()) { - contact.name = c.getName().get(); - } - if (c.getColor().isPresent()) { - contact.color = c.getColor().get(); + final ContactsMessage contactsMessage = syncMessage.getContacts().get(); + try (InputStream attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream().asPointer(), tmpFile)) { + DeviceContactsInputStream s = new DeviceContactsInputStream(attachmentAsStream); + if (contactsMessage.isComplete()) { + contactStore.clear(); } - contactStore.updateContact(contact); - - if (c.getAvatar().isPresent()) { - retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number); + DeviceContact c; + while ((c = s.read()) != null) { + ContactInfo contact = contactStore.getContact(c.getNumber()); + if (contact == null) { + contact = new ContactInfo(); + contact.number = c.getNumber(); + } + if (c.getName().isPresent()) { + contact.name = c.getName().get(); + } + if (c.getColor().isPresent()) { + contact.color = c.getColor().get(); + } + contactStore.updateContact(contact); + + if (c.getAvatar().isPresent()) { + retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number); + } } } } catch (Exception e) { @@ -1324,11 +1279,15 @@ class Manager implements Signal { try { Files.delete(tmpFile.toPath()); } catch (IOException e) { - System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage()); + System.err.println("Failed to delete received contacts temp file “" + tmpFile + "”: " + e.getMessage()); } } } } + if (syncMessage.getVerified().isPresent()) { + final VerifiedMessage verifiedMessage = syncMessage.getVerified().get(); + signalProtocolStore.saveIdentity(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); + } } } } @@ -1454,10 +1413,10 @@ class Manager implements Signal { } } - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT); File tmpFile = Util.createTempFile(); - try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile)) { + try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE)) { try (OutputStream output = new FileOutputStream(outputFile)) { byte[] buffer = new byte[4096]; int read; @@ -1473,15 +1432,15 @@ class Manager implements Signal { try { Files.delete(tmpFile.toPath()); } catch (IOException e) { - System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage()); + System.err.println("Failed to delete received attachment temp file “" + tmpFile + "”: " + e.getMessage()); } } return outputFile; } private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException { - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT); - return messageReceiver.retrieveAttachment(pointer, tmpFile); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT); + return messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE); } private String canonicalizeNumber(String number) throws InvalidNumberException { @@ -1527,7 +1486,7 @@ class Manager implements Signal { try { Files.delete(groupsFile.toPath()); } catch (IOException e) { - System.out.println("Failed to delete temp file “" + groupsFile + "”: " + e.getMessage()); + System.err.println("Failed to delete groups temp file “" + groupsFile + "”: " + e.getMessage()); } } } @@ -1539,8 +1498,23 @@ class Manager implements Signal { try (OutputStream fos = new FileOutputStream(contactsFile)) { DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos); for (ContactInfo record : contactStore.getContacts()) { + VerifiedMessage verifiedMessage = null; + if (getIdentities().containsKey(record.number)) { + JsonIdentityKeyStore.Identity currentIdentity = null; + for (JsonIdentityKeyStore.Identity id : getIdentities().get(record.number)) { + if (currentIdentity == null || id.getDateAdded().after(currentIdentity.getDateAdded())) { + currentIdentity = id; + } + } + if (currentIdentity != null) { + verifiedMessage = new VerifiedMessage(record.number, currentIdentity.getIdentityKey(), currentIdentity.getTrustLevel().toVerifiedState(), currentIdentity.getDateAdded().getTime()); + } + } + + // TODO include profile key out.write(new DeviceContact(record.number, Optional.fromNullable(record.name), - createContactAvatarAttachment(record.number), Optional.fromNullable(record.color))); + createContactAvatarAttachment(record.number), Optional.fromNullable(record.color), + Optional.fromNullable(verifiedMessage), Optional.absent())); } } @@ -1552,18 +1526,23 @@ class Manager implements Signal { .withLength(contactsFile.length()) .build(); - sendSyncMessage(SignalServiceSyncMessage.forContacts(attachmentStream)); + sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, true))); } } } finally { try { Files.delete(contactsFile.toPath()); } catch (IOException e) { - System.out.println("Failed to delete temp file “" + contactsFile + "”: " + e.getMessage()); + System.err.println("Failed to delete contacts temp file “" + contactsFile + "”: " + e.getMessage()); } } } + private void sendVerifiedMessage(String destination, IdentityKey identityKey, TrustLevel trustLevel) throws IOException, UntrustedIdentityException { + VerifiedMessage verifiedMessage = new VerifiedMessage(destination, identityKey, trustLevel.toVerifiedState(), System.currentTimeMillis()); + sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); + } + public ContactInfo getContact(String number) { return contactStore.getContact(number); } @@ -1597,6 +1576,11 @@ class Manager implements Signal { } signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + try { + sendVerifiedMessage(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + } catch (IOException | UntrustedIdentityException e) { + e.printStackTrace(); + } save(); return true; } @@ -1620,6 +1604,11 @@ class Manager implements Signal { } signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + try { + sendVerifiedMessage(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + } catch (IOException | UntrustedIdentityException e) { + e.printStackTrace(); + } save(); return true; } @@ -1639,6 +1628,11 @@ class Manager implements Signal { for (JsonIdentityKeyStore.Identity id : ids) { if (id.getTrustLevel() == TrustLevel.UNTRUSTED) { signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); + try { + sendVerifiedMessage(name, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); + } catch (IOException | UntrustedIdentityException e) { + e.printStackTrace(); + } } } save();