X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/08a217108a9b406dfd68907766c65da42420660e..f095d947f892830e1ced4af450df77a8f7ae6d5d:/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 47699c3f..3e36e319 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -18,6 +18,8 @@ package org.asamk.signal; 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; @@ -30,7 +32,6 @@ import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.ECKeyPair; import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.KeyHelper; import org.whispersystems.libsignal.util.Medium; @@ -45,8 +46,7 @@ import org.whispersystems.signalservice.api.messages.*; import org.whispersystems.signalservice.api.messages.multidevice.*; 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.*; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; @@ -56,8 +56,12 @@ 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.Paths; +import java.nio.file.StandardCopyOption; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -76,6 +80,10 @@ class Manager implements Signal { private final String settingsPath; private final String dataPath; private final String attachmentsPath; + private final String avatarsPath; + + private FileChannel fileChannel; + private FileLock lock; private final ObjectMapper jsonProcessot = new ObjectMapper(); private String username; @@ -87,20 +95,24 @@ class Manager implements Signal { private boolean registered = false; - private SignalProtocolStore signalProtocolStore; + private JsonSignalProtocolStore signalProtocolStore; private SignalServiceAccountManager accountManager; private JsonGroupStore groupStore; + private JsonContactsStore contactStore; public Manager(String username, String settingsPath) { this.username = username; this.settingsPath = settingsPath; this.dataPath = this.settingsPath + "/data"; 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); } public String getUsername() { @@ -137,8 +149,22 @@ class Manager implements Signal { return node; } + private void openFileChannel() throws IOException { + if (fileChannel != null) + return; + + fileChannel = new RandomAccessFile(new File(getFileName()), "rw").getChannel(); + lock = fileChannel.tryLock(); + if (lock == null) { + System.err.println("Config file is in use by another instance, waiting…"); + lock = fileChannel.lock(); + System.err.println("Config file lock acquired."); + } + } + public void load() throws IOException, InvalidKeyException { - JsonNode rootNode = jsonProcessot.readTree(new File(getFileName())); + openFileChannel(); + JsonNode rootNode = jsonProcessot.readTree(Channels.newInputStream(fileChannel)); JsonNode node = rootNode.get("deviceId"); if (node != null) { @@ -168,6 +194,33 @@ class Manager implements Signal { if (groupStore == null) { groupStore = new JsonGroupStore(); } + // 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) { + File avatarFile = getGroupAvatarFile(g.groupId); + File attachmentFile = getAttachmentFile(g.getAvatarId()); + if (!avatarFile.exists() && attachmentFile.exists()) { + try { + new File(avatarsPath).mkdirs(); + Files.copy(attachmentFile.toPath(), avatarFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (Exception e) { + // Ignore + } + } + } + groupStore.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) { @@ -180,6 +233,9 @@ class Manager implements Signal { } private void save() { + if (username == null) { + return; + } ObjectNode rootNode = jsonProcessot.createObjectNode(); rootNode.put("username", username) .put("deviceId", deviceId) @@ -190,9 +246,14 @@ class Manager implements Signal { .put("registered", registered) .putPOJO("axolotlStore", signalProtocolStore) .putPOJO("groupStore", groupStore) + .putPOJO("contactStore", contactStore) ; try { - jsonProcessot.writeValue(new File(getFileName()), rootNode); + openFileChannel(); + fileChannel.position(0); + jsonProcessot.writeValue(Channels.newOutputStream(fileChannel), rootNode); + fileChannel.truncate(fileChannel.position()); + fileChannel.force(false); } catch (Exception e) { System.err.println(String.format("Error saving file: %s", e.getMessage())); } @@ -211,12 +272,12 @@ class Manager implements Signal { return registered; } - public void register(boolean voiceVerication) throws IOException { + public void register(boolean voiceVerification) throws IOException { password = Util.getSecret(18); accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); - if (voiceVerication) + if (voiceVerification) accountManager.requestVoiceVerificationCode(); else accountManager.requestSmsVerificationCode(); @@ -290,7 +351,7 @@ class Manager implements Signal { } public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { - Map query = getQueryMap(linkUri.getQuery()); + Map query = getQueryMap(linkUri.getRawQuery()); String deviceIdentifier = query.get("uuid"); String publicKeyEncoded = query.get("pub_key"); @@ -300,10 +361,10 @@ class Manager implements Signal { ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0); - addDeviceLink(deviceIdentifier, deviceKey); + addDevice(deviceIdentifier, deviceKey); } - private void addDeviceLink(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { + private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { IdentityKeyPair identityKeyPair = signalProtocolStore.getIdentityKeyPair(); String verificationCode = accountManager.getNewDeviceVerificationCode(); @@ -389,7 +450,7 @@ class Manager implements Signal { SignalServiceAttachments = new ArrayList<>(attachments.size()); for (String attachment : attachments) { try { - SignalServiceAttachments.add(createAttachment(attachment)); + SignalServiceAttachments.add(createAttachment(new File(attachment))); } catch (IOException e) { throw new AttachmentInvalidException(attachment, e); } @@ -398,18 +459,38 @@ class Manager implements Signal { return SignalServiceAttachments; } - private static SignalServiceAttachment createAttachment(String attachment) throws IOException { - File attachmentFile = new File(attachment); + private static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException { InputStream attachmentStream = new FileInputStream(attachmentFile); final long attachmentSize = attachmentFile.length(); - String mime = Files.probeContentType(Paths.get(attachment)); + String mime = Files.probeContentType(attachmentFile.toPath()); + if (mime == null) { + mime = "application/octet-stream"; + } return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, null); } + private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { + File file = getGroupAvatarFile(groupId); + if (!file.exists()) { + return Optional.absent(); + } + + return Optional.of(createAttachment(file)); + } + + private Optional createContactAvatarAttachment(String number) throws IOException { + File file = getContactAvatarFile(number); + if (!file.exists()) { + return Optional.absent(); + } + + return Optional.of(createAttachment(file)); + } + @Override public void sendGroupMessage(String messageText, List attachments, byte[] groupId) - throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException { + throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(getSignalServiceAttachments(attachments)); @@ -422,12 +503,18 @@ class Manager implements Signal { } SignalServiceDataMessage message = messageBuilder.build(); - Set members = groupStore.getGroup(groupId).members; - members.remove(this.username); - sendMessage(message, members); + GroupInfo g = groupStore.getGroup(groupId); + if (g == null) { + throw new GroupNotFoundException(groupId); + } + + // Don't send group message to ourself + final List membersSend = new ArrayList<>(g.members); + membersSend.remove(this.username); + sendMessage(message, membersSend); } - public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, UntrustedIdentityException { + public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions { SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) .withId(groupId) .build(); @@ -437,13 +524,16 @@ class Manager implements Signal { .build(); final GroupInfo g = groupStore.getGroup(groupId); + if (g == null) { + throw new GroupNotFoundException(groupId); + } g.members.remove(this.username); groupStore.updateGroup(g); sendMessage(message, g.members); } - public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException { + public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { GroupInfo g; if (groupId == null) { // Create new group @@ -451,6 +541,9 @@ class Manager implements Signal { g.members.add(username); } else { g = groupStore.getGroup(groupId); + if (g == null) { + throw new GroupNotFoundException(groupId); + } } if (name != null) { @@ -474,11 +567,14 @@ class Manager implements Signal { .withName(g.name) .withMembers(new ArrayList<>(g.members)); + File aFile = getGroupAvatarFile(g.groupId); if (avatarFile != null) { + new File(avatarsPath).mkdirs(); + Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + if (aFile.exists()) { try { - group.withAvatar(createAttachment(avatarFile)); - // TODO - g.avatarId = 0; + group.withAvatar(createAttachment(aFile)); } catch (IOException e) { throw new AttachmentInvalidException(avatarFile, e); } @@ -490,7 +586,8 @@ class Manager implements Signal { .asGroupMessage(group.build()) .build(); - final Set membersSend = g.members; + // Don't send group message to ourself + final List membersSend = new ArrayList<>(g.members); membersSend.remove(this.username); sendMessage(message, membersSend); return g.groupId; @@ -498,7 +595,7 @@ class Manager implements Signal { @Override public void sendMessage(String message, List attachments, String recipient) - throws EncapsulatedExceptions, AttachmentInvalidException, IOException, UntrustedIdentityException { + throws EncapsulatedExceptions, AttachmentInvalidException, IOException { List recipients = new ArrayList<>(1); recipients.add(recipient); sendMessage(message, attachments, recipients); @@ -507,7 +604,7 @@ class Manager implements Signal { @Override public void sendMessage(String messageText, List attachments, List recipients) - throws IOException, EncapsulatedExceptions, AttachmentInvalidException, UntrustedIdentityException { + throws IOException, EncapsulatedExceptions, AttachmentInvalidException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(getSignalServiceAttachments(attachments)); @@ -518,7 +615,7 @@ class Manager implements Signal { } @Override - public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions, UntrustedIdentityException { + public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions { SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() .asEndSessionMessage() .build(); @@ -531,8 +628,6 @@ class Manager implements Signal { SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); try { sendMessage(message); - } catch (EncapsulatedExceptions encapsulatedExceptions) { - encapsulatedExceptions.printStackTrace(); } catch (UntrustedIdentityException e) { e.printStackTrace(); } @@ -543,65 +638,90 @@ class Manager implements Signal { SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); try { sendMessage(message); - } catch (EncapsulatedExceptions encapsulatedExceptions) { - encapsulatedExceptions.printStackTrace(); } catch (UntrustedIdentityException e) { e.printStackTrace(); } } private void sendMessage(SignalServiceSyncMessage message) - throws IOException, EncapsulatedExceptions, UntrustedIdentityException { + throws IOException, UntrustedIdentityException { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); - messageSender.sendMessage(message); + try { + messageSender.sendMessage(message); + } catch (UntrustedIdentityException e) { + signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + throw e; + } } private void sendMessage(SignalServiceDataMessage message, Collection recipients) - throws IOException, EncapsulatedExceptions, UntrustedIdentityException { + 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; + } + } + try { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); - Set recipientsTS = new HashSet<>(recipients.size()); - for (String recipient : recipients) { + if (message.getGroupInfo().isPresent()) { 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; + messageSender.sendMessage(new ArrayList<>(recipientsTS), message); + } catch (EncapsulatedExceptions encapsulatedExceptions) { + for (UntrustedIdentityException e : encapsulatedExceptions.getUntrustedIdentityExceptions()) { + signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + } } - } - - if (message.getGroupInfo().isPresent()) { - messageSender.sendMessage(new ArrayList<>(recipientsTS), message); } else { // Send to all individually, so sync messages are sent correctly + List untrustedIdentities = new LinkedList<>(); + List unregisteredUsers = new LinkedList<>(); + List networkExceptions = new LinkedList<>(); for (SignalServiceAddress address : recipientsTS) { - messageSender.sendMessage(address, message); + try { + messageSender.sendMessage(address, message); + } 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)); + } + } + if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) { + throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions); } } - + } finally { if (message.isEndSession()) { for (SignalServiceAddress recipient : recipientsTS) { handleEndSession(recipient.getNumber()); } } - } finally { save(); } } - private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) { + 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 { return cipher.decrypt(envelope); + } catch (org.whispersystems.libsignal.UntrustedIdentityException e) { + // TODO temporarily store message, until user has accepted the key + signalProtocolStore.saveIdentity(e.getName(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED); + throw e; } catch (Exception e) { - // TODO handle all exceptions - e.printStackTrace(); - return null; + throw e; } } @@ -610,31 +730,27 @@ class Manager implements Signal { } public interface ReceiveMessageHandler { - void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, GroupInfo group); + void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent); } - private GroupInfo handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) { - GroupInfo group = null; + private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) { if (message.getGroupInfo().isPresent()) { SignalServiceGroup groupInfo = message.getGroupInfo().get(); switch (groupInfo.getType()) { case UPDATE: - try { - group = groupStore.getGroup(groupInfo.getGroupId()); - } catch (GroupNotFoundException e) { + GroupInfo group; + group = groupStore.getGroup(groupInfo.getGroupId()); + if (group == null) { group = new GroupInfo(groupInfo.getGroupId()); } if (groupInfo.getAvatar().isPresent()) { SignalServiceAttachment avatar = groupInfo.getAvatar().get(); if (avatar.isPointer()) { - long avatarId = avatar.asPointer().getId(); try { - retrieveAttachment(avatar.asPointer()); - // TODO store group avatar in /avatar/groups folder - group.avatarId = avatarId; + retrieveGroupAvatarAttachment(avatar.asPointer(), group.groupId); } catch (IOException | InvalidMessageException e) { - System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage()); + System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getId() + "): " + e.getMessage()); } } } @@ -650,17 +766,12 @@ class Manager implements Signal { groupStore.updateGroup(group); break; case DELIVER: - try { - group = groupStore.getGroup(groupInfo.getGroupId()); - } catch (GroupNotFoundException e) { - } break; case QUIT: - try { - group = groupStore.getGroup(groupInfo.getGroupId()); + group = groupStore.getGroup(groupInfo.getGroupId()); + if (group != null) { group.members.remove(source); groupStore.updateGroup(group); - } catch (GroupNotFoundException e) { } break; } @@ -679,7 +790,6 @@ class Manager implements Signal { } } } - return group; } public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException { @@ -692,32 +802,40 @@ class Manager implements Signal { while (true) { SignalServiceEnvelope envelope; SignalServiceContent content = null; - GroupInfo group = null; try { envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS); if (!envelope.isReceipt()) { - content = decryptMessage(envelope); + Exception exception; + try { + content = decryptMessage(envelope); + } catch (Exception e) { + exception = e; + // TODO pass exception to handler instead + e.printStackTrace(); + } if (content != null) { if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); - group = handleSignalServiceDataMessage(message, false, envelope.getSource(), username); + handleSignalServiceDataMessage(message, false, envelope.getSource(), username); } if (content.getSyncMessage().isPresent()) { SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); if (syncMessage.getSent().isPresent()) { SignalServiceDataMessage message = syncMessage.getSent().get().getMessage(); - group = handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get()); + handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get()); } if (syncMessage.getRequest().isPresent()) { RequestMessage rm = syncMessage.getRequest().get(); if (rm.isContactsRequest()) { - // TODO implement when we have contacts + try { + sendContacts(); + } catch (UntrustedIdentityException e) { + e.printStackTrace(); + } } if (rm.isGroupsRequest()) { try { sendGroups(); - } catch (EncapsulatedExceptions encapsulatedExceptions) { - encapsulatedExceptions.printStackTrace(); } catch (UntrustedIdentityException e) { e.printStackTrace(); } @@ -728,10 +846,8 @@ class Manager implements Signal { DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer())); DeviceGroup g; while ((g = s.read()) != null) { - GroupInfo syncGroup; - try { - syncGroup = groupStore.getGroup(g.getId()); - } catch (GroupNotFoundException e) { + GroupInfo syncGroup = groupStore.getGroup(g.getId()); + if (syncGroup == null) { syncGroup = new GroupInfo(g.getId()); } if (g.getName().isPresent()) { @@ -741,9 +857,7 @@ class Manager implements Signal { syncGroup.active = g.isActive(); if (g.getAvatar().isPresent()) { - byte[] ava = new byte[(int) g.getAvatar().get().getLength()]; - org.whispersystems.signalservice.internal.util.Util.readFully(g.getAvatar().get().getInputStream(), ava); - // TODO store group avatar in /avatar/groups folder + retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId); } groupStore.updateGroup(syncGroup); } @@ -756,16 +870,15 @@ class Manager implements Signal { DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer())); DeviceContact c; while ((c = s.read()) != null) { - // TODO implement when we have contact storage + ContactInfo contact = new ContactInfo(); + contact.number = c.getNumber(); if (c.getName().isPresent()) { - c.getName().get(); + contact.name = c.getName().get(); } - c.getNumber(); + contactStore.updateContact(contact); if (c.getAvatar().isPresent()) { - byte[] ava = new byte[(int) c.getAvatar().get().getLength()]; - org.whispersystems.signalservice.internal.util.Util.readFully(c.getAvatar().get().getInputStream(), ava); - // TODO store contact avatar in /avatar/contacts folder + retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number); } } } catch (Exception e) { @@ -776,7 +889,7 @@ class Manager implements Signal { } } save(); - handler.handleMessage(envelope, content, group); + handler.handleMessage(envelope, content); } catch (TimeoutException e) { if (returnOnTimeout) return; @@ -790,18 +903,48 @@ class Manager implements Signal { } } + public File getContactAvatarFile(String number) { + return new File(avatarsPath, "contact-" + number); + } + + private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException { + new File(avatarsPath).mkdirs(); + if (attachment.isPointer()) { + SignalServiceAttachmentPointer pointer = attachment.asPointer(); + return retrieveAttachment(pointer, getContactAvatarFile(number), false); + } else { + SignalServiceAttachmentStream stream = attachment.asStream(); + return retrieveAttachment(stream, getContactAvatarFile(number)); + } + } + + public File getGroupAvatarFile(byte[] groupId) { + return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_")); + } + + private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException { + new File(avatarsPath).mkdirs(); + if (attachment.isPointer()) { + SignalServiceAttachmentPointer pointer = attachment.asPointer(); + return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false); + } else { + SignalServiceAttachmentStream stream = attachment.asStream(); + return retrieveAttachment(stream, getGroupAvatarFile(groupId)); + } + } + public File getAttachmentFile(long attachmentId) { return new File(attachmentsPath, attachmentId + ""); } private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException { - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); + new File(attachmentsPath).mkdirs(); + return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true); + } - File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp"); - InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile); + private File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException, InvalidMessageException { + InputStream input = stream.getInputStream(); - new File(attachmentsPath).mkdirs(); - File outputFile = getAttachmentFile(pointer.getId()); OutputStream output = null; try { output = new FileOutputStream(outputFile); @@ -817,14 +960,15 @@ class Manager implements Signal { } finally { if (output != null) { output.close(); - output = null; - } - if (!tmpFile.delete()) { - System.err.println("Failed to delete temp file: " + tmpFile); } } - if (pointer.getPreview().isPresent()) { + return outputFile; + } + + private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException { + if (storePreview && pointer.getPreview().isPresent()) { File previewFile = new File(outputFile + ".preview"); + OutputStream output = null; try { output = new FileOutputStream(previewFile); byte[] preview = pointer.getPreview().get(); @@ -838,6 +982,32 @@ class Manager implements Signal { } } } + + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); + + File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp"); + InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile); + + OutputStream output = null; + try { + output = new FileOutputStream(outputFile); + byte[] buffer = new byte[4096]; + int read; + + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } finally { + if (output != null) { + output.close(); + } + if (!tmpFile.delete()) { + System.err.println("Failed to delete temp file: " + tmpFile); + } + } return outputFile; } @@ -864,21 +1034,50 @@ class Manager implements Signal { return false; } - private void sendGroups() throws IOException, EncapsulatedExceptions, UntrustedIdentityException { - File contactsFile = File.createTempFile("multidevice-contact-update", ".tmp"); + private void sendGroups() throws IOException, UntrustedIdentityException { + File groupsFile = File.createTempFile("multidevice-group-update", ".tmp"); try { - DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(new FileOutputStream(contactsFile)); + DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(new FileOutputStream(groupsFile)); try { for (GroupInfo record : groupStore.getGroups()) { out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), - new ArrayList<>(record.members), Optional.of(new SignalServiceAttachmentStream(new FileInputStream("/home/sebastian/Bilder/00026_150512_14-00-18.JPG"), "octet", new File("/home/sebastian/Bilder/00026_150512_14-00-18.JPG").length(), null)), + new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId), record.active)); } } finally { out.close(); } + 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)); + } + } finally { + groupsFile.delete(); + } + } + + private void sendContacts() throws IOException, UntrustedIdentityException { + File contactsFile = File.createTempFile("multidevice-contact-update", ".tmp"); + + try { + DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactsFile)); + try { + for (ContactInfo record : contactStore.getContacts()) { + out.write(new DeviceContact(record.number, Optional.fromNullable(record.name), + createContactAvatarAttachment(record.number))); + } + } finally { + out.close(); + } + if (contactsFile.exists() && contactsFile.length() > 0) { FileInputStream contactsFileStream = new FileInputStream(contactsFile); SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder() @@ -887,10 +1086,68 @@ class Manager implements Signal { .withLength(contactsFile.length()) .build(); - sendMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); + sendMessage(SignalServiceSyncMessage.forContacts(attachmentStream)); } } finally { - if (contactsFile != null) contactsFile.delete(); + contactsFile.delete(); + } + } + + public ContactInfo getContact(String number) { + return contactStore.getContact(number); + } + + public GroupInfo getGroup(byte[] groupId) { + return groupStore.getGroup(groupId); + } + + public Map> getIdentities() { + return signalProtocolStore.getIdentities(); + } + + public List getIdentities(String number) { + return signalProtocolStore.getIdentities(number); + } + + /** + * Trust this the identity with this fingerprint + * + * @param name username of the identity + * @param fingerprint Fingerprint + */ + public boolean trustIdentityVerified(String name, byte[] fingerprint) { + List ids = signalProtocolStore.getIdentities(name); + if (ids == null) { + return false; + } + for (JsonIdentityKeyStore.Identity id : ids) { + if (!Arrays.equals(id.identityKey.serialize(), fingerprint)) { + continue; + } + + signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_VERIFIED); + save(); + return true; + } + return false; + } + + /** + * Trust all keys of this identity without verification + * + * @param name username of the identity + */ + public boolean trustIdentityAllKeys(String name) { + List ids = signalProtocolStore.getIdentities(name); + if (ids == null) { + return false; } + for (JsonIdentityKeyStore.Identity id : ids) { + if (id.trustLevel == TrustLevel.UNTRUSTED) { + signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_UNVERIFIED); + } + } + save(); + return true; } }