X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/aa8a23aceb6cd9cb63723f4d783c7ef07e365610..947818d3172a4257b6d1a160496f1504d8f514ab:/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 2fc54504..eb883bb8 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -23,10 +23,12 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.apache.http.util.TextUtils; import org.asamk.Signal; 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.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.state.SignedPreKeyRecord; @@ -40,6 +42,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.*; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.*; +import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.TrustStore; @@ -47,8 +50,13 @@ import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedE import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import java.io.*; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.net.URLEncoder; import java.nio.file.Files; import java.nio.file.Paths; import java.util.*; @@ -72,6 +80,7 @@ class Manager implements Signal { private final ObjectMapper jsonProcessot = new ObjectMapper(); private String username; + int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; private String password; private String signalingKey; private int preKeyIdOffset; @@ -95,12 +104,19 @@ class Manager implements Signal { jsonProcessot.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); } + public String getUsername() { + return username; + } + public String getFileName() { new File(dataPath).mkdirs(); return dataPath + "/" + username; } public boolean userExists() { + if (username == null) { + return false; + } File f = new File(getFileName()); return !(!f.exists() || f.isDirectory()); } @@ -121,6 +137,10 @@ class Manager implements Signal { public void load() throws IOException, InvalidKeyException { JsonNode rootNode = jsonProcessot.readTree(new File(getFileName())); + JsonNode node = rootNode.get("deviceId"); + if (node != null) { + deviceId = node.asInt(); + } username = getNotNullNode(rootNode, "username").asText(); password = getNotNullNode(rootNode, "password").asText(); if (rootNode.has("signalingKey")) { @@ -145,7 +165,7 @@ class Manager implements Signal { if (groupStore == null) { groupStore = new JsonGroupStore(); } - accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); + accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, deviceId, USER_AGENT); try { if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) { refreshPreKeys(); @@ -159,6 +179,7 @@ class Manager implements Signal { private void save() { ObjectNode rootNode = jsonProcessot.createObjectNode(); rootNode.put("username", username) + .put("deviceId", deviceId) .put("password", password) .put("signalingKey", signalingKey) .put("preKeyIdOffset", preKeyIdOffset) @@ -201,6 +222,80 @@ class Manager implements Signal { save(); } + public URI getDeviceLinkUri() throws TimeoutException, IOException { + password = Util.getSecret(18); + + accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); + String uuid = accountManager.getNewDeviceUuid(); + + registered = false; + try { + return new URI("tsdevice:/?uuid=" + URLEncoder.encode(uuid, "utf-8") + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(signalProtocolStore.getIdentityKeyPair().getPublicKey().serialize()), "utf-8")); + } catch (URISyntaxException e) { + // Shouldn't happen + return null; + } + } + + public void finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists { + signalingKey = Util.getSecret(52); + SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(signalProtocolStore.getIdentityKeyPair(), signalingKey, false, true, signalProtocolStore.getLocalRegistrationId(), deviceName); + deviceId = ret.getDeviceId(); + username = ret.getNumber(); + // TODO do this check before actually registering + if (userExists()) { + throw new UserAlreadyExists(username, getFileName()); + } + signalProtocolStore = new JsonSignalProtocolStore(ret.getIdentity(), signalProtocolStore.getLocalRegistrationId()); + + registered = true; + refreshPreKeys(); + save(); + } + + + public static Map getQueryMap(String query) { + String[] params = query.split("&"); + Map map = new HashMap<>(); + for (String param : params) { + String name = null; + try { + name = URLDecoder.decode(param.split("=")[0], "utf-8"); + } catch (UnsupportedEncodingException e) { + // Impossible + } + String value = null; + try { + value = URLDecoder.decode(param.split("=")[1], "utf-8"); + } catch (UnsupportedEncodingException e) { + // Impossible + } + map.put(name, value); + } + return map; + } + + public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { + Map query = getQueryMap(linkUri.getQuery()); + String deviceIdentifier = query.get("uuid"); + String publicKeyEncoded = query.get("pub_key"); + + if (TextUtils.isEmpty(deviceIdentifier) || TextUtils.isEmpty(publicKeyEncoded)) { + throw new RuntimeException("Invalid device link uri"); + } + + ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0); + + addDeviceLink(deviceIdentifier, deviceKey); + } + + private void addDeviceLink(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { + IdentityKeyPair identityKeyPair = signalProtocolStore.getIdentityKeyPair(); + String verificationCode = accountManager.getNewDeviceVerificationCode(); + + accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, verificationCode); + } + private List generatePreKeys() { List records = new LinkedList<>(); @@ -300,7 +395,7 @@ class Manager implements Signal { @Override public void sendGroupMessage(String messageText, List attachments, byte[] groupId) - throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(getSignalServiceAttachments(attachments)); @@ -316,7 +411,7 @@ class Manager implements Signal { sendMessage(message, groupStore.getGroup(groupId).members); } - public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions { + public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, UntrustedIdentityException { SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) .withId(groupId) .build(); @@ -328,7 +423,7 @@ class Manager implements Signal { sendMessage(message, groupStore.getGroup(groupId).members); } - public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException { GroupInfo g; if (groupId == null) { // Create new group @@ -381,7 +476,7 @@ class Manager implements Signal { @Override public void sendMessage(String message, List attachments, String recipient) - throws EncapsulatedExceptions, AttachmentInvalidException, IOException { + throws EncapsulatedExceptions, AttachmentInvalidException, IOException, UntrustedIdentityException { List recipients = new ArrayList<>(1); recipients.add(recipient); sendMessage(message, attachments, recipients); @@ -390,7 +485,7 @@ class Manager implements Signal { @Override public void sendMessage(String messageText, List attachments, List recipients) - throws IOException, EncapsulatedExceptions, AttachmentInvalidException { + throws IOException, EncapsulatedExceptions, AttachmentInvalidException, UntrustedIdentityException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(getSignalServiceAttachments(attachments)); @@ -401,7 +496,7 @@ class Manager implements Signal { } @Override - public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions { + public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions, UntrustedIdentityException { SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() .asEndSessionMessage() .build(); @@ -410,9 +505,9 @@ class Manager implements Signal { } private void sendMessage(SignalServiceDataMessage message, Collection recipients) - throws IOException, EncapsulatedExceptions { + throws IOException, EncapsulatedExceptions, UntrustedIdentityException { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, - signalProtocolStore, USER_AGENT, Optional.absent()); + deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); Set recipientsTS = new HashSet<>(recipients.size()); for (String recipient : recipients) { @@ -426,7 +521,14 @@ class Manager implements Signal { } } - messageSender.sendMessage(new ArrayList<>(recipientsTS), message); + if (message.getGroupInfo().isPresent()) { + messageSender.sendMessage(new ArrayList<>(recipientsTS), message); + } else { + // Send to all individually, so sync messages are sent correctly + for (SignalServiceAddress address : recipientsTS) { + messageSender.sendMessage(address, message); + } + } if (message.isEndSession()) { for (SignalServiceAddress recipient : recipientsTS) { @@ -523,7 +625,7 @@ class Manager implements Signal { } public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException { - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); SignalServiceMessagePipe messagePipe = null; try { @@ -577,7 +679,7 @@ class Manager implements Signal { } private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException { - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT); + 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);