X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/3e2024ff0a581c730c2ddd4b80e50cb12d721730..ee5062a2cc83078d1d1d33cba32bbaa89e96f52e:/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..68f66455 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,13 +56,21 @@ 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 TrustStore TRUST_STORE = new WhisperTrustStore(); @@ -79,6 +87,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 +100,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 +116,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() { @@ -116,10 +129,29 @@ class Manager implements Signal { } public String getFileName() { - new File(dataPath).mkdirs(); return dataPath + "/" + username; } + 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; @@ -141,8 +173,26 @@ class Manager implements Signal { return node; } + private void openFileChannel() throws IOException { + if (fileChannel != null) + return; + + createPrivateDirectories(dataPath); + if (!new File(getFileName()).exists()) { + createPrivateFile(getFileName()); + } + 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) { @@ -180,7 +230,7 @@ class Manager implements Signal { File attachmentFile = getAttachmentFile(g.getAvatarId()); if (!avatarFile.exists() && attachmentFile.exists()) { try { - new File(avatarsPath).mkdirs(); + createPrivateDirectories(avatarsPath); Files.copy(attachmentFile.toPath(), avatarFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } catch (Exception e) { // Ignore @@ -227,7 +277,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 +300,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 +491,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 +518,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 +535,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 +561,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 @@ -538,7 +597,7 @@ class Manager implements Signal { File aFile = getGroupAvatarFile(g.groupId); if (avatarFile != null) { - new File(avatarsPath).mkdirs(); + createPrivateDirectories(avatarsPath); Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } if (aFile.exists()) { @@ -555,7 +614,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 +623,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 +632,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 +643,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 +656,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 +666,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 +833,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 +857,6 @@ class Manager implements Signal { if (rm.isContactsRequest()) { try { sendContacts(); - } catch (EncapsulatedExceptions encapsulatedExceptions) { - encapsulatedExceptions.printStackTrace(); } catch (UntrustedIdentityException e) { e.printStackTrace(); } @@ -776,8 +864,6 @@ class Manager implements Signal { if (rm.isGroupsRequest()) { try { sendGroups(); - } catch (EncapsulatedExceptions encapsulatedExceptions) { - encapsulatedExceptions.printStackTrace(); } catch (UntrustedIdentityException e) { e.printStackTrace(); } @@ -850,7 +936,7 @@ class Manager implements Signal { } private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException { - new File(avatarsPath).mkdirs(); + createPrivateDirectories(avatarsPath); if (attachment.isPointer()) { SignalServiceAttachmentPointer pointer = attachment.asPointer(); return retrieveAttachment(pointer, getContactAvatarFile(number), false); @@ -865,7 +951,7 @@ class Manager implements Signal { } private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException { - new File(avatarsPath).mkdirs(); + createPrivateDirectories(avatarsPath); if (attachment.isPointer()) { SignalServiceAttachmentPointer pointer = attachment.asPointer(); return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false); @@ -880,7 +966,7 @@ class Manager implements Signal { } private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException { - new File(attachmentsPath).mkdirs(); + createPrivateDirectories(attachmentsPath); return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true); } @@ -976,7 +1062,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 +1092,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 { @@ -1042,4 +1128,54 @@ class Manager implements Signal { 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; + } }