mirror of
https://github.com/AsamK/signal-cli
synced 2025-09-02 04:20:38 +00:00
Merge remote-tracking branch 'asamk/master'
This commit is contained in:
commit
14b6d9aed1
108 changed files with 2882 additions and 1573 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -1,9 +1,26 @@
|
|||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- Disable registration lock before removing the PIN
|
||||
- Fix PIN hash version to match the official clients.
|
||||
If you had previously set a PIN you need to set it again to be able to unlock the registration lock later.
|
||||
|
||||
## [0.7.2] - 2020-12-31
|
||||
### Added
|
||||
- Implement new registration lock PIN with `setPin` and `removePin` (with KBS)
|
||||
- Include quotes, mentions and reactions in json output (Thanks @Atomic-Bean)
|
||||
|
||||
### Fixed
|
||||
- Retrieve avatars for v2 groups
|
||||
- Download attachment thumbnail for quoted attachments
|
||||
|
||||
## [0.7.1] - 2020-12-21
|
||||
### Added
|
||||
- Accept group invitation with `updateGroup -g GROUP_ID`
|
||||
- Decline group invitation with `quitGroup -g GROUP_ID`
|
||||
- Join group via invitation link `joinGroup --uri https://signal.group/#...`
|
||||
|
||||
### Fixed
|
||||
- Include group ids for v2 groups in json output
|
||||
|
|
|
@ -7,7 +7,7 @@ targetCompatibility = JavaVersion.VERSION_11
|
|||
|
||||
mainClassName = 'org.asamk.signal.Main'
|
||||
|
||||
version = '0.7.0'
|
||||
version = '0.7.2'
|
||||
|
||||
compileJava.options.encoding = 'UTF-8'
|
||||
|
||||
|
@ -18,10 +18,10 @@ repositories {
|
|||
|
||||
dependencies {
|
||||
implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_15'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.67'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.68'
|
||||
implementation 'net.sourceforge.argparse4j:argparse4j:0.8.1'
|
||||
implementation 'com.github.hypfvieh:dbus-java:3.2.4'
|
||||
implementation 'org.slf4j:slf4j-nop:1.7.30'
|
||||
implementation 'org.slf4j:slf4j-simple:1.7.30'
|
||||
}
|
||||
|
||||
jar {
|
||||
|
|
|
@ -124,6 +124,15 @@ Only works, if this is the master device.
|
|||
Specify the device you want to remove.
|
||||
Use listDevices to see the deviceIds.
|
||||
|
||||
=== getUserStatus
|
||||
|
||||
Uses a list of phone numbers to determine the statuses of those users. Shows if they are registered on the Signal Servers or not.
|
||||
|
||||
[NUMBER [NUMBER ...]]::
|
||||
One or more numbers to check.
|
||||
*--json*::
|
||||
Output the statuses as an array of json objects.
|
||||
|
||||
=== send
|
||||
|
||||
Send a message to another user or group.
|
||||
|
@ -178,6 +187,13 @@ Don’t download attachments of received messages.
|
|||
*--json*::
|
||||
Output received messages in json format, one object per line.
|
||||
|
||||
=== joinGroup
|
||||
|
||||
Join a group via an invitation link.
|
||||
|
||||
*--uri*::
|
||||
The invitation link URI (starts with `https://signal.group/#`)
|
||||
|
||||
=== updateGroup
|
||||
|
||||
Create or update a group.
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package org.asamk.signal;
|
||||
|
||||
import org.asamk.Signal;
|
||||
import org.asamk.signal.manager.GroupUtils;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.groups.GroupUtils;
|
||||
import org.freedesktop.dbus.connections.impl.DBusConnection;
|
||||
import org.freedesktop.dbus.exceptions.DBusException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
|
@ -65,7 +65,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
|
|||
} else if (content.getDataMessage().isPresent()) {
|
||||
SignalServiceDataMessage message = content.getDataMessage().get();
|
||||
|
||||
byte[] groupId = getGroupId(m, message);
|
||||
byte[] groupId = getGroupId(message);
|
||||
if (!message.isEndSession() && (
|
||||
groupId == null
|
||||
|| message.getGroupContext().get().getGroupV1Type() == null
|
||||
|
@ -91,7 +91,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
|
|||
.getGroupContext()
|
||||
.isPresent()) {
|
||||
SignalServiceDataMessage message = transcript.getMessage();
|
||||
byte[] groupId = getGroupId(m, message);
|
||||
byte[] groupId = getGroupId(message);
|
||||
|
||||
try {
|
||||
conn.sendMessage(new Signal.SyncMessageReceived(objectPath,
|
||||
|
@ -112,20 +112,9 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private static byte[] getGroupId(final Manager m, final SignalServiceDataMessage message) {
|
||||
byte[] groupId;
|
||||
if (message.getGroupContext().isPresent()) {
|
||||
if (message.getGroupContext().get().getGroupV1().isPresent()) {
|
||||
groupId = message.getGroupContext().get().getGroupV1().get().getGroupId();
|
||||
} else if (message.getGroupContext().get().getGroupV2().isPresent()) {
|
||||
groupId = GroupUtils.getGroupId(message.getGroupContext().get().getGroupV2().get().getMasterKey());
|
||||
} else {
|
||||
groupId = null;
|
||||
}
|
||||
} else {
|
||||
groupId = null;
|
||||
}
|
||||
return groupId;
|
||||
private static byte[] getGroupId(final SignalServiceDataMessage message) {
|
||||
return message.getGroupContext().isPresent() ? GroupUtils.getGroupId(message.getGroupContext().get())
|
||||
.serialize() : null;
|
||||
}
|
||||
|
||||
static private List<String> getAttachments(SignalServiceDataMessage message, Manager m) {
|
||||
|
|
|
@ -3,7 +3,6 @@ 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.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
|
@ -23,8 +22,7 @@ public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler
|
|||
public JsonReceiveMessageHandler(Manager m) {
|
||||
this.m = m;
|
||||
this.jsonProcessor = new ObjectMapper();
|
||||
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect
|
||||
jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
||||
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
|
||||
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
|
||||
}
|
||||
|
||||
|
@ -35,7 +33,7 @@ public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler
|
|||
result.putPOJO("error", new JsonError(exception));
|
||||
}
|
||||
if (envelope != null) {
|
||||
result.putPOJO("envelope", new JsonMessageEnvelope(envelope, content));
|
||||
result.putPOJO("envelope", new JsonMessageEnvelope(envelope, content, m));
|
||||
}
|
||||
try {
|
||||
jsonProcessor.writeValue(System.out, result);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright (C) 2015-2020 AsamK and contributors
|
||||
Copyright (C) 2015-2021 AsamK and contributors
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -32,16 +32,20 @@ import org.asamk.signal.commands.DbusCommand;
|
|||
import org.asamk.signal.commands.ExtendedDbusCommand;
|
||||
import org.asamk.signal.commands.LocalCommand;
|
||||
import org.asamk.signal.commands.ProvisioningCommand;
|
||||
import org.asamk.signal.commands.RegistrationCommand;
|
||||
import org.asamk.signal.dbus.DbusSignalImpl;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.NotRegisteredException;
|
||||
import org.asamk.signal.manager.ProvisioningManager;
|
||||
import org.asamk.signal.manager.RegistrationManager;
|
||||
import org.asamk.signal.manager.ServiceConfig;
|
||||
import org.asamk.signal.util.IOUtils;
|
||||
import org.asamk.signal.util.SecurityProvider;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.freedesktop.dbus.connections.impl.DBusConnection;
|
||||
import org.freedesktop.dbus.exceptions.DBusException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
|
||||
|
@ -50,10 +54,10 @@ import java.io.IOException;
|
|||
import java.security.Security;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
||||
|
||||
public class Main {
|
||||
|
||||
final static Logger logger = LoggerFactory.getLogger(Main.class);
|
||||
|
||||
public static void main(String[] args) {
|
||||
installSecurityProviderWorkaround();
|
||||
|
||||
|
@ -62,7 +66,7 @@ public class Main {
|
|||
System.exit(1);
|
||||
}
|
||||
|
||||
int res = handleCommands(ns);
|
||||
int res = init(ns);
|
||||
System.exit(res);
|
||||
}
|
||||
|
||||
|
@ -72,125 +76,148 @@ public class Main {
|
|||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
private static int handleCommands(Namespace ns) {
|
||||
final String username = ns.getString("username");
|
||||
public static int init(Namespace ns) {
|
||||
Command command = getCommand(ns);
|
||||
if (command == null) {
|
||||
logger.error("Command not implemented!");
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) {
|
||||
try {
|
||||
DBusConnection.DBusBusType busType;
|
||||
if (ns.getBoolean("dbus_system")) {
|
||||
busType = DBusConnection.DBusBusType.SYSTEM;
|
||||
} else {
|
||||
busType = DBusConnection.DBusBusType.SESSION;
|
||||
}
|
||||
try (DBusConnection dBusConn = DBusConnection.getConnection(busType)) {
|
||||
Signal ts = dBusConn.getRemoteObject(DbusConfig.SIGNAL_BUSNAME,
|
||||
DbusConfig.SIGNAL_OBJECTPATH,
|
||||
Signal.class);
|
||||
return initDbusClient(command, ns, ns.getBoolean("dbus_system"));
|
||||
}
|
||||
|
||||
return handleCommands(ns, ts, dBusConn);
|
||||
}
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
System.err.println("Missing native library dependency for dbus service: " + e.getMessage());
|
||||
return 1;
|
||||
} catch (DBusException | IOException e) {
|
||||
e.printStackTrace();
|
||||
final String username = ns.getString("username");
|
||||
|
||||
final File dataPath;
|
||||
String config = ns.getString("config");
|
||||
if (config != null) {
|
||||
dataPath = new File(config);
|
||||
} else {
|
||||
dataPath = getDefaultDataPath();
|
||||
}
|
||||
|
||||
final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration(
|
||||
BaseConfig.USER_AGENT);
|
||||
|
||||
if (!ServiceConfig.getCapabilities().isGv2()) {
|
||||
logger.warn("WARNING: Support for new group V2 is disabled,"
|
||||
+ " because the required native library dependency is missing: libzkgroup");
|
||||
}
|
||||
|
||||
if (username == null) {
|
||||
ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
|
||||
return handleCommand(command, ns, pm);
|
||||
}
|
||||
|
||||
if (command instanceof RegistrationCommand) {
|
||||
final RegistrationManager manager;
|
||||
try {
|
||||
manager = RegistrationManager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
|
||||
} catch (Throwable e) {
|
||||
logger.error("Error loading or creating state file: {}", e.getMessage());
|
||||
return 2;
|
||||
}
|
||||
try (RegistrationManager m = manager) {
|
||||
return handleCommand(command, ns, m);
|
||||
} catch (Exception e) {
|
||||
logger.error("Cleanup failed", e);
|
||||
return 3;
|
||||
}
|
||||
} else {
|
||||
String dataPath = ns.getString("config");
|
||||
if (isEmpty(dataPath)) {
|
||||
dataPath = getDefaultDataPath();
|
||||
}
|
||||
}
|
||||
|
||||
final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration(
|
||||
BaseConfig.USER_AGENT);
|
||||
Manager manager;
|
||||
try {
|
||||
manager = Manager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
|
||||
} catch (NotRegisteredException e) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
} catch (Throwable e) {
|
||||
logger.error("Error loading state file: {}", e.getMessage());
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (username == null) {
|
||||
ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
|
||||
return handleCommands(ns, pm);
|
||||
}
|
||||
|
||||
Manager manager;
|
||||
try (Manager m = manager) {
|
||||
try {
|
||||
manager = Manager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
|
||||
} catch (Throwable e) {
|
||||
System.err.println("Error loading state file: " + e.getMessage());
|
||||
m.checkAccountState();
|
||||
} catch (IOException e) {
|
||||
logger.error("Error while checking account: {}", e.getMessage());
|
||||
return 2;
|
||||
}
|
||||
|
||||
try (Manager m = manager) {
|
||||
try {
|
||||
m.checkAccountState();
|
||||
} catch (AuthorizationFailedException e) {
|
||||
if (!"register".equals(ns.getString("command"))) {
|
||||
// Register command should still be possible, if current authorization fails
|
||||
System.err.println("Authorization failed, was the number registered elsewhere?");
|
||||
return 2;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("Error while checking account: " + e.getMessage());
|
||||
return 2;
|
||||
}
|
||||
|
||||
return handleCommands(ns, m);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return 3;
|
||||
}
|
||||
return handleCommand(command, ns, m);
|
||||
} catch (IOException e) {
|
||||
logger.error("Cleanup failed", e);
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
private static int handleCommands(Namespace ns, Signal ts, DBusConnection dBusConn) {
|
||||
String commandKey = ns.getString("command");
|
||||
final Map<String, Command> commands = Commands.getCommands();
|
||||
if (commands.containsKey(commandKey)) {
|
||||
Command command = commands.get(commandKey);
|
||||
|
||||
if (command instanceof ExtendedDbusCommand) {
|
||||
return ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn);
|
||||
} else if (command instanceof DbusCommand) {
|
||||
return ((DbusCommand) command).handleCommand(ns, ts);
|
||||
private static int initDbusClient(final Command command, final Namespace ns, final boolean systemBus) {
|
||||
try {
|
||||
DBusConnection.DBusBusType busType;
|
||||
if (systemBus) {
|
||||
busType = DBusConnection.DBusBusType.SYSTEM;
|
||||
} else {
|
||||
System.err.println(commandKey + " is not yet implemented via dbus");
|
||||
return 1;
|
||||
busType = DBusConnection.DBusBusType.SESSION;
|
||||
}
|
||||
try (DBusConnection dBusConn = DBusConnection.getConnection(busType)) {
|
||||
Signal ts = dBusConn.getRemoteObject(DbusConfig.SIGNAL_BUSNAME,
|
||||
DbusConfig.SIGNAL_OBJECTPATH,
|
||||
Signal.class);
|
||||
|
||||
return handleCommand(command, ns, ts, dBusConn);
|
||||
}
|
||||
} catch (DBusException | IOException e) {
|
||||
logger.error("Dbus client failed", e);
|
||||
return 3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int handleCommands(Namespace ns, ProvisioningManager pm) {
|
||||
private static Command getCommand(Namespace ns) {
|
||||
String commandKey = ns.getString("command");
|
||||
final Map<String, Command> commands = Commands.getCommands();
|
||||
if (commands.containsKey(commandKey)) {
|
||||
Command command = commands.get(commandKey);
|
||||
|
||||
if (command instanceof ProvisioningCommand) {
|
||||
return ((ProvisioningCommand) command).handleCommand(ns, pm);
|
||||
} else {
|
||||
System.err.println(commandKey + " only works with a username");
|
||||
return 1;
|
||||
}
|
||||
if (!commands.containsKey(commandKey)) {
|
||||
return null;
|
||||
}
|
||||
return 0;
|
||||
return commands.get(commandKey);
|
||||
}
|
||||
|
||||
private static int handleCommands(Namespace ns, Manager m) {
|
||||
String commandKey = ns.getString("command");
|
||||
final Map<String, Command> commands = Commands.getCommands();
|
||||
if (commands.containsKey(commandKey)) {
|
||||
Command command = commands.get(commandKey);
|
||||
|
||||
if (command instanceof LocalCommand) {
|
||||
return ((LocalCommand) command).handleCommand(ns, m);
|
||||
} else if (command instanceof DbusCommand) {
|
||||
return ((DbusCommand) command).handleCommand(ns, new DbusSignalImpl(m));
|
||||
} else if (command instanceof ExtendedDbusCommand) {
|
||||
System.err.println(commandKey + " only works via dbus");
|
||||
}
|
||||
private static int handleCommand(Command command, Namespace ns, Signal ts, DBusConnection dBusConn) {
|
||||
if (command instanceof ExtendedDbusCommand) {
|
||||
return ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn);
|
||||
} else if (command instanceof DbusCommand) {
|
||||
return ((DbusCommand) command).handleCommand(ns, ts);
|
||||
} else {
|
||||
System.err.println("Command is not yet implemented via dbus");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static int handleCommand(Command command, Namespace ns, ProvisioningManager pm) {
|
||||
if (command instanceof ProvisioningCommand) {
|
||||
return ((ProvisioningCommand) command).handleCommand(ns, pm);
|
||||
} else {
|
||||
System.err.println("Command only works with a username");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static int handleCommand(Command command, Namespace ns, RegistrationManager m) {
|
||||
if (command instanceof RegistrationCommand) {
|
||||
return ((RegistrationCommand) command).handleCommand(ns, m);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int handleCommand(Command command, Namespace ns, Manager m) {
|
||||
if (command instanceof LocalCommand) {
|
||||
return ((LocalCommand) command).handleCommand(ns, m);
|
||||
} else if (command instanceof DbusCommand) {
|
||||
return ((DbusCommand) command).handleCommand(ns, new DbusSignalImpl(m));
|
||||
} else {
|
||||
System.err.println("Command only works via dbus");
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -200,19 +227,21 @@ public class Main {
|
|||
*
|
||||
* @return the data directory to be used by signal-cli.
|
||||
*/
|
||||
private static String getDefaultDataPath() {
|
||||
String dataPath = IOUtils.getDataHomeDir() + "/signal-cli";
|
||||
if (new File(dataPath).exists()) {
|
||||
private static File getDefaultDataPath() {
|
||||
File dataPath = new File(IOUtils.getDataHomeDir(), "signal-cli");
|
||||
if (dataPath.exists()) {
|
||||
return dataPath;
|
||||
}
|
||||
|
||||
String legacySettingsPath = System.getProperty("user.home") + "/.config/signal";
|
||||
if (new File(legacySettingsPath).exists()) {
|
||||
File configPath = new File(System.getProperty("user.home"), ".config");
|
||||
|
||||
File legacySettingsPath = new File(configPath, "signal");
|
||||
if (legacySettingsPath.exists()) {
|
||||
return legacySettingsPath;
|
||||
}
|
||||
|
||||
legacySettingsPath = System.getProperty("user.home") + "/.config/textsecure";
|
||||
if (new File(legacySettingsPath).exists()) {
|
||||
legacySettingsPath = new File(configPath, "textsecure");
|
||||
if (legacySettingsPath.exists()) {
|
||||
return legacySettingsPath;
|
||||
}
|
||||
|
||||
|
@ -220,34 +249,7 @@ public class Main {
|
|||
}
|
||||
|
||||
private static Namespace parseArgs(String[] args) {
|
||||
ArgumentParser parser = ArgumentParsers.newFor("signal-cli")
|
||||
.build()
|
||||
.defaultHelp(true)
|
||||
.description("Commandline interface for Signal.")
|
||||
.version(BaseConfig.PROJECT_NAME + " " + BaseConfig.PROJECT_VERSION);
|
||||
|
||||
parser.addArgument("-v", "--version").help("Show package version.").action(Arguments.version());
|
||||
parser.addArgument("--config")
|
||||
.help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).");
|
||||
parser.addArgument("-n", "--busname")
|
||||
.help("Name of the DBus.");
|
||||
|
||||
MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup();
|
||||
mut.addArgument("-u", "--username").help("Specify your phone number, that will be used for verification.");
|
||||
mut.addArgument("--dbus").help("Make request via user dbus.").action(Arguments.storeTrue());
|
||||
mut.addArgument("--dbus-system").help("Make request via system dbus.").action(Arguments.storeTrue());
|
||||
|
||||
Subparsers subparsers = parser.addSubparsers()
|
||||
.title("subcommands")
|
||||
.dest("command")
|
||||
.description("valid subcommands")
|
||||
.help("additional help");
|
||||
|
||||
final Map<String, Command> commands = Commands.getCommands();
|
||||
for (Map.Entry<String, Command> entry : commands.entrySet()) {
|
||||
Subparser subparser = subparsers.addParser(entry.getKey());
|
||||
entry.getValue().attachToSubparser(subparser);
|
||||
}
|
||||
ArgumentParser parser = buildArgumentParser();
|
||||
|
||||
Namespace ns;
|
||||
try {
|
||||
|
@ -280,4 +282,36 @@ public class Main {
|
|||
}
|
||||
return ns;
|
||||
}
|
||||
|
||||
private static ArgumentParser buildArgumentParser() {
|
||||
ArgumentParser parser = ArgumentParsers.newFor("signal-cli")
|
||||
.build()
|
||||
.defaultHelp(true)
|
||||
.description("Commandline interface for Signal.")
|
||||
.version(BaseConfig.PROJECT_NAME + " " + BaseConfig.PROJECT_VERSION);
|
||||
|
||||
parser.addArgument("-v", "--version").help("Show package version.").action(Arguments.version());
|
||||
parser.addArgument("--config")
|
||||
.help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).");
|
||||
parser.addArgument("-n", "--busname")
|
||||
.help("Name of the DBus.");
|
||||
|
||||
MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup();
|
||||
mut.addArgument("-u", "--username").help("Specify your phone number, that will be used for verification.");
|
||||
mut.addArgument("--dbus").help("Make request via user dbus.").action(Arguments.storeTrue());
|
||||
mut.addArgument("--dbus-system").help("Make request via system dbus.").action(Arguments.storeTrue());
|
||||
|
||||
Subparsers subparsers = parser.addSubparsers()
|
||||
.title("subcommands")
|
||||
.dest("command")
|
||||
.description("valid subcommands")
|
||||
.help("additional help");
|
||||
|
||||
final Map<String, Command> commands = Commands.getCommands();
|
||||
for (Map.Entry<String, Command> entry : commands.entrySet()) {
|
||||
Subparser subparser = subparsers.addParser(entry.getKey());
|
||||
entry.getValue().attachToSubparser(subparser);
|
||||
}
|
||||
return parser;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package org.asamk.signal;
|
||||
|
||||
import org.asamk.signal.manager.GroupUtils;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.storage.contacts.ContactInfo;
|
||||
import org.asamk.signal.storage.groups.GroupInfo;
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupUtils;
|
||||
import org.asamk.signal.manager.storage.contacts.ContactInfo;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
||||
import org.asamk.signal.util.DateUtils;
|
||||
import org.asamk.signal.util.Util;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
|
@ -328,8 +329,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
|
|||
System.out.println(" - Timestamp: " + DateUtils.formatTimestamp(typingMessage.getTimestamp()));
|
||||
if (typingMessage.getGroupId().isPresent()) {
|
||||
System.out.println(" - Group Info:");
|
||||
System.out.println(" Id: " + Base64.encodeBytes(typingMessage.getGroupId().get()));
|
||||
GroupInfo group = m.getGroup(typingMessage.getGroupId().get());
|
||||
final GroupId groupId = GroupId.unknownVersion(typingMessage.getGroupId().get());
|
||||
System.out.println(" Id: " + groupId.toBase64());
|
||||
GroupInfo group = m.getGroup(groupId);
|
||||
if (group != null) {
|
||||
System.out.println(" Name: " + group.getTitle());
|
||||
} else {
|
||||
|
@ -356,13 +358,14 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
|
|||
if (message.getGroupContext().isPresent()) {
|
||||
System.out.println("Group info:");
|
||||
final SignalServiceGroupContext groupContext = message.getGroupContext().get();
|
||||
final GroupId groupId = GroupUtils.getGroupId(groupContext);
|
||||
if (groupContext.getGroupV1().isPresent()) {
|
||||
SignalServiceGroup groupInfo = groupContext.getGroupV1().get();
|
||||
System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId()));
|
||||
System.out.println(" Id: " + groupId.toBase64());
|
||||
if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) {
|
||||
System.out.println(" Name: " + groupInfo.getName().get());
|
||||
} else {
|
||||
GroupInfo group = m.getGroup(groupInfo.getGroupId());
|
||||
GroupInfo group = m.getGroup(groupId);
|
||||
if (group != null) {
|
||||
System.out.println(" Name: " + group.getTitle());
|
||||
} else {
|
||||
|
@ -381,8 +384,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
|
|||
}
|
||||
} else if (groupContext.getGroupV2().isPresent()) {
|
||||
final SignalServiceGroupV2 groupInfo = groupContext.getGroupV2().get();
|
||||
byte[] groupId = GroupUtils.getGroupId(groupInfo.getMasterKey());
|
||||
System.out.println(" Id: " + Base64.encodeBytes(groupId));
|
||||
System.out.println(" Id: " + groupId.toBase64());
|
||||
GroupInfo group = m.getGroup(groupId);
|
||||
if (group != null) {
|
||||
System.out.println(" Name: " + group.getTitle());
|
||||
|
@ -447,14 +449,20 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
|
|||
if (message.getQuote().isPresent()) {
|
||||
SignalServiceDataMessage.Quote quote = message.getQuote().get();
|
||||
System.out.println("Quote: (" + quote.getId() + ")");
|
||||
System.out.println(" Author: " + quote.getAuthor().getLegacyIdentifier());
|
||||
System.out.println(" Author: " + m.resolveSignalServiceAddress(quote.getAuthor()).getLegacyIdentifier());
|
||||
System.out.println(" Text: " + quote.getText());
|
||||
if (quote.getMentions() != null && quote.getMentions().size() > 0) {
|
||||
System.out.println(" Mentions: ");
|
||||
for (SignalServiceDataMessage.Mention mention : quote.getMentions()) {
|
||||
printMention(mention, m);
|
||||
}
|
||||
}
|
||||
if (quote.getAttachments().size() > 0) {
|
||||
System.out.println(" Attachments: ");
|
||||
for (SignalServiceDataMessage.Quote.QuotedAttachment attachment : quote.getAttachments()) {
|
||||
System.out.println(" Filename: " + attachment.getFileName());
|
||||
System.out.println(" Type: " + attachment.getContentType());
|
||||
System.out.println(" Thumbnail:");
|
||||
System.out.println(" - Filename: " + attachment.getFileName());
|
||||
System.out.println(" Type: " + attachment.getContentType());
|
||||
System.out.println(" Thumbnail:");
|
||||
if (attachment.getThumbnail() != null) {
|
||||
printAttachment(attachment.getThumbnail());
|
||||
}
|
||||
|
@ -467,16 +475,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
|
|||
System.out.println("Remote delete message: timestamp = " + remoteDelete.getTargetSentTimestamp());
|
||||
}
|
||||
if (message.getMentions().isPresent()) {
|
||||
final List<SignalServiceDataMessage.Mention> mentions = message.getMentions().get();
|
||||
System.out.println("Mentions: ");
|
||||
for (SignalServiceDataMessage.Mention mention : mentions) {
|
||||
System.out.println("- "
|
||||
+ mention.getUuid()
|
||||
+ ": "
|
||||
+ mention.getStart()
|
||||
+ " (length: "
|
||||
+ mention.getLength()
|
||||
+ ")");
|
||||
for (SignalServiceDataMessage.Mention mention : message.getMentions().get()) {
|
||||
printMention(mention, m);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -488,6 +489,11 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private void printMention(SignalServiceDataMessage.Mention mention, Manager m) {
|
||||
System.out.println("- " + m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid(), null))
|
||||
.getLegacyIdentifier() + ": " + mention.getStart() + " (length: " + mention.getLength() + ")");
|
||||
}
|
||||
|
||||
private void printAttachment(SignalServiceAttachment attachment) {
|
||||
System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (
|
||||
attachment.isStream() ? "Stream" : ""
|
||||
|
|
|
@ -23,10 +23,6 @@ public class AddDeviceCommand implements LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
m.addDeviceLink(new URI(ns.getString("uri")));
|
||||
return 0;
|
||||
|
|
|
@ -3,9 +3,10 @@ package org.asamk.signal.commands;
|
|||
import net.sourceforge.argparse4j.inf.Namespace;
|
||||
import net.sourceforge.argparse4j.inf.Subparser;
|
||||
|
||||
import org.asamk.signal.manager.GroupNotFoundException;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.util.GroupIdFormatException;
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupIdFormatException;
|
||||
import org.asamk.signal.manager.groups.GroupNotFoundException;
|
||||
import org.asamk.signal.util.Util;
|
||||
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
||||
|
||||
|
@ -20,11 +21,6 @@ public class BlockCommand implements LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (String contact_number : ns.<String>getList("contact")) {
|
||||
try {
|
||||
m.setContactBlocked(contact_number, true);
|
||||
|
@ -36,7 +32,7 @@ public class BlockCommand implements LocalCommand {
|
|||
if (ns.<String>getList("group") != null) {
|
||||
for (String groupIdString : ns.<String>getList("group")) {
|
||||
try {
|
||||
byte[] groupId = Util.decodeGroupId(groupIdString);
|
||||
GroupId groupId = Util.decodeGroupId(groupIdString);
|
||||
m.setGroupBlocked(groupId, true);
|
||||
} catch (GroupIdFormatException | GroupNotFoundException e) {
|
||||
System.err.println(e.getMessage());
|
||||
|
|
|
@ -11,11 +11,13 @@ public class Commands {
|
|||
addCommand("addDevice", new AddDeviceCommand());
|
||||
addCommand("block", new BlockCommand());
|
||||
addCommand("daemon", new DaemonCommand());
|
||||
addCommand("getUserStatus", new GetUserStatusCommand());
|
||||
addCommand("link", new LinkCommand());
|
||||
addCommand("listContacts", new ListContactsCommand());
|
||||
addCommand("listDevices", new ListDevicesCommand());
|
||||
addCommand("listGroups", new ListGroupsCommand());
|
||||
addCommand("listIdentities", new ListIdentitiesCommand());
|
||||
addCommand("joinGroup", new JoinGroupCommand());
|
||||
addCommand("quitGroup", new QuitGroupCommand());
|
||||
addCommand("receive", new ReceiveCommand());
|
||||
addCommand("register", new RegisterCommand());
|
||||
|
|
|
@ -35,10 +35,6 @@ public class DaemonCommand implements LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
DBusConnection conn = null;
|
||||
try {
|
||||
try {
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
package org.asamk.signal.commands;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import net.sourceforge.argparse4j.impl.Arguments;
|
||||
import net.sourceforge.argparse4j.inf.Namespace;
|
||||
import net.sourceforge.argparse4j.inf.Subparser;
|
||||
|
||||
import org.asamk.signal.manager.Manager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class GetUserStatusCommand implements LocalCommand {
|
||||
|
||||
@Override
|
||||
public void attachToSubparser(final Subparser subparser) {
|
||||
subparser.addArgument("number").help("Phone number").nargs("+");
|
||||
subparser.help("Check if the specified phone number/s have been registered");
|
||||
subparser.addArgument("--json")
|
||||
.help("Output received messages in json format, one json object per line.")
|
||||
.action(Arguments.storeTrue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
// Setup the json object mapper
|
||||
ObjectMapper jsonProcessor = new ObjectMapper();
|
||||
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
|
||||
|
||||
// Get a map of registration statuses
|
||||
Map<String, Boolean> registered;
|
||||
try {
|
||||
registered = m.areUsersRegistered(new HashSet<>(ns.getList("number")));
|
||||
} catch (IOException e) {
|
||||
System.err.println("Unable to check if users are registered");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Output
|
||||
if (ns.getBoolean("json")) {
|
||||
List<JsonIsRegistered> objects = registered.entrySet()
|
||||
.stream()
|
||||
.map(entry -> new JsonIsRegistered(entry.getKey(), entry.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
try {
|
||||
jsonProcessor.writeValue(System.out, objects);
|
||||
System.out.println();
|
||||
} catch (IOException e) {
|
||||
System.err.println(e.getMessage());
|
||||
}
|
||||
} else {
|
||||
for (Map.Entry<String, Boolean> entry : registered.entrySet()) {
|
||||
System.out.println(entry.getKey() + ": " + entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static final class JsonIsRegistered {
|
||||
|
||||
public String name;
|
||||
|
||||
public boolean isRegistered;
|
||||
|
||||
public JsonIsRegistered(String name, boolean isRegistered) {
|
||||
this.name = name;
|
||||
this.isRegistered = isRegistered;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package org.asamk.signal.commands;
|
||||
|
||||
import net.sourceforge.argparse4j.inf.Namespace;
|
||||
import net.sourceforge.argparse4j.inf.Subparser;
|
||||
|
||||
import org.asamk.Signal;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
|
||||
import org.freedesktop.dbus.exceptions.DBusExecutionException;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.GroupPatchNotAcceptedException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
|
||||
import static org.asamk.signal.util.ErrorUtils.handleIOException;
|
||||
import static org.asamk.signal.util.ErrorUtils.handleTimestampAndSendMessageResults;
|
||||
|
||||
public class JoinGroupCommand implements LocalCommand {
|
||||
|
||||
@Override
|
||||
public void attachToSubparser(final Subparser subparser) {
|
||||
subparser.addArgument("--uri").required(true).help("Specify the uri with the group invitation link.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
final GroupInviteLinkUrl linkUrl;
|
||||
String uri = ns.getString("uri");
|
||||
try {
|
||||
linkUrl = GroupInviteLinkUrl.fromUri(uri);
|
||||
} catch (GroupInviteLinkUrl.InvalidGroupLinkException e) {
|
||||
System.err.println("Group link is invalid: " + e.getMessage());
|
||||
return 2;
|
||||
} catch (GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
|
||||
System.err.println("Group link was created with an incompatible version: " + e.getMessage());
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (linkUrl == null) {
|
||||
System.err.println("Link is not a signal group invitation link");
|
||||
return 2;
|
||||
}
|
||||
|
||||
try {
|
||||
final Pair<GroupId, List<SendMessageResult>> results = m.joinGroup(linkUrl);
|
||||
GroupId newGroupId = results.first();
|
||||
if (!m.getGroup(newGroupId).isMember(m.getSelfAddress())) {
|
||||
System.out.println("Requested to join group \"" + newGroupId.toBase64() + "\"");
|
||||
} else {
|
||||
System.out.println("Joined group \"" + newGroupId.toBase64() + "\"");
|
||||
}
|
||||
return handleTimestampAndSendMessageResults(0, results.second());
|
||||
} catch (AssertionError e) {
|
||||
handleAssertionError(e);
|
||||
return 1;
|
||||
} catch (GroupPatchNotAcceptedException e) {
|
||||
System.err.println("Failed to join group, maybe already a member");
|
||||
return 1;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
handleIOException(e);
|
||||
return 1;
|
||||
} catch (Signal.Error.AttachmentInvalid e) {
|
||||
System.err.println("Failed to add avatar attachment for group\": " + e.getMessage());
|
||||
return 1;
|
||||
} catch (DBusExecutionException e) {
|
||||
System.err.println("Failed to send message: " + e.getMessage());
|
||||
return 1;
|
||||
} catch (GroupLinkNotActiveException e) {
|
||||
System.err.println("Group link is not valid: " + e.getMessage());
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace;
|
|||
import net.sourceforge.argparse4j.inf.Subparser;
|
||||
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.storage.contacts.ContactInfo;
|
||||
import org.asamk.signal.manager.storage.contacts.ContactInfo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@ -16,10 +16,6 @@ public class ListContactsCommand implements LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
List<ContactInfo> contacts = m.getContacts();
|
||||
for (ContactInfo c : contacts) {
|
||||
System.out.println(String.format("Number: %s Name: %s Blocked: %b", c.number, c.name, c.blocked));
|
||||
|
|
|
@ -18,10 +18,6 @@ public class ListDevicesCommand implements LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
List<DeviceInfo> devices = m.getLinkedDevices();
|
||||
for (DeviceInfo d : devices) {
|
||||
|
|
|
@ -5,9 +5,9 @@ import net.sourceforge.argparse4j.inf.Namespace;
|
|||
import net.sourceforge.argparse4j.inf.Subparser;
|
||||
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.storage.groups.GroupInfo;
|
||||
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
@ -35,18 +35,21 @@ public class ListGroupsCommand implements LocalCommand {
|
|||
.map(SignalServiceAddress::getLegacyIdentifier)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
final GroupInviteLinkUrl groupInviteLink = group.getGroupInviteLink();
|
||||
|
||||
System.out.println(String.format(
|
||||
"Id: %s Name: %s Active: %s Blocked: %b Members: %s Pending members: %s Requesting members: %s",
|
||||
Base64.encodeBytes(group.groupId),
|
||||
"Id: %s Name: %s Active: %s Blocked: %b Members: %s Pending members: %s Requesting members: %s Link: %s",
|
||||
group.getGroupId().toBase64(),
|
||||
group.getTitle(),
|
||||
group.isMember(m.getSelfAddress()),
|
||||
group.isBlocked(),
|
||||
members,
|
||||
pendingMembers,
|
||||
requestingMembers));
|
||||
requestingMembers,
|
||||
groupInviteLink == null ? '-' : groupInviteLink.getUrl()));
|
||||
} else {
|
||||
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b",
|
||||
Base64.encodeBytes(group.groupId),
|
||||
group.getGroupId().toBase64(),
|
||||
group.getTitle(),
|
||||
group.isMember(m.getSelfAddress()),
|
||||
group.isBlocked()));
|
||||
|
@ -61,11 +64,6 @@ public class ListGroupsCommand implements LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
List<GroupInfo> groups = m.getGroups();
|
||||
boolean detailed = ns.getBoolean("detailed");
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace;
|
|||
import net.sourceforge.argparse4j.inf.Subparser;
|
||||
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.storage.protocol.JsonIdentityKeyStore;
|
||||
import org.asamk.signal.manager.storage.protocol.IdentityInfo;
|
||||
import org.asamk.signal.util.Hex;
|
||||
import org.asamk.signal.util.Util;
|
||||
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
||||
|
@ -13,7 +13,7 @@ import java.util.List;
|
|||
|
||||
public class ListIdentitiesCommand implements LocalCommand {
|
||||
|
||||
private static void printIdentityFingerprint(Manager m, JsonIdentityKeyStore.Identity theirId) {
|
||||
private static void printIdentityFingerprint(Manager m, IdentityInfo theirId) {
|
||||
String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirId.getAddress(), theirId.getIdentityKey()));
|
||||
System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s",
|
||||
theirId.getAddress().getNumber().orNull(),
|
||||
|
@ -30,19 +30,15 @@ public class ListIdentitiesCommand implements LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
if (ns.get("number") == null) {
|
||||
for (JsonIdentityKeyStore.Identity identity : m.getIdentities()) {
|
||||
for (IdentityInfo identity : m.getIdentities()) {
|
||||
printIdentityFingerprint(m, identity);
|
||||
}
|
||||
} else {
|
||||
String number = ns.getString("number");
|
||||
try {
|
||||
List<JsonIdentityKeyStore.Identity> identities = m.getIdentities(number);
|
||||
for (JsonIdentityKeyStore.Identity id : identities) {
|
||||
List<IdentityInfo> identities = m.getIdentities(number);
|
||||
for (IdentityInfo id : identities) {
|
||||
printIdentityFingerprint(m, id);
|
||||
}
|
||||
} catch (InvalidNumberException e) {
|
||||
|
|
|
@ -3,10 +3,11 @@ package org.asamk.signal.commands;
|
|||
import net.sourceforge.argparse4j.inf.Namespace;
|
||||
import net.sourceforge.argparse4j.inf.Subparser;
|
||||
|
||||
import org.asamk.signal.manager.GroupNotFoundException;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.NotAGroupMemberException;
|
||||
import org.asamk.signal.util.GroupIdFormatException;
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupIdFormatException;
|
||||
import org.asamk.signal.manager.groups.GroupNotFoundException;
|
||||
import org.asamk.signal.manager.groups.NotAGroupMemberException;
|
||||
import org.asamk.signal.util.Util;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
|
@ -30,13 +31,8 @@ public class QuitGroupCommand implements LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
final byte[] groupId = Util.decodeGroupId(ns.getString("group"));
|
||||
final GroupId groupId = Util.decodeGroupId(ns.getString("group"));
|
||||
final Pair<Long, List<SendMessageResult>> results = m.sendQuitGroupMessage(groupId);
|
||||
return handleTimestampAndSendMessageResults(results.first(), results.second());
|
||||
} catch (IOException e) {
|
||||
|
|
|
@ -44,7 +44,7 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
|
|||
final ObjectMapper jsonProcessor;
|
||||
if (ns.getBoolean("json")) {
|
||||
jsonProcessor = new ObjectMapper();
|
||||
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect
|
||||
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
|
||||
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
|
||||
} else {
|
||||
jsonProcessor = null;
|
||||
|
@ -146,10 +146,6 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
double timeout = 5;
|
||||
if (ns.getDouble("timeout") != null) {
|
||||
timeout = ns.getDouble("timeout");
|
||||
|
|
|
@ -4,12 +4,12 @@ import net.sourceforge.argparse4j.impl.Arguments;
|
|||
import net.sourceforge.argparse4j.inf.Namespace;
|
||||
import net.sourceforge.argparse4j.inf.Subparser;
|
||||
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.RegistrationManager;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class RegisterCommand implements LocalCommand {
|
||||
public class RegisterCommand implements RegistrationCommand {
|
||||
|
||||
@Override
|
||||
public void attachToSubparser(final Subparser subparser) {
|
||||
|
@ -21,7 +21,7 @@ public class RegisterCommand implements LocalCommand {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
public int handleCommand(final Namespace ns, final RegistrationManager m) {
|
||||
try {
|
||||
final boolean voiceVerification = ns.getBoolean("voice");
|
||||
final String captcha = ns.getString("captcha");
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package org.asamk.signal.commands;
|
||||
|
||||
import net.sourceforge.argparse4j.inf.Namespace;
|
||||
|
||||
import org.asamk.signal.manager.RegistrationManager;
|
||||
|
||||
public interface RegistrationCommand extends Command {
|
||||
|
||||
int handleCommand(Namespace ns, RegistrationManager m);
|
||||
}
|
|
@ -19,10 +19,6 @@ public class RemoveDeviceCommand implements LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
int deviceId = ns.getInt("deviceId");
|
||||
m.removeLinkedDevices(deviceId);
|
||||
|
|
|
@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Subparser;
|
|||
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
@ -16,14 +17,10 @@ public class RemovePinCommand implements LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
m.setRegistrationLockPin(Optional.absent());
|
||||
return 0;
|
||||
} catch (IOException e) {
|
||||
} catch (IOException | UnauthenticatedResponseException e) {
|
||||
System.err.println("Remove pin error: " + e.getMessage());
|
||||
return 3;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import net.sourceforge.argparse4j.inf.Namespace;
|
|||
import net.sourceforge.argparse4j.inf.Subparser;
|
||||
|
||||
import org.asamk.Signal;
|
||||
import org.asamk.signal.util.GroupIdFormatException;
|
||||
import org.asamk.signal.manager.groups.GroupIdFormatException;
|
||||
import org.asamk.signal.util.IOUtils;
|
||||
import org.asamk.signal.util.Util;
|
||||
import org.freedesktop.dbus.exceptions.DBusExecutionException;
|
||||
|
@ -79,7 +79,7 @@ public class SendCommand implements DbusCommand {
|
|||
if (ns.getString("group") != null) {
|
||||
byte[] groupId;
|
||||
try {
|
||||
groupId = Util.decodeGroupId(ns.getString("group"));
|
||||
groupId = Util.decodeGroupId(ns.getString("group")).serialize();
|
||||
} catch (GroupIdFormatException e) {
|
||||
handleGroupIdFormatException(e);
|
||||
return 1;
|
||||
|
|
|
@ -17,10 +17,6 @@ public class SendContactsCommand implements LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
m.sendContacts();
|
||||
return 0;
|
||||
|
|
|
@ -4,10 +4,11 @@ import net.sourceforge.argparse4j.impl.Arguments;
|
|||
import net.sourceforge.argparse4j.inf.Namespace;
|
||||
import net.sourceforge.argparse4j.inf.Subparser;
|
||||
|
||||
import org.asamk.signal.manager.GroupNotFoundException;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.NotAGroupMemberException;
|
||||
import org.asamk.signal.util.GroupIdFormatException;
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupIdFormatException;
|
||||
import org.asamk.signal.manager.groups.GroupNotFoundException;
|
||||
import org.asamk.signal.manager.groups.NotAGroupMemberException;
|
||||
import org.asamk.signal.util.Util;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
|
@ -46,11 +47,6 @@ public class SendReactionCommand implements LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ((ns.getList("recipient") == null || ns.getList("recipient").size() == 0) && ns.getString("group") == null) {
|
||||
System.err.println("No recipients given");
|
||||
System.err.println("Aborting sending.");
|
||||
|
@ -65,7 +61,7 @@ public class SendReactionCommand implements LocalCommand {
|
|||
try {
|
||||
final Pair<Long, List<SendMessageResult>> results;
|
||||
if (ns.getString("group") != null) {
|
||||
byte[] groupId = Util.decodeGroupId(ns.getString("group"));
|
||||
GroupId groupId = Util.decodeGroupId(ns.getString("group"));
|
||||
results = m.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId);
|
||||
} else {
|
||||
results = m.sendMessageReaction(emoji,
|
||||
|
@ -74,8 +70,7 @@ public class SendReactionCommand implements LocalCommand {
|
|||
targetTimestamp,
|
||||
ns.getList("recipient"));
|
||||
}
|
||||
handleTimestampAndSendMessageResults(results.first(), results.second());
|
||||
return 0;
|
||||
return handleTimestampAndSendMessageResults(results.first(), results.second());
|
||||
} catch (IOException e) {
|
||||
handleIOException(e);
|
||||
return 3;
|
||||
|
|
|
@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Subparser;
|
|||
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
@ -18,15 +19,11 @@ public class SetPinCommand implements LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
String registrationLockPin = ns.getString("registrationLockPin");
|
||||
m.setRegistrationLockPin(Optional.of(registrationLockPin));
|
||||
return 0;
|
||||
} catch (IOException e) {
|
||||
} catch (IOException | UnauthenticatedResponseException e) {
|
||||
System.err.println("Set pin error: " + e.getMessage());
|
||||
return 3;
|
||||
}
|
||||
|
|
|
@ -27,10 +27,6 @@ public class TrustCommand implements LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
String number = ns.getString("number");
|
||||
if (ns.getBoolean("trust_all_known_keys")) {
|
||||
boolean res = m.trustIdentityAllKeys(number);
|
||||
|
|
|
@ -3,9 +3,10 @@ package org.asamk.signal.commands;
|
|||
import net.sourceforge.argparse4j.inf.Namespace;
|
||||
import net.sourceforge.argparse4j.inf.Subparser;
|
||||
|
||||
import org.asamk.signal.manager.GroupNotFoundException;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.util.GroupIdFormatException;
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupIdFormatException;
|
||||
import org.asamk.signal.manager.groups.GroupNotFoundException;
|
||||
import org.asamk.signal.util.Util;
|
||||
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
||||
|
||||
|
@ -20,11 +21,6 @@ public class UnblockCommand implements LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (String contact_number : ns.<String>getList("contact")) {
|
||||
try {
|
||||
m.setContactBlocked(contact_number, false);
|
||||
|
@ -36,7 +32,7 @@ public class UnblockCommand implements LocalCommand {
|
|||
if (ns.<String>getList("group") != null) {
|
||||
for (String groupIdString : ns.<String>getList("group")) {
|
||||
try {
|
||||
byte[] groupId = Util.decodeGroupId(groupIdString);
|
||||
GroupId groupId = Util.decodeGroupId(groupIdString);
|
||||
m.setGroupBlocked(groupId, false);
|
||||
} catch (GroupIdFormatException | GroupNotFoundException e) {
|
||||
System.err.println(e.getMessage());
|
||||
|
|
|
@ -16,10 +16,6 @@ public class UnregisterCommand implements LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
m.unregister();
|
||||
return 0;
|
||||
|
|
|
@ -16,10 +16,6 @@ public class UpdateAccountCommand implements LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
m.updateAccountAttributes();
|
||||
return 0;
|
||||
|
|
|
@ -23,11 +23,6 @@ public class UpdateContactCommand implements LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
String number = ns.getString("number");
|
||||
String name = ns.getString("name");
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace;
|
|||
import net.sourceforge.argparse4j.inf.Subparser;
|
||||
|
||||
import org.asamk.Signal;
|
||||
import org.asamk.signal.util.GroupIdFormatException;
|
||||
import org.asamk.signal.manager.groups.GroupIdFormatException;
|
||||
import org.asamk.signal.util.Util;
|
||||
import org.freedesktop.dbus.exceptions.DBusExecutionException;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
@ -35,7 +35,7 @@ public class UpdateGroupCommand implements DbusCommand {
|
|||
byte[] groupId = null;
|
||||
if (ns.getString("group") != null) {
|
||||
try {
|
||||
groupId = Util.decodeGroupId(ns.getString("group"));
|
||||
groupId = Util.decodeGroupId(ns.getString("group")).serialize();
|
||||
} catch (GroupIdFormatException e) {
|
||||
handleGroupIdFormatException(e);
|
||||
return 1;
|
||||
|
|
|
@ -25,11 +25,6 @@ public class UpdateProfileCommand implements LocalCommand {
|
|||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (!m.isRegistered()) {
|
||||
System.err.println("User is not registered.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
String name = ns.getString("name");
|
||||
String avatarPath = ns.getString("avatar");
|
||||
boolean removeAvatar = ns.getBoolean("remove_avatar");
|
||||
|
|
|
@ -6,6 +6,7 @@ import net.sourceforge.argparse4j.inf.Subparser;
|
|||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.StickerPackInvalidException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class UploadStickerPackCommand implements LocalCommand {
|
||||
|
@ -19,7 +20,7 @@ public class UploadStickerPackCommand implements LocalCommand {
|
|||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
try {
|
||||
String path = ns.getString("path");
|
||||
File path = new File(ns.getString("path"));
|
||||
String url = m.uploadStickerPack(path);
|
||||
System.out.println(url);
|
||||
return 0;
|
||||
|
|
|
@ -3,12 +3,14 @@ package org.asamk.signal.commands;
|
|||
import net.sourceforge.argparse4j.inf.Namespace;
|
||||
import net.sourceforge.argparse4j.inf.Subparser;
|
||||
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.RegistrationManager;
|
||||
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
|
||||
import org.whispersystems.signalservice.internal.push.LockedException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class VerifyCommand implements LocalCommand {
|
||||
public class VerifyCommand implements RegistrationCommand {
|
||||
|
||||
@Override
|
||||
public void attachToSubparser(final Subparser subparser) {
|
||||
|
@ -17,11 +19,7 @@ public class VerifyCommand implements LocalCommand {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int handleCommand(final Namespace ns, final Manager m) {
|
||||
if (m.isRegistered()) {
|
||||
System.err.println("User registration is already verified");
|
||||
return 1;
|
||||
}
|
||||
public int handleCommand(final Namespace ns, final RegistrationManager m) {
|
||||
try {
|
||||
String verificationCode = ns.getString("verificationCode");
|
||||
String pin = ns.getString("pin");
|
||||
|
@ -31,6 +29,12 @@ public class VerifyCommand implements LocalCommand {
|
|||
System.err.println("Verification failed! This number is locked with a pin. Hours remaining until reset: "
|
||||
+ (e.getTimeRemaining() / 1000 / 60 / 60));
|
||||
System.err.println("Use '--pin PIN_CODE' to specify the registration lock PIN");
|
||||
return 1;
|
||||
} catch (KeyBackupServicePinException e) {
|
||||
System.err.println("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining());
|
||||
return 1;
|
||||
} catch (KeyBackupSystemNoDataException e) {
|
||||
System.err.println("Verification failed! No KBS data.");
|
||||
return 3;
|
||||
} catch (IOException e) {
|
||||
System.err.println("Verify error: " + e.getMessage());
|
||||
|
|
|
@ -2,10 +2,11 @@ package org.asamk.signal.dbus;
|
|||
|
||||
import org.asamk.Signal;
|
||||
import org.asamk.signal.manager.AttachmentInvalidException;
|
||||
import org.asamk.signal.manager.GroupNotFoundException;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.NotAGroupMemberException;
|
||||
import org.asamk.signal.storage.groups.GroupInfo;
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupNotFoundException;
|
||||
import org.asamk.signal.manager.groups.NotAGroupMemberException;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
||||
import org.asamk.signal.util.ErrorUtils;
|
||||
import org.freedesktop.dbus.exceptions.DBusExecutionException;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
@ -15,7 +16,6 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
@ -92,7 +92,9 @@ public class DbusSignalImpl implements Signal {
|
|||
@Override
|
||||
public long sendGroupMessage(final String message, final List<String> attachments, final byte[] groupId) {
|
||||
try {
|
||||
Pair<Long, List<SendMessageResult>> results = m.sendGroupMessage(message, attachments, groupId);
|
||||
Pair<Long, List<SendMessageResult>> results = m.sendGroupMessage(message,
|
||||
attachments,
|
||||
GroupId.unknownVersion(groupId));
|
||||
checkSendMessageResults(results.first(), results.second());
|
||||
return results.first();
|
||||
} catch (IOException e) {
|
||||
|
@ -134,7 +136,7 @@ public class DbusSignalImpl implements Signal {
|
|||
@Override
|
||||
public void setGroupBlocked(final byte[] groupId, final boolean blocked) {
|
||||
try {
|
||||
m.setGroupBlocked(groupId, blocked);
|
||||
m.setGroupBlocked(GroupId.unknownVersion(groupId), blocked);
|
||||
} catch (GroupNotFoundException e) {
|
||||
throw new Error.GroupNotFound(e.getMessage());
|
||||
}
|
||||
|
@ -145,14 +147,14 @@ public class DbusSignalImpl implements Signal {
|
|||
List<GroupInfo> groups = m.getGroups();
|
||||
List<byte[]> ids = new ArrayList<>(groups.size());
|
||||
for (GroupInfo group : groups) {
|
||||
ids.add(group.groupId);
|
||||
ids.add(group.getGroupId().serialize());
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroupName(final byte[] groupId) {
|
||||
GroupInfo group = m.getGroup(groupId);
|
||||
GroupInfo group = m.getGroup(GroupId.unknownVersion(groupId));
|
||||
if (group == null) {
|
||||
return "";
|
||||
} else {
|
||||
|
@ -162,9 +164,9 @@ public class DbusSignalImpl implements Signal {
|
|||
|
||||
@Override
|
||||
public List<String> getGroupMembers(final byte[] groupId) {
|
||||
GroupInfo group = m.getGroup(groupId);
|
||||
GroupInfo group = m.getGroup(GroupId.unknownVersion(groupId));
|
||||
if (group == null) {
|
||||
return Collections.emptyList();
|
||||
return List.of();
|
||||
} else {
|
||||
return group.getMembers()
|
||||
.stream()
|
||||
|
@ -189,9 +191,11 @@ public class DbusSignalImpl implements Signal {
|
|||
if (avatar.isEmpty()) {
|
||||
avatar = null;
|
||||
}
|
||||
final Pair<byte[], List<SendMessageResult>> results = m.updateGroup(groupId, name, members, avatar);
|
||||
final Pair<GroupId, List<SendMessageResult>> results = m.updateGroup(groupId == null
|
||||
? null
|
||||
: GroupId.unknownVersion(groupId), name, members, avatar);
|
||||
checkSendMessageResults(0, results.second());
|
||||
return results.first();
|
||||
return results.first().serialize();
|
||||
} catch (IOException e) {
|
||||
throw new Error.Failure(e.getMessage());
|
||||
} catch (GroupNotFoundException | NotAGroupMemberException e) {
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
package org.asamk.signal.json;
|
||||
|
||||
import org.asamk.Signal;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
@ -15,10 +14,14 @@ class JsonDataMessage {
|
|||
long timestamp;
|
||||
String message;
|
||||
int expiresInSeconds;
|
||||
|
||||
JsonReaction reaction;
|
||||
JsonQuote quote;
|
||||
List<JsonMention> mentions;
|
||||
List<JsonAttachment> attachments;
|
||||
JsonGroupInfo groupInfo;
|
||||
|
||||
JsonDataMessage(SignalServiceDataMessage dataMessage) {
|
||||
JsonDataMessage(SignalServiceDataMessage dataMessage, Manager m) {
|
||||
this.timestamp = dataMessage.getTimestamp();
|
||||
if (dataMessage.getGroupContext().isPresent()) {
|
||||
if (dataMessage.getGroupContext().get().getGroupV1().isPresent()) {
|
||||
|
@ -33,13 +36,29 @@ class JsonDataMessage {
|
|||
this.message = dataMessage.getBody().get();
|
||||
}
|
||||
this.expiresInSeconds = dataMessage.getExpiresInSeconds();
|
||||
if (dataMessage.getAttachments().isPresent()) {
|
||||
this.attachments = new ArrayList<>(dataMessage.getAttachments().get().size());
|
||||
for (SignalServiceAttachment attachment : dataMessage.getAttachments().get()) {
|
||||
this.attachments.add(new JsonAttachment(attachment));
|
||||
}
|
||||
if (dataMessage.getReaction().isPresent()) {
|
||||
this.reaction = new JsonReaction(dataMessage.getReaction().get(), m);
|
||||
}
|
||||
if (dataMessage.getQuote().isPresent()) {
|
||||
this.quote = new JsonQuote(dataMessage.getQuote().get(), m);
|
||||
}
|
||||
if (dataMessage.getMentions().isPresent()) {
|
||||
this.mentions = dataMessage.getMentions()
|
||||
.get()
|
||||
.stream()
|
||||
.map(mention -> new JsonMention(mention, m))
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
this.attachments = new ArrayList<>();
|
||||
this.mentions = List.of();
|
||||
}
|
||||
if (dataMessage.getAttachments().isPresent()) {
|
||||
this.attachments = dataMessage.getAttachments()
|
||||
.get()
|
||||
.stream()
|
||||
.map(JsonAttachment::new)
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
this.attachments = List.of();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,6 +66,9 @@ class JsonDataMessage {
|
|||
timestamp = messageReceived.getTimestamp();
|
||||
message = messageReceived.getMessage();
|
||||
groupInfo = new JsonGroupInfo(messageReceived.getGroupId());
|
||||
reaction = null; // TODO Replace these 3 with the proper commands
|
||||
quote = null;
|
||||
mentions = null;
|
||||
attachments = messageReceived.getAttachments().stream().map(JsonAttachment::new).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
@ -54,6 +76,9 @@ class JsonDataMessage {
|
|||
timestamp = messageReceived.getTimestamp();
|
||||
message = messageReceived.getMessage();
|
||||
groupInfo = new JsonGroupInfo(messageReceived.getGroupId());
|
||||
reaction = null; // TODO Replace these 3 with the proper commands
|
||||
quote = null;
|
||||
mentions = null;
|
||||
attachments = messageReceived.getAttachments().stream().map(JsonAttachment::new).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package org.asamk.signal.json;
|
||||
|
||||
import org.asamk.signal.manager.GroupUtils;
|
||||
import org.asamk.signal.manager.groups.GroupUtils;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
@ -31,7 +31,7 @@ class JsonGroupInfo {
|
|||
}
|
||||
|
||||
JsonGroupInfo(SignalServiceGroupV2 groupInfo) {
|
||||
this.groupId = Base64.encodeBytes(GroupUtils.getGroupId(groupInfo.getMasterKey()));
|
||||
this.groupId = GroupUtils.getGroupIdV2(groupInfo.getMasterKey()).toBase64();
|
||||
this.type = groupInfo.hasSignedGroupChange() ? "UPDATE" : "DELIVER";
|
||||
}
|
||||
|
||||
|
|
19
src/main/java/org/asamk/signal/json/JsonMention.java
Normal file
19
src/main/java/org/asamk/signal/json/JsonMention.java
Normal file
|
@ -0,0 +1,19 @@
|
|||
package org.asamk.signal.json;
|
||||
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
public class JsonMention {
|
||||
|
||||
String name;
|
||||
int start;
|
||||
int length;
|
||||
|
||||
JsonMention(SignalServiceDataMessage.Mention mention, Manager m) {
|
||||
this.name = m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid(), null))
|
||||
.getLegacyIdentifier();
|
||||
this.start = mention.getStart();
|
||||
this.length = mention.getLength();
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package org.asamk.signal.json;
|
||||
|
||||
import org.asamk.Signal;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
@ -18,7 +19,7 @@ public class JsonMessageEnvelope {
|
|||
JsonCallMessage callMessage;
|
||||
JsonReceiptMessage receiptMessage;
|
||||
|
||||
public JsonMessageEnvelope(SignalServiceEnvelope envelope, SignalServiceContent content) {
|
||||
public JsonMessageEnvelope(SignalServiceEnvelope envelope, SignalServiceContent content, Manager m) {
|
||||
if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
|
||||
SignalServiceAddress source = envelope.getSourceAddress();
|
||||
this.source = source.getLegacyIdentifier();
|
||||
|
@ -35,10 +36,10 @@ public class JsonMessageEnvelope {
|
|||
this.sourceDevice = content.getSenderDevice();
|
||||
}
|
||||
if (content.getDataMessage().isPresent()) {
|
||||
this.dataMessage = new JsonDataMessage(content.getDataMessage().get());
|
||||
this.dataMessage = new JsonDataMessage(content.getDataMessage().get(), m);
|
||||
}
|
||||
if (content.getSyncMessage().isPresent()) {
|
||||
this.syncMessage = new JsonSyncMessage(content.getSyncMessage().get());
|
||||
this.syncMessage = new JsonSyncMessage(content.getSyncMessage().get(), m);
|
||||
}
|
||||
if (content.getCallMessage().isPresent()) {
|
||||
this.callMessage = new JsonCallMessage(content.getCallMessage().get());
|
||||
|
|
40
src/main/java/org/asamk/signal/json/JsonQuote.java
Normal file
40
src/main/java/org/asamk/signal/json/JsonQuote.java
Normal file
|
@ -0,0 +1,40 @@
|
|||
package org.asamk.signal.json;
|
||||
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class JsonQuote {
|
||||
|
||||
long id;
|
||||
String author;
|
||||
String text;
|
||||
|
||||
List<JsonMention> mentions;
|
||||
List<JsonQuotedAttachment> attachments;
|
||||
|
||||
JsonQuote(SignalServiceDataMessage.Quote quote, Manager m) {
|
||||
this.id = quote.getId();
|
||||
this.author = m.resolveSignalServiceAddress(quote.getAuthor()).getLegacyIdentifier();
|
||||
this.text = quote.getText();
|
||||
|
||||
if (quote.getMentions() != null && quote.getMentions().size() > 0) {
|
||||
this.mentions = quote.getMentions()
|
||||
.stream()
|
||||
.map(quotedMention -> new JsonMention(quotedMention, m))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (quote.getAttachments().size() > 0) {
|
||||
this.attachments = quote.getAttachments()
|
||||
.stream()
|
||||
.map(JsonQuotedAttachment::new)
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
this.attachments = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package org.asamk.signal.json;
|
||||
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
|
||||
public class JsonQuotedAttachment {
|
||||
|
||||
String contentType;
|
||||
String filename;
|
||||
JsonAttachment thumbnail;
|
||||
|
||||
JsonQuotedAttachment(SignalServiceDataMessage.Quote.QuotedAttachment quotedAttachment) {
|
||||
contentType = quotedAttachment.getContentType();
|
||||
filename = quotedAttachment.getFileName();
|
||||
if (quotedAttachment.getThumbnail() != null) {
|
||||
thumbnail = new JsonAttachment(quotedAttachment.getThumbnail());
|
||||
} else {
|
||||
thumbnail = null;
|
||||
}
|
||||
}
|
||||
}
|
19
src/main/java/org/asamk/signal/json/JsonReaction.java
Normal file
19
src/main/java/org/asamk/signal/json/JsonReaction.java
Normal file
|
@ -0,0 +1,19 @@
|
|||
package org.asamk.signal.json;
|
||||
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Reaction;
|
||||
|
||||
public class JsonReaction {
|
||||
|
||||
String emoji;
|
||||
String targetAuthor;
|
||||
long targetSentTimestamp;
|
||||
boolean isRemove;
|
||||
|
||||
JsonReaction(Reaction reaction, Manager m) {
|
||||
this.emoji = reaction.getEmoji();
|
||||
this.targetAuthor = m.resolveSignalServiceAddress(reaction.getTargetAuthor()).getLegacyIdentifier();
|
||||
this.targetSentTimestamp = reaction.getTargetSentTimestamp();
|
||||
this.isRemove = reaction.isRemove();
|
||||
}
|
||||
}
|
|
@ -1,14 +1,15 @@
|
|||
package org.asamk.signal.json;
|
||||
|
||||
import org.asamk.Signal;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||
|
||||
class JsonSyncDataMessage extends JsonDataMessage {
|
||||
|
||||
String destination;
|
||||
|
||||
JsonSyncDataMessage(SentTranscriptMessage transcriptMessage) {
|
||||
super(transcriptMessage.getMessage());
|
||||
JsonSyncDataMessage(SentTranscriptMessage transcriptMessage, Manager m) {
|
||||
super(transcriptMessage.getMessage(), m);
|
||||
if (transcriptMessage.getDestination().isPresent()) {
|
||||
this.destination = transcriptMessage.getDestination().get().getLegacyIdentifier();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.asamk.signal.json;
|
||||
|
||||
import org.asamk.Signal;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
@ -21,9 +22,9 @@ class JsonSyncMessage {
|
|||
List<ReadMessage> readMessages;
|
||||
JsonSyncMessageType type;
|
||||
|
||||
JsonSyncMessage(SignalServiceSyncMessage syncMessage) {
|
||||
JsonSyncMessage(SignalServiceSyncMessage syncMessage, Manager m) {
|
||||
if (syncMessage.getSent().isPresent()) {
|
||||
this.sentMessage = new JsonSyncDataMessage(syncMessage.getSent().get());
|
||||
this.sentMessage = new JsonSyncDataMessage(syncMessage.getSent().get(), m);
|
||||
}
|
||||
if (syncMessage.getBlockedList().isPresent()) {
|
||||
this.blockedNumbers = new ArrayList<>(syncMessage.getBlockedList().get().getAddresses().size());
|
||||
|
|
65
src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java
Normal file
65
src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java
Normal file
|
@ -0,0 +1,65 @@
|
|||
package org.asamk.signal.manager;
|
||||
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URLDecoder;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
||||
|
||||
public class DeviceLinkInfo {
|
||||
|
||||
final String deviceIdentifier;
|
||||
final ECPublicKey deviceKey;
|
||||
|
||||
public static DeviceLinkInfo parseDeviceLinkUri(URI linkUri) throws IOException, InvalidKeyException {
|
||||
final String rawQuery = linkUri.getRawQuery();
|
||||
if (isEmpty(rawQuery)) {
|
||||
throw new RuntimeException("Invalid device link uri");
|
||||
}
|
||||
|
||||
Map<String, String> query = getQueryMap(rawQuery);
|
||||
String deviceIdentifier = query.get("uuid");
|
||||
String publicKeyEncoded = query.get("pub_key");
|
||||
|
||||
if (isEmpty(deviceIdentifier) || isEmpty(publicKeyEncoded)) {
|
||||
throw new RuntimeException("Invalid device link uri");
|
||||
}
|
||||
|
||||
ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0);
|
||||
|
||||
return new DeviceLinkInfo(deviceIdentifier, deviceKey);
|
||||
}
|
||||
|
||||
private static Map<String, String> getQueryMap(String query) {
|
||||
String[] params = query.split("&");
|
||||
Map<String, String> map = new HashMap<>();
|
||||
for (String param : params) {
|
||||
final String[] paramParts = param.split("=");
|
||||
String name = URLDecoder.decode(paramParts[0], StandardCharsets.UTF_8);
|
||||
String value = URLDecoder.decode(paramParts[1], StandardCharsets.UTF_8);
|
||||
map.put(name, value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
public DeviceLinkInfo(final String deviceIdentifier, final ECPublicKey deviceKey) {
|
||||
this.deviceIdentifier = deviceIdentifier;
|
||||
this.deviceKey = deviceKey;
|
||||
}
|
||||
|
||||
public String createDeviceLinkUri() {
|
||||
return "tsdevice:/?uuid="
|
||||
+ URLEncoder.encode(deviceIdentifier, StandardCharsets.UTF_8)
|
||||
+ "&pub_key="
|
||||
+ URLEncoder.encode(Base64.encodeBytesWithoutPadding(deviceKey.serialize()), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package org.asamk.signal.manager;
|
||||
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
public class GroupNotFoundException extends Exception {
|
||||
|
||||
public GroupNotFoundException(byte[] groupId) {
|
||||
super("Group not found: " + Base64.encodeBytes(groupId));
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
package org.asamk.signal.manager;
|
||||
|
||||
import org.asamk.signal.manager.groups.GroupIdV1;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
interface HandleAction {
|
||||
|
@ -93,9 +93,9 @@ class SendSyncBlockedListAction implements HandleAction {
|
|||
class SendGroupInfoRequestAction implements HandleAction {
|
||||
|
||||
private final SignalServiceAddress address;
|
||||
private final byte[] groupId;
|
||||
private final GroupIdV1 groupId;
|
||||
|
||||
public SendGroupInfoRequestAction(final SignalServiceAddress address, final byte[] groupId) {
|
||||
public SendGroupInfoRequestAction(final SignalServiceAddress address, final GroupIdV1 groupId) {
|
||||
this.address = address;
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
@ -109,14 +109,17 @@ class SendGroupInfoRequestAction implements HandleAction {
|
|||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
final SendGroupInfoRequestAction that = (SendGroupInfoRequestAction) o;
|
||||
return address.equals(that.address) && Arrays.equals(groupId, that.groupId);
|
||||
|
||||
if (!address.equals(that.address)) return false;
|
||||
return groupId.equals(that.groupId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(address);
|
||||
result = 31 * result + Arrays.hashCode(groupId);
|
||||
int result = address.hashCode();
|
||||
result = 31 * result + groupId.hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -124,9 +127,9 @@ class SendGroupInfoRequestAction implements HandleAction {
|
|||
class SendGroupUpdateAction implements HandleAction {
|
||||
|
||||
private final SignalServiceAddress address;
|
||||
private final byte[] groupId;
|
||||
private final GroupIdV1 groupId;
|
||||
|
||||
public SendGroupUpdateAction(final SignalServiceAddress address, final byte[] groupId) {
|
||||
public SendGroupUpdateAction(final SignalServiceAddress address, final GroupIdV1 groupId) {
|
||||
this.address = address;
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
@ -140,14 +143,17 @@ class SendGroupUpdateAction implements HandleAction {
|
|||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
final SendGroupUpdateAction that = (SendGroupUpdateAction) o;
|
||||
return address.equals(that.address) && Arrays.equals(groupId, that.groupId);
|
||||
|
||||
if (!address.equals(that.address)) return false;
|
||||
return groupId.equals(that.groupId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(address);
|
||||
result = 31 * result + Arrays.hashCode(groupId);
|
||||
int result = address.hashCode();
|
||||
result = 31 * result + groupId.hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
package org.asamk.signal.manager;
|
||||
|
||||
import org.asamk.signal.util.RandomUtils;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
class KeyUtils {
|
||||
|
||||
private KeyUtils() {
|
||||
}
|
||||
|
||||
static String createSignalingKey() {
|
||||
return getSecret(52);
|
||||
}
|
||||
|
||||
static ProfileKey createProfileKey() {
|
||||
try {
|
||||
return new ProfileKey(getSecretBytes(32));
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError("Profile key is guaranteed to be 32 bytes here");
|
||||
}
|
||||
}
|
||||
|
||||
static String createPassword() {
|
||||
return getSecret(18);
|
||||
}
|
||||
|
||||
static byte[] createGroupId() {
|
||||
return getSecretBytes(16);
|
||||
}
|
||||
|
||||
static byte[] createStickerUploadKey() {
|
||||
return getSecretBytes(32);
|
||||
}
|
||||
|
||||
private static String getSecret(int size) {
|
||||
byte[] secret = getSecretBytes(size);
|
||||
return Base64.encodeBytes(secret);
|
||||
}
|
||||
|
||||
private static byte[] getSecretBytes(int size) {
|
||||
byte[] secret = new byte[size];
|
||||
RandomUtils.getSecureRandom().nextBytes(secret);
|
||||
return secret;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,10 +0,0 @@
|
|||
package org.asamk.signal.manager;
|
||||
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
public class NotAGroupMemberException extends Exception {
|
||||
|
||||
public NotAGroupMemberException(byte[] groupId, String groupName) {
|
||||
super("User is not a member in group: " + groupName + " (" + Base64.encodeBytes(groupId) + ")");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.asamk.signal.manager;
|
||||
|
||||
public class NotRegisteredException extends Exception {
|
||||
|
||||
public NotRegisteredException() {
|
||||
super("User is not registered.");
|
||||
}
|
||||
}
|
|
@ -1,30 +1,34 @@
|
|||
package org.asamk.signal.manager;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class PathConfig {
|
||||
|
||||
private final String dataPath;
|
||||
private final String attachmentsPath;
|
||||
private final String avatarsPath;
|
||||
private final File dataPath;
|
||||
private final File attachmentsPath;
|
||||
private final File avatarsPath;
|
||||
|
||||
public static PathConfig createDefault(final String settingsPath) {
|
||||
return new PathConfig(settingsPath + "/data", settingsPath + "/attachments", settingsPath + "/avatars");
|
||||
public static PathConfig createDefault(final File settingsPath) {
|
||||
return new PathConfig(new File(settingsPath, "data"),
|
||||
new File(settingsPath, "attachments"),
|
||||
new File(settingsPath, "avatars"));
|
||||
}
|
||||
|
||||
private PathConfig(final String dataPath, final String attachmentsPath, final String avatarsPath) {
|
||||
private PathConfig(final File dataPath, final File attachmentsPath, final File avatarsPath) {
|
||||
this.dataPath = dataPath;
|
||||
this.attachmentsPath = attachmentsPath;
|
||||
this.avatarsPath = avatarsPath;
|
||||
}
|
||||
|
||||
public String getDataPath() {
|
||||
public File getDataPath() {
|
||||
return dataPath;
|
||||
}
|
||||
|
||||
public String getAttachmentsPath() {
|
||||
public File getAttachmentsPath() {
|
||||
return attachmentsPath;
|
||||
}
|
||||
|
||||
public String getAvatarsPath() {
|
||||
public File getAvatarsPath() {
|
||||
return avatarsPath;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright (C) 2015-2020 AsamK and contributors
|
||||
Copyright (C) 2015-2021 AsamK and contributors
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -16,7 +16,8 @@
|
|||
*/
|
||||
package org.asamk.signal.manager;
|
||||
|
||||
import org.asamk.signal.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
|
@ -31,6 +32,7 @@ import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
|
|||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
|
@ -45,12 +47,12 @@ public class ProvisioningManager {
|
|||
private final int registrationId;
|
||||
private final String password;
|
||||
|
||||
public ProvisioningManager(String settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) {
|
||||
public ProvisioningManager(File settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) {
|
||||
this.pathConfig = PathConfig.createDefault(settingsPath);
|
||||
this.serviceConfiguration = serviceConfiguration;
|
||||
this.userAgent = userAgent;
|
||||
|
||||
identityKey = KeyHelper.generateIdentityKeyPair();
|
||||
identityKey = KeyUtils.generateIdentityKeyPair();
|
||||
registrationId = KeyHelper.generateRegistrationId(false);
|
||||
password = KeyUtils.createPassword();
|
||||
final SleepTimer timer = new UptimeSleepTimer();
|
||||
|
@ -70,8 +72,7 @@ public class ProvisioningManager {
|
|||
public String getDeviceLinkUri() throws TimeoutException, IOException {
|
||||
String deviceUuid = accountManager.getNewDeviceUuid();
|
||||
|
||||
return Utils.createDeviceLinkUri(new Utils.DeviceLinkInfo(deviceUuid,
|
||||
identityKey.getPublicKey().getPublicKey()));
|
||||
return new DeviceLinkInfo(deviceUuid, identityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
|
||||
}
|
||||
|
||||
public String finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists {
|
||||
|
@ -123,8 +124,10 @@ public class ProvisioningManager {
|
|||
m.requestSyncBlocked();
|
||||
m.requestSyncConfiguration();
|
||||
|
||||
m.saveAccount();
|
||||
m.close(false);
|
||||
}
|
||||
|
||||
account.save();
|
||||
}
|
||||
|
||||
return username;
|
||||
|
|
194
src/main/java/org/asamk/signal/manager/RegistrationManager.java
Normal file
194
src/main/java/org/asamk/signal/manager/RegistrationManager.java
Normal file
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
Copyright (C) 2015-2021 AsamK and contributors
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.asamk.signal.manager;
|
||||
|
||||
import org.asamk.signal.manager.helper.PinHelper;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.util.KeyHelper;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.KbsPinData;
|
||||
import org.whispersystems.signalservice.api.KeyBackupService;
|
||||
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.SleepTimer;
|
||||
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.push.LockedException;
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
||||
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
|
||||
public class RegistrationManager implements AutoCloseable {
|
||||
|
||||
private SignalAccount account;
|
||||
private final PathConfig pathConfig;
|
||||
private final SignalServiceConfiguration serviceConfiguration;
|
||||
private final String userAgent;
|
||||
|
||||
private final SignalServiceAccountManager accountManager;
|
||||
private final PinHelper pinHelper;
|
||||
|
||||
public RegistrationManager(
|
||||
SignalAccount account,
|
||||
PathConfig pathConfig,
|
||||
SignalServiceConfiguration serviceConfiguration,
|
||||
String userAgent
|
||||
) {
|
||||
this.account = account;
|
||||
this.pathConfig = pathConfig;
|
||||
this.serviceConfiguration = serviceConfiguration;
|
||||
this.userAgent = userAgent;
|
||||
|
||||
final SleepTimer timer = new UptimeSleepTimer();
|
||||
this.accountManager = new SignalServiceAccountManager(serviceConfiguration, new DynamicCredentialsProvider(
|
||||
// Using empty UUID, because registering doesn't work otherwise
|
||||
null,
|
||||
account.getUsername(),
|
||||
account.getPassword(),
|
||||
account.getSignalingKey(),
|
||||
SignalServiceAddress.DEFAULT_DEVICE_ID), userAgent, null, timer);
|
||||
final KeyBackupService keyBackupService = ServiceConfig.createKeyBackupService(accountManager);
|
||||
this.pinHelper = new PinHelper(keyBackupService);
|
||||
}
|
||||
|
||||
public static RegistrationManager init(
|
||||
String username, File settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent
|
||||
) throws IOException {
|
||||
PathConfig pathConfig = PathConfig.createDefault(settingsPath);
|
||||
|
||||
if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) {
|
||||
IdentityKeyPair identityKey = KeyUtils.generateIdentityKeyPair();
|
||||
int registrationId = KeyHelper.generateRegistrationId(false);
|
||||
|
||||
ProfileKey profileKey = KeyUtils.createProfileKey();
|
||||
SignalAccount account = SignalAccount.create(pathConfig.getDataPath(),
|
||||
username,
|
||||
identityKey,
|
||||
registrationId,
|
||||
profileKey);
|
||||
account.save();
|
||||
|
||||
return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent);
|
||||
}
|
||||
|
||||
SignalAccount account = SignalAccount.load(pathConfig.getDataPath(), username);
|
||||
|
||||
return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent);
|
||||
}
|
||||
|
||||
public void register(boolean voiceVerification, String captcha) throws IOException {
|
||||
if (account.getPassword() == null) {
|
||||
account.setPassword(KeyUtils.createPassword());
|
||||
}
|
||||
|
||||
if (voiceVerification) {
|
||||
accountManager.requestVoiceVerificationCode(Locale.getDefault(),
|
||||
Optional.fromNullable(captcha),
|
||||
Optional.absent());
|
||||
} else {
|
||||
accountManager.requestSmsVerificationCode(false, Optional.fromNullable(captcha), Optional.absent());
|
||||
}
|
||||
|
||||
account.setRegistered(false);
|
||||
account.save();
|
||||
}
|
||||
|
||||
public void verifyAccount(
|
||||
String verificationCode, String pin
|
||||
) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException {
|
||||
verificationCode = verificationCode.replace("-", "");
|
||||
if (account.getSignalingKey() == null) {
|
||||
account.setSignalingKey(KeyUtils.createSignalingKey());
|
||||
}
|
||||
VerifyAccountResponse response;
|
||||
try {
|
||||
response = verifyAccountWithCode(verificationCode, pin, null);
|
||||
account.setPinMasterKey(null);
|
||||
} catch (LockedException e) {
|
||||
if (pin == null) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
KbsPinData registrationLockData = pinHelper.getRegistrationLockData(pin, e);
|
||||
if (registrationLockData == null) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
String registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock();
|
||||
try {
|
||||
response = verifyAccountWithCode(verificationCode, null, registrationLock);
|
||||
} catch (LockedException _e) {
|
||||
throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
|
||||
}
|
||||
account.setPinMasterKey(registrationLockData.getMasterKey());
|
||||
}
|
||||
|
||||
// TODO response.isStorageCapable()
|
||||
//accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
|
||||
|
||||
account.setDeviceId(SignalServiceAddress.DEFAULT_DEVICE_ID);
|
||||
account.setMultiDevice(false);
|
||||
account.setRegistered(true);
|
||||
account.setUuid(UuidUtil.parseOrNull(response.getUuid()));
|
||||
account.setRegistrationLockPin(pin);
|
||||
account.getSignalProtocolStore()
|
||||
.saveIdentity(account.getSelfAddress(),
|
||||
account.getSignalProtocolStore().getIdentityKeyPair().getPublicKey(),
|
||||
TrustLevel.TRUSTED_VERIFIED);
|
||||
|
||||
try (Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent)) {
|
||||
|
||||
m.refreshPreKeys();
|
||||
|
||||
m.close(false);
|
||||
}
|
||||
|
||||
account.save();
|
||||
}
|
||||
|
||||
private VerifyAccountResponse verifyAccountWithCode(
|
||||
final String verificationCode, final String legacyPin, final String registrationLock
|
||||
) throws IOException {
|
||||
return accountManager.verifyAccountWithCode(verificationCode,
|
||||
account.getSignalingKey(),
|
||||
account.getSignalProtocolStore().getLocalRegistrationId(),
|
||||
true,
|
||||
legacyPin,
|
||||
registrationLock,
|
||||
account.getSelfUnidentifiedAccessKey(),
|
||||
account.isUnrestrictedUnidentifiedAccess(),
|
||||
ServiceConfig.capabilities,
|
||||
account.isDiscoverableByPhoneNumber());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
if (account != null) {
|
||||
account.close();
|
||||
account = null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,13 @@
|
|||
package org.asamk.signal.manager;
|
||||
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.signal.zkgroup.ServerPublicParams;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.KeyBackupService;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
|
||||
|
@ -10,14 +16,13 @@ import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupSe
|
|||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.Collections;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -26,7 +31,8 @@ import okhttp3.Interceptor;
|
|||
|
||||
public class ServiceConfig {
|
||||
|
||||
final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF";
|
||||
final static byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
|
||||
.decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF");
|
||||
final static int PREKEY_MINIMUM_COUNT = 20;
|
||||
final static int PREKEY_BATCH_SIZE = 100;
|
||||
final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
|
||||
|
@ -35,6 +41,11 @@ public class ServiceConfig {
|
|||
|
||||
final static String CDS_MRENCLAVE = "c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15";
|
||||
|
||||
final static String KEY_BACKUP_ENCLAVE_NAME = "fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe";
|
||||
final static byte[] KEY_BACKUP_SERVICE_ID = Hex.decode(
|
||||
"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe");
|
||||
final static String KEY_BACKUP_MRENCLAVE = "a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87";
|
||||
|
||||
private final static String URL = "https://textsecure-service.whispersystems.org";
|
||||
private final static String CDN_URL = "https://cdn.signal.org";
|
||||
private final static String CDN2_URL = "https://cdn2.signal.org";
|
||||
|
@ -46,18 +57,12 @@ public class ServiceConfig {
|
|||
|
||||
private final static Optional<Dns> dns = Optional.absent();
|
||||
|
||||
private final static String zkGroupServerPublicParamsHex = "AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=";
|
||||
private final static byte[] zkGroupServerPublicParams;
|
||||
private final static byte[] zkGroupServerPublicParams = Base64.getDecoder()
|
||||
.decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=");
|
||||
|
||||
static final AccountAttributes.Capabilities capabilities;
|
||||
|
||||
static {
|
||||
try {
|
||||
zkGroupServerPublicParams = Base64.decode(zkGroupServerPublicParamsHex);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
boolean zkGroupAvailable;
|
||||
try {
|
||||
new ServerPublicParams(zkGroupServerPublicParams);
|
||||
|
@ -74,7 +79,7 @@ public class ServiceConfig {
|
|||
.header("User-Agent", userAgent)
|
||||
.build());
|
||||
|
||||
final List<Interceptor> interceptors = Collections.singletonList(userAgentInterceptor);
|
||||
final List<Interceptor> interceptors = List.of(userAgentInterceptor);
|
||||
|
||||
return new SignalServiceConfiguration(new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)},
|
||||
makeSignalCdnUrlMapFor(new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)},
|
||||
|
@ -88,6 +93,10 @@ public class ServiceConfig {
|
|||
zkGroupServerPublicParams);
|
||||
}
|
||||
|
||||
public static AccountAttributes.Capabilities getCapabilities() {
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
static KeyStore getIasKeyStore() {
|
||||
try {
|
||||
TrustStore contactTrustStore = IAS_TRUST_STORE;
|
||||
|
@ -102,6 +111,24 @@ public class ServiceConfig {
|
|||
}
|
||||
}
|
||||
|
||||
static KeyBackupService createKeyBackupService(SignalServiceAccountManager accountManager) {
|
||||
KeyStore keyStore = ServiceConfig.getIasKeyStore();
|
||||
|
||||
return accountManager.getKeyBackupService(keyStore,
|
||||
ServiceConfig.KEY_BACKUP_ENCLAVE_NAME,
|
||||
ServiceConfig.KEY_BACKUP_SERVICE_ID,
|
||||
ServiceConfig.KEY_BACKUP_MRENCLAVE,
|
||||
10);
|
||||
}
|
||||
|
||||
static ECPublicKey getUnidentifiedSenderTrustRoot() {
|
||||
try {
|
||||
return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<Integer, SignalCdnUrl[]> makeSignalCdnUrlMapFor(
|
||||
SignalCdnUrl[] cdn0Urls, SignalCdnUrl[] cdn2Urls
|
||||
) {
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package org.asamk.signal.manager;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class UserAlreadyExists extends Exception {
|
||||
|
||||
private final String username;
|
||||
private final String fileName;
|
||||
private final File fileName;
|
||||
|
||||
public UserAlreadyExists(String username, String fileName) {
|
||||
public UserAlreadyExists(String username, File fileName) {
|
||||
this.username = username;
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
@ -14,7 +16,7 @@ public class UserAlreadyExists extends Exception {
|
|||
return username;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
public File getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,304 +0,0 @@
|
|||
package org.asamk.signal.manager;
|
||||
|
||||
import org.signal.libsignal.metadata.certificate.CertificateValidator;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
||||
import org.whispersystems.libsignal.fingerprint.Fingerprint;
|
||||
import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLDecoder;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
||||
|
||||
class Utils {
|
||||
|
||||
static List<SignalServiceAttachment> getSignalServiceAttachments(List<String> attachments) throws AttachmentInvalidException {
|
||||
List<SignalServiceAttachment> signalServiceAttachments = null;
|
||||
if (attachments != null) {
|
||||
signalServiceAttachments = new ArrayList<>(attachments.size());
|
||||
for (String attachment : attachments) {
|
||||
try {
|
||||
signalServiceAttachments.add(createAttachment(new File(attachment)));
|
||||
} catch (IOException e) {
|
||||
throw new AttachmentInvalidException(attachment, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return signalServiceAttachments;
|
||||
}
|
||||
|
||||
static String getFileMimeType(File file, String defaultMimeType) throws IOException {
|
||||
String mime = Files.probeContentType(file.toPath());
|
||||
if (mime == null) {
|
||||
try (InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) {
|
||||
mime = URLConnection.guessContentTypeFromStream(bufferedStream);
|
||||
}
|
||||
}
|
||||
if (mime == null) {
|
||||
return defaultMimeType;
|
||||
}
|
||||
return mime;
|
||||
}
|
||||
|
||||
static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException {
|
||||
InputStream attachmentStream = new FileInputStream(attachmentFile);
|
||||
final long attachmentSize = attachmentFile.length();
|
||||
final String mime = getFileMimeType(attachmentFile, "application/octet-stream");
|
||||
// TODO mabybe add a parameter to set the voiceNote, borderless, preview, width, height and caption option
|
||||
final long uploadTimestamp = System.currentTimeMillis();
|
||||
Optional<byte[]> preview = Optional.absent();
|
||||
Optional<String> caption = Optional.absent();
|
||||
Optional<String> blurHash = Optional.absent();
|
||||
final Optional<ResumableUploadSpec> resumableUploadSpec = Optional.absent();
|
||||
return new SignalServiceAttachmentStream(attachmentStream,
|
||||
mime,
|
||||
attachmentSize,
|
||||
Optional.of(attachmentFile.getName()),
|
||||
false,
|
||||
false,
|
||||
preview,
|
||||
0,
|
||||
0,
|
||||
uploadTimestamp,
|
||||
caption,
|
||||
blurHash,
|
||||
null,
|
||||
null,
|
||||
resumableUploadSpec);
|
||||
}
|
||||
|
||||
static StreamDetails createStreamDetailsFromFile(File file) throws IOException {
|
||||
InputStream stream = new FileInputStream(file);
|
||||
final long size = file.length();
|
||||
String mime = Files.probeContentType(file.toPath());
|
||||
if (mime == null) {
|
||||
mime = "application/octet-stream";
|
||||
}
|
||||
return new StreamDetails(stream, mime, size);
|
||||
}
|
||||
|
||||
static CertificateValidator getCertificateValidator() {
|
||||
try {
|
||||
ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(ServiceConfig.UNIDENTIFIED_SENDER_TRUST_ROOT),
|
||||
0);
|
||||
return new CertificateValidator(unidentifiedSenderTrustRoot);
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, String> getQueryMap(String query) {
|
||||
String[] params = query.split("&");
|
||||
Map<String, String> map = new HashMap<>();
|
||||
for (String param : params) {
|
||||
final String[] paramParts = param.split("=");
|
||||
String name = URLDecoder.decode(paramParts[0], StandardCharsets.UTF_8);
|
||||
String value = URLDecoder.decode(paramParts[1], StandardCharsets.UTF_8);
|
||||
map.put(name, value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
static String createDeviceLinkUri(DeviceLinkInfo info) {
|
||||
return "tsdevice:/?uuid="
|
||||
+ URLEncoder.encode(info.deviceIdentifier, StandardCharsets.UTF_8)
|
||||
+ "&pub_key="
|
||||
+ URLEncoder.encode(Base64.encodeBytesWithoutPadding(info.deviceKey.serialize()),
|
||||
StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
static DeviceLinkInfo parseDeviceLinkUri(URI linkUri) throws IOException, InvalidKeyException {
|
||||
Map<String, String> query = getQueryMap(linkUri.getRawQuery());
|
||||
String deviceIdentifier = query.get("uuid");
|
||||
String publicKeyEncoded = query.get("pub_key");
|
||||
|
||||
if (isEmpty(deviceIdentifier) || isEmpty(publicKeyEncoded)) {
|
||||
throw new RuntimeException("Invalid device link uri");
|
||||
}
|
||||
|
||||
ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0);
|
||||
|
||||
return new DeviceLinkInfo(deviceIdentifier, deviceKey);
|
||||
}
|
||||
|
||||
static SignalServiceEnvelope loadEnvelope(File file) throws IOException {
|
||||
try (FileInputStream f = new FileInputStream(file)) {
|
||||
DataInputStream in = new DataInputStream(f);
|
||||
int version = in.readInt();
|
||||
if (version > 4) {
|
||||
return null;
|
||||
}
|
||||
int type = in.readInt();
|
||||
String source = in.readUTF();
|
||||
UUID sourceUuid = null;
|
||||
if (version >= 3) {
|
||||
sourceUuid = UuidUtil.parseOrNull(in.readUTF());
|
||||
}
|
||||
int sourceDevice = in.readInt();
|
||||
if (version == 1) {
|
||||
// read legacy relay field
|
||||
in.readUTF();
|
||||
}
|
||||
long timestamp = in.readLong();
|
||||
byte[] content = null;
|
||||
int contentLen = in.readInt();
|
||||
if (contentLen > 0) {
|
||||
content = new byte[contentLen];
|
||||
in.readFully(content);
|
||||
}
|
||||
byte[] legacyMessage = null;
|
||||
int legacyMessageLen = in.readInt();
|
||||
if (legacyMessageLen > 0) {
|
||||
legacyMessage = new byte[legacyMessageLen];
|
||||
in.readFully(legacyMessage);
|
||||
}
|
||||
long serverReceivedTimestamp = 0;
|
||||
String uuid = null;
|
||||
if (version >= 2) {
|
||||
serverReceivedTimestamp = in.readLong();
|
||||
uuid = in.readUTF();
|
||||
if ("".equals(uuid)) {
|
||||
uuid = null;
|
||||
}
|
||||
}
|
||||
long serverDeliveredTimestamp = 0;
|
||||
if (version >= 4) {
|
||||
serverDeliveredTimestamp = in.readLong();
|
||||
}
|
||||
Optional<SignalServiceAddress> addressOptional = sourceUuid == null && source.isEmpty()
|
||||
? Optional.absent()
|
||||
: Optional.of(new SignalServiceAddress(sourceUuid, source));
|
||||
return new SignalServiceEnvelope(type,
|
||||
addressOptional,
|
||||
sourceDevice,
|
||||
timestamp,
|
||||
legacyMessage,
|
||||
content,
|
||||
serverReceivedTimestamp,
|
||||
serverDeliveredTimestamp,
|
||||
uuid);
|
||||
}
|
||||
}
|
||||
|
||||
static void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException {
|
||||
try (FileOutputStream f = new FileOutputStream(file)) {
|
||||
try (DataOutputStream out = new DataOutputStream(f)) {
|
||||
out.writeInt(4); // version
|
||||
out.writeInt(envelope.getType());
|
||||
out.writeUTF(envelope.getSourceE164().isPresent() ? envelope.getSourceE164().get() : "");
|
||||
out.writeUTF(envelope.getSourceUuid().isPresent() ? envelope.getSourceUuid().get() : "");
|
||||
out.writeInt(envelope.getSourceDevice());
|
||||
out.writeLong(envelope.getTimestamp());
|
||||
if (envelope.hasContent()) {
|
||||
out.writeInt(envelope.getContent().length);
|
||||
out.write(envelope.getContent());
|
||||
} else {
|
||||
out.writeInt(0);
|
||||
}
|
||||
if (envelope.hasLegacyMessage()) {
|
||||
out.writeInt(envelope.getLegacyMessage().length);
|
||||
out.write(envelope.getLegacyMessage());
|
||||
} else {
|
||||
out.writeInt(0);
|
||||
}
|
||||
out.writeLong(envelope.getServerReceivedTimestamp());
|
||||
String uuid = envelope.getUuid();
|
||||
out.writeUTF(uuid == null ? "" : uuid);
|
||||
out.writeLong(envelope.getServerDeliveredTimestamp());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException {
|
||||
InputStream input = stream.getInputStream();
|
||||
|
||||
try (OutputStream 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;
|
||||
}
|
||||
return outputFile;
|
||||
}
|
||||
|
||||
static String computeSafetyNumber(
|
||||
SignalServiceAddress ownAddress,
|
||||
IdentityKey ownIdentityKey,
|
||||
SignalServiceAddress theirAddress,
|
||||
IdentityKey theirIdentityKey
|
||||
) {
|
||||
int version;
|
||||
byte[] ownId;
|
||||
byte[] theirId;
|
||||
|
||||
if (ServiceConfig.capabilities.isUuid() && ownAddress.getUuid().isPresent() && theirAddress.getUuid()
|
||||
.isPresent()) {
|
||||
// Version 2: UUID user
|
||||
version = 2;
|
||||
ownId = UuidUtil.toByteArray(ownAddress.getUuid().get());
|
||||
theirId = UuidUtil.toByteArray(theirAddress.getUuid().get());
|
||||
} else {
|
||||
// Version 1: E164 user
|
||||
version = 1;
|
||||
if (!ownAddress.getNumber().isPresent() || !theirAddress.getNumber().isPresent()) {
|
||||
return "INVALID ID";
|
||||
}
|
||||
ownId = ownAddress.getNumber().get().getBytes();
|
||||
theirId = theirAddress.getNumber().get().getBytes();
|
||||
}
|
||||
|
||||
Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(version,
|
||||
ownId,
|
||||
ownIdentityKey,
|
||||
theirId,
|
||||
theirIdentityKey);
|
||||
return fingerprint.getDisplayableFingerprint().getDisplayText();
|
||||
}
|
||||
|
||||
static class DeviceLinkInfo {
|
||||
|
||||
final String deviceIdentifier;
|
||||
final ECPublicKey deviceKey;
|
||||
|
||||
DeviceLinkInfo(final String deviceIdentifier, final ECPublicKey deviceKey) {
|
||||
this.deviceIdentifier = deviceIdentifier;
|
||||
this.deviceKey = deviceKey;
|
||||
}
|
||||
}
|
||||
}
|
63
src/main/java/org/asamk/signal/manager/groups/GroupId.java
Normal file
63
src/main/java/org/asamk/signal/manager/groups/GroupId.java
Normal file
|
@ -0,0 +1,63 @@
|
|||
package org.asamk.signal.manager.groups;
|
||||
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public abstract class GroupId {
|
||||
|
||||
private final byte[] id;
|
||||
|
||||
public static GroupIdV1 v1(byte[] id) {
|
||||
return new GroupIdV1(id);
|
||||
}
|
||||
|
||||
public static GroupIdV2 v2(byte[] id) {
|
||||
return new GroupIdV2(id);
|
||||
}
|
||||
|
||||
public static GroupId unknownVersion(byte[] id) {
|
||||
if (id.length == 16) {
|
||||
return new GroupIdV1(id);
|
||||
} else if (id.length == 32) {
|
||||
return new GroupIdV2(id);
|
||||
}
|
||||
|
||||
throw new AssertionError("Invalid group id of size " + id.length);
|
||||
}
|
||||
|
||||
public static GroupId fromBase64(String id) throws GroupIdFormatException {
|
||||
try {
|
||||
return unknownVersion(java.util.Base64.getDecoder().decode(id));
|
||||
} catch (Throwable e) {
|
||||
throw new GroupIdFormatException(id, e);
|
||||
}
|
||||
}
|
||||
|
||||
public GroupId(final byte[] id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String toBase64() {
|
||||
return Base64.encodeBytes(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
final GroupId groupId = (GroupId) o;
|
||||
|
||||
return Arrays.equals(id, groupId.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(id);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.asamk.signal.manager.groups;
|
||||
|
||||
public class GroupIdFormatException extends Exception {
|
||||
|
||||
public GroupIdFormatException(String groupId, Throwable e) {
|
||||
super("Failed to decode groupId (must be base64) \"" + groupId + "\": " + e.getMessage(), e);
|
||||
}
|
||||
}
|
14
src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java
Normal file
14
src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java
Normal file
|
@ -0,0 +1,14 @@
|
|||
package org.asamk.signal.manager.groups;
|
||||
|
||||
import static org.asamk.signal.manager.util.KeyUtils.getSecretBytes;
|
||||
|
||||
public class GroupIdV1 extends GroupId {
|
||||
|
||||
public static GroupIdV1 createRandom() {
|
||||
return new GroupIdV1(getSecretBytes(16));
|
||||
}
|
||||
|
||||
public GroupIdV1(final byte[] id) {
|
||||
super(id);
|
||||
}
|
||||
}
|
14
src/main/java/org/asamk/signal/manager/groups/GroupIdV2.java
Normal file
14
src/main/java/org/asamk/signal/manager/groups/GroupIdV2.java
Normal file
|
@ -0,0 +1,14 @@
|
|||
package org.asamk.signal.manager.groups;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
public class GroupIdV2 extends GroupId {
|
||||
|
||||
public static GroupIdV2 fromBase64(String groupId) {
|
||||
return new GroupIdV2(Base64.getDecoder().decode(groupId));
|
||||
}
|
||||
|
||||
public GroupIdV2(final byte[] id) {
|
||||
super(id);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
package org.asamk.signal.manager.groups;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.storageservice.protos.groups.GroupInviteLink;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.whispersystems.util.Base64UrlSafe;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
public final class GroupInviteLinkUrl {
|
||||
|
||||
private static final String GROUP_URL_HOST = "signal.group";
|
||||
private static final String GROUP_URL_PREFIX = "https://" + GROUP_URL_HOST + "/#";
|
||||
|
||||
private final GroupMasterKey groupMasterKey;
|
||||
private final GroupLinkPassword password;
|
||||
private final String url;
|
||||
|
||||
public static GroupInviteLinkUrl forGroup(GroupMasterKey groupMasterKey, DecryptedGroup group) {
|
||||
return new GroupInviteLinkUrl(groupMasterKey,
|
||||
GroupLinkPassword.fromBytes(group.getInviteLinkPassword().toByteArray()));
|
||||
}
|
||||
|
||||
public static boolean isGroupLink(String urlString) {
|
||||
return getGroupUrl(urlString) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null iff not a group url.
|
||||
* @throws InvalidGroupLinkException If group url, but cannot be parsed.
|
||||
*/
|
||||
public static GroupInviteLinkUrl fromUri(String urlString) throws InvalidGroupLinkException, UnknownGroupLinkVersionException {
|
||||
URI uri = getGroupUrl(urlString);
|
||||
|
||||
if (uri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!"/".equals(uri.getPath()) && uri.getPath().length() > 0) {
|
||||
throw new InvalidGroupLinkException("No path was expected in uri");
|
||||
}
|
||||
|
||||
String encoding = uri.getFragment();
|
||||
|
||||
if (encoding == null || encoding.length() == 0) {
|
||||
throw new InvalidGroupLinkException("No reference was in the uri");
|
||||
}
|
||||
|
||||
byte[] bytes = Base64UrlSafe.decodePaddingAgnostic(encoding);
|
||||
GroupInviteLink groupInviteLink = GroupInviteLink.parseFrom(bytes);
|
||||
|
||||
switch (groupInviteLink.getContentsCase()) {
|
||||
case V1CONTENTS: {
|
||||
GroupInviteLink.GroupInviteLinkContentsV1 groupInviteLinkContentsV1 = groupInviteLink.getV1Contents();
|
||||
GroupMasterKey groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.getGroupMasterKey()
|
||||
.toByteArray());
|
||||
GroupLinkPassword password = GroupLinkPassword.fromBytes(groupInviteLinkContentsV1.getInviteLinkPassword()
|
||||
.toByteArray());
|
||||
|
||||
return new GroupInviteLinkUrl(groupMasterKey, password);
|
||||
}
|
||||
default:
|
||||
throw new UnknownGroupLinkVersionException("Url contains no known group link content");
|
||||
}
|
||||
} catch (InvalidInputException | IOException e) {
|
||||
throw new InvalidGroupLinkException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@link URI} if the host name matches.
|
||||
*/
|
||||
private static URI getGroupUrl(String urlString) {
|
||||
try {
|
||||
URI url = new URI(urlString);
|
||||
|
||||
if (!"https".equalsIgnoreCase(url.getScheme()) && !"sgnl".equalsIgnoreCase(url.getScheme())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return GROUP_URL_HOST.equalsIgnoreCase(url.getHost()) ? url : null;
|
||||
} catch (URISyntaxException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private GroupInviteLinkUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) {
|
||||
this.groupMasterKey = groupMasterKey;
|
||||
this.password = password;
|
||||
this.url = createUrl(groupMasterKey, password);
|
||||
}
|
||||
|
||||
protected static String createUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) {
|
||||
GroupInviteLink groupInviteLink = GroupInviteLink.newBuilder()
|
||||
.setV1Contents(GroupInviteLink.GroupInviteLinkContentsV1.newBuilder()
|
||||
.setGroupMasterKey(ByteString.copyFrom(groupMasterKey.serialize()))
|
||||
.setInviteLinkPassword(ByteString.copyFrom(password.serialize())))
|
||||
.build();
|
||||
|
||||
String encoding = Base64UrlSafe.encodeBytesWithoutPadding(groupInviteLink.toByteArray());
|
||||
|
||||
return GROUP_URL_PREFIX + encoding;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public GroupMasterKey getGroupMasterKey() {
|
||||
return groupMasterKey;
|
||||
}
|
||||
|
||||
public GroupLinkPassword getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public final static class InvalidGroupLinkException extends Exception {
|
||||
|
||||
public InvalidGroupLinkException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InvalidGroupLinkException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
public final static class UnknownGroupLinkVersionException extends Exception {
|
||||
|
||||
public UnknownGroupLinkVersionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package org.asamk.signal.manager.groups;
|
||||
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class GroupLinkPassword {
|
||||
|
||||
private static final int SIZE = 16;
|
||||
|
||||
private final byte[] bytes;
|
||||
|
||||
public static GroupLinkPassword createNew() {
|
||||
return new GroupLinkPassword(KeyUtils.getSecretBytes(SIZE));
|
||||
}
|
||||
|
||||
public static GroupLinkPassword fromBytes(byte[] bytes) {
|
||||
return new GroupLinkPassword(bytes);
|
||||
}
|
||||
|
||||
private GroupLinkPassword(byte[] bytes) {
|
||||
this.bytes = bytes;
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
return bytes.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (!(other instanceof GroupLinkPassword)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Arrays.equals(bytes, ((GroupLinkPassword) other).bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(bytes);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.asamk.signal.manager.groups;
|
||||
|
||||
public class GroupNotFoundException extends Exception {
|
||||
|
||||
public GroupNotFoundException(GroupId groupId) {
|
||||
super("Group not found: " + groupId.toBase64());
|
||||
}
|
||||
}
|
|
@ -1,14 +1,15 @@
|
|||
package org.asamk.signal.manager;
|
||||
package org.asamk.signal.manager.groups;
|
||||
|
||||
import org.asamk.signal.storage.groups.GroupInfo;
|
||||
import org.asamk.signal.storage.groups.GroupInfoV1;
|
||||
import org.asamk.signal.storage.groups.GroupInfoV2;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.zkgroup.groups.GroupSecretParams;
|
||||
import org.whispersystems.libsignal.kdf.HKDFv3;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
||||
|
||||
public class GroupUtils {
|
||||
|
@ -18,7 +19,7 @@ public class GroupUtils {
|
|||
) {
|
||||
if (groupInfo instanceof GroupInfoV1) {
|
||||
SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
|
||||
.withId(groupInfo.groupId)
|
||||
.withId(groupInfo.getGroupId().serialize())
|
||||
.build();
|
||||
messageBuilder.asGroupMessage(group);
|
||||
} else {
|
||||
|
@ -30,14 +31,34 @@ public class GroupUtils {
|
|||
}
|
||||
}
|
||||
|
||||
public static byte[] getGroupId(GroupMasterKey groupMasterKey) {
|
||||
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
||||
return groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
|
||||
public static GroupId getGroupId(SignalServiceGroupContext context) {
|
||||
if (context.getGroupV1().isPresent()) {
|
||||
return GroupId.v1(context.getGroupV1().get().getGroupId());
|
||||
} else if (context.getGroupV2().isPresent()) {
|
||||
return getGroupIdV2(context.getGroupV2().get().getMasterKey());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static GroupMasterKey deriveV2MigrationMasterKey(byte[] groupId) {
|
||||
public static GroupIdV2 getGroupIdV2(GroupSecretParams groupSecretParams) {
|
||||
return GroupId.v2(groupSecretParams.getPublicParams().getGroupIdentifier().serialize());
|
||||
}
|
||||
|
||||
public static GroupIdV2 getGroupIdV2(GroupMasterKey groupMasterKey) {
|
||||
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
||||
return getGroupIdV2(groupSecretParams);
|
||||
}
|
||||
|
||||
public static GroupIdV2 getGroupIdV2(GroupIdV1 groupIdV1) {
|
||||
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(deriveV2MigrationMasterKey(
|
||||
groupIdV1));
|
||||
return getGroupIdV2(groupSecretParams);
|
||||
}
|
||||
|
||||
private static GroupMasterKey deriveV2MigrationMasterKey(GroupIdV1 groupIdV1) {
|
||||
try {
|
||||
return new GroupMasterKey(new HKDFv3().deriveSecrets(groupId,
|
||||
return new GroupMasterKey(new HKDFv3().deriveSecrets(groupIdV1.serialize(),
|
||||
"GV2 Migration".getBytes(),
|
||||
GroupMasterKey.SIZE));
|
||||
} catch (InvalidInputException e) {
|
|
@ -0,0 +1,8 @@
|
|||
package org.asamk.signal.manager.groups;
|
||||
|
||||
public class NotAGroupMemberException extends Exception {
|
||||
|
||||
public NotAGroupMemberException(GroupId groupId, String groupName) {
|
||||
super("User is not a member in group: " + groupName + " (" + groupId.toBase64() + ")");
|
||||
}
|
||||
}
|
|
@ -2,12 +2,18 @@ package org.asamk.signal.manager.helper;
|
|||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.asamk.signal.storage.groups.GroupInfoV2;
|
||||
import org.asamk.signal.util.IOUtils;
|
||||
import org.asamk.signal.manager.groups.GroupIdV2;
|
||||
import org.asamk.signal.manager.groups.GroupLinkPassword;
|
||||
import org.asamk.signal.manager.groups.GroupUtils;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
|
||||
import org.asamk.signal.manager.storage.profiles.SignalProfile;
|
||||
import org.asamk.signal.manager.util.IOUtils;
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.Member;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
|
@ -15,10 +21,13 @@ import org.signal.zkgroup.groups.GroupMasterKey;
|
|||
import org.signal.zkgroup.groups.GroupSecretParams;
|
||||
import org.signal.zkgroup.groups.UuidCiphertext;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
|
@ -38,6 +47,8 @@ import java.util.stream.Collectors;
|
|||
|
||||
public class GroupHelper {
|
||||
|
||||
final static Logger logger = LoggerFactory.getLogger(GroupHelper.class);
|
||||
|
||||
private final ProfileKeyCredentialProvider profileKeyCredentialProvider;
|
||||
|
||||
private final ProfileProvider profileProvider;
|
||||
|
@ -66,6 +77,27 @@ public class GroupHelper {
|
|||
this.groupAuthorizationProvider = groupAuthorizationProvider;
|
||||
}
|
||||
|
||||
public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) {
|
||||
try {
|
||||
final GroupsV2AuthorizationString groupsV2AuthorizationString = groupAuthorizationProvider.getAuthorizationForToday(
|
||||
groupSecretParams);
|
||||
return groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString);
|
||||
} catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
|
||||
logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public DecryptedGroupJoinInfo getDecryptedGroupJoinInfo(
|
||||
GroupMasterKey groupMasterKey, GroupLinkPassword password
|
||||
) throws IOException, GroupLinkNotActiveException {
|
||||
GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
||||
|
||||
return groupsV2Api.getGroupJoinInfo(groupSecretParams,
|
||||
Optional.fromNullable(password).transform(GroupLinkPassword::serialize),
|
||||
groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
|
||||
}
|
||||
|
||||
public GroupInfoV2 createGroupV2(
|
||||
String name, Collection<SignalServiceAddress> members, String avatarFile
|
||||
) throws IOException {
|
||||
|
@ -84,15 +116,15 @@ public class GroupHelper {
|
|||
groupsV2Api.putNewGroup(newGroup, groupAuthForToday);
|
||||
decryptedGroup = groupsV2Api.getGroup(groupSecretParams, groupAuthForToday);
|
||||
} catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
|
||||
System.err.println("Failed to create V2 group: " + e.getMessage());
|
||||
logger.warn("Failed to create V2 group: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
if (decryptedGroup == null) {
|
||||
System.err.println("Failed to create V2 group!");
|
||||
logger.warn("Failed to create V2 group, unknown error!");
|
||||
return null;
|
||||
}
|
||||
|
||||
final byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
|
||||
final GroupIdV2 groupId = GroupUtils.getGroupIdV2(groupSecretParams);
|
||||
final GroupMasterKey masterKey = groupSecretParams.getMasterKey();
|
||||
GroupInfoV2 g = new GroupInfoV2(groupId, masterKey);
|
||||
g.setGroup(decryptedGroup);
|
||||
|
@ -114,7 +146,7 @@ public class GroupHelper {
|
|||
final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
|
||||
selfAddressProvider.getSelfAddress());
|
||||
if (profileKeyCredential == null) {
|
||||
System.err.println("Cannot create a V2 group as self does not have a versioned profile");
|
||||
logger.warn("Cannot create a V2 group as self does not have a versioned profile");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -138,22 +170,23 @@ public class GroupHelper {
|
|||
}
|
||||
|
||||
private boolean areMembersValid(final Collection<SignalServiceAddress> members) {
|
||||
final int noUuidCapability = members.stream()
|
||||
final Set<String> noUuidCapability = members.stream()
|
||||
.filter(address -> !address.getUuid().isPresent())
|
||||
.collect(Collectors.toUnmodifiableSet())
|
||||
.size();
|
||||
if (noUuidCapability > 0) {
|
||||
System.err.println("Cannot create a V2 group as " + noUuidCapability + " members don't have a UUID.");
|
||||
.map(SignalServiceAddress::getLegacyIdentifier)
|
||||
.collect(Collectors.toSet());
|
||||
if (noUuidCapability.size() > 0) {
|
||||
logger.warn("Cannot create a V2 group as some members don't have a UUID: {}",
|
||||
String.join(", ", noUuidCapability));
|
||||
return false;
|
||||
}
|
||||
|
||||
final int noGv2Capability = members.stream()
|
||||
final Set<SignalProfile> noGv2Capability = members.stream()
|
||||
.map(profileProvider::getProfile)
|
||||
.filter(profile -> profile != null && !profile.getCapabilities().gv2)
|
||||
.collect(Collectors.toUnmodifiableSet())
|
||||
.size();
|
||||
if (noGv2Capability > 0) {
|
||||
System.err.println("Cannot create a V2 group as " + noGv2Capability + " members don't support Groups V2.");
|
||||
.collect(Collectors.toSet());
|
||||
if (noGv2Capability.size() > 0) {
|
||||
logger.warn("Cannot create a V2 group as some members don't support Groups V2: {}",
|
||||
noGv2Capability.stream().map(SignalProfile::getName).collect(Collectors.joining(", ")));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -192,7 +225,9 @@ public class GroupHelper {
|
|||
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
|
||||
GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
|
||||
|
||||
if (!areMembersValid(newMembers)) return null;
|
||||
if (!areMembersValid(newMembers)) {
|
||||
throw new IOException("Failed to update group");
|
||||
}
|
||||
|
||||
Set<GroupCandidate> candidates = newMembers.stream()
|
||||
.map(member -> new GroupCandidate(member.getUuid().get(),
|
||||
|
@ -223,6 +258,32 @@ public class GroupHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public GroupChange joinGroup(
|
||||
GroupMasterKey groupMasterKey,
|
||||
GroupLinkPassword groupLinkPassword,
|
||||
DecryptedGroupJoinInfo decryptedGroupJoinInfo
|
||||
) throws IOException {
|
||||
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
||||
final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
|
||||
|
||||
final SignalServiceAddress selfAddress = this.selfAddressProvider.getSelfAddress();
|
||||
final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
|
||||
selfAddress);
|
||||
if (profileKeyCredential == null) {
|
||||
throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
|
||||
}
|
||||
|
||||
boolean requestToJoin = decryptedGroupJoinInfo.getAddFromInviteLink()
|
||||
== AccessControl.AccessRequired.ADMINISTRATOR;
|
||||
GroupChange.Actions.Builder change = requestToJoin
|
||||
? groupOperations.createGroupJoinRequest(profileKeyCredential)
|
||||
: groupOperations.createGroupJoinDirect(profileKeyCredential);
|
||||
|
||||
change.setSourceUuid(UuidUtil.toByteString(selfAddress.getUuid().get()));
|
||||
|
||||
return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword);
|
||||
}
|
||||
|
||||
public Pair<DecryptedGroup, GroupChange> acceptInvite(GroupInfoV2 groupInfoV2) throws IOException {
|
||||
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
|
||||
final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
|
||||
|
@ -284,13 +345,27 @@ public class GroupHelper {
|
|||
throw new IOException(e);
|
||||
}
|
||||
|
||||
GroupChange signedGroupChange = groupsV2Api.patchGroup(change.build(),
|
||||
GroupChange signedGroupChange = groupsV2Api.patchGroup(changeActions,
|
||||
groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
|
||||
Optional.absent());
|
||||
|
||||
return new Pair<>(decryptedGroupState, signedGroupChange);
|
||||
}
|
||||
|
||||
private GroupChange commitChange(
|
||||
GroupSecretParams groupSecretParams,
|
||||
int currentRevision,
|
||||
GroupChange.Actions.Builder change,
|
||||
GroupLinkPassword password
|
||||
) throws IOException {
|
||||
final int nextRevision = currentRevision + 1;
|
||||
final GroupChange.Actions changeActions = change.setRevision(nextRevision).build();
|
||||
|
||||
return groupsV2Api.patchGroup(changeActions,
|
||||
groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
|
||||
Optional.fromNullable(password).transform(GroupLinkPassword::serialize));
|
||||
}
|
||||
|
||||
public DecryptedGroup getUpdatedDecryptedGroup(
|
||||
DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey
|
||||
) {
|
||||
|
|
89
src/main/java/org/asamk/signal/manager/helper/PinHelper.java
Normal file
89
src/main/java/org/asamk/signal/manager/helper/PinHelper.java
Normal file
|
@ -0,0 +1,89 @@
|
|||
package org.asamk.signal.manager.helper;
|
||||
|
||||
import org.asamk.signal.manager.util.PinHashing;
|
||||
import org.whispersystems.signalservice.api.KbsPinData;
|
||||
import org.whispersystems.signalservice.api.KeyBackupService;
|
||||
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
import org.whispersystems.signalservice.internal.push.LockedException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class PinHelper {
|
||||
|
||||
private final KeyBackupService keyBackupService;
|
||||
|
||||
public PinHelper(final KeyBackupService keyBackupService) {
|
||||
this.keyBackupService = keyBackupService;
|
||||
}
|
||||
|
||||
public void setRegistrationLockPin(
|
||||
String pin, MasterKey masterKey
|
||||
) throws IOException, UnauthenticatedResponseException {
|
||||
final KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
|
||||
final HashedPin hashedPin = PinHashing.hashPin(pin, pinChangeSession);
|
||||
|
||||
pinChangeSession.setPin(hashedPin, masterKey);
|
||||
pinChangeSession.enableRegistrationLock(masterKey);
|
||||
}
|
||||
|
||||
public void removeRegistrationLockPin() throws IOException, UnauthenticatedResponseException {
|
||||
final KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
|
||||
pinChangeSession.disableRegistrationLock();
|
||||
pinChangeSession.removePin();
|
||||
}
|
||||
|
||||
public KbsPinData getRegistrationLockData(
|
||||
String pin, LockedException e
|
||||
) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException {
|
||||
String basicStorageCredentials = e.getBasicStorageCredentials();
|
||||
if (basicStorageCredentials == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getRegistrationLockData(pin, basicStorageCredentials);
|
||||
}
|
||||
|
||||
private KbsPinData getRegistrationLockData(
|
||||
String pin, String basicStorageCredentials
|
||||
) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException {
|
||||
TokenResponse tokenResponse = keyBackupService.getToken(basicStorageCredentials);
|
||||
if (tokenResponse == null || tokenResponse.getTries() == 0) {
|
||||
throw new IOException("KBS Account locked");
|
||||
}
|
||||
|
||||
KbsPinData registrationLockData = restoreMasterKey(pin, basicStorageCredentials, tokenResponse);
|
||||
if (registrationLockData == null) {
|
||||
throw new AssertionError("Failed to restore master key");
|
||||
}
|
||||
return registrationLockData;
|
||||
}
|
||||
|
||||
private KbsPinData restoreMasterKey(
|
||||
String pin, String basicStorageCredentials, TokenResponse tokenResponse
|
||||
) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException {
|
||||
if (pin == null) return null;
|
||||
|
||||
if (basicStorageCredentials == null) {
|
||||
throw new AssertionError("Cannot restore KBS key, no storage credentials supplied");
|
||||
}
|
||||
|
||||
KeyBackupService.RestoreSession session = keyBackupService.newRegistrationSession(basicStorageCredentials,
|
||||
tokenResponse);
|
||||
|
||||
try {
|
||||
HashedPin hashedPin = PinHashing.hashPin(pin, session);
|
||||
KbsPinData kbsData = session.restorePin(hashedPin);
|
||||
if (kbsData == null) {
|
||||
throw new AssertionError("Null not expected");
|
||||
}
|
||||
return kbsData;
|
||||
} catch (UnauthenticatedResponseException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -95,7 +95,17 @@ public final class ProfileHelper {
|
|||
? unidentifiedPipe
|
||||
: messagePipeProvider.getMessagePipe(false);
|
||||
if (pipe != null) {
|
||||
return pipe.getProfile(address, profileKey, unidentifiedAccess, requestType);
|
||||
try {
|
||||
return pipe.getProfile(address, profileKey, unidentifiedAccess, requestType);
|
||||
} catch (NoClassDefFoundError e) {
|
||||
// Native zkgroup lib not available for ProfileKey
|
||||
if (!address.getNumber().isPresent()) {
|
||||
throw new NotFoundException("Can't request profile without number");
|
||||
}
|
||||
SignalServiceAddress addressWithoutUuid = new SignalServiceAddress(Optional.absent(),
|
||||
address.getNumber());
|
||||
return pipe.getProfile(addressWithoutUuid, profileKey, unidentifiedAccess, requestType);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IOException("No pipe available!");
|
||||
|
@ -106,9 +116,18 @@ public final class ProfileHelper {
|
|||
Optional<ProfileKey> profileKey,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
SignalServiceProfile.RequestType requestType
|
||||
) {
|
||||
) throws NotFoundException {
|
||||
SignalServiceMessageReceiver receiver = messageReceiverProvider.getMessageReceiver();
|
||||
return receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType);
|
||||
try {
|
||||
return receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType);
|
||||
} catch (NoClassDefFoundError e) {
|
||||
// Native zkgroup lib not available for ProfileKey
|
||||
if (!address.getNumber().isPresent()) {
|
||||
throw new NotFoundException("Can't request profile without number");
|
||||
}
|
||||
SignalServiceAddress addressWithoutUuid = new SignalServiceAddress(Optional.absent(), address.getNumber());
|
||||
return receiver.retrieveProfile(addressWithoutUuid, profileKey, unidentifiedAccess, requestType);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<UnidentifiedAccess> getUnidentifiedAccess(SignalServiceAddress recipient) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package org.asamk.signal.manager.helper;
|
||||
|
||||
import org.asamk.signal.storage.profiles.SignalProfile;
|
||||
import org.asamk.signal.manager.storage.profiles.SignalProfile;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
public interface ProfileProvider {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package org.asamk.signal.manager.helper;
|
||||
|
||||
import org.asamk.signal.storage.profiles.SignalProfile;
|
||||
import org.asamk.signal.manager.storage.profiles.SignalProfile;
|
||||
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
@ -36,7 +36,7 @@ public class UnidentifiedAccessHelper {
|
|||
this.senderCertificateProvider = senderCertificateProvider;
|
||||
}
|
||||
|
||||
public byte[] getSelfUnidentifiedAccessKey() {
|
||||
private byte[] getSelfUnidentifiedAccessKey() {
|
||||
return UnidentifiedAccess.deriveAccessKeyFrom(selfProfileKeyProvider.getProfileKey());
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage;
|
||||
package org.asamk.signal.manager.storage;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
|
@ -10,29 +10,36 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import org.asamk.signal.storage.contacts.ContactInfo;
|
||||
import org.asamk.signal.storage.contacts.JsonContactsStore;
|
||||
import org.asamk.signal.storage.groups.GroupInfo;
|
||||
import org.asamk.signal.storage.groups.GroupInfoV1;
|
||||
import org.asamk.signal.storage.groups.JsonGroupStore;
|
||||
import org.asamk.signal.storage.profiles.ProfileStore;
|
||||
import org.asamk.signal.storage.protocol.JsonIdentityKeyStore;
|
||||
import org.asamk.signal.storage.protocol.JsonSignalProtocolStore;
|
||||
import org.asamk.signal.storage.protocol.RecipientStore;
|
||||
import org.asamk.signal.storage.protocol.SessionInfo;
|
||||
import org.asamk.signal.storage.protocol.SignalServiceAddressResolver;
|
||||
import org.asamk.signal.storage.stickers.StickerStore;
|
||||
import org.asamk.signal.storage.threads.LegacyJsonThreadStore;
|
||||
import org.asamk.signal.storage.threads.ThreadInfo;
|
||||
import org.asamk.signal.util.IOUtils;
|
||||
import org.asamk.signal.util.Util;
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.storage.contacts.ContactInfo;
|
||||
import org.asamk.signal.manager.storage.contacts.JsonContactsStore;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
||||
import org.asamk.signal.manager.storage.groups.JsonGroupStore;
|
||||
import org.asamk.signal.manager.storage.messageCache.MessageCache;
|
||||
import org.asamk.signal.manager.storage.profiles.ProfileStore;
|
||||
import org.asamk.signal.manager.storage.protocol.IdentityInfo;
|
||||
import org.asamk.signal.manager.storage.protocol.JsonSignalProtocolStore;
|
||||
import org.asamk.signal.manager.storage.protocol.RecipientStore;
|
||||
import org.asamk.signal.manager.storage.protocol.SessionInfo;
|
||||
import org.asamk.signal.manager.storage.protocol.SignalServiceAddressResolver;
|
||||
import org.asamk.signal.manager.storage.stickers.StickerStore;
|
||||
import org.asamk.signal.manager.storage.threads.LegacyJsonThreadStore;
|
||||
import org.asamk.signal.manager.storage.threads.ThreadInfo;
|
||||
import org.asamk.signal.manager.util.IOUtils;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.asamk.signal.manager.util.Utils;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libsignal.util.Medium;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
|
@ -52,6 +59,8 @@ import java.util.stream.Collectors;
|
|||
|
||||
public class SignalAccount implements Closeable {
|
||||
|
||||
final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
|
||||
|
||||
private final ObjectMapper jsonProcessor = new ObjectMapper();
|
||||
private final FileChannel fileChannel;
|
||||
private final FileLock lock;
|
||||
|
@ -61,6 +70,7 @@ public class SignalAccount implements Closeable {
|
|||
private boolean isMultiDevice = false;
|
||||
private String password;
|
||||
private String registrationLockPin;
|
||||
private MasterKey pinMasterKey;
|
||||
private String signalingKey;
|
||||
private ProfileKey profileKey;
|
||||
private int preKeyIdOffset;
|
||||
|
@ -75,6 +85,8 @@ public class SignalAccount implements Closeable {
|
|||
private ProfileStore profileStore;
|
||||
private StickerStore stickerStore;
|
||||
|
||||
private MessageCache messageCache;
|
||||
|
||||
private SignalAccount(final FileChannel fileChannel, final FileLock lock) {
|
||||
this.fileChannel = fileChannel;
|
||||
this.lock = lock;
|
||||
|
@ -85,12 +97,14 @@ public class SignalAccount implements Closeable {
|
|||
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
|
||||
}
|
||||
|
||||
public static SignalAccount load(String dataPath, String username) throws IOException {
|
||||
final String fileName = getFileName(dataPath, username);
|
||||
public static SignalAccount load(File dataPath, String username) throws IOException {
|
||||
final File fileName = getFileName(dataPath, username);
|
||||
final Pair<FileChannel, FileLock> pair = openFileChannel(fileName);
|
||||
try {
|
||||
SignalAccount account = new SignalAccount(pair.first(), pair.second());
|
||||
account.load(dataPath);
|
||||
account.migrateLegacyConfigs();
|
||||
|
||||
return account;
|
||||
} catch (Throwable e) {
|
||||
pair.second().close();
|
||||
|
@ -100,11 +114,11 @@ public class SignalAccount implements Closeable {
|
|||
}
|
||||
|
||||
public static SignalAccount create(
|
||||
String dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey
|
||||
File dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey
|
||||
) throws IOException {
|
||||
IOUtils.createPrivateDirectories(dataPath);
|
||||
String fileName = getFileName(dataPath, username);
|
||||
if (!new File(fileName).exists()) {
|
||||
File fileName = getFileName(dataPath, username);
|
||||
if (!fileName.exists()) {
|
||||
IOUtils.createPrivateFile(fileName);
|
||||
}
|
||||
|
||||
|
@ -119,13 +133,16 @@ public class SignalAccount implements Closeable {
|
|||
account.recipientStore = new RecipientStore();
|
||||
account.profileStore = new ProfileStore();
|
||||
account.stickerStore = new StickerStore();
|
||||
|
||||
account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
|
||||
|
||||
account.registered = false;
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
public static SignalAccount createLinkedAccount(
|
||||
String dataPath,
|
||||
File dataPath,
|
||||
String username,
|
||||
UUID uuid,
|
||||
String password,
|
||||
|
@ -136,8 +153,8 @@ public class SignalAccount implements Closeable {
|
|||
ProfileKey profileKey
|
||||
) throws IOException {
|
||||
IOUtils.createPrivateDirectories(dataPath);
|
||||
String fileName = getFileName(dataPath, username);
|
||||
if (!new File(fileName).exists()) {
|
||||
File fileName = getFileName(dataPath, username);
|
||||
if (!fileName.exists()) {
|
||||
IOUtils.createPrivateFile(fileName);
|
||||
}
|
||||
|
||||
|
@ -156,29 +173,65 @@ public class SignalAccount implements Closeable {
|
|||
account.recipientStore = new RecipientStore();
|
||||
account.profileStore = new ProfileStore();
|
||||
account.stickerStore = new StickerStore();
|
||||
|
||||
account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
|
||||
|
||||
account.registered = true;
|
||||
account.isMultiDevice = true;
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
public static String getFileName(String dataPath, String username) {
|
||||
return dataPath + "/" + username;
|
||||
public void migrateLegacyConfigs() {
|
||||
if (getProfileKey() == null && isRegistered()) {
|
||||
// Old config file, creating new profile key
|
||||
setProfileKey(KeyUtils.createProfileKey());
|
||||
save();
|
||||
}
|
||||
// Store profile keys only in profile store
|
||||
for (ContactInfo contact : getContactStore().getContacts()) {
|
||||
String profileKeyString = contact.profileKey;
|
||||
if (profileKeyString == null) {
|
||||
continue;
|
||||
}
|
||||
final ProfileKey profileKey;
|
||||
try {
|
||||
profileKey = new ProfileKey(Base64.decode(profileKeyString));
|
||||
} catch (InvalidInputException | IOException e) {
|
||||
continue;
|
||||
}
|
||||
contact.profileKey = null;
|
||||
getProfileStore().storeProfileKey(contact.getAddress(), profileKey);
|
||||
}
|
||||
// Ensure our profile key is stored in profile store
|
||||
getProfileStore().storeProfileKey(getSelfAddress(), getProfileKey());
|
||||
}
|
||||
|
||||
private static File getGroupCachePath(String dataPath, String username) {
|
||||
return new File(new File(dataPath, username + ".d"), "group-cache");
|
||||
public static File getFileName(File dataPath, String username) {
|
||||
return new File(dataPath, username);
|
||||
}
|
||||
|
||||
public static boolean userExists(String dataPath, String username) {
|
||||
private static File getUserPath(final File dataPath, final String username) {
|
||||
return new File(dataPath, username + ".d");
|
||||
}
|
||||
|
||||
public static File getMessageCachePath(File dataPath, String username) {
|
||||
return new File(getUserPath(dataPath, username), "msg-cache");
|
||||
}
|
||||
|
||||
private static File getGroupCachePath(File dataPath, String username) {
|
||||
return new File(getUserPath(dataPath, username), "group-cache");
|
||||
}
|
||||
|
||||
public static boolean userExists(File dataPath, String username) {
|
||||
if (username == null) {
|
||||
return false;
|
||||
}
|
||||
File f = new File(getFileName(dataPath, username));
|
||||
File f = getFileName(dataPath, username);
|
||||
return !(!f.exists() || f.isDirectory());
|
||||
}
|
||||
|
||||
private void load(String dataPath) throws IOException {
|
||||
private void load(File dataPath) throws IOException {
|
||||
JsonNode rootNode;
|
||||
synchronized (fileChannel) {
|
||||
fileChannel.position(0);
|
||||
|
@ -198,28 +251,32 @@ public class SignalAccount implements Closeable {
|
|||
deviceId = node.asInt();
|
||||
}
|
||||
if (rootNode.has("isMultiDevice")) {
|
||||
isMultiDevice = Util.getNotNullNode(rootNode, "isMultiDevice").asBoolean();
|
||||
isMultiDevice = Utils.getNotNullNode(rootNode, "isMultiDevice").asBoolean();
|
||||
}
|
||||
username = Util.getNotNullNode(rootNode, "username").asText();
|
||||
password = Util.getNotNullNode(rootNode, "password").asText();
|
||||
username = Utils.getNotNullNode(rootNode, "username").asText();
|
||||
password = Utils.getNotNullNode(rootNode, "password").asText();
|
||||
JsonNode pinNode = rootNode.get("registrationLockPin");
|
||||
registrationLockPin = pinNode == null || pinNode.isNull() ? null : pinNode.asText();
|
||||
JsonNode pinMasterKeyNode = rootNode.get("pinMasterKey");
|
||||
pinMasterKey = pinMasterKeyNode == null || pinMasterKeyNode.isNull()
|
||||
? null
|
||||
: new MasterKey(Base64.decode(pinMasterKeyNode.asText()));
|
||||
if (rootNode.has("signalingKey")) {
|
||||
signalingKey = Util.getNotNullNode(rootNode, "signalingKey").asText();
|
||||
signalingKey = Utils.getNotNullNode(rootNode, "signalingKey").asText();
|
||||
}
|
||||
if (rootNode.has("preKeyIdOffset")) {
|
||||
preKeyIdOffset = Util.getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
|
||||
preKeyIdOffset = Utils.getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
|
||||
} else {
|
||||
preKeyIdOffset = 0;
|
||||
}
|
||||
if (rootNode.has("nextSignedPreKeyId")) {
|
||||
nextSignedPreKeyId = Util.getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
|
||||
nextSignedPreKeyId = Utils.getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
|
||||
} else {
|
||||
nextSignedPreKeyId = 0;
|
||||
}
|
||||
if (rootNode.has("profileKey")) {
|
||||
try {
|
||||
profileKey = new ProfileKey(Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText()));
|
||||
profileKey = new ProfileKey(Base64.decode(Utils.getNotNullNode(rootNode, "profileKey").asText()));
|
||||
} catch (InvalidInputException e) {
|
||||
throw new IOException(
|
||||
"Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes",
|
||||
|
@ -227,9 +284,9 @@ public class SignalAccount implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"),
|
||||
signalProtocolStore = jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"),
|
||||
JsonSignalProtocolStore.class);
|
||||
registered = Util.getNotNullNode(rootNode, "registered").asBoolean();
|
||||
registered = Utils.getNotNullNode(rootNode, "registered").asBoolean();
|
||||
JsonNode groupStoreNode = rootNode.get("groupStore");
|
||||
if (groupStoreNode != null) {
|
||||
groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
|
||||
|
@ -273,7 +330,7 @@ public class SignalAccount implements Closeable {
|
|||
session.address = recipientStore.resolveServiceAddress(session.address);
|
||||
}
|
||||
|
||||
for (JsonIdentityKeyStore.Identity identity : signalProtocolStore.getIdentities()) {
|
||||
for (IdentityInfo identity : signalProtocolStore.getIdentities()) {
|
||||
identity.setAddress(recipientStore.resolveServiceAddress(identity.getAddress()));
|
||||
}
|
||||
}
|
||||
|
@ -294,6 +351,8 @@ public class SignalAccount implements Closeable {
|
|||
stickerStore = new StickerStore();
|
||||
}
|
||||
|
||||
messageCache = new MessageCache(getMessageCachePath(dataPath, username));
|
||||
|
||||
JsonNode threadStoreNode = rootNode.get("threadStore");
|
||||
if (threadStoreNode != null) {
|
||||
LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode,
|
||||
|
@ -309,7 +368,7 @@ public class SignalAccount implements Closeable {
|
|||
contactInfo.messageExpirationTime = thread.messageExpirationTime;
|
||||
contactStore.updateContact(contactInfo);
|
||||
} else {
|
||||
GroupInfo groupInfo = groupStore.getGroup(Base64.decode(thread.id));
|
||||
GroupInfo groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
|
||||
if (groupInfo instanceof GroupInfoV1) {
|
||||
((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime;
|
||||
groupStore.updateGroup(groupInfo);
|
||||
|
@ -332,6 +391,7 @@ public class SignalAccount implements Closeable {
|
|||
.put("isMultiDevice", isMultiDevice)
|
||||
.put("password", password)
|
||||
.put("registrationLockPin", registrationLockPin)
|
||||
.put("pinMasterKey", pinMasterKey == null ? null : Base64.encodeBytes(pinMasterKey.serialize()))
|
||||
.put("signalingKey", signalingKey)
|
||||
.put("preKeyIdOffset", preKeyIdOffset)
|
||||
.put("nextSignedPreKeyId", nextSignedPreKeyId)
|
||||
|
@ -356,17 +416,17 @@ public class SignalAccount implements Closeable {
|
|||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println(String.format("Error saving file: %s", e.getMessage()));
|
||||
logger.error("Error saving file: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static Pair<FileChannel, FileLock> openFileChannel(String fileName) throws IOException {
|
||||
FileChannel fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel();
|
||||
private static Pair<FileChannel, FileLock> openFileChannel(File fileName) throws IOException {
|
||||
FileChannel fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
|
||||
FileLock lock = fileChannel.tryLock();
|
||||
if (lock == null) {
|
||||
System.err.println("Config file is in use by another instance, waiting…");
|
||||
logger.info("Config file is in use by another instance, waiting…");
|
||||
lock = fileChannel.lock();
|
||||
System.err.println("Config file lock acquired.");
|
||||
logger.info("Config file lock acquired.");
|
||||
}
|
||||
return new Pair<>(fileChannel, lock);
|
||||
}
|
||||
|
@ -411,6 +471,10 @@ public class SignalAccount implements Closeable {
|
|||
return stickerStore;
|
||||
}
|
||||
|
||||
public MessageCache getMessageCache() {
|
||||
return messageCache;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
@ -431,6 +495,10 @@ public class SignalAccount implements Closeable {
|
|||
return deviceId;
|
||||
}
|
||||
|
||||
public void setDeviceId(final int deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
@ -443,14 +511,18 @@ public class SignalAccount implements Closeable {
|
|||
return registrationLockPin;
|
||||
}
|
||||
|
||||
public String getRegistrationLock() {
|
||||
return null; // TODO implement KBS
|
||||
}
|
||||
|
||||
public void setRegistrationLockPin(final String registrationLockPin) {
|
||||
this.registrationLockPin = registrationLockPin;
|
||||
}
|
||||
|
||||
public MasterKey getPinMasterKey() {
|
||||
return pinMasterKey;
|
||||
}
|
||||
|
||||
public void setPinMasterKey(final MasterKey pinMasterKey) {
|
||||
this.pinMasterKey = pinMasterKey;
|
||||
}
|
||||
|
||||
public String getSignalingKey() {
|
||||
return signalingKey;
|
||||
}
|
||||
|
@ -467,6 +539,10 @@ public class SignalAccount implements Closeable {
|
|||
this.profileKey = profileKey;
|
||||
}
|
||||
|
||||
public byte[] getSelfUnidentifiedAccessKey() {
|
||||
return UnidentifiedAccess.deriveAccessKeyFrom(getProfileKey());
|
||||
}
|
||||
|
||||
public int getPreKeyIdOffset() {
|
||||
return preKeyIdOffset;
|
||||
}
|
||||
|
@ -491,8 +567,19 @@ public class SignalAccount implements Closeable {
|
|||
isMultiDevice = multiDevice;
|
||||
}
|
||||
|
||||
public boolean isUnrestrictedUnidentifiedAccess() {
|
||||
// TODO make configurable
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isDiscoverableByPhoneNumber() {
|
||||
// TODO make configurable
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
save();
|
||||
synchronized (fileChannel) {
|
||||
try {
|
||||
lock.close();
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage.contacts;
|
||||
package org.asamk.signal.manager.storage.contacts;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage.contacts;
|
||||
package org.asamk.signal.manager.storage.contacts;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
package org.asamk.signal.storage.groups;
|
||||
package org.asamk.signal.manager.storage.groups;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.util.Set;
|
||||
|
@ -11,16 +12,15 @@ import java.util.stream.Stream;
|
|||
|
||||
public abstract class GroupInfo {
|
||||
|
||||
@JsonProperty
|
||||
public final byte[] groupId;
|
||||
|
||||
public GroupInfo(byte[] groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
@JsonIgnore
|
||||
public abstract GroupId getGroupId();
|
||||
|
||||
@JsonIgnore
|
||||
public abstract String getTitle();
|
||||
|
||||
@JsonIgnore
|
||||
public abstract GroupInviteLinkUrl getGroupInviteLink();
|
||||
|
||||
@JsonIgnore
|
||||
public abstract Set<SignalServiceAddress> getMembers();
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage.groups;
|
||||
package org.asamk.signal.manager.storage.groups;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
@ -13,6 +13,11 @@ import com.fasterxml.jackson.databind.SerializerProvider;
|
|||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupIdV1;
|
||||
import org.asamk.signal.manager.groups.GroupIdV2;
|
||||
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
|
||||
import org.asamk.signal.manager.groups.GroupUtils;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -25,8 +30,9 @@ public class GroupInfoV1 extends GroupInfo {
|
|||
|
||||
private static final ObjectMapper jsonProcessor = new ObjectMapper();
|
||||
|
||||
@JsonProperty
|
||||
public byte[] expectedV2Id;
|
||||
private final GroupIdV1 groupId;
|
||||
|
||||
private GroupIdV2 expectedV2Id;
|
||||
|
||||
@JsonProperty
|
||||
public String name;
|
||||
|
@ -46,13 +52,8 @@ public class GroupInfoV1 extends GroupInfo {
|
|||
@JsonProperty(defaultValue = "false")
|
||||
public boolean archived;
|
||||
|
||||
public GroupInfoV1(byte[] groupId) {
|
||||
super(groupId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return name;
|
||||
public GroupInfoV1(GroupIdV1 groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public GroupInfoV1(
|
||||
|
@ -68,8 +69,8 @@ public class GroupInfoV1 extends GroupInfo {
|
|||
@JsonProperty("messageExpirationTime") int messageExpirationTime,
|
||||
@JsonProperty("active") boolean _ignored_active
|
||||
) {
|
||||
super(groupId);
|
||||
this.expectedV2Id = expectedV2Id;
|
||||
this.groupId = GroupId.v1(groupId);
|
||||
this.expectedV2Id = GroupId.v2(expectedV2Id);
|
||||
this.name = name;
|
||||
this.members.addAll(members);
|
||||
this.color = color;
|
||||
|
@ -79,6 +80,40 @@ public class GroupInfoV1 extends GroupInfo {
|
|||
this.messageExpirationTime = messageExpirationTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public GroupIdV1 getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
@JsonProperty("groupId")
|
||||
private byte[] getGroupIdJackson() {
|
||||
return groupId.serialize();
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public GroupIdV2 getExpectedV2Id() {
|
||||
if (expectedV2Id == null) {
|
||||
expectedV2Id = GroupUtils.getGroupIdV2(groupId);
|
||||
}
|
||||
return expectedV2Id;
|
||||
}
|
||||
|
||||
@JsonProperty("expectedV2Id")
|
||||
private byte[] getExpectedV2IdJackson() {
|
||||
return getExpectedV2Id().serialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupInviteLinkUrl getGroupInviteLink() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Set<SignalServiceAddress> getMembers() {
|
||||
return members;
|
|
@ -1,26 +1,34 @@
|
|||
package org.asamk.signal.storage.groups;
|
||||
package org.asamk.signal.manager.storage.groups;
|
||||
|
||||
import org.asamk.signal.manager.groups.GroupIdV2;
|
||||
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class GroupInfoV2 extends GroupInfo {
|
||||
|
||||
private final GroupIdV2 groupId;
|
||||
private final GroupMasterKey masterKey;
|
||||
|
||||
private boolean blocked;
|
||||
private DecryptedGroup group; // stored as a file with hexadecimal groupId as name
|
||||
|
||||
public GroupInfoV2(final byte[] groupId, final GroupMasterKey masterKey) {
|
||||
super(groupId);
|
||||
public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) {
|
||||
this.groupId = groupId;
|
||||
this.masterKey = masterKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupIdV2 getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public GroupMasterKey getMasterKey() {
|
||||
return masterKey;
|
||||
}
|
||||
|
@ -41,10 +49,23 @@ public class GroupInfoV2 extends GroupInfo {
|
|||
return this.group.getTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupInviteLinkUrl getGroupInviteLink() {
|
||||
if (this.group == null || this.group.getInviteLinkPassword() == null || (
|
||||
this.group.getAccessControl().getAddFromInviteLink() != AccessControl.AccessRequired.ANY
|
||||
&& this.group.getAccessControl().getAddFromInviteLink()
|
||||
!= AccessControl.AccessRequired.ADMINISTRATOR
|
||||
)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return GroupInviteLinkUrl.forGroup(masterKey, group);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<SignalServiceAddress> getMembers() {
|
||||
if (this.group == null) {
|
||||
return Collections.emptySet();
|
||||
return Set.of();
|
||||
}
|
||||
return group.getMembersList()
|
||||
.stream()
|
||||
|
@ -55,7 +76,7 @@ public class GroupInfoV2 extends GroupInfo {
|
|||
@Override
|
||||
public Set<SignalServiceAddress> getPendingMembers() {
|
||||
if (this.group == null) {
|
||||
return Collections.emptySet();
|
||||
return Set.of();
|
||||
}
|
||||
return group.getPendingMembersList()
|
||||
.stream()
|
||||
|
@ -66,7 +87,7 @@ public class GroupInfoV2 extends GroupInfo {
|
|||
@Override
|
||||
public Set<SignalServiceAddress> getRequestingMembers() {
|
||||
if (this.group == null) {
|
||||
return Collections.emptySet();
|
||||
return Set.of();
|
||||
}
|
||||
return group.getRequestingMembersList()
|
||||
.stream()
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage.groups;
|
||||
package org.asamk.signal.manager.storage.groups;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
|
@ -12,12 +12,17 @@ import com.fasterxml.jackson.databind.SerializerProvider;
|
|||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
import org.asamk.signal.manager.GroupUtils;
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupIdV1;
|
||||
import org.asamk.signal.manager.groups.GroupIdV2;
|
||||
import org.asamk.signal.manager.groups.GroupUtils;
|
||||
import org.asamk.signal.manager.util.IOUtils;
|
||||
import org.asamk.signal.util.Hex;
|
||||
import org.asamk.signal.util.IOUtils;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -25,7 +30,6 @@ import java.io.FileInputStream;
|
|||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
@ -33,13 +37,15 @@ import java.util.Map;
|
|||
|
||||
public class JsonGroupStore {
|
||||
|
||||
final static Logger logger = LoggerFactory.getLogger(JsonGroupStore.class);
|
||||
|
||||
private static final ObjectMapper jsonProcessor = new ObjectMapper();
|
||||
public File groupCachePath;
|
||||
|
||||
@JsonProperty("groups")
|
||||
@JsonSerialize(using = GroupsSerializer.class)
|
||||
@JsonDeserialize(using = GroupsDeserializer.class)
|
||||
private final Map<String, GroupInfo> groups = new HashMap<>();
|
||||
private final Map<GroupId, GroupInfo> groups = new HashMap<>();
|
||||
|
||||
private JsonGroupStore() {
|
||||
}
|
||||
|
@ -49,70 +55,78 @@ public class JsonGroupStore {
|
|||
}
|
||||
|
||||
public void updateGroup(GroupInfo group) {
|
||||
groups.put(Base64.encodeBytes(group.groupId), group);
|
||||
groups.put(group.getGroupId(), group);
|
||||
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) {
|
||||
try {
|
||||
IOUtils.createPrivateDirectories(groupCachePath);
|
||||
try (FileOutputStream stream = new FileOutputStream(getGroupFile(group.groupId))) {
|
||||
try (FileOutputStream stream = new FileOutputStream(getGroupFile(group.getGroupId()))) {
|
||||
((GroupInfoV2) group).getGroup().writeTo(stream);
|
||||
}
|
||||
final File groupFileLegacy = getGroupFileLegacy(group.getGroupId());
|
||||
if (groupFileLegacy.exists()) {
|
||||
groupFileLegacy.delete();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to cache group, ignoring ...");
|
||||
logger.warn("Failed to cache group, ignoring: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteGroup(byte[] groupId) {
|
||||
groups.remove(Base64.encodeBytes(groupId));
|
||||
public void deleteGroup(GroupId groupId) {
|
||||
groups.remove(groupId);
|
||||
}
|
||||
|
||||
public GroupInfo getGroup(byte[] groupId) {
|
||||
final GroupInfo group = groups.get(Base64.encodeBytes(groupId));
|
||||
if (group == null & groupId.length == 16) {
|
||||
return getGroupByV1Id(groupId);
|
||||
public GroupInfo getGroup(GroupId groupId) {
|
||||
GroupInfo group = groups.get(groupId);
|
||||
if (group == null) {
|
||||
if (groupId instanceof GroupIdV1) {
|
||||
group = groups.get(GroupUtils.getGroupIdV2((GroupIdV1) groupId));
|
||||
} else if (groupId instanceof GroupIdV2) {
|
||||
group = getGroupV1ByV2Id((GroupIdV2) groupId);
|
||||
}
|
||||
}
|
||||
loadDecryptedGroup(group);
|
||||
return group;
|
||||
}
|
||||
|
||||
public GroupInfo getGroupByV1Id(byte[] groupIdV1) {
|
||||
GroupInfo group = groups.get(Base64.encodeBytes(groupIdV1));
|
||||
if (group == null) {
|
||||
group = groups.get(Base64.encodeBytes(GroupUtils.getGroupId(GroupUtils.deriveV2MigrationMasterKey(groupIdV1))));
|
||||
}
|
||||
loadDecryptedGroup(group);
|
||||
return group;
|
||||
}
|
||||
|
||||
public GroupInfo getGroupByV2Id(byte[] groupIdV2) {
|
||||
GroupInfo group = groups.get(Base64.encodeBytes(groupIdV2));
|
||||
if (group == null) {
|
||||
for (GroupInfo g : groups.values()) {
|
||||
if (g instanceof GroupInfoV1 && Arrays.equals(groupIdV2, ((GroupInfoV1) g).expectedV2Id)) {
|
||||
group = g;
|
||||
break;
|
||||
private GroupInfoV1 getGroupV1ByV2Id(GroupIdV2 groupIdV2) {
|
||||
for (GroupInfo g : groups.values()) {
|
||||
if (g instanceof GroupInfoV1) {
|
||||
final GroupInfoV1 gv1 = (GroupInfoV1) g;
|
||||
if (groupIdV2.equals(gv1.getExpectedV2Id())) {
|
||||
return gv1;
|
||||
}
|
||||
}
|
||||
}
|
||||
loadDecryptedGroup(group);
|
||||
return group;
|
||||
return null;
|
||||
}
|
||||
|
||||
private void loadDecryptedGroup(final GroupInfo group) {
|
||||
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
|
||||
try (FileInputStream stream = new FileInputStream(getGroupFile(group.groupId))) {
|
||||
File groupFile = getGroupFile(group.getGroupId());
|
||||
if (!groupFile.exists()) {
|
||||
groupFile = getGroupFileLegacy(group.getGroupId());
|
||||
}
|
||||
if (!groupFile.exists()) {
|
||||
return;
|
||||
}
|
||||
try (FileInputStream stream = new FileInputStream(groupFile)) {
|
||||
((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream));
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private File getGroupFile(final byte[] groupId) {
|
||||
return new File(groupCachePath, Hex.toStringCondensed(groupId));
|
||||
private File getGroupFileLegacy(final GroupId groupId) {
|
||||
return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize()));
|
||||
}
|
||||
|
||||
public GroupInfoV1 getOrCreateGroupV1(byte[] groupId) {
|
||||
GroupInfo group = groups.get(Base64.encodeBytes(groupId));
|
||||
private File getGroupFile(final GroupId groupId) {
|
||||
return new File(groupCachePath, groupId.toBase64().replace("/", "_"));
|
||||
}
|
||||
|
||||
public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) {
|
||||
GroupInfo group = getGroup(groupId);
|
||||
if (group instanceof GroupInfoV1) {
|
||||
return (GroupInfoV1) group;
|
||||
}
|
||||
|
@ -146,7 +160,7 @@ public class JsonGroupStore {
|
|||
} else if (group instanceof GroupInfoV2) {
|
||||
final GroupInfoV2 groupV2 = (GroupInfoV2) group;
|
||||
jgen.writeStartObject();
|
||||
jgen.writeStringField("groupId", Base64.encodeBytes(groupV2.groupId));
|
||||
jgen.writeStringField("groupId", groupV2.getGroupId().toBase64());
|
||||
jgen.writeStringField("masterKey", Base64.encodeBytes(groupV2.getMasterKey().serialize()));
|
||||
jgen.writeBooleanField("blocked", groupV2.isBlocked());
|
||||
jgen.writeEndObject();
|
||||
|
@ -158,34 +172,31 @@ public class JsonGroupStore {
|
|||
}
|
||||
}
|
||||
|
||||
private static class GroupsDeserializer extends JsonDeserializer<Map<String, GroupInfo>> {
|
||||
private static class GroupsDeserializer extends JsonDeserializer<Map<GroupId, GroupInfo>> {
|
||||
|
||||
@Override
|
||||
public Map<String, GroupInfo> deserialize(
|
||||
public Map<GroupId, GroupInfo> deserialize(
|
||||
JsonParser jsonParser, DeserializationContext deserializationContext
|
||||
) throws IOException {
|
||||
Map<String, GroupInfo> groups = new HashMap<>();
|
||||
Map<GroupId, GroupInfo> groups = new HashMap<>();
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
for (JsonNode n : node) {
|
||||
GroupInfo g;
|
||||
if (n.has("masterKey")) {
|
||||
// a v2 group
|
||||
byte[] groupId = Base64.decode(n.get("groupId").asText());
|
||||
GroupIdV2 groupId = GroupIdV2.fromBase64(n.get("groupId").asText());
|
||||
try {
|
||||
GroupMasterKey masterKey = new GroupMasterKey(Base64.decode(n.get("masterKey").asText()));
|
||||
g = new GroupInfoV2(groupId, masterKey);
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError("Invalid master key for group " + Base64.encodeBytes(groupId));
|
||||
throw new AssertionError("Invalid master key for group " + groupId.toBase64());
|
||||
}
|
||||
g.setBlocked(n.get("blocked").asBoolean(false));
|
||||
} else {
|
||||
GroupInfoV1 gv1 = jsonProcessor.treeToValue(n, GroupInfoV1.class);
|
||||
if (gv1.expectedV2Id == null) {
|
||||
gv1.expectedV2Id = GroupUtils.getGroupId(GroupUtils.deriveV2MigrationMasterKey(gv1.groupId));
|
||||
}
|
||||
g = gv1;
|
||||
}
|
||||
groups.put(Base64.encodeBytes(g.groupId), g);
|
||||
groups.put(g.getGroupId(), g);
|
||||
}
|
||||
|
||||
return groups;
|
|
@ -0,0 +1,38 @@
|
|||
package org.asamk.signal.manager.storage.messageCache;
|
||||
|
||||
import org.asamk.signal.manager.util.MessageCacheUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
public final class CachedMessage {
|
||||
|
||||
final static Logger logger = LoggerFactory.getLogger(CachedMessage.class);
|
||||
|
||||
private final File file;
|
||||
|
||||
CachedMessage(final File file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
public SignalServiceEnvelope loadEnvelope() {
|
||||
try {
|
||||
return MessageCacheUtils.loadEnvelope(file);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to load cached message envelope “{}”: {}", file, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void delete() {
|
||||
try {
|
||||
Files.delete(file.toPath());
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to delete cached message file “{}”, ignoring: {}", file, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package org.asamk.signal.manager.storage.messageCache;
|
||||
|
||||
import org.asamk.signal.manager.util.IOUtils;
|
||||
import org.asamk.signal.manager.util.MessageCacheUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class MessageCache {
|
||||
|
||||
final static Logger logger = LoggerFactory.getLogger(MessageCache.class);
|
||||
|
||||
private final File messageCachePath;
|
||||
|
||||
public MessageCache(final File messageCachePath) {
|
||||
this.messageCachePath = messageCachePath;
|
||||
}
|
||||
|
||||
public Iterable<CachedMessage> getCachedMessages() {
|
||||
if (!messageCachePath.exists()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Arrays.stream(Objects.requireNonNull(messageCachePath.listFiles())).flatMap(dir -> {
|
||||
if (dir.isFile()) {
|
||||
return Stream.of(dir);
|
||||
}
|
||||
|
||||
final File[] files = Objects.requireNonNull(dir.listFiles());
|
||||
if (files.length == 0) {
|
||||
try {
|
||||
Files.delete(dir.toPath());
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to delete cache dir “{}”, ignoring: {}", dir, e.getMessage());
|
||||
}
|
||||
return Stream.empty();
|
||||
}
|
||||
return Arrays.stream(files).filter(File::isFile);
|
||||
}).map(CachedMessage::new).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public CachedMessage cacheMessage(SignalServiceEnvelope envelope) {
|
||||
final long now = new Date().getTime();
|
||||
final String source = envelope.hasSource() ? envelope.getSourceAddress().getLegacyIdentifier() : "";
|
||||
|
||||
try {
|
||||
File cacheFile = getMessageCacheFile(source, now, envelope.getTimestamp());
|
||||
MessageCacheUtils.storeEnvelope(envelope, cacheFile);
|
||||
return new CachedMessage(cacheFile);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to store encrypted message in disk cache, ignoring: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private File getMessageCachePath(String sender) {
|
||||
if (sender == null || sender.isEmpty()) {
|
||||
return messageCachePath;
|
||||
}
|
||||
|
||||
return new File(messageCachePath, sender.replace("/", "_"));
|
||||
}
|
||||
|
||||
private File getMessageCacheFile(String sender, long now, long timestamp) throws IOException {
|
||||
File cachePath = getMessageCachePath(sender);
|
||||
IOUtils.createPrivateDirectories(cachePath);
|
||||
return new File(cachePath, now + "_" + timestamp);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage.profiles;
|
||||
package org.asamk.signal.manager.storage.profiles;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
|
@ -112,7 +112,7 @@ public class ProfileStore {
|
|||
try {
|
||||
profileKeyCredential = new ProfileKeyCredential(Base64.decode(entry.get(
|
||||
"profileKeyCredential").asText()));
|
||||
} catch (InvalidInputException ignored) {
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
long lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong();
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage.profiles;
|
||||
package org.asamk.signal.manager.storage.profiles;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage.profiles;
|
||||
package org.asamk.signal.manager.storage.profiles;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
|
@ -0,0 +1,57 @@
|
|||
package org.asamk.signal.manager.storage.protocol;
|
||||
|
||||
import org.asamk.signal.manager.TrustLevel;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class IdentityInfo {
|
||||
|
||||
SignalServiceAddress address;
|
||||
IdentityKey identityKey;
|
||||
TrustLevel trustLevel;
|
||||
Date added;
|
||||
|
||||
public IdentityInfo(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel) {
|
||||
this.address = address;
|
||||
this.identityKey = identityKey;
|
||||
this.trustLevel = trustLevel;
|
||||
this.added = new Date();
|
||||
}
|
||||
|
||||
IdentityInfo(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel, Date added) {
|
||||
this.address = address;
|
||||
this.identityKey = identityKey;
|
||||
this.trustLevel = trustLevel;
|
||||
this.added = added;
|
||||
}
|
||||
|
||||
public SignalServiceAddress getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public void setAddress(final SignalServiceAddress address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
boolean isTrusted() {
|
||||
return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED;
|
||||
}
|
||||
|
||||
public IdentityKey getIdentityKey() {
|
||||
return this.identityKey;
|
||||
}
|
||||
|
||||
public TrustLevel getTrustLevel() {
|
||||
return this.trustLevel;
|
||||
}
|
||||
|
||||
public Date getDateAdded() {
|
||||
return this.added;
|
||||
}
|
||||
|
||||
public byte[] getFingerprint() {
|
||||
return identityKey.getPublicKey().serialize();
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage.protocol;
|
||||
package org.asamk.signal.manager.storage.protocol;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
|
@ -9,7 +9,9 @@ import com.fasterxml.jackson.databind.JsonSerializer;
|
|||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
|
||||
import org.asamk.signal.manager.TrustLevel;
|
||||
import org.asamk.signal.util.Util;
|
||||
import org.asamk.signal.manager.util.Utils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
|
@ -27,7 +29,9 @@ import java.util.UUID;
|
|||
|
||||
public class JsonIdentityKeyStore implements IdentityKeyStore {
|
||||
|
||||
private final List<Identity> identities = new ArrayList<>();
|
||||
final static Logger logger = LoggerFactory.getLogger(JsonIdentityKeyStore.class);
|
||||
|
||||
private final List<IdentityInfo> identities = new ArrayList<>();
|
||||
|
||||
private final IdentityKeyPair identityKeyPair;
|
||||
private final int localRegistrationId;
|
||||
|
@ -47,7 +51,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
|
|||
if (resolver != null) {
|
||||
return resolver.resolveSignalServiceAddress(identifier);
|
||||
} else {
|
||||
return Util.getSignalServiceAddressFromIdentifier(identifier);
|
||||
return Utils.getSignalServiceAddressFromIdentifier(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,7 +85,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
|
|||
public boolean saveIdentity(
|
||||
SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel, Date added
|
||||
) {
|
||||
for (Identity id : identities) {
|
||||
for (IdentityInfo id : identities) {
|
||||
if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) {
|
||||
continue;
|
||||
}
|
||||
|
@ -93,7 +97,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
|
|||
return true;
|
||||
}
|
||||
|
||||
identities.add(new Identity(serviceAddress, identityKey, trustLevel, added != null ? added : new Date()));
|
||||
identities.add(new IdentityInfo(serviceAddress, identityKey, trustLevel, added != null ? added : new Date()));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -107,7 +111,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
|
|||
public void setIdentityTrustLevel(
|
||||
SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel
|
||||
) {
|
||||
for (Identity id : identities) {
|
||||
for (IdentityInfo id : identities) {
|
||||
if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) {
|
||||
continue;
|
||||
}
|
||||
|
@ -119,7 +123,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
|
|||
return;
|
||||
}
|
||||
|
||||
identities.add(new Identity(serviceAddress, identityKey, trustLevel, new Date()));
|
||||
identities.add(new IdentityInfo(serviceAddress, identityKey, trustLevel, new Date()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -128,7 +132,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
|
|||
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
|
||||
boolean trustOnFirstUse = true;
|
||||
|
||||
for (Identity id : identities) {
|
||||
for (IdentityInfo id : identities) {
|
||||
if (!id.address.matches(serviceAddress)) {
|
||||
continue;
|
||||
}
|
||||
|
@ -146,14 +150,14 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
|
|||
@Override
|
||||
public IdentityKey getIdentity(SignalProtocolAddress address) {
|
||||
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
|
||||
Identity identity = getIdentity(serviceAddress);
|
||||
IdentityInfo identity = getIdentity(serviceAddress);
|
||||
return identity == null ? null : identity.getIdentityKey();
|
||||
}
|
||||
|
||||
public Identity getIdentity(SignalServiceAddress serviceAddress) {
|
||||
public IdentityInfo getIdentity(SignalServiceAddress serviceAddress) {
|
||||
long maxDate = 0;
|
||||
Identity maxIdentity = null;
|
||||
for (Identity id : this.identities) {
|
||||
IdentityInfo maxIdentity = null;
|
||||
for (IdentityInfo id : this.identities) {
|
||||
if (!id.address.matches(serviceAddress)) {
|
||||
continue;
|
||||
}
|
||||
|
@ -167,14 +171,14 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
|
|||
return maxIdentity;
|
||||
}
|
||||
|
||||
public List<Identity> getIdentities() {
|
||||
public List<IdentityInfo> getIdentities() {
|
||||
// TODO deep copy
|
||||
return identities;
|
||||
}
|
||||
|
||||
public List<Identity> getIdentities(SignalServiceAddress serviceAddress) {
|
||||
List<Identity> identities = new ArrayList<>();
|
||||
for (Identity identity : this.identities) {
|
||||
public List<IdentityInfo> getIdentities(SignalServiceAddress serviceAddress) {
|
||||
List<IdentityInfo> identities = new ArrayList<>();
|
||||
for (IdentityInfo identity : this.identities) {
|
||||
if (identity.address.matches(serviceAddress)) {
|
||||
identities.add(identity);
|
||||
}
|
||||
|
@ -209,7 +213,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
|
|||
UUID uuid = trustedKey.hasNonNull("uuid") ? UuidUtil.parseOrNull(trustedKey.get("uuid")
|
||||
.asText()) : null;
|
||||
final SignalServiceAddress serviceAddress = uuid == null
|
||||
? Util.getSignalServiceAddressFromIdentifier(trustedKeyName)
|
||||
? Utils.getSignalServiceAddressFromIdentifier(trustedKeyName)
|
||||
: new SignalServiceAddress(uuid, trustedKeyName);
|
||||
try {
|
||||
IdentityKey id = new IdentityKey(Base64.decode(trustedKey.get("identityKey").asText()), 0);
|
||||
|
@ -219,7 +223,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
|
|||
.asLong()) : new Date();
|
||||
keyStore.saveIdentity(serviceAddress, id, trustLevel, added);
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
System.out.println(String.format("Error while decoding key for: %s", trustedKeyName));
|
||||
logger.warn("Error while decoding key for {}: {}", trustedKeyName, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -242,7 +246,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
|
|||
json.writeStringField("identityKey",
|
||||
Base64.encodeBytes(jsonIdentityKeyStore.getIdentityKeyPair().serialize()));
|
||||
json.writeArrayFieldStart("trustedKeys");
|
||||
for (Identity trustedKey : jsonIdentityKeyStore.identities) {
|
||||
for (IdentityInfo trustedKey : jsonIdentityKeyStore.identities) {
|
||||
json.writeStartObject();
|
||||
if (trustedKey.getAddress().getNumber().isPresent()) {
|
||||
json.writeStringField("name", trustedKey.getAddress().getNumber().get());
|
||||
|
@ -260,53 +264,4 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
|
|||
}
|
||||
}
|
||||
|
||||
public static class Identity {
|
||||
|
||||
SignalServiceAddress address;
|
||||
IdentityKey identityKey;
|
||||
TrustLevel trustLevel;
|
||||
Date added;
|
||||
|
||||
public Identity(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel) {
|
||||
this.address = address;
|
||||
this.identityKey = identityKey;
|
||||
this.trustLevel = trustLevel;
|
||||
this.added = new Date();
|
||||
}
|
||||
|
||||
Identity(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel, Date added) {
|
||||
this.address = address;
|
||||
this.identityKey = identityKey;
|
||||
this.trustLevel = trustLevel;
|
||||
this.added = added;
|
||||
}
|
||||
|
||||
public SignalServiceAddress getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public void setAddress(final SignalServiceAddress address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
boolean isTrusted() {
|
||||
return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED;
|
||||
}
|
||||
|
||||
public IdentityKey getIdentityKey() {
|
||||
return this.identityKey;
|
||||
}
|
||||
|
||||
public TrustLevel getTrustLevel() {
|
||||
return this.trustLevel;
|
||||
}
|
||||
|
||||
public Date getDateAdded() {
|
||||
return this.added;
|
||||
}
|
||||
|
||||
public byte[] getFingerprint() {
|
||||
return identityKey.getPublicKey().serialize();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage.protocol;
|
||||
package org.asamk.signal.manager.storage.protocol;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
|
@ -8,6 +8,8 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.libsignal.InvalidKeyIdException;
|
||||
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.PreKeyStore;
|
||||
|
@ -19,6 +21,8 @@ import java.util.Map;
|
|||
|
||||
class JsonPreKeyStore implements PreKeyStore {
|
||||
|
||||
final static Logger logger = LoggerFactory.getLogger(JsonPreKeyStore.class);
|
||||
|
||||
private final Map<Integer, byte[]> store = new HashMap<>();
|
||||
|
||||
public JsonPreKeyStore() {
|
||||
|
@ -72,7 +76,7 @@ class JsonPreKeyStore implements PreKeyStore {
|
|||
try {
|
||||
preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText()));
|
||||
} catch (IOException e) {
|
||||
System.err.println(String.format("Error while decoding prekey for: %s", preKeyId));
|
||||
logger.warn("Error while decoding prekey for {}: {}", preKeyId, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage.protocol;
|
||||
package org.asamk.signal.manager.storage.protocol;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
|
@ -8,7 +8,9 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
|
||||
import org.asamk.signal.util.Util;
|
||||
import org.asamk.signal.manager.util.Utils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.state.SessionRecord;
|
||||
import org.whispersystems.libsignal.state.SessionStore;
|
||||
|
@ -24,6 +26,8 @@ import java.util.UUID;
|
|||
|
||||
class JsonSessionStore implements SessionStore {
|
||||
|
||||
final static Logger logger = LoggerFactory.getLogger(JsonSessionStore.class);
|
||||
|
||||
private final List<SessionInfo> sessions = new ArrayList<>();
|
||||
|
||||
private SignalServiceAddressResolver resolver;
|
||||
|
@ -39,7 +43,7 @@ class JsonSessionStore implements SessionStore {
|
|||
if (resolver != null) {
|
||||
return resolver.resolveSignalServiceAddress(identifier);
|
||||
} else {
|
||||
return Util.getSignalServiceAddressFromIdentifier(identifier);
|
||||
return Utils.getSignalServiceAddressFromIdentifier(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,7 +55,7 @@ class JsonSessionStore implements SessionStore {
|
|||
try {
|
||||
return new SessionRecord(info.sessionRecord);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to load session, resetting session: " + e);
|
||||
logger.warn("Failed to load session, resetting session: {}", e.getMessage());
|
||||
final SessionRecord sessionRecord = new SessionRecord();
|
||||
info.sessionRecord = sessionRecord.serialize();
|
||||
return sessionRecord;
|
||||
|
@ -143,7 +147,7 @@ class JsonSessionStore implements SessionStore {
|
|||
|
||||
UUID uuid = session.hasNonNull("uuid") ? UuidUtil.parseOrNull(session.get("uuid").asText()) : null;
|
||||
final SignalServiceAddress serviceAddress = uuid == null
|
||||
? Util.getSignalServiceAddressFromIdentifier(sessionName)
|
||||
? Utils.getSignalServiceAddressFromIdentifier(sessionName)
|
||||
: new SignalServiceAddress(uuid, sessionName);
|
||||
final int deviceId = session.get("deviceId").asInt();
|
||||
final String record = session.get("record").asText();
|
||||
|
@ -151,7 +155,7 @@ class JsonSessionStore implements SessionStore {
|
|||
SessionInfo sessionInfo = new SessionInfo(serviceAddress, deviceId, Base64.decode(record));
|
||||
sessionStore.sessions.add(sessionInfo);
|
||||
} catch (IOException e) {
|
||||
System.err.println(String.format("Error while decoding session for: %s", sessionName));
|
||||
logger.warn("Error while decoding session for {}: {}", sessionName, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage.protocol;
|
||||
package org.asamk.signal.manager.storage.protocol;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
@ -91,11 +91,11 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
|
|||
identityKeyStore.setIdentityTrustLevel(serviceAddress, identityKey, trustLevel);
|
||||
}
|
||||
|
||||
public List<JsonIdentityKeyStore.Identity> getIdentities() {
|
||||
public List<IdentityInfo> getIdentities() {
|
||||
return identityKeyStore.getIdentities();
|
||||
}
|
||||
|
||||
public List<JsonIdentityKeyStore.Identity> getIdentities(SignalServiceAddress serviceAddress) {
|
||||
public List<IdentityInfo> getIdentities(SignalServiceAddress serviceAddress) {
|
||||
return identityKeyStore.getIdentities(serviceAddress);
|
||||
}
|
||||
|
||||
|
@ -109,7 +109,7 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
|
|||
return identityKeyStore.getIdentity(address);
|
||||
}
|
||||
|
||||
public JsonIdentityKeyStore.Identity getIdentity(SignalServiceAddress serviceAddress) {
|
||||
public IdentityInfo getIdentity(SignalServiceAddress serviceAddress) {
|
||||
return identityKeyStore.getIdentity(serviceAddress);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage.protocol;
|
||||
package org.asamk.signal.manager.storage.protocol;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
|
@ -8,6 +8,8 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.libsignal.InvalidKeyIdException;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyStore;
|
||||
|
@ -21,6 +23,8 @@ import java.util.Map;
|
|||
|
||||
class JsonSignedPreKeyStore implements SignedPreKeyStore {
|
||||
|
||||
final static Logger logger = LoggerFactory.getLogger(JsonSignedPreKeyStore.class);
|
||||
|
||||
private final Map<Integer, byte[]> store = new HashMap<>();
|
||||
|
||||
public JsonSignedPreKeyStore() {
|
||||
|
@ -89,7 +93,7 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore {
|
|||
try {
|
||||
preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText()));
|
||||
} catch (IOException e) {
|
||||
System.err.println(String.format("Error while decoding prekey for: %s", preKeyId));
|
||||
logger.warn("Error while decoding prekey for {}: {}", preKeyId, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage.protocol;
|
||||
package org.asamk.signal.manager.storage.protocol;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage.protocol;
|
||||
package org.asamk.signal.manager.storage.protocol;
|
||||
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage.protocol;
|
||||
package org.asamk.signal.manager.storage.protocol;
|
||||
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage.stickers;
|
||||
package org.asamk.signal.manager.storage.stickers;
|
||||
|
||||
public class Sticker {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage.stickers;
|
||||
package org.asamk.signal.manager.storage.stickers;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage.threads;
|
||||
package org.asamk.signal.manager.storage.threads;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
|
@ -1,4 +1,4 @@
|
|||
package org.asamk.signal.storage.threads;
|
||||
package org.asamk.signal.manager.storage.threads;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package org.asamk.signal.manager.util;
|
||||
|
||||
import org.asamk.signal.manager.AttachmentInvalidException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AttachmentUtils {
|
||||
|
||||
public static List<SignalServiceAttachment> getSignalServiceAttachments(List<String> attachments) throws AttachmentInvalidException {
|
||||
List<SignalServiceAttachment> signalServiceAttachments = null;
|
||||
if (attachments != null) {
|
||||
signalServiceAttachments = new ArrayList<>(attachments.size());
|
||||
for (String attachment : attachments) {
|
||||
try {
|
||||
signalServiceAttachments.add(createAttachment(new File(attachment)));
|
||||
} catch (IOException e) {
|
||||
throw new AttachmentInvalidException(attachment, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return signalServiceAttachments;
|
||||
}
|
||||
|
||||
public static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException {
|
||||
InputStream attachmentStream = new FileInputStream(attachmentFile);
|
||||
final long attachmentSize = attachmentFile.length();
|
||||
final String mime = Utils.getFileMimeType(attachmentFile, "application/octet-stream");
|
||||
// TODO mabybe add a parameter to set the voiceNote, borderless, preview, width, height and caption option
|
||||
final long uploadTimestamp = System.currentTimeMillis();
|
||||
Optional<byte[]> preview = Optional.absent();
|
||||
Optional<String> caption = Optional.absent();
|
||||
Optional<String> blurHash = Optional.absent();
|
||||
final Optional<ResumableUploadSpec> resumableUploadSpec = Optional.absent();
|
||||
return new SignalServiceAttachmentStream(attachmentStream,
|
||||
mime,
|
||||
attachmentSize,
|
||||
Optional.of(attachmentFile.getName()),
|
||||
false,
|
||||
false,
|
||||
preview,
|
||||
0,
|
||||
0,
|
||||
uploadTimestamp,
|
||||
caption,
|
||||
blurHash,
|
||||
null,
|
||||
null,
|
||||
resumableUploadSpec);
|
||||
}
|
||||
|
||||
public static File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException {
|
||||
InputStream input = stream.getInputStream();
|
||||
|
||||
try (OutputStream 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;
|
||||
}
|
||||
return outputFile;
|
||||
}
|
||||
}
|
72
src/main/java/org/asamk/signal/manager/util/IOUtils.java
Normal file
72
src/main/java/org/asamk/signal/manager/util/IOUtils.java
Normal file
|
@ -0,0 +1,72 @@
|
|||
package org.asamk.signal.manager.util;
|
||||
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE;
|
||||
import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
|
||||
import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
|
||||
|
||||
public class IOUtils {
|
||||
|
||||
public static File createTempFile() throws IOException {
|
||||
return File.createTempFile("signal_tmp_", ".tmp");
|
||||
}
|
||||
|
||||
public static byte[] readFully(InputStream in) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
Util.copy(in, baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
public static void createPrivateDirectories(File file) throws IOException {
|
||||
if (file.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Path path = file.toPath();
|
||||
try {
|
||||
Set<PosixFilePermission> perms = EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE);
|
||||
Files.createDirectories(path, PosixFilePermissions.asFileAttribute(perms));
|
||||
} catch (UnsupportedOperationException e) {
|
||||
Files.createDirectories(path);
|
||||
}
|
||||
}
|
||||
|
||||
public static void createPrivateFile(File path) throws IOException {
|
||||
final Path file = path.toPath();
|
||||
try {
|
||||
Set<PosixFilePermission> perms = EnumSet.of(OWNER_READ, OWNER_WRITE);
|
||||
Files.createFile(file, PosixFilePermissions.asFileAttribute(perms));
|
||||
} catch (UnsupportedOperationException e) {
|
||||
Files.createFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
public static void copyStreamToFile(InputStream input, File outputFile) throws IOException {
|
||||
copyStreamToFile(input, outputFile, 8192);
|
||||
}
|
||||
|
||||
public static void copyStreamToFile(InputStream input, File outputFile, int bufferSize) throws IOException {
|
||||
try (OutputStream output = new FileOutputStream(outputFile)) {
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
int read;
|
||||
|
||||
while ((read = input.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue