X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/b2289568efb9f9c0fed3fac019ae082a05cd39f5..ae3e5be1241b3ae9d27fff950c3bb54bdcdc32e7:/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 6bd8c657..77647d27 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -31,6 +31,8 @@ import org.whispersystems.libsignal.*; import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.ECKeyPair; import org.whispersystems.libsignal.ecc.ECPublicKey; +import org.whispersystems.libsignal.fingerprint.Fingerprint; +import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.KeyHelper; @@ -91,7 +93,7 @@ class Manager implements Signal { private FileChannel fileChannel; private FileLock lock; - private final ObjectMapper jsonProcessot = new ObjectMapper(); + private final ObjectMapper jsonProcessor = new ObjectMapper(); private String username; private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; private String password; @@ -105,6 +107,7 @@ class Manager implements Signal { private SignalServiceAccountManager accountManager; private JsonGroupStore groupStore; private JsonContactsStore contactStore; + private JsonThreadStore threadStore; public Manager(String username, String settingsPath) { this.username = username; @@ -113,18 +116,22 @@ class Manager implements Signal { this.attachmentsPath = this.settingsPath + "/attachments"; this.avatarsPath = this.settingsPath + "/avatars"; - jsonProcessot.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect - jsonProcessot.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it. - jsonProcessot.enable(SerializationFeature.WRITE_NULL_MAP_VALUES); - jsonProcessot.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - jsonProcessot.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE); - jsonProcessot.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); + jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect + jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it. + jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES); + jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE); + jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); } public String getUsername() { return username; } + private IdentityKey getIdentity() { + return signalProtocolStore.getIdentityKeyPair().getPublicKey(); + } + public int getDeviceId() { return deviceId; } @@ -205,9 +212,25 @@ class Manager implements Signal { } } - public void load() throws IOException, InvalidKeyException { + public void init() throws IOException { + load(); + + migrateLegacyConfigs(); + + accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, deviceId, USER_AGENT); + try { + if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) { + refreshPreKeys(); + save(); + } + } catch (AuthorizationFailedException e) { + System.err.println("Authorization failed, was the number registered elsewhere?"); + } + } + + private void load() throws IOException { openFileChannel(); - JsonNode rootNode = jsonProcessot.readTree(Channels.newInputStream(fileChannel)); + JsonNode rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel)); JsonNode node = rootNode.get("deviceId"); if (node != null) { @@ -228,19 +251,37 @@ class Manager implements Signal { } else { nextSignedPreKeyId = 0; } - signalProtocolStore = jsonProcessot.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class); + signalProtocolStore = jsonProcessor.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class); registered = getNotNullNode(rootNode, "registered").asBoolean(); JsonNode groupStoreNode = rootNode.get("groupStore"); if (groupStoreNode != null) { - groupStore = jsonProcessot.convertValue(groupStoreNode, JsonGroupStore.class); + groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class); } if (groupStore == null) { groupStore = new JsonGroupStore(); } + + JsonNode contactStoreNode = rootNode.get("contactStore"); + if (contactStoreNode != null) { + contactStore = jsonProcessor.convertValue(contactStoreNode, JsonContactsStore.class); + } + if (contactStore == null) { + contactStore = new JsonContactsStore(); + } + JsonNode threadStoreNode = rootNode.get("threadStore"); + if (threadStoreNode != null) { + threadStore = jsonProcessor.convertValue(threadStoreNode, JsonThreadStore.class); + } + if (threadStore == null) { + threadStore = new JsonThreadStore(); + } + } + + private void migrateLegacyConfigs() { // Copy group avatars that were previously stored in the attachments folder // to the new avatar folder - if (groupStore.groupsWithLegacyAvatarId.size() > 0) { - for (GroupInfo g : groupStore.groupsWithLegacyAvatarId) { + if (JsonGroupStore.groupsWithLegacyAvatarId.size() > 0) { + for (GroupInfo g : JsonGroupStore.groupsWithLegacyAvatarId) { File avatarFile = getGroupAvatarFile(g.groupId); File attachmentFile = getAttachmentFile(g.getAvatarId()); if (!avatarFile.exists() && attachmentFile.exists()) { @@ -252,34 +293,16 @@ class Manager implements Signal { } } } - groupStore.groupsWithLegacyAvatarId.clear(); + JsonGroupStore.groupsWithLegacyAvatarId.clear(); save(); } - - JsonNode contactStoreNode = rootNode.get("contactStore"); - if (contactStoreNode != null) { - contactStore = jsonProcessot.convertValue(contactStoreNode, JsonContactsStore.class); - } - if (contactStore == null) { - contactStore = new JsonContactsStore(); - } - - accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, deviceId, USER_AGENT); - try { - if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) { - refreshPreKeys(); - save(); - } - } catch (AuthorizationFailedException e) { - System.err.println("Authorization failed, was the number registered elsewhere?"); - } } private void save() { if (username == null) { return; } - ObjectNode rootNode = jsonProcessot.createObjectNode(); + ObjectNode rootNode = jsonProcessor.createObjectNode(); rootNode.put("username", username) .put("deviceId", deviceId) .put("password", password) @@ -290,11 +313,12 @@ class Manager implements Signal { .putPOJO("axolotlStore", signalProtocolStore) .putPOJO("groupStore", groupStore) .putPOJO("contactStore", contactStore) + .putPOJO("threadStore", threadStore) ; try { openFileChannel(); fileChannel.position(0); - jsonProcessot.writeValue(Channels.newOutputStream(fileChannel), rootNode); + jsonProcessor.writeValue(Channels.newOutputStream(fileChannel), rootNode); fileChannel.truncate(fileChannel.position()); fileChannel.force(false); } catch (Exception e) { @@ -557,14 +581,17 @@ class Manager implements Signal { .build(); messageBuilder.asGroupMessage(group); } - SignalServiceDataMessage message = messageBuilder.build(); + ThreadInfo thread = threadStore.getThread(Base64.encodeBytes(groupId)); + if (thread != null) { + messageBuilder.withExpiration(thread.messageExpirationTime); + } final GroupInfo g = getGroupForSending(groupId); // Don't send group message to ourself final List membersSend = new ArrayList<>(g.members); membersSend.remove(this.username); - sendMessage(message, membersSend); + sendMessage(messageBuilder, membersSend); } public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions { @@ -572,15 +599,14 @@ class Manager implements Signal { .withId(groupId) .build(); - SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() - .asGroupMessage(group) - .build(); + SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() + .asGroupMessage(group); final GroupInfo g = getGroupForSending(groupId); g.members.remove(this.username); groupStore.updateGroup(g); - sendMessage(message, g.members); + sendMessage(messageBuilder, g.members); } private static String join(CharSequence separator, Iterable list) { @@ -637,35 +663,75 @@ class Manager implements Signal { } } + if (avatarFile != null) { + createPrivateDirectories(avatarsPath); + File aFile = getGroupAvatarFile(g.groupId); + Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + + groupStore.updateGroup(g); + + SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g); + + // Don't send group message to ourself + final List membersSend = new ArrayList<>(g.members); + membersSend.remove(this.username); + sendMessage(messageBuilder, membersSend); + return g.groupId; + } + + private void sendUpdateGroupMessage(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions { + if (groupId == null) { + return; + } + GroupInfo g = getGroupForSending(groupId); + + if (!g.members.contains(recipient)) { + return; + } + + SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g); + + // Send group message only to the recipient who requested it + final List membersSend = new ArrayList<>(); + membersSend.add(recipient); + sendMessage(messageBuilder, membersSend); + } + + private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) { SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE) .withId(g.groupId) .withName(g.name) .withMembers(new ArrayList<>(g.members)); File aFile = getGroupAvatarFile(g.groupId); - if (avatarFile != null) { - createPrivateDirectories(avatarsPath); - Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - } if (aFile.exists()) { try { group.withAvatar(createAttachment(aFile)); } catch (IOException e) { - throw new AttachmentInvalidException(avatarFile, e); + throw new AttachmentInvalidException(aFile.toString(), e); } } - groupStore.updateGroup(g); + return SignalServiceDataMessage.newBuilder() + .asGroupMessage(group.build()); + } - SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() - .asGroupMessage(group.build()) - .build(); + private void sendGroupInfoRequest(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions { + if (groupId == null) { + return; + } - // Don't send group message to ourself - final List membersSend = new ArrayList<>(g.members); - membersSend.remove(this.username); - sendMessage(message, membersSend); - return g.groupId; + SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO) + .withId(groupId); + + SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() + .asGroupMessage(group.build()); + + // 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); } @Override @@ -684,25 +750,22 @@ class Manager implements Signal { if (attachments != null) { messageBuilder.withAttachments(getSignalServiceAttachments(attachments)); } - SignalServiceDataMessage message = messageBuilder.build(); - - sendMessage(message, recipients); + sendMessage(messageBuilder, recipients); } @Override public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions { - SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() - .asEndSessionMessage() - .build(); + SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() + .asEndSessionMessage(); - sendMessage(message, recipients); + sendMessage(messageBuilder, recipients); } 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)); try { - sendMessage(message); + sendSyncMessage(message); } catch (UntrustedIdentityException e) { e.printStackTrace(); } @@ -712,13 +775,13 @@ class Manager implements Signal { SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build(); SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); try { - sendMessage(message); + sendSyncMessage(message); } catch (UntrustedIdentityException e) { e.printStackTrace(); } } - private void sendMessage(SignalServiceSyncMessage message) + private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); @@ -730,24 +793,17 @@ class Manager implements Signal { } } - private void sendMessage(SignalServiceDataMessage message, Collection recipients) + private void sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) throws EncapsulatedExceptions, IOException { - Set recipientsTS = new HashSet<>(recipients.size()); - for (String recipient : recipients) { - try { - recipientsTS.add(getPushAddress(recipient)); - } catch (InvalidNumberException e) { - System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); - System.err.println("Aborting sending."); - save(); - return; - } - } + Set recipientsTS = getSignalServiceAddresses(recipients); + if (recipientsTS == null) return; + SignalServiceDataMessage message = null; try { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); + message = messageBuilder.build(); if (message.getGroupInfo().isPresent()) { try { messageSender.sendMessage(new ArrayList<>(recipientsTS), message); @@ -762,6 +818,13 @@ class Manager implements Signal { List unregisteredUsers = new LinkedList<>(); List networkExceptions = new LinkedList<>(); for (SignalServiceAddress address : recipientsTS) { + ThreadInfo thread = threadStore.getThread(address.getNumber()); + if (thread != null) { + messageBuilder.withExpiration(thread.messageExpirationTime); + } else { + messageBuilder.withExpiration(0); + } + message = messageBuilder.build(); try { messageSender.sendMessage(address, message); } catch (UntrustedIdentityException e) { @@ -778,7 +841,7 @@ class Manager implements Signal { } } } finally { - if (message.isEndSession()) { + if (message != null && message.isEndSession()) { for (SignalServiceAddress recipient : recipientsTS) { handleEndSession(recipient.getNumber()); } @@ -787,6 +850,21 @@ class Manager implements Signal { } } + private Set getSignalServiceAddresses(Collection recipients) { + Set recipientsTS = new HashSet<>(recipients.size()); + for (String recipient : recipients) { + try { + recipientsTS.add(getPushAddress(recipient)); + } catch (InvalidNumberException e) { + System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); + System.err.println("Aborting sending."); + save(); + return null; + } + } + 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); try { @@ -806,12 +884,13 @@ class Manager implements Signal { } private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) { + String threadId; if (message.getGroupInfo().isPresent()) { SignalServiceGroup groupInfo = message.getGroupInfo().get(); + threadId = Base64.encodeBytes(groupInfo.getGroupId()); + GroupInfo group = groupStore.getGroup(groupInfo.getGroupId()); switch (groupInfo.getType()) { case UPDATE: - GroupInfo group; - group = groupStore.getGroup(groupInfo.getGroupId()); if (group == null) { group = new GroupInfo(groupInfo.getGroupId()); } @@ -838,19 +917,59 @@ class Manager implements Signal { groupStore.updateGroup(group); break; case DELIVER: + if (group == null) { + try { + sendGroupInfoRequest(groupInfo.getGroupId(), source); + } catch (IOException | EncapsulatedExceptions e) { + e.printStackTrace(); + } + } break; case QUIT: - group = groupStore.getGroup(groupInfo.getGroupId()); - if (group != null) { + if (group == null) { + try { + sendGroupInfoRequest(groupInfo.getGroupId(), source); + } catch (IOException | EncapsulatedExceptions e) { + e.printStackTrace(); + } + } else { group.members.remove(source); groupStore.updateGroup(group); } break; + case REQUEST_INFO: + if (group != null) { + try { + sendUpdateGroupMessage(groupInfo.getGroupId(), source); + } catch (IOException | EncapsulatedExceptions e) { + e.printStackTrace(); + } catch (NotAGroupMemberException e) { + // We have left this group, so don't send a group update message + } + } + break; + } + } else { + if (isSync) { + threadId = destination; + } else { + threadId = source; } } if (message.isEndSession()) { handleEndSession(isSync ? destination : source); } + if (message.isExpirationUpdate() || message.getBody().isPresent()) { + ThreadInfo thread = threadStore.getThread(threadId); + if (thread == null) { + thread = new ThreadInfo(); + thread.id = threadId; + } + if (thread.messageExpirationTime != message.getExpiresInSeconds()) { + thread.messageExpirationTime = message.getExpiresInSeconds(); + threadStore.updateThread(thread); + } + } if (message.getAttachments().isPresent()) { for (SignalServiceAttachment attachment : message.getAttachments().get()) { if (attachment.isPointer()) { @@ -1017,17 +1136,26 @@ class Manager implements Signal { } catch (Exception e) { e.printStackTrace(); } + if (syncMessage.getBlockedList().isPresent()) { + // TODO store list of blocked numbers + } } if (syncMessage.getContacts().isPresent()) { try { DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer())); DeviceContact c; while ((c = s.read()) != null) { - ContactInfo contact = new ContactInfo(); - contact.number = c.getNumber(); + 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()) { @@ -1196,6 +1324,7 @@ class Manager implements Signal { if (output != null) { output.close(); } + input.close(); if (!tmpFile.delete()) { System.err.println("Failed to delete temp file: " + tmpFile); } @@ -1243,13 +1372,17 @@ class Manager implements Signal { if (groupsFile.exists() && groupsFile.length() > 0) { FileInputStream contactsFileStream = new FileInputStream(groupsFile); - SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder() - .withStream(contactsFileStream) - .withContentType("application/octet-stream") - .withLength(groupsFile.length()) - .build(); - - sendMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); + try { + SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder() + .withStream(contactsFileStream) + .withContentType("application/octet-stream") + .withLength(groupsFile.length()) + .build(); + + sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); + } finally { + contactsFileStream.close(); + } } } finally { groupsFile.delete(); @@ -1264,7 +1397,7 @@ class Manager implements Signal { try { for (ContactInfo record : contactStore.getContacts()) { out.write(new DeviceContact(record.number, Optional.fromNullable(record.name), - createContactAvatarAttachment(record.number))); + createContactAvatarAttachment(record.number), Optional.fromNullable(record.color))); } } finally { out.close(); @@ -1278,7 +1411,7 @@ class Manager implements Signal { .withLength(contactsFile.length()) .build(); - sendMessage(SignalServiceSyncMessage.forContacts(attachmentStream)); + sendSyncMessage(SignalServiceSyncMessage.forContacts(attachmentStream)); } } finally { contactsFile.delete(); @@ -1324,6 +1457,29 @@ class Manager implements Signal { return false; } + /** + * Trust this the identity with this safety number + * + * @param name username of the identity + * @param safetyNumber Safety number + */ + public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) { + List ids = signalProtocolStore.getIdentities(name); + if (ids == null) { + return false; + } + for (JsonIdentityKeyStore.Identity id : ids) { + if (!safetyNumber.equals(computeSafetyNumber(name, id.identityKey))) { + continue; + } + + signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_VERIFIED); + save(); + return true; + } + return false; + } + /** * Trust all keys of this identity without verification * @@ -1342,4 +1498,9 @@ class Manager implements Signal { save(); return true; } + + public String computeSafetyNumber(String theirUsername, IdentityKey theirIdentityKey) { + Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(username, getIdentity(), theirUsername, theirIdentityKey); + return fingerprint.getDisplayableFingerprint().getDisplayText(); + } }