From 6da5c375040efc21a3dc7ccf916d7690de028eb7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 13:50:06 +0200 Subject: [PATCH 01/11] Prevent attaching files from the signal-cli data directory --- .../signal/manager/helper/AttachmentHelper.java | 17 +++++++++++++++++ .../signal/manager/storage/SignalAccount.java | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java index afafeb24..e863be05 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java @@ -34,8 +34,10 @@ public class AttachmentHelper { private final SignalDependencies dependencies; private final AttachmentStore attachmentStore; + private final Context context; public AttachmentHelper(final Context context) { + this.context = context; this.dependencies = context.getDependencies(); this.attachmentStore = context.getAttachmentStore(); } @@ -92,6 +94,21 @@ public class AttachmentHelper { final boolean voiceNote ) throws AttachmentInvalidException { try { + // Reject local files that point into the signal-cli data directory + if (attachment != null && !attachment.startsWith("data:")) { + try { + final var file = new File(attachment); + final var canonical = file.getCanonicalFile(); + final var dataPath = context.getAccount().getDataPath().getCanonicalFile(); + if (canonical.toPath().startsWith(dataPath.toPath())) { + throw new AttachmentInvalidException(attachment, + new IOException("Attaching files from the signal-cli data directory is not allowed")); + } + } catch (IOException e) { + throw new AttachmentInvalidException(attachment, e); + } + } + final var streamDetailsAndFileName = Utils.createStreamDetails(attachment); final var streamDetails = streamDetailsAndFileName.first(); final var uploadSpec = getResumableUploadSpec(streamDetails); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 35f600b6..c7a5c6d8 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -192,6 +192,10 @@ public class SignalAccount implements Closeable { this.lock = lock; } + public File getDataPath() { + return dataPath; + } + public static SignalAccount load( File dataPath, String accountPath, From 46ce552589da95264175eecc17a816a447d0ef42 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 14:17:28 +0200 Subject: [PATCH 02/11] Normalize attachment ids --- .../signal/manager/storage/AttachmentStore.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/AttachmentStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/AttachmentStore.java index d25f1a72..00a284dd 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/AttachmentStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/AttachmentStore.java @@ -44,7 +44,8 @@ public class AttachmentStore { } public StreamDetails retrieveAttachment(final String id) throws IOException { - final var attachmentFile = new File(attachmentsPath, id); + final var safeId = sanitizeId(id); + final var attachmentFile = new File(attachmentsPath, safeId); return Utils.createStreamDetailsFromFile(attachmentFile); } @@ -61,7 +62,8 @@ public class AttachmentStore { Optional contentType ) { final var extension = getAttachmentExtension(filename, contentType); - return new File(attachmentsPath, attachmentId.toString() + extension + ".preview"); + final var safe = sanitizeId(attachmentId.toString()); + return new File(attachmentsPath, safe + extension + ".preview"); } private File getAttachmentFile( @@ -70,7 +72,15 @@ public class AttachmentStore { Optional contentType ) { final var extension = getAttachmentExtension(filename, contentType); - return new File(attachmentsPath, attachmentId.toString() + extension); + final var safe = sanitizeId(attachmentId.toString()); + return new File(attachmentsPath, safe + extension); + } + + private static String sanitizeId(final String id) { + if (id == null) { + return ""; + } + return id.replaceAll("[^A-Za-z0-9_.-]", "_"); } private static String getAttachmentExtension(final Optional filename, final Optional contentType) { From f34b5520545510b60262bceb3187c73b1338ccf2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 14:21:25 +0200 Subject: [PATCH 03/11] Validate host header for http daemon --- .../asamk/signal/http/HttpServerHandler.java | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/main/java/org/asamk/signal/http/HttpServerHandler.java b/src/main/java/org/asamk/signal/http/HttpServerHandler.java index 325b0eec..551c5b04 100644 --- a/src/main/java/org/asamk/signal/http/HttpServerHandler.java +++ b/src/main/java/org/asamk/signal/http/HttpServerHandler.java @@ -19,8 +19,11 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.InetSocketAddress; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; @@ -37,12 +40,14 @@ public class HttpServerHandler implements AutoCloseable { private final Manager m; private HttpServer server; private final AtomicBoolean shutdown = new AtomicBoolean(false); + private final Set allowedHosts; public HttpServerHandler(final InetSocketAddress address, final Manager m) { this.address = address; commandHandler = new SignalJsonRpcCommandHandler(m, Commands::getCommand); this.c = null; this.m = m; + this.allowedHosts = buildAllowedHosts(address); } public HttpServerHandler(final InetSocketAddress address, final MultiAccountManager c) { @@ -50,6 +55,7 @@ public class HttpServerHandler implements AutoCloseable { commandHandler = new SignalJsonRpcCommandHandler(c, Commands::getCommand); this.c = c; this.m = null; + this.allowedHosts = buildAllowedHosts(address); } public void init() throws IOException { @@ -67,6 +73,7 @@ public class HttpServerHandler implements AutoCloseable { server.start(); logger.info("Started HTTP server on {}", address); + logger.warn("HTTP server has no authentication; Host header is pinned to {}", allowedHosts); } @Override @@ -99,6 +106,12 @@ public class HttpServerHandler implements AutoCloseable { } private void handleRpcEndpoint(HttpExchange httpExchange) throws IOException { + if (!isHostAllowed(httpExchange)) { + logger.warn("Rejected RPC request with invalid Host header: {} from {}", + httpExchange.getRequestHeaders().getFirst("Host"), httpExchange.getRemoteAddress()); + sendResponse(421, null, httpExchange); + return; + } if (!"/api/v1/rpc".equals(httpExchange.getRequestURI().getPath())) { sendResponse(404, null, httpExchange); return; @@ -146,6 +159,12 @@ public class HttpServerHandler implements AutoCloseable { } private void handleEventsEndpoint(HttpExchange httpExchange) throws IOException { + if (!isHostAllowed(httpExchange)) { + logger.warn("Rejected Events request with invalid Host header: {} from {}", + httpExchange.getRequestHeaders().getFirst("Host"), httpExchange.getRemoteAddress()); + sendResponse(421, null, httpExchange); + return; + } if (!"/api/v1/events".equals(httpExchange.getRequestURI().getPath())) { sendResponse(404, null, httpExchange); return; @@ -273,4 +292,59 @@ public class HttpServerHandler implements AutoCloseable { void call(); } + + private Set buildAllowedHosts(final InetSocketAddress address) { + final var s = new HashSet(); + final var host = address == null ? null : address.getHostString(); + if (host != null && !host.isEmpty()) { + s.add(host.toLowerCase(Locale.ROOT)); + } + s.add("localhost"); + s.add("127.0.0.1"); + s.add("::1"); + return s; + } + + private boolean isHostAllowed(final HttpExchange httpExchange) { + final var hostHeader = httpExchange.getRequestHeaders().getFirst("Host"); + if (hostHeader == null || hostHeader.isEmpty()) { + return false; + } + + String hostPart = hostHeader; + String portPart = null; + if (hostHeader.startsWith("[")) { + final var idx = hostHeader.indexOf(']'); + if (idx == -1) return false; + hostPart = hostHeader.substring(1, idx); + if (hostHeader.length() > idx + 1 && hostHeader.charAt(idx + 1) == ':') { + portPart = hostHeader.substring(idx + 2); + } + } else { + final var colon = hostHeader.lastIndexOf(':'); + if (colon != -1) { + final var possiblePort = hostHeader.substring(colon + 1); + if (possiblePort.chars().allMatch(Character::isDigit)) { + hostPart = hostHeader.substring(0, colon); + portPart = possiblePort; + } + } + } + + hostPart = hostPart.toLowerCase(Locale.ROOT); + if (!allowedHosts.contains(hostPart)) { + return false; + } + + if (portPart != null) { + try { + final var port = Integer.parseInt(portPart); + if (port != address.getPort()) return false; + } catch (NumberFormatException e) { + return false; + } + } + + return true; + } } From 393e1efcd1633d31d450212bf26ebfae77dc1cf1 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 14:30:33 +0200 Subject: [PATCH 04/11] Create temp file with limited permissions --- .../org/asamk/signal/manager/util/IOUtils.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java index 65d3b1d8..0061e0d9 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java @@ -21,9 +21,19 @@ import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; public class IOUtils { public static File createTempFile() throws IOException { - final var tempFile = File.createTempFile("signal-cli_tmp_", ".tmp"); - tempFile.deleteOnExit(); - return tempFile; + final var prefix = "signal-cli_tmp_"; + final var suffix = ".tmp"; + try { + Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE); + var path = Files.createTempFile(prefix, suffix, PosixFilePermissions.asFileAttribute(perms)); + var tempFile = path.toFile(); + tempFile.deleteOnExit(); + return tempFile; + } catch (UnsupportedOperationException e) { + final var tempFile = File.createTempFile(prefix, suffix); + tempFile.deleteOnExit(); + return tempFile; + } } public static byte[] readFully(InputStream in) throws IOException { From 44d54b32157742fda0c09bc56937f456fa1329fe Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 16:35:22 +0200 Subject: [PATCH 05/11] Add support for a global configuration file --- man/signal-cli-dbus.5.adoc | 2 +- man/signal-cli-jsonrpc.5.adoc | 4 +- man/signal-cli.1.adoc | 23 +++++- src/main/java/org/asamk/signal/App.java | 55 ++++++++----- .../java/org/asamk/signal/ConfigLoader.java | 81 +++++++++++++++++++ .../java/org/asamk/signal/GlobalConfig.java | 36 +++++++++ src/main/java/org/asamk/signal/Main.java | 64 ++++++++++----- .../java/org/asamk/signal/OutputType.java | 15 +++- .../asamk/signal/ServiceEnvironmentCli.java | 15 +++- .../org/asamk/signal/TrustNewIdentityCli.java | 16 +++- .../signal-cli/reachability-metadata.json | 55 +++++++++++++ 11 files changed, 315 insertions(+), 51 deletions(-) create mode 100644 src/main/java/org/asamk/signal/ConfigLoader.java create mode 100644 src/main/java/org/asamk/signal/GlobalConfig.java diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 732efe43..9246e9ed 100644 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -13,7 +13,7 @@ signal-cli-dbus - A commandline and dbus interface for the Signal messenger == Synopsis -*signal-cli* [--verbose] [--config CONFIG] [-a ACCOUNT] [-o {plain-text,json}] daemon [--dbus] [--dbus-system] +*signal-cli* [--verbose] [--data-dir DATA_DIR] [-a ACCOUNT] [-o {plain-text,json}] daemon [--dbus] [--dbus-system] *dbus-send* [--system | --session] [--print-reply] --type=method_call --dest="org.asamk.Signal" /org/asamk/Signal[/_] org.asamk.Signal. [string:] [array::] diff --git a/man/signal-cli-jsonrpc.5.adoc b/man/signal-cli-jsonrpc.5.adoc index 1c035af8..b9abb8ce 100644 --- a/man/signal-cli-jsonrpc.5.adoc +++ b/man/signal-cli-jsonrpc.5.adoc @@ -13,9 +13,9 @@ signal-cli-jsonrpc - A commandline and dbus interface for the Signal messenger == Synopsis -*signal-cli* [--verbose] [--config CONFIG] [-a ACCOUNT] daemon [--socket[=SOCKET_PATH]] [--tcp[=HOST:PORT]] [--http[=HOST:PORT]] +*signal-cli* [--verbose] [--data-dir DATA_DIR] [-a ACCOUNT] daemon [--socket[=SOCKET_PATH]] [--tcp[=HOST:PORT]] [--http[=HOST:PORT]] -*signal-cli* [--verbose] [--config CONFIG] [-a ACCOUNT] jsonRpc +*signal-cli* [--verbose] [--data-dir DATA_DIR] [-a ACCOUNT] jsonRpc == Description diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index e349a2ad..f6ed732d 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -13,7 +13,7 @@ signal-cli - A commandline interface for the Signal messenger == Synopsis -*signal-cli* [--config CONFIG] [-h | -v | -a ACCOUNT | --dbus | --dbus-system] command [command-options] +*signal-cli* [--data-dir DATA_DIR] [-h | -v | -a ACCOUNT | --dbus | --dbus-system] command [command-options] == Description @@ -57,8 +57,8 @@ If `--verbose` is also given, the detailed logs will only be written to the log Scrub possibly sensitive information from the log, like phone numbers and UUIDs. Doesn't work reliably on dbus logs with very verbose logging (`-vvv`) -*--config* CONFIG:: -Set the path, where to store the config. +*-d* DATA_DIR, *--data-dir* DATA_DIR, *-c* CONFIG, *--config* CONFIG:: +Set the path where to store account data and local configuration. Make sure you have full read/write access to the given directory. (Default: `$XDG_DATA_HOME/signal-cli` (`$HOME/.local/share/signal-cli`)) @@ -1172,10 +1172,25 @@ signal-cli -a ACCOUNT trust -a RECIPIENT == Files -The password and cryptographic keys are created when registering and stored in the current users home directory, the directory can be changed with *--config*: +The password and cryptographic keys are created when registering and stored in the current users home directory, the directory can be changed with *--data-dir* (legacy *--config*): `$XDG_DATA_HOME/signal-cli/` (`$HOME/.local/share/signal-cli/`) +=== Configuration file + +signal-cli supports a JSON-based global configuration file that provides defaults for CLI options. +Keys use camelCase and generally match the long CLI parameter names (for example `dataDir`, `verbose`, `logFile`, `serviceEnvironment`, `trustNewIdentities`, `output`, `disableSendLog`, `account`). + +Configuration files are read and merged in this order; later files override earlier ones: + +- `/etc/signal-cli/config.json` (system-wide defaults) +- the path in the `SIGNAL_CLI_CONFIG` environment variable (if set) +- `$XDG_CONFIG_HOME/signal-cli/config.json` (per-user; defaults to `$HOME/.config/signal-cli/config.json`) + +When multiple configuration files are present their settings are merged; values from later files override earlier values. +Command-line options always take precedence over configuration file values. +Overall precedence (highest → lowest): command-line options → per-user config → system config → built-in defaults. + == Authors Maintained by AsamK , who is assisted by other open source contributors. diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index bcf3c605..d29cc2f8 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -46,7 +46,9 @@ public class App { private final Namespace ns; - static ArgumentParser buildArgumentParser() { + static ArgumentParser buildArgumentParser(GlobalConfig config) { + final var cfg = config == null ? GlobalConfig.DEFAULT : config; + var parser = ArgumentParsers.newFor("signal-cli", VERSION_0_9_0_DEFAULT_SETTINGS) .includeArgumentNamesAsKeysInResult(true) .build() @@ -57,47 +59,60 @@ public class App { parser.addArgument("--version").help("Show package version.").action(Arguments.version()); parser.addArgument("-v", "--verbose") .help("Raise log level and include lib signal logs. Specify multiple times for even more logs.") - .action(Arguments.count()); + .action(Arguments.count()) + .setDefault(cfg.verbose()); parser.addArgument("--log-file") .type(File.class) - .help("Write log output to the given file. If --verbose is also given, the detailed logs will only be written to the log file."); + .help("Write log output to the given file. If --verbose is also given, the detailed logs will only be written to the log file.") + .setDefault(cfg.logFile() == null ? null : new File(cfg.logFile())); parser.addArgument("--scrub-log") .action(Arguments.storeTrue()) - .help("Scrub possibly sensitive information from the log, like phone numbers and UUIDs."); - parser.addArgument("-c", "--config") - .help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli)."); + .help("Scrub possibly sensitive information from the log, like phone numbers and UUIDs.") + .setDefault(cfg.scrubLog()); + parser.addArgument("-d", "--data-dir", "-c", "--config") + .help("Set the path where to store data (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).") + .setDefault(cfg.dataDir()); parser.addArgument("-a", "--account", "-u", "--username") .help("Specify your phone number, that will be your identifier."); var mut = parser.addMutuallyExclusiveGroup(); - mut.addArgument("--dbus").dest("global-dbus").help("Make request via user dbus.").action(Arguments.storeTrue()); + mut.addArgument("--dbus") + .dest("global-dbus") + .help("Make request via user dbus.") + .action(Arguments.storeTrue()) + .setDefault(cfg.dbus()); mut.addArgument("--dbus-system") .dest("global-dbus-system") .help("Make request via system dbus.") - .action(Arguments.storeTrue()); + .action(Arguments.storeTrue()) + .setDefault(cfg.dbusSystem()); parser.addArgument("--bus-name") .dest("global-bus-name") - .setDefault(DbusConfig.getBusname()) + .setDefault(cfg.busName() != null ? cfg.busName() : DbusConfig.getBusname()) .help("Specify the D-Bus bus name to connect to."); parser.addArgument("-o", "--output") .help("Choose to output in plain text or JSON") - .type(Arguments.enumStringType(OutputType.class)); + .type(Arguments.enumStringType(OutputType.class)) + .setDefault(cfg.output() == null ? null : cfg.output()); parser.addArgument("--service-environment") .help("Choose the server environment to use.") .type(Arguments.enumStringType(ServiceEnvironmentCli.class)) - .setDefault(ServiceEnvironmentCli.LIVE); + .setDefault(cfg.serviceEnvironment() != null ? cfg.serviceEnvironment() : ServiceEnvironmentCli.LIVE); parser.addArgument("--trust-new-identities") .help("Choose when to trust new identities.") .type(Arguments.enumStringType(TrustNewIdentityCli.class)) - .setDefault(TrustNewIdentityCli.ON_FIRST_USE); + .setDefault(cfg.trustNewIdentities() != null + ? cfg.trustNewIdentities() + : TrustNewIdentityCli.ON_FIRST_USE); parser.addArgument("--disable-send-log") .help("Disable message send log (for resending messages that recipient couldn't decrypt)") - .action(Arguments.storeTrue()); + .action(Arguments.storeTrue()) + .setDefault(cfg.disableSendLog()); parser.epilog( "The global arguments are shown with 'signal-cli -h' and need to come before the subcommand, while the subcommand-specific arguments (shown with 'signal-cli SUBCOMMAND -h') need to be given after the subcommand."); @@ -219,12 +234,12 @@ public class App { } private SignalAccountFiles loadSignalAccountFiles() throws IOErrorException { - final File configPath; - final var config = ns.getString("config"); - if (config != null) { - configPath = new File(config); + final File dataPath; + final var dataDir = ns.getString("data-dir"); + if (dataDir != null) { + dataPath = new File(dataDir); } else { - configPath = getDefaultConfigPath(); + dataPath = getDefaultDataPath(); } final var serviceEnvironmentCli = ns.get("service-environment"); @@ -240,7 +255,7 @@ public class App { final var disableSendLog = Boolean.TRUE.equals(ns.getBoolean("disable-send-log")); try { - return new SignalAccountFiles(configPath, + return new SignalAccountFiles(dataPath, serviceEnvironment, BaseConfig.USER_AGENT, new Settings(trustNewIdentity, disableSendLog)); @@ -339,7 +354,7 @@ public class App { /** * @return the default data directory to be used by signal-cli. */ - private static File getDefaultConfigPath() { + private static File getDefaultDataPath() { return new File(IOUtils.getDataHomeDir(), "signal-cli"); } } diff --git a/src/main/java/org/asamk/signal/ConfigLoader.java b/src/main/java/org/asamk/signal/ConfigLoader.java new file mode 100644 index 00000000..861de2e0 --- /dev/null +++ b/src/main/java/org/asamk/signal/ConfigLoader.java @@ -0,0 +1,81 @@ +package org.asamk.signal; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.asamk.signal.commands.exceptions.UserErrorException; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Loads and merges configuration files. Merge order (later files override earlier): + * - /etc/signal-cli/config.json + * - file pointed to by SIGNAL_CLI_CONFIG (if set) + * - $XDG_CONFIG_HOME/signal-cli/config.json or $HOME/.config/signal-cli/config.json + */ +public final class ConfigLoader { + + private ConfigLoader() { + } + + public static GlobalConfig load() throws UserErrorException { + final ObjectMapper mapper = new ObjectMapper(); + final ObjectNode merged = mapper.createObjectNode(); + + // System config + addIfExists(merged, mapper, Paths.get("/etc/signal-cli/config.json")); + + // User config via env (if set) else XDG or ~/.config + final String env = System.getenv("SIGNAL_CLI_CONFIG"); + if (env != null && !env.isEmpty()) { + addIfExists(merged, mapper, Paths.get(env)); + } else { + final String xdg = System.getenv("XDG_CONFIG_HOME"); + if (xdg != null && !xdg.isEmpty()) { + addIfExists(merged, mapper, Paths.get(xdg, "signal-cli", "config.json")); + } else { + addIfExists(merged, + mapper, + Paths.get(System.getProperty("user.home"), ".config", "signal-cli", "config.json")); + } + } + + try { + if (merged.isEmpty()) { + return GlobalConfig.DEFAULT; + } + return mapper.treeToValue(merged, GlobalConfig.class); + } catch (Exception e) { + throw new UserErrorException("Failed to parse configuration file(s): " + e.getMessage(), e); + } + } + + private static void addIfExists(ObjectNode merged, ObjectMapper mapper, Path p) throws UserErrorException { + if (p == null) return; + try { + if (Files.exists(p)) { + final JsonNode node = mapper.readTree(p.toFile()); + merge(merged, node); + } + } catch (IOException e) { + throw new UserErrorException("Failed to load config from " + p + ": " + e.getMessage(), e); + } + } + + private static void merge(ObjectNode target, JsonNode source) { + source.properties().forEach(entry -> { + final String name = entry.getKey(); + final JsonNode value = entry.getValue(); + final JsonNode existing = target.get(name); + if (existing != null && existing.isObject() && value.isObject()) { + merge((ObjectNode) existing, value); + } else { + target.set(name, value); + } + }); + } +} diff --git a/src/main/java/org/asamk/signal/GlobalConfig.java b/src/main/java/org/asamk/signal/GlobalConfig.java new file mode 100644 index 00000000..1205df7c --- /dev/null +++ b/src/main/java/org/asamk/signal/GlobalConfig.java @@ -0,0 +1,36 @@ +package org.asamk.signal; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record GlobalConfig( + @JsonProperty("verbose") Integer verbose, + @JsonProperty("logFile") String logFile, + @JsonProperty("scrubLog") Boolean scrubLog, + @JsonProperty("dataDir") String dataDir, + @JsonProperty("busName") String busName, + @JsonProperty("dbus") Boolean dbus, + @JsonProperty("dbusSystem") Boolean dbusSystem, + @JsonProperty("output") OutputType output, + @JsonProperty("serviceEnvironment") ServiceEnvironmentCli serviceEnvironment, + @JsonProperty("trustNewIdentities") TrustNewIdentityCli trustNewIdentities, + @JsonProperty("disableSendLog") Boolean disableSendLog, + @JsonProperty("account") String account +) { + + public static final GlobalConfig DEFAULT = new GlobalConfig(null, + null, + null, + null, + null, + null, + null, + null, + ServiceEnvironmentCli.LIVE, + TrustNewIdentityCli.ON_FIRST_USE, + null, + null); + + public static GlobalConfig empty() { + return new GlobalConfig(null, null, null, null, null, null, null, null, null, null, null, null); + } +} diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 568f58c6..800b0ee6 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -40,27 +40,32 @@ import java.security.Security; public class Main { - public static void main(String[] args) { + static void main(String[] args) { // enable unlimited strength crypto via Policy, supported on relevant JREs Security.setProperty("crypto.policy", "unlimited"); installSecurityProviderWorkaround(); + // Load global config early so we can use its values as parser defaults + final GlobalConfig globalConfig; + try { + globalConfig = ConfigLoader.load(); + } catch (UserErrorException e) { + System.exit(handleCommandException(e, null)); + return; + } + // Configuring the logger needs to happen before any logger is initialized - final var loggingConfig = parseLoggingConfig(args); + final var loggingConfig = parseLoggingConfig(args, globalConfig); configureLogging(loggingConfig); - final var parser = App.buildArgumentParser(); + final var parser = App.buildArgumentParser(globalConfig); final var ns = parser.parseArgsOrFail(args); int status = 0; try { new App(ns).init(); } catch (CommandException e) { - System.err.println(e.getMessage()); - if (loggingConfig.verboseLevel > 0 && e.getCause() != null) { - e.getCause().printStackTrace(System.err); - } - status = getStatusForError(e); + status = handleCommandException(e, loggingConfig); } catch (Throwable e) { e.printStackTrace(System.err); status = 2; @@ -69,16 +74,27 @@ public class Main { System.exit(status); } + private static int handleCommandException(final CommandException e, final LoggingConfig loggingConfig) { + System.err.println(e.getMessage()); + if (loggingConfig != null && loggingConfig.verboseLevel > 0 && e.getCause() != null) { + e.getCause().printStackTrace(System.err); + } + return getStatusForError(e); + } + private static void installSecurityProviderWorkaround() { // Register our own security provider Security.insertProviderAt(new SecurityProvider(), 1); Security.addProvider(new BouncyCastleProvider()); } - private static LoggingConfig parseLoggingConfig(final String[] args) { - final var nsLog = parseArgs(args); + private static LoggingConfig parseLoggingConfig(final String[] args, final GlobalConfig config) { + final var nsLog = parseArgs(args, config); if (nsLog == null) { - return new LoggingConfig(0, null, false); + final var verbose = config != null && config.verbose() != null ? config.verbose() : 0; + final var logFile = config != null && config.logFile() != null ? new File(config.logFile()) : null; + final var scrubLog = config != null && Boolean.TRUE.equals(config.scrubLog()); + return new LoggingConfig(verbose, logFile, scrubLog); } final var verboseLevel = nsLog.getInt("verbose"); @@ -90,14 +106,20 @@ public class Main { /** * This method only parses commandline args relevant for logging configuration. */ - private static Namespace parseArgs(String[] args) { + private static Namespace parseArgs(String[] args, final GlobalConfig config) { var parser = ArgumentParsers.newFor("signal-cli", DefaultSettings.VERSION_0_9_0_DEFAULT_SETTINGS) .includeArgumentNamesAsKeysInResult(true) .build() .defaultHelp(false); - parser.addArgument("-v", "--verbose").action(Arguments.count()); - parser.addArgument("--log-file").type(File.class); - parser.addArgument("--scrub-log").action(Arguments.storeTrue()); + parser.addArgument("-v", "--verbose") + .action(Arguments.count()) + .setDefault(config == null ? null : config.verbose()); + parser.addArgument("--log-file") + .type(File.class) + .setDefault(config == null || config.logFile() == null ? null : new File(config.logFile())); + parser.addArgument("--scrub-log") + .action(Arguments.storeTrue()) + .setDefault(config == null ? null : config.scrubLog()); try { return parser.parseKnownArgs(args, null); @@ -124,12 +146,12 @@ public class Main { private static int getStatusForError(final CommandException e) { return switch (e) { - case UserErrorException userErrorException -> 1; - case UnexpectedErrorException unexpectedErrorException -> 2; - case IOErrorException ioErrorException -> 3; - case UntrustedKeyErrorException untrustedKeyErrorException -> 4; - case RateLimitErrorException rateLimitErrorException -> 5; - case CaptchaRejectedErrorException captchaRejectedErrorException -> 6; + case UserErrorException _ -> 1; + case UnexpectedErrorException _ -> 2; + case IOErrorException _ -> 3; + case UntrustedKeyErrorException _ -> 4; + case RateLimitErrorException _ -> 5; + case CaptchaRejectedErrorException _ -> 6; case null -> 2; }; } diff --git a/src/main/java/org/asamk/signal/OutputType.java b/src/main/java/org/asamk/signal/OutputType.java index 383d635f..2da6b5db 100644 --- a/src/main/java/org/asamk/signal/OutputType.java +++ b/src/main/java/org/asamk/signal/OutputType.java @@ -1,5 +1,7 @@ package org.asamk.signal; +import com.fasterxml.jackson.annotation.JsonCreator; + public enum OutputType { PLAIN_TEXT { @Override @@ -12,5 +14,16 @@ public enum OutputType { public String toString() { return "json"; } - }, + }; + + @JsonCreator + public static OutputType fromString(String value) { + if (value == null) return null; + final var norm = value.trim().toLowerCase().replaceAll("[^a-z0-9]", ""); + return switch (norm) { + case "plaintext" -> PLAIN_TEXT; + case "json" -> JSON; + default -> throw new IllegalArgumentException("Invalid output type: " + value); + }; + } } diff --git a/src/main/java/org/asamk/signal/ServiceEnvironmentCli.java b/src/main/java/org/asamk/signal/ServiceEnvironmentCli.java index 9010f3f8..f61a6833 100644 --- a/src/main/java/org/asamk/signal/ServiceEnvironmentCli.java +++ b/src/main/java/org/asamk/signal/ServiceEnvironmentCli.java @@ -1,5 +1,7 @@ package org.asamk.signal; +import com.fasterxml.jackson.annotation.JsonCreator; + public enum ServiceEnvironmentCli { LIVE { @Override @@ -12,5 +14,16 @@ public enum ServiceEnvironmentCli { public String toString() { return "staging"; } - }, + }; + + @JsonCreator + public static ServiceEnvironmentCli fromString(String value) { + if (value == null) return null; + final var norm = value.trim().toLowerCase(); + return switch (norm) { + case "live" -> LIVE; + case "staging" -> STAGING; + default -> throw new IllegalArgumentException("Invalid service-environment: " + value); + }; + } } diff --git a/src/main/java/org/asamk/signal/TrustNewIdentityCli.java b/src/main/java/org/asamk/signal/TrustNewIdentityCli.java index 5cc36bbd..665ab90b 100644 --- a/src/main/java/org/asamk/signal/TrustNewIdentityCli.java +++ b/src/main/java/org/asamk/signal/TrustNewIdentityCli.java @@ -1,5 +1,7 @@ package org.asamk.signal; +import com.fasterxml.jackson.annotation.JsonCreator; + public enum TrustNewIdentityCli { ALWAYS { @Override @@ -18,5 +20,17 @@ public enum TrustNewIdentityCli { public String toString() { return "never"; } - }, + }; + + @JsonCreator + public static TrustNewIdentityCli fromString(String value) { + if (value == null) return null; + final var norm = value.trim().toLowerCase().replaceAll("[^a-z0-9]", ""); + return switch (norm) { + case "always" -> ALWAYS; + case "onfirstuse" -> ON_FIRST_USE; + case "never" -> NEVER; + default -> throw new IllegalArgumentException("Invalid trust-new-identities: " + value); + }; + } } diff --git a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json index 4513328b..cb971b26 100644 --- a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json +++ b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json @@ -1317,6 +1317,14 @@ } ] }, + { + "type": "java.util.concurrent.CopyOnWriteArrayList", + "fields": [ + { + "name": "lock" + } + ] + }, { "type": "java.util.concurrent.ForkJoinTask", "fields": [ @@ -1979,6 +1987,28 @@ { "type": "org.asamk.SignalControl.Error.InvalidNumber" }, + { + "type": "org.asamk.signal.GlobalConfig", + "methods": [ + { + "name": "", + "parameterTypes": [ + "java.lang.Integer", + "java.lang.String", + "java.lang.Boolean", + "java.lang.String", + "java.lang.String", + "java.lang.Boolean", + "java.lang.Boolean", + "org.asamk.signal.OutputType", + "org.asamk.signal.ServiceEnvironmentCli", + "org.asamk.signal.TrustNewIdentityCli", + "java.lang.Boolean", + "java.lang.String" + ] + } + ] + }, { "type": "org.asamk.signal.Main", "jniAccessible": true, @@ -1995,6 +2025,31 @@ } ] }, + { + "type": "org.asamk.signal.OutputType" + }, + { + "type": "org.asamk.signal.ServiceEnvironmentCli", + "methods": [ + { + "name": "fromString", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "type": "org.asamk.signal.TrustNewIdentityCli", + "methods": [ + { + "name": "fromString", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, { "type": "org.asamk.signal.commands.AcceptCallCommand$JsonCallInfo", "allDeclaredFields": true, From ced95600402b819da4704272a196b5f190928f6f Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 18:46:39 +0200 Subject: [PATCH 06/11] Update libsignal-service --- gradle/libs.versions.toml | 2 +- .../signal/manager/api/MessageEnvelope.java | 1 - .../signal/manager/helper/AccountHelper.java | 2 +- .../manager/helper/AttachmentHelper.java | 2 +- .../signal/manager/helper/GroupV2Helper.java | 2 +- .../signal/manager/helper/PreKeyHelper.java | 7 +-- .../signal/manager/helper/ProfileHelper.java | 2 +- .../manager/helper/RecipientHelper.java | 2 +- .../signal/manager/helper/SyncHelper.java | 2 +- .../signal/manager/internal/ManagerImpl.java | 3 +- .../internal/RegistrationManagerImpl.java | 1 + .../manager/internal/SignalDependencies.java | 46 ++++++++++++++++--- .../SignalWebSocketHealthMonitor.java | 8 +++- .../storage/sessions/SessionStore.java | 2 +- .../manager/util/NumberVerificationUtils.java | 2 +- .../org/asamk/signal/manager/util/Utils.java | 2 +- .../signal-cli/reachability-metadata.json | 27 +++++++++++ 17 files changed, 90 insertions(+), 23 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e36c07bb..972d5595 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ slf4j = "2.0.18" junit = "6.0.3" micronaut-json-schema = "2.0.0" micronaut-core = "5.0.0" -signal-service = "2.15.3_unofficial_146" +signal-service = "2.15.3_unofficial_147" [libraries] bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84" diff --git a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java index 975e1639..34b2274d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java @@ -4,7 +4,6 @@ import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.helper.RecipientAddressResolver; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.asamk.signal.manager.util.MimeUtils; -import org.signal.core.models.ServiceId; import org.signal.libsignal.metadata.ProtocolException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java index 2396b36e..a4f037f1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java @@ -25,6 +25,7 @@ import org.signal.libsignal.protocol.state.SignedPreKeyRecord; import org.signal.libsignal.protocol.util.KeyHelper; import org.signal.libsignal.usernames.BaseUsernameException; import org.signal.libsignal.usernames.Username; +import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest; @@ -37,7 +38,6 @@ import org.whispersystems.signalservice.api.push.UsernameLinkComponents; import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException; import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java index e863be05..1fefcb24 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java @@ -126,7 +126,7 @@ public class AttachmentHelper { final var streamLength = streamDetails.getLength(); final var ciphertextLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize( streamLength)); - return dependencies.getMessageSender().getResumableUploadSpec(ciphertextLength); + return dependencies.getCdnService().getResumableUploadSpecBlocking(ciphertextLength); } public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index e2bc2a82..d4df661e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -21,6 +21,7 @@ import org.signal.libsignal.zkgroup.groups.GroupMasterKey; import org.signal.libsignal.zkgroup.groups.GroupSecretParams; import org.signal.libsignal.zkgroup.groups.UuidCiphertext; import org.signal.libsignal.zkgroup.profiles.ProfileKey; +import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.signal.storageservice.storage.protos.groups.AccessControl; import org.signal.storageservice.storage.protos.groups.GroupChange; import org.signal.storageservice.storage.protos.groups.GroupChangeResponse; @@ -43,7 +44,6 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException; import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException; import java.io.IOException; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java index bf7ad580..18a7b165 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java @@ -9,6 +9,7 @@ import org.signal.libsignal.protocol.InvalidKeyIdException; import org.signal.libsignal.protocol.state.KyberPreKeyRecord; import org.signal.libsignal.protocol.state.PreKeyRecord; import org.signal.libsignal.protocol.state.SignedPreKeyRecord; +import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.NetworkResultUtil; @@ -16,7 +17,6 @@ import org.whispersystems.signalservice.api.account.PreKeyUpload; import org.whispersystems.signalservice.api.keys.OneTimePreKeyCounts; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import java.io.IOException; import java.util.List; @@ -84,7 +84,8 @@ public class PreKeyHelper { ) throws IOException { OneTimePreKeyCounts preKeyCounts; try { - preKeyCounts = handleResponseException(dependencies.getKeysApi().getAvailablePreKeyCounts(serviceIdType)); + preKeyCounts = handleResponseException(dependencies.getKeysApi() + .getAvailablePreKeyCountsSync(serviceIdType)); } catch (AuthorizationFailedException e) { logger.debug("Failed to get pre key count, ignoring: " + e.getClass().getSimpleName()); preKeyCounts = new OneTimePreKeyCounts(0, 0); @@ -145,7 +146,7 @@ public class PreKeyHelper { kyberPreKeyRecords); var needsReset = false; try { - NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeys(preKeyUpload)); + NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeysSync(preKeyUpload)); try { if (preKeyRecords != null) { account.addPreKeys(serviceIdType, preKeyRecords); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index 7906fc86..64e3a764 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -21,6 +21,7 @@ import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; +import org.signal.network.exceptions.PushNetworkException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.NetworkResultUtil; @@ -30,7 +31,6 @@ import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; -import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.services.ProfileService; import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java index e84a3059..c865f38f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java @@ -11,13 +11,13 @@ import org.signal.core.models.ServiceId.ACI; import org.signal.core.models.ServiceId.PNI; import org.signal.libsignal.usernames.BaseUsernameException; import org.signal.libsignal.usernames.Username; +import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.cds.CdsiV2Service; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException; import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import java.io.IOException; import java.util.Collection; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java index 53ab3fa4..ba2ccad1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -160,7 +160,7 @@ public class SyncHelper { try { try (OutputStream fos = new FileOutputStream(contactsFile)) { - var out = new DeviceContactsOutputStream(fos, true, true); + var out = new DeviceContactsOutputStream(fos); for (var contactPair : account.getContactStore().getContacts()) { final var recipientId = contactPair.first(); final var contact = contactPair.second(); diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java index 394fa894..18c261b5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java @@ -101,6 +101,7 @@ import org.signal.core.util.Base64; import org.signal.core.util.Hex; import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.usernames.BaseUsernameException; +import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; @@ -117,7 +118,6 @@ import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMess import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.internal.util.Util; @@ -190,6 +190,7 @@ public class ManagerImpl implements Manager { userAgent, account.getCredentialsProvider(), account.getSignalServiceDataStore(), + account.getDeviceId(), executor, sessionLock); final var avatarStore = new AvatarStore(pathConfig.avatarsPath()); diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java index e90bb154..1fd86c26 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java @@ -231,6 +231,7 @@ public class RegistrationManagerImpl implements RegistrationManager { userAgent, account.getCredentialsProvider(), account.getSignalServiceDataStore(), + 0, null, new ReentrantSignalSessionLock()); handleResponseException(dependencies.getAccountApi() diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java index 78d658f4..55795714 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java @@ -5,13 +5,17 @@ import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.util.Utils; import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.signal.libsignal.net.Network; +import org.signal.libsignal.protocol.SignalProtocolAddress; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; +import org.signal.network.api.AttachmentApi; import org.signal.network.api.CallingApi; import org.signal.network.api.CdsApi; import org.signal.network.api.CertificateApi; import org.signal.network.api.LinkDeviceApi; import org.signal.network.api.RateLimitChallengeApi; import org.signal.network.api.UsernameApi; +import org.signal.network.rest.SignalRestClient; +import org.signal.network.service.CdnService; import org.signal.network.service.StorageServiceService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,12 +25,12 @@ import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.account.AccountApi; -import org.whispersystems.signalservice.api.attachment.AttachmentApi; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.keys.KeysApi; +import org.whispersystems.signalservice.api.keys.PreKeyRepository; import org.whispersystems.signalservice.api.message.MessageApi; import org.whispersystems.signalservice.api.profiles.ProfileApi; import org.whispersystems.signalservice.api.push.ServiceIdType; @@ -61,6 +65,7 @@ public class SignalDependencies { private final String userAgent; private final CredentialsProvider credentialsProvider; private final SignalServiceDataStore dataStore; + private final int deviceId; private final ExecutorService executor; private final SignalSessionLock sessionLock; @@ -82,6 +87,11 @@ public class SignalDependencies { private KeysApi keysApi; private GroupsV2Operations groupsV2Operations; private ClientZkOperations clientZkOperations; + private ProfileService profileService; + private ProfileApi profileApi; + private CdnService cdnService; + private PreKeyRepository preKeyRepository; + private SignalRestClient signalRestClient; private PushServiceSocket pushServiceSocket; private Network libSignalNetwork; @@ -91,14 +101,13 @@ public class SignalDependencies { private SignalServiceMessageSender messageSender; private List secureValueRecovery; - private ProfileService profileService; - private ProfileApi profileApi; SignalDependencies( final ServiceEnvironmentConfig serviceEnvironmentConfig, final String userAgent, final CredentialsProvider credentialsProvider, final SignalServiceDataStore dataStore, + final int deviceId, final ExecutorService executor, final SignalSessionLock sessionLock ) { @@ -106,6 +115,7 @@ public class SignalDependencies { this.userAgent = userAgent; this.credentialsProvider = credentialsProvider; this.dataStore = dataStore; + this.deviceId = deviceId; this.executor = executor; this.sessionLock = sessionLock; } @@ -326,12 +336,33 @@ public class SignalDependencies { () -> messageReceiver = new SignalServiceMessageReceiver(getPushServiceSocket())); } + private SignalRestClient getSignalRestClient() { + return getOrCreate(() -> signalRestClient, + () -> signalRestClient = new SignalRestClient(serviceEnvironmentConfig.signalServiceConfiguration(), + userAgent, + credentialsProvider, + ServiceConfig.AUTOMATIC_NETWORK_RETRY)); + } + + public CdnService getCdnService() { + return getOrCreate(() -> cdnService, + () -> cdnService = new CdnService(getSignalRestClient(), getAttachmentApi())); + } + + public PreKeyRepository getPreKeyRepository() { + final SignalProtocolAddress localProtocolAddress = credentialsProvider.getAci().toProtocolAddress(deviceId); + return getOrCreate(() -> preKeyRepository, + () -> preKeyRepository = new PreKeyRepository(getKeysApi(), + dataStore.aci(), + localProtocolAddress, + Runnable::run)); + } + public SignalServiceMessageSender getMessageSender() { return getOrCreate(() -> messageSender, () -> messageSender = new SignalServiceMessageSender(getPushServiceSocket(), dataStore, sessionLock, - getAttachmentApi(), getMessageApi(), getKeysApi(), Optional.empty(), @@ -339,8 +370,7 @@ public class SignalDependencies { ServiceConfig.MAX_ENVELOPE_SIZE, ServiceConfig.MAX_INCREMENTAL_MACS_PER_ENVELOPE, () -> true, - true, - true)); + getPreKeyRepository())); } public List getSecureValueRecovery() { @@ -368,7 +398,9 @@ public class SignalDependencies { public SignalServiceCipher getCipher(ServiceIdType serviceIdType) { final var certificateValidator = new CertificateValidator(serviceEnvironmentConfig.unidentifiedSenderTrustRoots()); - final var serviceId = serviceIdType == ServiceIdType.ACI ? credentialsProvider.getAci() : credentialsProvider.getPni(); + final var serviceId = serviceIdType == ServiceIdType.ACI + ? credentialsProvider.getAci() + : credentialsProvider.getPni(); final var address = new SignalServiceAddress(serviceId, credentialsProvider.getE164()); final var deviceId = credentialsProvider.getDeviceId(); return new SignalServiceCipher(address, diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java index 39a447ac..a63ab6d7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java @@ -1,8 +1,9 @@ package org.asamk.signal.manager.internal; +import org.jetbrains.annotations.NotNull; +import org.signal.network.util.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.util.Preconditions; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.websocket.HealthMonitor; import org.whispersystems.signalservice.api.websocket.SignalWebSocket; @@ -94,6 +95,11 @@ final class SignalWebSocketHealthMonitor implements HealthMonitor { return needsKeepAlive && webSocket != null && webSocket.shouldSendKeepAlives(); } + @Override + public void onReceivedAlerts(@NotNull final String[] strings, final boolean b) { + logger.info("Received alerts: {}", String.join(", ", strings)); + } + /** * Sends periodic heartbeats/keep-alives over the WebSocket to prevent connection timeouts. If * the WebSocket fails to get a return heartbeat after [KEEP_ALIVE_TIMEOUT] seconds, it is forced to be recreated. diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java index 85b28bd7..fdc59e0c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java @@ -403,7 +403,7 @@ public class SessionStore implements SignalServiceSessionStore { } private static boolean isActive(SessionRecord record) { - return record != null && record.hasSenderChain(); + return record != null && record.hasSenderChain(0.0); } record Key(String address, int deviceId) {} diff --git a/lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java index 1908c8f5..8733deb5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java @@ -10,11 +10,11 @@ import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.VerificationMethodNotAvailableException; import org.asamk.signal.manager.helper.PinHelper; import org.signal.core.models.MasterKey; +import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.push.exceptions.ChallengeRequiredException; import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionException; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException; import org.whispersystems.signalservice.api.registration.RegistrationApi; import org.whispersystems.signalservice.internal.push.LockedException; diff --git a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java index aee133ab..1871798b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java @@ -7,9 +7,9 @@ import org.signal.libsignal.net.RequestResult; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.fingerprint.Fingerprint; import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator; +import org.signal.network.NetworkResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.NetworkResult; import org.whispersystems.signalservice.api.NetworkResultUtil; import org.whispersystems.signalservice.api.util.StreamDetails; diff --git a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json index cb971b26..cf17ea05 100644 --- a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json +++ b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json @@ -6543,6 +6543,33 @@ "type": "org.signal.libsignal.zkgroup.profiles.ProfileKey", "allDeclaredFields": true }, + { + "type": "org.signal.network.api.SenderCertificate", + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setCertificate", + "parameterTypes": [ + "byte[]" + ] + } + ] + }, + { + "type": "org.signal.network.api.SenderCertificate$ByteArrayDeserializer", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "type": "org.signal.network.api.SenderCertificate$ByteArraySerializer" + }, { "type": "org.signal.storageservice.storage.protos.groups.AccessControl", "fields": [ From 2a827f1285dbd66f36e547dd812ad7a2e53360bc Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 21:34:33 +0200 Subject: [PATCH 07/11] Don't log empty alerts --- .../signal/manager/internal/SignalWebSocketHealthMonitor.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java index a63ab6d7..656b6646 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java @@ -97,6 +97,9 @@ final class SignalWebSocketHealthMonitor implements HealthMonitor { @Override public void onReceivedAlerts(@NotNull final String[] strings, final boolean b) { + if (strings.length == 0) { + return; + } logger.info("Received alerts: {}", String.join(", ", strings)); } From 40b19288443a1c896f17d066060fb82d736a3639 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 22:38:33 +0200 Subject: [PATCH 08/11] Fix removal of local only unregistered accounts in storage sync --- .../signal/manager/helper/StorageHelper.java | 48 +++++++++++-------- .../storage/recipients/RecipientStore.java | 2 +- .../DefaultStorageRecordProcessor.java | 9 ++++ 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java index 6f60f734..d01bf44b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java @@ -2,6 +2,7 @@ package org.asamk.signal.manager.helper; import org.asamk.signal.manager.api.GroupIdV1; import org.asamk.signal.manager.api.GroupIdV2; +import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Profile; import org.asamk.signal.manager.internal.SignalDependencies; import org.asamk.signal.manager.storage.SignalAccount; @@ -38,6 +39,7 @@ import java.util.ArrayList; import java.util.Base64; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -211,20 +213,23 @@ public class StorageHelper { remoteOnlyRecords.size()); } - // This logic is wrong, records should only be deleted if they're deleted remotely, not if the remote record is updated -// if (!idDifference.localOnlyIds().isEmpty()) { -// final var updated = account.getRecipientStore() -// .removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection, -// idDifference.localOnlyIds()); -// -// if (updated > 0) { -// logger.warn( -// "Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.", -// updated); -// } -// } -// - final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords); + final var listListPair = processKnownRecords(connection, remoteOnlyRecords); + final var unknownInserts = listListPair.first(); + final var updatedStorageIds = listListPair.second(); + final var oldUnregisteredLocalOnlyIds = new HashSet<>(idDifference.localOnlyIds()); + updatedStorageIds.forEach(oldUnregisteredLocalOnlyIds::remove); + if (!idDifference.localOnlyIds().isEmpty()) { + final var updated = account.getRecipientStore() + .removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection, + oldUnregisteredLocalOnlyIds); + + if (updated > 0) { + logger.warn( + "Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.", + updated); + } + } + final var unknownDeletes = idDifference.localOnlyIds() .stream() .filter(id -> !KNOWN_TYPES.contains(id.getType())) @@ -480,13 +485,13 @@ public class StorageHelper { private Map generateGroupV1StorageIds(List groupIds) { return groupIds.stream() .collect(Collectors.toMap(recipientId -> recipientId, - recipientId -> StorageId.forGroupV1(KeyUtils.createRawStorageId()))); + _ -> StorageId.forGroupV1(KeyUtils.createRawStorageId()))); } private Map generateGroupV2StorageIds(List groupIds) { return groupIds.stream() .collect(Collectors.toMap(recipientId -> recipientId, - recipientId -> StorageId.forGroupV2(KeyUtils.createRawStorageId()))); + _ -> StorageId.forGroupV2(KeyUtils.createRawStorageId()))); } private void storeManifestLocally( @@ -630,16 +635,17 @@ public class StorageHelper { return new IdDifferenceResult(remoteOnlyKeys, localOnlyKeys, hasTypeMismatch); } - private List processKnownRecords( + private Pair, List> processKnownRecords( final Connection connection, List records ) throws SQLException { final var unknownRecords = new ArrayList(); + final var processedRecords = new ArrayList(); final var accountRecordProcessor = new AccountRecordProcessor(account, connection, context.getJobExecutor()); - final var contactRecordProcessor = new ContactRecordProcessor(account, connection, context.getJobExecutor()); final var groupV1RecordProcessor = new GroupV1RecordProcessor(account, connection); final var groupV2RecordProcessor = new GroupV2RecordProcessor(account, connection); + final var contactRecordProcessor = new ContactRecordProcessor(account, connection, context.getJobExecutor()); for (final var record : records) { if (record.getProto().account != null) { @@ -662,8 +668,12 @@ public class StorageHelper { unknownRecords.add(record.getId()); } } + processedRecords.addAll(accountRecordProcessor.getUpdatedStorageIds()); + processedRecords.addAll(groupV1RecordProcessor.getUpdatedStorageIds()); + processedRecords.addAll(groupV2RecordProcessor.getUpdatedStorageIds()); + processedRecords.addAll(contactRecordProcessor.getUpdatedStorageIds()); - return unknownRecords; + return new Pair<>(unknownRecords, processedRecords); } /** diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index 8836a1d3..c80b3454 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -878,7 +878,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re public int removeStorageIdsFromLocalOnlyUnregisteredRecipients( final Connection connection, - final List storageIds + final Collection storageIds ) throws SQLException { final var sql = ( """ diff --git a/lib/src/main/java/org/asamk/signal/manager/syncStorage/DefaultStorageRecordProcessor.java b/lib/src/main/java/org/asamk/signal/manager/syncStorage/DefaultStorageRecordProcessor.java index 87acb991..83fd5b5f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/syncStorage/DefaultStorageRecordProcessor.java +++ b/lib/src/main/java/org/asamk/signal/manager/syncStorage/DefaultStorageRecordProcessor.java @@ -6,7 +6,9 @@ import org.whispersystems.signalservice.api.storage.SignalRecord; import org.whispersystems.signalservice.api.storage.StorageId; import java.sql.SQLException; +import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.TreeSet; @@ -24,6 +26,7 @@ abstract class DefaultStorageRecordProcessor> implemen private static final Logger logger = LoggerFactory.getLogger(DefaultStorageRecordProcessor.class); private final Set matchedRecords = new TreeSet<>(this); + private final Set updatedStorageIds = new HashSet<>(); /** * One type of invalid remote data this handles is two records mapping to the same local data. We @@ -50,6 +53,7 @@ abstract class DefaultStorageRecordProcessor> implemen if (local.isEmpty()) { debug(remote.getId(), remote, "[Local Insert] No matching local record. Inserting."); + updatedStorageIds.add(remote.getId()); insertLocal(remote); return; } @@ -64,6 +68,7 @@ abstract class DefaultStorageRecordProcessor> implemen matchedRecords.add(local.get()); final var merged = merge(remote, local.get()); + updatedStorageIds.add(merged.getId()); if (!merged.equals(remote)) { debug(remote.getId(), remote, "[Remote Update] " + merged.describeDiff(remote)); } @@ -75,6 +80,10 @@ abstract class DefaultStorageRecordProcessor> implemen } } + public Set getUpdatedStorageIds() { + return Collections.unmodifiableSet(updatedStorageIds); + } + private void debug(StorageId i, E record, String message) { logger.debug("[{}][{}] {}", i, record.getClass().getSimpleName(), message); } From f057c5031c41d7008caa9a62c407fb022656a998 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 22:55:54 +0200 Subject: [PATCH 09/11] Fix use of deprecated API --- src/test/java/org/asamk/signal/http/SseInitialFlushTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java b/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java index 4796ae5d..d62c5220 100644 --- a/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java +++ b/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java @@ -39,7 +39,7 @@ import java.io.InputStream; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.ServerSocket; -import java.net.URL; +import java.net.URI; import java.time.Duration; import java.util.Collection; import java.util.List; @@ -97,7 +97,7 @@ class SseInitialFlushTest { @Test void sseEndpointReturnsHeadersWithinTwoSeconds() { assertDoesNotThrow(() -> { - var url = new URL("http://127.0.0.1:" + port + "/api/v1/events"); + var url = new URI("http", null, "127.0.0.1", port, "/api/v1/events", null, null).toURL(); var conn = (HttpURLConnection) url.openConnection(); conn.setRequestProperty("Accept", "text/event-stream"); conn.setReadTimeout(2_000); // 2 s — fails before fix (15 s flush), passes after From 8d6264e02ee3be56520da21697aca34f3451b65f Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 22:56:09 +0200 Subject: [PATCH 10/11] Update dependencies --- gradle/libs.versions.toml | 4 ++-- src/main/java/org/asamk/signal/BaseConfig.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 972d5595..ef39a50d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] slf4j = "2.0.18" -junit = "6.0.3" -micronaut-json-schema = "2.0.0" +junit = "6.1.0" +micronaut-json-schema = "2.0.1" micronaut-core = "5.0.0" signal-service = "2.15.3_unofficial_147" diff --git a/src/main/java/org/asamk/signal/BaseConfig.java b/src/main/java/org/asamk/signal/BaseConfig.java index 7a35165b..da2e5422 100644 --- a/src/main/java/org/asamk/signal/BaseConfig.java +++ b/src/main/java/org/asamk/signal/BaseConfig.java @@ -8,7 +8,7 @@ public class BaseConfig { public static final String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion(); static final String USER_AGENT_SIGNAL_ANDROID = Optional.ofNullable(System.getenv("SIGNAL_CLI_USER_AGENT")) - .orElse("Signal-Android/8.8.0"); + .orElse("Signal-Android/8.12.1"); static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null ? "signal-cli" : PROJECT_NAME + "/" + PROJECT_VERSION; From 46c61c5aac428cc7caa44c4deda87ca23fd57ae8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 23:01:29 +0200 Subject: [PATCH 11/11] Bump version to 0.14.4 --- CHANGELOG.md | 16 +++++++++++++++- build.gradle.kts | 4 ++-- data/org.asamk.SignalCli.metainfo.xml | 3 +++ libsignal-version | 2 +- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f046f4b..d781a4ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ # Changelog -## [Unreleased] +## [0.14.4] - 2026-05-23 + +### Added + +- Support for a global configuration file to set system-wide defaults + +### Fixed + +- Group admins can now see profile information for users requesting to join groups. +- Storage sync with unregistered contacts fixed +- Incoming messages are validated more accurately, fixing receiving messages from new contacts + +### Improved + +- Some security and stability improvements, including HTTP HOST header validation and safer temporary file handling. ## [0.14.3] - 2026-04-22 diff --git a/build.gradle.kts b/build.gradle.kts index 5ed47048..31b0d72c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ plugins { allprojects { group = "org.asamk" - version = "0.14.4-SNAPSHOT" + version = "0.14.4" } java { @@ -102,7 +102,7 @@ dependencies { implementation(libs.micronaut.json.schema.annotations) if (gradle.startParameter.taskNames.any { it.contains("jsonSchemas") }) { implementation(libs.micronaut.json.schema.generator) - } + } implementation(project(":libsignal-cli")) testImplementation(libs.junit.jupiter) diff --git a/data/org.asamk.SignalCli.metainfo.xml b/data/org.asamk.SignalCli.metainfo.xml index 3bf04d8c..231492be 100644 --- a/data/org.asamk.SignalCli.metainfo.xml +++ b/data/org.asamk.SignalCli.metainfo.xml @@ -45,6 +45,9 @@ intense + + https://github.com/AsamK/signal-cli/releases/tag/v0.14.4 + https://github.com/AsamK/signal-cli/releases/tag/v0.14.3 diff --git a/libsignal-version b/libsignal-version index da011ce4..6bc8ee7b 100644 --- a/libsignal-version +++ b/libsignal-version @@ -1 +1 @@ -0.92.1 +0.94.1