X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/e63e6e1fa2b90ba68a5351029a365bf477393301..7443225d96fd830c8abb2afd49381f6b38ce5aec:/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 585da2e8..dc3916de 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -35,7 +35,11 @@ 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.IOUtils; +import org.asamk.signal.util.KeyUtils; import org.asamk.signal.util.Util; +import org.signal.libsignal.metadata.*; +import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.whispersystems.libsignal.*; import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.ECKeyPair; @@ -52,16 +56,24 @@ import org.whispersystems.signalservice.api.SignalServiceMessagePipe; 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.UnidentifiedAccess; +import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; 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.*; +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.UnregisteredUserException; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.api.util.SleepTimer; +import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; +import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; @@ -70,31 +82,27 @@ import org.whispersystems.signalservice.internal.util.Base64; 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 SignalServiceConfiguration serviceConfiguration = new SignalServiceConfiguration( new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)}, - new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)} + new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, + new SignalContactDiscoveryUrl[0] ); + private final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"; public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle(); public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion(); @@ -115,8 +123,11 @@ class Manager implements Signal { private final ObjectMapper jsonProcessor = new ObjectMapper(); private String username; private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; + private boolean isMultiDevice = false; private String password; + private String registrationLockPin; private String signalingKey; + private byte[] profileKey; private int preKeyIdOffset; private int nextSignedPreKeyId; @@ -128,6 +139,9 @@ class Manager implements Signal { private JsonContactsStore contactStore; private JsonThreadStore threadStore; private SignalServiceMessagePipe messagePipe = null; + private SignalServiceMessagePipe unidentifiedMessagePipe = null; + + private SleepTimer timer = new UptimeSleepTimer(); public Manager(String username, String settingsPath) { this.username = username; @@ -170,30 +184,10 @@ class Manager implements Signal { private File getMessageCacheFile(String sender, long now, long timestamp) throws IOException { String cachePath = getMessageCachePath(sender); - createPrivateDirectories(cachePath); + IOUtils.createPrivateDirectories(cachePath); return new File(cachePath + "/" + now + "_" + timestamp); } - private static void createPrivateDirectories(String path) throws IOException { - final Path file = new File(path).toPath(); - try { - Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE); - Files.createDirectories(file, PosixFilePermissions.asFileAttribute(perms)); - } catch (UnsupportedOperationException e) { - Files.createDirectories(file); - } - } - - private static void createPrivateFile(String path) throws IOException { - final Path file = new File(path).toPath(); - try { - Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE); - Files.createFile(file, PosixFilePermissions.asFileAttribute(perms)); - } catch (UnsupportedOperationException e) { - Files.createFile(file); - } - } - public boolean userExists() { if (username == null) { return false; @@ -219,9 +213,9 @@ class Manager implements Signal { if (fileChannel != null) return; - createPrivateDirectories(dataPath); + IOUtils.createPrivateDirectories(dataPath); if (!new File(getFileName()).exists()) { - createPrivateFile(getFileName()); + IOUtils.createPrivateFile(getFileName()); } fileChannel = new RandomAccessFile(new File(getFileName()), "rw").getChannel(); lock = fileChannel.tryLock(); @@ -237,7 +231,7 @@ class Manager implements Signal { migrateLegacyConfigs(); - accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, deviceId, USER_AGENT); + accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, deviceId, USER_AGENT, timer); try { if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) { refreshPreKeys(); @@ -258,6 +252,8 @@ class Manager implements Signal { } username = getNotNullNode(rootNode, "username").asText(); password = getNotNullNode(rootNode, "password").asText(); + JsonNode pinNode = rootNode.get("registrationLockPin"); + registrationLockPin = pinNode == null ? null : pinNode.asText(); if (rootNode.has("signalingKey")) { signalingKey = getNotNullNode(rootNode, "signalingKey").asText(); } @@ -271,6 +267,13 @@ class Manager implements Signal { } else { nextSignedPreKeyId = 0; } + if (rootNode.has("profileKey")) { + profileKey = Base64.decode(getNotNullNode(rootNode, "profileKey").asText()); + } else { + // Old config file, creating new profile key + profileKey = KeyUtils.createProfileKey(); + } + signalProtocolStore = jsonProcessor.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class); registered = getNotNullNode(rootNode, "registered").asBoolean(); JsonNode groupStoreNode = rootNode.get("groupStore"); @@ -306,7 +309,7 @@ class Manager implements Signal { File attachmentFile = getAttachmentFile(g.getAvatarId()); if (!avatarFile.exists() && attachmentFile.exists()) { try { - createPrivateDirectories(avatarsPath); + IOUtils.createPrivateDirectories(avatarsPath); Files.copy(attachmentFile.toPath(), avatarFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } catch (Exception e) { // Ignore @@ -326,6 +329,7 @@ class Manager implements Signal { rootNode.put("username", username) .put("deviceId", deviceId) .put("password", password) + .put("registrationLockPin", registrationLockPin) .put("signalingKey", signalingKey) .put("preKeyIdOffset", preKeyIdOffset) .put("nextSignedPreKeyId", nextSignedPreKeyId) @@ -360,9 +364,9 @@ class Manager implements Signal { } public void register(boolean voiceVerification) throws IOException { - password = Util.getSecret(18); + password = KeyUtils.createPassword(); - accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, USER_AGENT); + accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, USER_AGENT, timer); if (voiceVerification) accountManager.requestVoiceVerificationCode(); @@ -374,7 +378,7 @@ class Manager implements Signal { } public void updateAccountAttributes() throws IOException { - accountManager.setAccountAttributes(signalingKey, signalProtocolStore.getLocalRegistrationId(), true); + accountManager.setAccountAttributes(signalingKey, signalProtocolStore.getLocalRegistrationId(), true, registrationLockPin, getSelfUnidentifiedAccessKey(), false); } public void unregister() throws IOException { @@ -385,9 +389,9 @@ class Manager implements Signal { } public URI getDeviceLinkUri() throws TimeoutException, IOException { - password = Util.getSecret(18); + password = KeyUtils.createPassword(); - accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, USER_AGENT); + accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, USER_AGENT, timer); String uuid = accountManager.getNewDeviceUuid(); registered = false; @@ -400,7 +404,7 @@ class Manager implements Signal { } public void finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists { - signalingKey = Util.getSecret(52); + signalingKey = KeyUtils.createSignalingKey(); SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(signalProtocolStore.getIdentityKeyPair(), signalingKey, false, true, signalProtocolStore.getLocalRegistrationId(), deviceName); deviceId = ret.getDeviceId(); username = ret.getNumber(); @@ -411,6 +415,7 @@ class Manager implements Signal { signalProtocolStore = new JsonSignalProtocolStore(ret.getIdentity(), signalProtocolStore.getLocalRegistrationId()); registered = true; + isMultiDevice = true; refreshPreKeys(); requestSyncGroups(); @@ -420,36 +425,17 @@ class Manager implements Signal { } public List getLinkedDevices() throws IOException { - return accountManager.getDevices(); + List devices = accountManager.getDevices(); + isMultiDevice = devices.size() > 1; + return devices; } public void removeLinkedDevices(int deviceId) throws IOException { accountManager.removeDevice(deviceId); } - public static Map getQueryMap(String query) { - String[] params = query.split("&"); - Map map = new HashMap<>(); - for (String param : params) { - String name = null; - try { - name = URLDecoder.decode(param.split("=")[0], "utf-8"); - } catch (UnsupportedEncodingException e) { - // Impossible - } - String value = null; - try { - value = URLDecoder.decode(param.split("=")[1], "utf-8"); - } catch (UnsupportedEncodingException e) { - // Impossible - } - map.put(name, value); - } - return map; - } - public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { - Map query = getQueryMap(linkUri.getRawQuery()); + Map query = Util.getQueryMap(linkUri.getRawQuery()); String deviceIdentifier = query.get("uuid"); String publicKeyEncoded = query.get("pub_key"); @@ -466,8 +452,8 @@ class Manager implements Signal { IdentityKeyPair identityKeyPair = signalProtocolStore.getIdentityKeyPair(); String verificationCode = accountManager.getNewDeviceVerificationCode(); - // TODO send profile key - accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.absent(), verificationCode); + accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.of(profileKey), verificationCode); + isMultiDevice = true; } private List generatePreKeys() { @@ -504,18 +490,28 @@ class Manager implements Signal { } } - public void verifyAccount(String verificationCode) throws IOException { + public void verifyAccount(String verificationCode, String pin) throws IOException { verificationCode = verificationCode.replace("-", ""); - signalingKey = Util.getSecret(52); - accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), true); + signalingKey = KeyUtils.createSignalingKey(); + accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), true, pin, getSelfUnidentifiedAccessKey(), false); //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); registered = true; + registrationLockPin = pin; refreshPreKeys(); save(); } + public void setRegistrationLockPin(Optional pin) throws IOException { + accountManager.setPin(pin); + if (pin.isPresent()) { + registrationLockPin = pin.get(); + } else { + registrationLockPin = null; + } + } + private void refreshPreKeys() throws IOException { List oneTimePreKeys = generatePreKeys(); SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(signalProtocolStore.getIdentityKeyPair()); @@ -546,8 +542,10 @@ class Manager implements Signal { if (mime == null) { mime = "application/octet-stream"; } - // 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); + // TODO mabybe add a parameter to set the voiceNote, preview, width, height and caption option + Optional preview = Optional.absent(); + Optional caption = Optional.absent(); + return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, caption, null); } private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { @@ -609,7 +607,7 @@ class Manager implements Signal { // Don't send group message to ourself final List membersSend = new ArrayList<>(g.members); membersSend.remove(this.username); - sendMessage(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, membersSend); } public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions { @@ -624,26 +622,14 @@ class Manager implements Signal { g.members.remove(this.username); groupStore.updateGroup(g); - sendMessage(messageBuilder, g.members); - } - - private static String join(CharSequence separator, Iterable list) { - StringBuilder buf = new StringBuilder(); - for (CharSequence str : list) { - if (buf.length() > 0) { - buf.append(separator); - } - buf.append(str); - } - - return buf.toString(); + sendMessageLegacy(messageBuilder, g.members); } public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { GroupInfo g; if (groupId == null) { // Create new group - g = new GroupInfo(Util.getSecretBytes(16)); + g = new GroupInfo(KeyUtils.createGroupId()); g.members.add(username); } else { g = getGroupForSending(groupId); @@ -675,14 +661,14 @@ class Manager implements Signal { for (ContactTokenDetails contact : contacts) { newMembers.remove(contact.getNumber()); } - System.err.println("Failed to add members " + join(", ", newMembers) + " to group: Not registered on Signal"); + System.err.println("Failed to add members " + Util.join(", ", newMembers) + " to group: Not registered on Signal"); System.err.println("Aborting…"); System.exit(1); } } if (avatarFile != null) { - createPrivateDirectories(avatarsPath); + IOUtils.createPrivateDirectories(avatarsPath); File aFile = getGroupAvatarFile(g.groupId); Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } @@ -694,7 +680,7 @@ class Manager implements Signal { // Don't send group message to ourself final List membersSend = new ArrayList<>(g.members); membersSend.remove(this.username); - sendMessage(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, membersSend); return g.groupId; } @@ -713,7 +699,7 @@ class Manager implements Signal { // Send group message only to the recipient who requested it final List membersSend = new ArrayList<>(); membersSend.add(recipient); - sendMessage(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, membersSend); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) { @@ -749,7 +735,7 @@ class Manager implements Signal { // Send group info request message to the recipient who sent us a message with this groupId final List membersSend = new ArrayList<>(); membersSend.add(recipient); - sendMessage(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, membersSend); } @Override @@ -768,7 +754,7 @@ class Manager implements Signal { if (attachments != null) { messageBuilder.withAttachments(getSignalServiceAttachments(attachments)); } - sendMessage(messageBuilder, recipients); + sendMessageLegacy(messageBuilder, recipients); } @Override @@ -776,7 +762,7 @@ class Manager implements Signal { SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .asEndSessionMessage(); - sendMessage(messageBuilder, recipients); + sendMessageLegacy(messageBuilder, recipients); } @Override @@ -804,6 +790,16 @@ class Manager implements Signal { save(); } + @Override + public List getGroupIds() { + List groups = getGroups(); + List ids = new ArrayList(groups.size()); + for (GroupInfo group : groups) { + ids.add(group.groupId); + } + return ids; + } + @Override public String getGroupName(byte[] groupId) { GroupInfo group = getGroup(groupId); @@ -861,42 +857,97 @@ class Manager implements Signal { } } + private byte[] getSelfUnidentifiedAccessKey() { + return UnidentifiedAccess.deriveAccessKeyFrom(profileKey); + } + + private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) { + // TODO implement + return null; + } + + public Optional getAccessForSync() { + // TODO implement + return Optional.absent(); + } + + public List> getAccessFor(Collection recipients) { + List> result = new ArrayList<>(recipients.size()); + for (SignalServiceAddress recipient : recipients) { + result.add(Optional.absent()); + } + return result; + } + + public Optional getAccessFor(SignalServiceAddress recipient) { + // TODO implement + return Optional.absent(); + } + private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceConfiguration, username, password, - deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.absent()); + deviceId, signalProtocolStore, USER_AGENT, isMultiDevice, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent()); try { - messageSender.sendMessage(message); + messageSender.sendMessage(message, getAccessForSync()); } catch (UntrustedIdentityException e) { signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); throw e; } } - private void sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) + /** + * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult. + */ + private void sendMessageLegacy(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) throws EncapsulatedExceptions, IOException { + List results = sendMessage(messageBuilder, recipients); + + List untrustedIdentities = new LinkedList<>(); + List unregisteredUsers = new LinkedList<>(); + List networkExceptions = new LinkedList<>(); + + for (SendMessageResult result : results) { + if (result.isUnregisteredFailure()) { + unregisteredUsers.add(new UnregisteredUserException(result.getAddress().getNumber(), null)); + } else if (result.isNetworkFailure()) { + networkExceptions.add(new NetworkFailureException(result.getAddress().getNumber(), null)); + } else if (result.getIdentityFailure() != null) { + untrustedIdentities.add(new UntrustedIdentityException("Untrusted", result.getAddress().getNumber(), result.getIdentityFailure().getIdentityKey())); + } + } + if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) { + throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions); + } + } + + private List sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) + throws IOException { Set recipientsTS = getSignalServiceAddresses(recipients); - if (recipientsTS == null) return; + if (recipientsTS == null) return Collections.emptyList(); SignalServiceDataMessage message = null; try { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceConfiguration, username, password, - deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.absent()); + deviceId, signalProtocolStore, USER_AGENT, isMultiDevice, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent()); message = messageBuilder.build(); if (message.getGroupInfo().isPresent()) { try { - messageSender.sendMessage(new ArrayList<>(recipientsTS), message); - } catch (EncapsulatedExceptions encapsulatedExceptions) { - for (UntrustedIdentityException e : encapsulatedExceptions.getUntrustedIdentityExceptions()) { - signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + List result = messageSender.sendMessage(new ArrayList<>(recipientsTS), getAccessFor(recipientsTS), message); + for (SendMessageResult r : result) { + if (r.getIdentityFailure() != null) { + signalProtocolStore.saveIdentity(r.getAddress().getNumber(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED); + } } + return result; + } catch (UntrustedIdentityException e) { + signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + return Collections.emptyList(); } } else { // Send to all individually, so sync messages are sent correctly - List untrustedIdentities = new LinkedList<>(); - List unregisteredUsers = new LinkedList<>(); - List networkExceptions = new LinkedList<>(); + List results = new ArrayList<>(recipientsTS.size()); for (SignalServiceAddress address : recipientsTS) { ThreadInfo thread = threadStore.getThread(address.getNumber()); if (thread != null) { @@ -906,19 +957,14 @@ class Manager implements Signal { } message = messageBuilder.build(); try { - messageSender.sendMessage(address, message); + SendMessageResult result = messageSender.sendMessage(address, getAccessFor(address), message); + results.add(result); } catch (UntrustedIdentityException e) { signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); - untrustedIdentities.add(e); - } catch (UnregisteredUserException e) { - unregisteredUsers.add(e); - } catch (PushNetworkException e) { - networkExceptions.add(new NetworkFailureException(address.getNumber(), e)); + results.add(SendMessageResult.identityFailure(address, e.getIdentityKey())); } } - if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) { - throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions); - } + return results; } } finally { if (message != null && message.isEndSession()) { @@ -945,12 +991,22 @@ class Manager implements Signal { return recipientsTS; } - private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws NoSessionException, LegacyMessageException, InvalidVersionException, InvalidMessageException, DuplicateMessageException, InvalidKeyException, InvalidKeyIdException, org.whispersystems.libsignal.UntrustedIdentityException { - SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore); + public static CertificateValidator getCertificateValidator() { + try { + ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(UNIDENTIFIED_SENDER_TRUST_ROOT), 0); + return new CertificateValidator(unidentifiedSenderTrustRoot); + } catch (InvalidKeyException | IOException e) { + throw new AssertionError(e); + } + } + + private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws org.whispersystems.libsignal.UntrustedIdentityException, InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, ProtocolUntrustedIdentityException, SelfSendException { + SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore, getCertificateValidator()); try { return cipher.decrypt(envelope); - } catch (org.whispersystems.libsignal.UntrustedIdentityException e) { - signalProtocolStore.saveIdentity(e.getName(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED); + } catch (ProtocolUntrustedIdentityException e) { + // TODO We don't get the new untrusted identity from ProtocolUntrustedIdentityException anymore ... we need to get it from somewhere else +// signalProtocolStore.saveIdentity(e.getSender(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED); throw e; } } @@ -1061,6 +1117,14 @@ class Manager implements Signal { } } } + if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) { + ContactInfo contact = contactStore.getContact(source); + if (contact == null) { + contact = new ContactInfo(); + contact.number = source; + } + contact.profileKey = Base64.encodeBytes(message.getProfileKey().get()); + } } public void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { @@ -1111,7 +1175,7 @@ class Manager implements Signal { public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException { retryFailedReceivedMessages(handler, ignoreAttachments); - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null, timer); try { if (messagePipe == null) { @@ -1181,6 +1245,7 @@ class Manager implements Signal { handleSignalServiceDataMessage(message, false, envelope.getSource(), username, ignoreAttachments); } if (content.getSyncMessage().isPresent()) { + isMultiDevice = true; SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); if (syncMessage.getSent().isPresent()) { SignalServiceDataMessage message = syncMessage.getSent().get().getMessage(); @@ -1206,7 +1271,7 @@ class Manager implements Signal { if (syncMessage.getGroups().isPresent()) { File tmpFile = null; try { - tmpFile = Util.createTempFile(); + tmpFile = IOUtils.createTempFile(); try (InputStream attachmentAsStream = retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer(), tmpFile)) { DeviceGroupsInputStream s = new DeviceGroupsInputStream(attachmentAsStream); DeviceGroup g; @@ -1220,6 +1285,9 @@ class Manager implements Signal { } syncGroup.members.addAll(g.getMembers()); syncGroup.active = g.isActive(); + if (g.getColor().isPresent()) { + syncGroup.color = g.getColor().get(); + } if (g.getAvatar().isPresent()) { retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId); @@ -1245,7 +1313,7 @@ class Manager implements Signal { if (syncMessage.getContacts().isPresent()) { File tmpFile = null; try { - tmpFile = Util.createTempFile(); + tmpFile = IOUtils.createTempFile(); final ContactsMessage contactsMessage = syncMessage.getContacts().get(); try (InputStream attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream().asPointer(), tmpFile)) { DeviceContactsInputStream s = new DeviceContactsInputStream(attachmentAsStream); @@ -1265,6 +1333,21 @@ class Manager implements Signal { if (c.getColor().isPresent()) { contact.color = c.getColor().get(); } + if (c.getProfileKey().isPresent()) { + contact.profileKey = Base64.encodeBytes(c.getProfileKey().get()); + } + if (c.getVerified().isPresent()) { + final VerifiedMessage verifiedMessage = c.getVerified().get(); + signalProtocolStore.saveIdentity(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); + } + if (c.getExpirationTimer().isPresent()) { + ThreadInfo thread = threadStore.getThread(c.getNumber()); + thread.messageExpirationTime = c.getExpirationTimer().get(); + threadStore.updateThread(thread); + } + if (c.isBlocked()) { + // TODO store list of blocked numbers + } contactStore.updateContact(contact); if (c.getAvatar().isPresent()) { @@ -1296,13 +1379,16 @@ class Manager implements Signal { try (FileInputStream f = new FileInputStream(file)) { DataInputStream in = new DataInputStream(f); int version = in.readInt(); - if (version != 1) { + if (version > 2) { return null; } int type = in.readInt(); String source = in.readUTF(); int sourceDevice = in.readInt(); - String relay = in.readUTF(); + if (version == 1) { + // read legacy relay field + in.readUTF(); + } long timestamp = in.readLong(); byte[] content = null; int contentLen = in.readInt(); @@ -1316,18 +1402,26 @@ class Manager implements Signal { legacyMessage = new byte[legacyMessageLen]; in.readFully(legacyMessage); } - return new SignalServiceEnvelope(type, source, sourceDevice, relay, timestamp, legacyMessage, content); + long serverTimestamp = 0; + String uuid = null; + if (version == 2) { + serverTimestamp = in.readLong(); + uuid = in.readUTF(); + if ("".equals(uuid)) { + uuid = null; + } + } + return new SignalServiceEnvelope(type, source, sourceDevice, timestamp, legacyMessage, content, serverTimestamp, uuid); } } private void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException { try (FileOutputStream f = new FileOutputStream(file)) { try (DataOutputStream out = new DataOutputStream(f)) { - out.writeInt(1); // version + out.writeInt(2); // version out.writeInt(envelope.getType()); out.writeUTF(envelope.getSource()); out.writeInt(envelope.getSourceDevice()); - out.writeUTF(envelope.getRelay()); out.writeLong(envelope.getTimestamp()); if (envelope.hasContent()) { out.writeInt(envelope.getContent().length); @@ -1341,6 +1435,9 @@ class Manager implements Signal { } else { out.writeInt(0); } + out.writeLong(envelope.getServerTimestamp()); + String uuid = envelope.getUuid(); + out.writeUTF(uuid == null ? "" : uuid); } } } @@ -1350,7 +1447,7 @@ class Manager implements Signal { } private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException { - createPrivateDirectories(avatarsPath); + IOUtils.createPrivateDirectories(avatarsPath); if (attachment.isPointer()) { SignalServiceAttachmentPointer pointer = attachment.asPointer(); return retrieveAttachment(pointer, getContactAvatarFile(number), false); @@ -1365,7 +1462,7 @@ class Manager implements Signal { } private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException { - createPrivateDirectories(avatarsPath); + IOUtils.createPrivateDirectories(avatarsPath); if (attachment.isPointer()) { SignalServiceAttachmentPointer pointer = attachment.asPointer(); return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false); @@ -1380,7 +1477,7 @@ class Manager implements Signal { } private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException { - createPrivateDirectories(attachmentsPath); + IOUtils.createPrivateDirectories(attachmentsPath); return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true); } @@ -1413,9 +1510,9 @@ class Manager implements Signal { } } - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null, timer); - File tmpFile = Util.createTempFile(); + File tmpFile = IOUtils.createTempFile(); try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE)) { try (OutputStream output = new FileOutputStream(outputFile)) { byte[] buffer = new byte[4096]; @@ -1439,7 +1536,7 @@ class Manager implements Signal { } private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException { - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null, timer); return messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE); } @@ -1459,15 +1556,17 @@ class Manager implements Signal { } private void sendGroups() throws IOException, UntrustedIdentityException { - File groupsFile = Util.createTempFile(); + File groupsFile = IOUtils.createTempFile(); try { try (OutputStream fos = new FileOutputStream(groupsFile)) { DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos); for (GroupInfo record : groupStore.getGroups()) { + ThreadInfo info = threadStore.getThread(Base64.encodeBytes(record.groupId)); out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId), - record.active)); + record.active, Optional.fromNullable(info != null ? info.messageExpirationTime : null), + Optional.fromNullable(record.color), false)); } } @@ -1492,13 +1591,14 @@ class Manager implements Signal { } private void sendContacts() throws IOException, UntrustedIdentityException { - File contactsFile = Util.createTempFile(); + File contactsFile = IOUtils.createTempFile(); try { try (OutputStream fos = new FileOutputStream(contactsFile)) { DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos); for (ContactInfo record : contactStore.getContacts()) { VerifiedMessage verifiedMessage = null; + ThreadInfo info = threadStore.getThread(record.number); if (getIdentities().containsKey(record.number)) { JsonIdentityKeyStore.Identity currentIdentity = null; for (JsonIdentityKeyStore.Identity id : getIdentities().get(record.number)) { @@ -1511,10 +1611,12 @@ class Manager implements Signal { } } - // TODO include profile key + byte[] profileKey = record.profileKey == null ? null : Base64.decode(record.profileKey); + // TODO store list of blocked numbers + boolean blocked = false; out.write(new DeviceContact(record.number, Optional.fromNullable(record.name), createContactAvatarAttachment(record.number), Optional.fromNullable(record.color), - Optional.fromNullable(verifiedMessage), Optional.absent())); + Optional.fromNullable(verifiedMessage), Optional.fromNullable(profileKey), blocked, Optional.fromNullable(info != null ? info.messageExpirationTime : null))); } }