mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 02:20:39 +00:00
Implement device linking
This commit is contained in:
parent
f6b9222eda
commit
33956bde62
4 changed files with 114 additions and 6 deletions
|
@ -18,7 +18,7 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile 'org.whispersystems:signal-service-java:2.1.1fetchMessages'
|
compile 'org.whispersystems:signal-service-java:2.1.1fetchMessages_provisioning'
|
||||||
compile 'org.bouncycastle:bcprov-jdk15on:1.54'
|
compile 'org.bouncycastle:bcprov-jdk15on:1.54'
|
||||||
compile 'commons-io:commons-io:2.4'
|
compile 'commons-io:commons-io:2.4'
|
||||||
compile 'net.sourceforge.argparse4j:argparse4j:0.7.0'
|
compile 'net.sourceforge.argparse4j:argparse4j:0.7.0'
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.freedesktop.dbus.DBusConnection;
|
||||||
import org.freedesktop.dbus.DBusSigHandler;
|
import org.freedesktop.dbus.DBusSigHandler;
|
||||||
import org.freedesktop.dbus.exceptions.DBusException;
|
import org.freedesktop.dbus.exceptions.DBusException;
|
||||||
import org.freedesktop.dbus.exceptions.DBusExecutionException;
|
import org.freedesktop.dbus.exceptions.DBusExecutionException;
|
||||||
|
import org.whispersystems.libsignal.InvalidKeyException;
|
||||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||||
import org.whispersystems.signalservice.api.messages.*;
|
import org.whispersystems.signalservice.api.messages.*;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
|
||||||
|
@ -42,6 +43,7 @@ import java.io.IOException;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
public class Main {
|
public class Main {
|
||||||
|
|
||||||
|
@ -144,6 +146,37 @@ public class Main {
|
||||||
System.exit(3);
|
System.exit(3);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "link":
|
||||||
|
if (dBusConn != null) {
|
||||||
|
System.err.println("link is not yet implemented via dbus");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When linking, username is null and we always have to create keys
|
||||||
|
m.createNewIdentity();
|
||||||
|
|
||||||
|
String deviceName = ns.getString("name");
|
||||||
|
if (deviceName == null) {
|
||||||
|
deviceName = "cli";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
System.out.println(m.getDeviceLinkUri());
|
||||||
|
m.finishDeviceLink(deviceName);
|
||||||
|
System.out.println("Associated with: " + m.getUsername());
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
System.err.println("Link request timed out, please try again.");
|
||||||
|
System.exit(3);
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.err.println("Link request error: " + e.getMessage());
|
||||||
|
System.exit(3);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
System.exit(3);
|
||||||
|
} catch (UserAlreadyExists e) {
|
||||||
|
System.err.println("The user " + e.getUsername() + " already exists\nDelete \"" + e.getFileName() + "\" before trying again.");
|
||||||
|
System.exit(3);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case "send":
|
case "send":
|
||||||
if (dBusConn == null && !m.isRegistered()) {
|
if (dBusConn == null && !m.isRegistered()) {
|
||||||
System.err.println("User is not registered.");
|
System.err.println("User is not registered.");
|
||||||
|
@ -425,6 +458,10 @@ public class Main {
|
||||||
.description("valid subcommands")
|
.description("valid subcommands")
|
||||||
.help("additional help");
|
.help("additional help");
|
||||||
|
|
||||||
|
Subparser parserLink = subparsers.addParser("link");
|
||||||
|
parserLink.addArgument("-n", "--name")
|
||||||
|
.help("Specify a name to describe this new device.");
|
||||||
|
|
||||||
Subparser parserRegister = subparsers.addParser("register");
|
Subparser parserRegister = subparsers.addParser("register");
|
||||||
parserRegister.addArgument("-v", "--voice")
|
parserRegister.addArgument("-v", "--voice")
|
||||||
.help("The verification should be done over voice, not sms.")
|
.help("The verification should be done over voice, not sms.")
|
||||||
|
@ -477,7 +514,13 @@ public class Main {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Namespace ns = parser.parseArgs(args);
|
Namespace ns = parser.parseArgs(args);
|
||||||
if (!ns.getBoolean("dbus") && !ns.getBoolean("dbus_system")) {
|
if ("link".equals(ns.getString("command"))) {
|
||||||
|
if (ns.getString("username") != null) {
|
||||||
|
parser.printUsage();
|
||||||
|
System.err.println("You cannot specify a username (phone number) when linking");
|
||||||
|
System.exit(2);
|
||||||
|
}
|
||||||
|
} else if (!ns.getBoolean("dbus") && !ns.getBoolean("dbus_system")) {
|
||||||
if (ns.getString("username") == null) {
|
if (ns.getString("username") == null) {
|
||||||
parser.printUsage();
|
parser.printUsage();
|
||||||
System.err.println("You need to specify a username (phone number)");
|
System.err.println("You need to specify a username (phone number)");
|
||||||
|
|
|
@ -49,6 +49,9 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
||||||
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
@ -72,6 +75,7 @@ class Manager implements Signal {
|
||||||
|
|
||||||
private final ObjectMapper jsonProcessot = new ObjectMapper();
|
private final ObjectMapper jsonProcessot = new ObjectMapper();
|
||||||
private String username;
|
private String username;
|
||||||
|
int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
|
||||||
private String password;
|
private String password;
|
||||||
private String signalingKey;
|
private String signalingKey;
|
||||||
private int preKeyIdOffset;
|
private int preKeyIdOffset;
|
||||||
|
@ -95,12 +99,19 @@ class Manager implements Signal {
|
||||||
jsonProcessot.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
jsonProcessot.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
public String getFileName() {
|
public String getFileName() {
|
||||||
new File(dataPath).mkdirs();
|
new File(dataPath).mkdirs();
|
||||||
return dataPath + "/" + username;
|
return dataPath + "/" + username;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean userExists() {
|
public boolean userExists() {
|
||||||
|
if (username == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
File f = new File(getFileName());
|
File f = new File(getFileName());
|
||||||
return !(!f.exists() || f.isDirectory());
|
return !(!f.exists() || f.isDirectory());
|
||||||
}
|
}
|
||||||
|
@ -121,6 +132,10 @@ class Manager implements Signal {
|
||||||
public void load() throws IOException, InvalidKeyException {
|
public void load() throws IOException, InvalidKeyException {
|
||||||
JsonNode rootNode = jsonProcessot.readTree(new File(getFileName()));
|
JsonNode rootNode = jsonProcessot.readTree(new File(getFileName()));
|
||||||
|
|
||||||
|
JsonNode node = rootNode.get("deviceId");
|
||||||
|
if (node != null) {
|
||||||
|
deviceId = node.asInt();
|
||||||
|
}
|
||||||
username = getNotNullNode(rootNode, "username").asText();
|
username = getNotNullNode(rootNode, "username").asText();
|
||||||
password = getNotNullNode(rootNode, "password").asText();
|
password = getNotNullNode(rootNode, "password").asText();
|
||||||
if (rootNode.has("signalingKey")) {
|
if (rootNode.has("signalingKey")) {
|
||||||
|
@ -145,7 +160,7 @@ class Manager implements Signal {
|
||||||
if (groupStore == null) {
|
if (groupStore == null) {
|
||||||
groupStore = new JsonGroupStore();
|
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 {
|
try {
|
||||||
if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) {
|
if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) {
|
||||||
refreshPreKeys();
|
refreshPreKeys();
|
||||||
|
@ -159,6 +174,7 @@ class Manager implements Signal {
|
||||||
private void save() {
|
private void save() {
|
||||||
ObjectNode rootNode = jsonProcessot.createObjectNode();
|
ObjectNode rootNode = jsonProcessot.createObjectNode();
|
||||||
rootNode.put("username", username)
|
rootNode.put("username", username)
|
||||||
|
.put("deviceId", deviceId)
|
||||||
.put("password", password)
|
.put("password", password)
|
||||||
.put("signalingKey", signalingKey)
|
.put("signalingKey", signalingKey)
|
||||||
.put("preKeyIdOffset", preKeyIdOffset)
|
.put("preKeyIdOffset", preKeyIdOffset)
|
||||||
|
@ -201,6 +217,36 @@ class Manager implements Signal {
|
||||||
save();
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
private List<PreKeyRecord> generatePreKeys() {
|
private List<PreKeyRecord> generatePreKeys() {
|
||||||
List<PreKeyRecord> records = new LinkedList<>();
|
List<PreKeyRecord> records = new LinkedList<>();
|
||||||
|
|
||||||
|
@ -412,7 +458,7 @@ class Manager implements Signal {
|
||||||
private void sendMessage(SignalServiceDataMessage message, Collection<String> recipients)
|
private void sendMessage(SignalServiceDataMessage message, Collection<String> recipients)
|
||||||
throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
|
throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
|
||||||
SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password,
|
SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password,
|
||||||
signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent());
|
deviceId, signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent());
|
||||||
|
|
||||||
Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size());
|
Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size());
|
||||||
for (String recipient : recipients) {
|
for (String recipient : recipients) {
|
||||||
|
@ -530,7 +576,7 @@ class Manager implements Signal {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException {
|
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;
|
SignalServiceMessagePipe messagePipe = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -584,7 +630,7 @@ class Manager implements Signal {
|
||||||
}
|
}
|
||||||
|
|
||||||
private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
|
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");
|
File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp");
|
||||||
InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile);
|
InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile);
|
||||||
|
|
19
src/main/java/org/asamk/signal/UserAlreadyExists.java
Normal file
19
src/main/java/org/asamk/signal/UserAlreadyExists.java
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package org.asamk.signal;
|
||||||
|
|
||||||
|
public class UserAlreadyExists extends Exception {
|
||||||
|
private String username;
|
||||||
|
private String fileName;
|
||||||
|
|
||||||
|
public UserAlreadyExists(String username, String fileName) {
|
||||||
|
this.username = username;
|
||||||
|
this.fileName = fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFileName() {
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue