X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/3e2024ff0a581c730c2ddd4b80e50cb12d721730..bfb51e414b42c69538498e1aa5cbb7421138d5e2:/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 29e75ee9..3ce9dfbe 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,6 +56,9 @@ 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; @@ -79,6 +82,9 @@ class Manager implements Signal { private final String attachmentsPath; private final String avatarsPath; + private FileChannel fileChannel; + private FileLock lock; + private final ObjectMapper jsonProcessot = new ObjectMapper(); private String username; private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; @@ -89,7 +95,7 @@ class Manager implements Signal { private boolean registered = false; - private SignalProtocolStore signalProtocolStore; + private JsonSignalProtocolStore signalProtocolStore; private SignalServiceAccountManager accountManager; private JsonGroupStore groupStore; private JsonContactsStore contactStore; @@ -105,6 +111,8 @@ class Manager implements Signal { 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() { @@ -141,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) { @@ -227,7 +249,11 @@ class Manager implements Signal { .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())); } @@ -246,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(); @@ -437,6 +463,9 @@ class Manager implements Signal { InputStream attachmentStream = new FileInputStream(attachmentFile); final long attachmentSize = attachmentFile.length(); String mime = Files.probeContentType(attachmentFile.toPath()); + if (mime == null) { + mime = "application/octet-stream"; + } return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, null); } @@ -461,7 +490,7 @@ class Manager implements Signal { @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)); @@ -478,12 +507,14 @@ class Manager implements Signal { if (g == null) { throw new GroupNotFoundException(groupId); } - Set members = g.members; - members.remove(this.username); - sendMessage(message, members); + + // 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(); @@ -502,7 +533,7 @@ class Manager implements Signal { 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 @@ -555,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; @@ -563,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); @@ -572,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)); @@ -583,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(); @@ -596,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(); } @@ -608,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; } } @@ -750,7 +805,14 @@ class Manager implements Signal { 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(); @@ -767,8 +829,6 @@ class Manager implements Signal { if (rm.isContactsRequest()) { try { sendContacts(); - } catch (EncapsulatedExceptions encapsulatedExceptions) { - encapsulatedExceptions.printStackTrace(); } catch (UntrustedIdentityException e) { e.printStackTrace(); } @@ -776,8 +836,6 @@ class Manager implements Signal { if (rm.isGroupsRequest()) { try { sendGroups(); - } catch (EncapsulatedExceptions encapsulatedExceptions) { - encapsulatedExceptions.printStackTrace(); } catch (UntrustedIdentityException e) { e.printStackTrace(); } @@ -976,7 +1034,7 @@ class Manager implements Signal { return false; } - private void sendGroups() throws IOException, EncapsulatedExceptions, UntrustedIdentityException { + private void sendGroups() throws IOException, UntrustedIdentityException { File groupsFile = File.createTempFile("multidevice-group-update", ".tmp"); try { @@ -1006,7 +1064,7 @@ class Manager implements Signal { } } - private void sendContacts() throws IOException, EncapsulatedExceptions, UntrustedIdentityException { + private void sendContacts() throws IOException, UntrustedIdentityException { File contactsFile = File.createTempFile("multidevice-contact-update", ".tmp"); try {