X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/5ee375c74d13fa18ed353221c403772511abcbb1..a4e22539a3d87262f43d399fbd79823c4dc2fde0:/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 315eac13..42f5ba85 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; @@ -113,18 +115,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 +211,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 +250,30 @@ 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(); + } + } + + 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 +285,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) @@ -294,7 +309,7 @@ class Manager implements Signal { 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) { @@ -864,7 +879,49 @@ class Manager implements Signal { } } + public void retryFailedReceivedMessages(ReceiveMessageHandler handler) { + final File cachePath = new File(getMessageCachePath()); + if (!cachePath.exists()) { + return; + } + for (final File dir : cachePath.listFiles()) { + if (!dir.isDirectory()) { + continue; + } + + String sender = dir.getName(); + for (final File fileEntry : dir.listFiles()) { + if (!fileEntry.isFile()) { + continue; + } + SignalServiceEnvelope envelope; + try { + envelope = loadEnvelope(fileEntry); + if (envelope == null) { + continue; + } + } catch (IOException e) { + e.printStackTrace(); + continue; + } + SignalServiceContent content = null; + if (!envelope.isReceipt()) { + try { + content = decryptMessage(envelope); + } catch (Exception e) { + continue; + } + handleMessage(envelope, content); + } + save(); + handler.handleMessage(envelope, content, null); + fileEntry.delete(); + } + } + } + public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException { + retryFailedReceivedMessages(handler); final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); SignalServiceMessagePipe messagePipe = null; @@ -975,17 +1032,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()) { @@ -1000,6 +1066,34 @@ class Manager implements Signal { } } + private SignalServiceEnvelope loadEnvelope(File file) throws IOException { + try (FileInputStream f = new FileInputStream(file)) { + DataInputStream in = new DataInputStream(f); + int version = in.readInt(); + if (version != 1) { + return null; + } + int type = in.readInt(); + String source = in.readUTF(); + int sourceDevice = in.readInt(); + String relay = in.readUTF(); + long timestamp = in.readLong(); + byte[] content = null; + int contentLen = in.readInt(); + if (contentLen > 0) { + content = new byte[contentLen]; + in.readFully(content); + } + byte[] legacyMessage = null; + int legacyMessageLen = in.readInt(); + if (legacyMessageLen > 0) { + legacyMessage = new byte[legacyMessageLen]; + in.readFully(legacyMessage); + } + return new SignalServiceEnvelope(type, source, sourceDevice, relay, timestamp, legacyMessage, content); + } + } + private void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException { try (FileOutputStream f = new FileOutputStream(file)) { DataOutputStream out = new DataOutputStream(f); @@ -1194,7 +1288,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(); @@ -1254,6 +1348,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 * @@ -1272,4 +1389,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(); + } }