Compare commits

...

96 commits

Author SHA1 Message Date
AsamK
f6d81e3c05 Update gradle
Some checks failed
signal-cli CI / build (21) (push) Has been cancelled
signal-cli CI / build (24) (push) Has been cancelled
signal-cli CI / build-graalvm (push) Has been cancelled
signal-cli CI / build-client (macos) (push) Has been cancelled
signal-cli CI / build-client (ubuntu) (push) Has been cancelled
signal-cli CI / build-client (windows) (push) Has been cancelled
CodeQL / Analyse (push) Has been cancelled
2025-08-17 17:35:59 +02:00
AsamK
42f10670b6 Replace deprecated groovy utils 2025-08-17 17:35:14 +02:00
AsamK
b453d7a0b9 Add new svr2 mrenclave 2025-08-02 12:05:01 +02:00
AsamK
f9a36c6e04 Fix send parameters to be all camel case
Fixes #1814
2025-07-16 20:59:26 +02:00
AsamK
be48afb2b5 Fix container build 2025-07-16 20:56:02 +02:00
AsamK
a0960fcabd Prepare next release 2025-07-16 20:55:50 +02:00
AsamK
dbc454ba9e Bump version to 0.13.18 2025-07-16 19:40:10 +02:00
AsamK
2225e69277 Update sqlite-jdbc 2025-07-16 19:37:03 +02:00
AsamK
783201d12e Fix incorrect error message 2025-07-16 19:17:21 +02:00
AsamK
3e981d66e9 Fix null pointer regression 2025-07-14 18:52:41 +02:00
AsamK
7c7fc76a64 Add support for sending view once messages
Closes #1812
2025-07-14 16:42:06 +02:00
AsamK
c924d5c03a Update libsignal-service-java 2025-07-14 16:21:47 +02:00
AsamK
dc787be17b Build rust json-rpc client in CI 2025-07-12 11:57:23 +02:00
AsamK
3d4070a139 Compile UnixStream support only on unix systems 2025-07-12 11:42:12 +02:00
AsamK
dbdff83132 Update README
Fixes #1803
2025-07-12 11:09:24 +02:00
AsamK
4ce194afe2 Add missing username parameter to getUserStatus command in json-rpc client 2025-07-12 11:03:54 +02:00
AsamK
ca33249170 Handle rate limit exception correctly when querying usernames
Fixes #1797
2025-07-12 11:03:28 +02:00
AsamK
a96626c468 Update to rust 2024 edition 2025-07-12 10:16:57 +02:00
AsamK
d54be747da Remove unused dependency 2025-07-12 10:15:27 +02:00
AsamK
ff846bc678 Fix clippy warnings 2025-07-12 10:05:57 +02:00
AsamK
1b7f755590 Update dependencies 2025-07-12 10:05:14 +02:00
AsamK
887ed3bb44 Show better error message when sending fails due to missing pre keys 2025-07-08 17:35:17 +02:00
AsamK
3180eba836 Exit if account check fails at startup
Fixes #1804
2025-07-08 17:34:04 +02:00
AsamK
cb06cbdcca Shut down when dbus daemon connection goes away unexpectedly
Fixes #1800
2025-06-29 11:22:30 +02:00
AsamK
069325af47 Extend shutdown request with optional error 2025-06-29 11:22:30 +02:00
AsamK
e7ca02f1fb Prepare next release 2025-06-29 11:22:30 +02:00
AsamK
fa9bb3c210 Bump version to 0.13.17 2025-06-28 14:57:20 +02:00
AsamK
e6113d4d96 Update libsignal-service-java 2025-06-28 14:35:56 +02:00
AsamK
6cc3a6f561 Update dependencies 2025-06-25 00:20:42 +02:00
AsamK
70c79eac01 Keep all unhandled fields of remote storage record
Fixes #1792
2025-06-24 23:13:00 +02:00
AsamK
5dc66f839d Close attachment input streams after upload
Fixes #1790
2025-06-10 19:36:52 +02:00
AsamK
a0d5744c49 Improve behavior when pin data doesn't exist on the server 2025-06-08 16:22:03 +02:00
AsamK
6b60a6d5a5 Fix NPR when loading an inactive group
Fixes #1786
2025-06-08 14:48:25 +02:00
AsamK
0257344940 Prepare next release 2025-06-08 14:47:20 +02:00
AsamK
17cd99be59 Bump version to 0.13.16 2025-06-07 16:58:00 +02:00
AsamK
2f8328847c Update dependencies 2025-06-07 16:14:55 +02:00
AsamK
7e9727aa38 Update tests 2025-06-07 16:14:55 +02:00
AsamK
bf87fcc652 Ensure messages are created with a unique timestamp
Fixes #1783
2025-06-03 22:22:51 +02:00
AsamK
6b46314eab Update dependencies 2025-06-03 21:59:38 +02:00
AsamK
e89803464b Update libsignal-service 2025-06-01 21:51:03 +02:00
AsamK
a9bb8d9aae Update gradle 2025-06-01 16:11:21 +02:00
AsamK
74909408c4 Add missing reflect config
Fixes #1768
2025-05-10 10:18:03 +02:00
AsamK
bb124a922d Prepare next release 2025-05-08 22:56:55 +02:00
AsamK
56e11d0857 Update codeql v3 2025-05-08 22:55:26 +02:00
AsamK
d0d0021f57 Bump version to 0.13.15 2025-05-08 21:49:51 +02:00
AsamK
7aafb05995 Update dependencies 2025-05-08 21:31:17 +02:00
AsamK
e594f3b237 Update libsignal-service 2025-05-08 21:31:17 +02:00
AsamK
bb86830a61 Add compatibility flag for graalvm native build 2025-05-08 20:42:13 +02:00
AsamK
bcc1eadc7d Remove unused e164 field from account record 2025-05-08 20:36:27 +02:00
AsamK
4fd9e55c3c Enable native access required by Java 24
Fixes #1765
2025-05-08 20:04:49 +02:00
AsamK
a2900085c9 Use java 24 in CI 2025-05-08 19:37:57 +02:00
AsamK
5e11cf1c50 Update gradle 2025-05-08 19:37:41 +02:00
AsamK
4e455d85d6 Add more logging to register 2025-04-26 09:04:05 +02:00
AsamK
1e685c7cab Extend merge/split logging 2025-04-09 20:44:10 +02:00
AsamK
ce813e4529 Update client dependencies 2025-04-08 16:24:57 +02:00
AsamK
bd7948e246 Prepare next release 2025-04-08 16:24:30 +02:00
AsamK
b998f322f5 Bump version to 0.13.14 2025-04-06 20:10:46 +02:00
AsamK
db2182aa7d Update libsignal-service 2025-04-06 20:02:02 +02:00
AsamK
69a9b30732 Update libsignal-service 2025-03-31 14:56:56 +02:00
AsamK
3dc8844cb4 Update libraries 2025-03-31 09:19:20 +02:00
AsamK
adb6787d5b Refresh prekeys when receiving message with invalid key id 2025-03-31 09:11:28 +02:00
AsamK
14b07be0dc Always renew session when failing to decrypt message 2025-03-31 09:11:05 +02:00
AsamK
6befda7ef1 Update graalvm build tools 2025-03-22 10:55:16 +01:00
AsamK
67302eb9c3 Replace cached envelopes when moving
Fixes #1730
2025-03-18 18:20:44 +01:00
AsamK
f18015ff2e f 2025-03-18 18:13:38 +01:00
AsamK
1295ef69ca Use record patterns 2025-03-16 22:07:29 +01:00
AsamK
f26a0d2891 Update libsignal-service 2025-03-16 22:06:58 +01:00
AsamK
2b150112ff Remove previous prekeys when importing legacy prekeys 2025-03-16 12:22:21 +01:00
AsamK
7aede7c17f Remove previous prekeys when importing legacy prekeys 2025-03-16 12:18:59 +01:00
AsamK
b92cbc6a7c Exclude libsignal-client testing libraries 2025-03-04 10:04:34 +01:00
AsamK
68b7416e57 Update gradle wrapper 2025-03-04 08:32:56 +01:00
AsamK
4feba68afd Replace deprecated gradle api 2025-03-04 08:29:05 +01:00
AsamK
4eb34c7a93 Update apt repository before installing packages 2025-02-28 09:46:45 +01:00
AsamK
26fd3e379a Prepare next release 2025-02-28 09:46:28 +01:00
AsamK
93d281e712 Bump version to 0.13.13 2025-02-28 09:36:27 +01:00
AsamK
985af6e445 Update libsignal-service 2025-02-28 09:32:17 +01:00
AsamK
5693d871f7 Update dependencies 2025-02-28 09:32:17 +01:00
AsamK
dba8cf7a6f Update reflect-config 2025-02-27 18:01:21 +01:00
AsamK
141d3326ab Add in-memory cache to KeyValueStore 2025-02-27 17:21:31 +01:00
AsamK
d3d2caac5a Tweak hikari config 2025-02-27 17:21:31 +01:00
AsamK
e1f4dae5c2 Show better error message when receiving an empty JSON RPC line
Fixes #1715
2025-02-27 11:39:31 +01:00
AsamK
cf5c943127 Remove legacy SVR2 enclave 2025-02-27 11:34:23 +01:00
AsamK
ed79e0b377 Check if required quote-author parameter is missing
Fixes #1716
2025-02-27 11:14:54 +01:00
Enguerran P.
a089a5ef04 Update README > landline procedure
Update landline (`--voice`) registration procedure.

This closes #1666 (🤘)
2025-02-07 23:16:46 +01:00
AsamK
90145655f4 Update CHANGELOG.md 2025-02-07 19:15:55 +01:00
AsamK
3cd07ae9cd Set libsignal network proxy to match java proxy
Fixes #1523
2025-02-07 18:30:10 +01:00
AsamK
8aa71c132f Fix log message 2025-02-07 18:30:10 +01:00
AsamK
b579935846 Update README 2025-02-07 18:30:10 +01:00
AsamK
dfa886fae9 Update dependencies 2025-02-07 18:30:10 +01:00
AsamK
f04f789231 Update gradle action 2025-01-31 16:45:47 +01:00
AsamK
a6ec71dc31 Add --mobilecoin-address as alias to updateProfile
Closes #1638
2025-01-30 20:18:07 +01:00
AsamK
47d65586cd Improve handling of unknown storage records
Fixes #1696
2025-01-30 19:57:44 +01:00
AsamK
b8d8413a22 Fix creating builder from contact
Fixes #1678
2025-01-23 17:12:40 +01:00
AsamK
5e16123632 Extend updateContact command with nick given/family name and note 2025-01-23 17:11:33 +01:00
AsamK
d57442bd2a Prepare next release 2025-01-19 13:26:12 +01:00
AsamK
70313c45a9 Update reflect-config.json
Fixes #1686
2025-01-19 13:25:08 +01:00
96 changed files with 1794 additions and 992 deletions

View file

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
java: [ '21', '23' ] java: [ '21', '24' ]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -26,11 +26,11 @@ jobs:
distribution: 'zulu' distribution: 'zulu'
java-version: ${{ matrix.java }} java-version: ${{ matrix.java }}
- name: Setup Gradle - name: Setup Gradle
uses: gradle/gradle-build-action@v2 uses: gradle/actions/setup-gradle@v4
with: with:
dependency-graph: generate-and-submit dependency-graph: generate-and-submit
- name: Install asciidoc - name: Install asciidoc
run: sudo apt --no-install-recommends install -y asciidoc-base run: sudo apt update && sudo apt --no-install-recommends install -y asciidoc-base
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew --no-daemon build run: ./gradlew --no-daemon build
- name: Build man page - name: Build man page
@ -69,3 +69,28 @@ jobs:
with: with:
name: signal-cli-native name: signal-cli-native
path: build/native/nativeCompile/signal-cli path: build/native/nativeCompile/signal-cli
build-client:
strategy:
matrix:
os:
- ubuntu
- macos
- windows
runs-on: ${{ matrix.os }}-latest
defaults:
run:
working-directory: ./client
steps:
- uses: actions/checkout@v4
- name: Install rust
run: rustup default stable
- name: Build client
run: cargo build --release --verbose
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: signal-cli-client-${{ matrix.os }}
path: |
client/target/release/signal-cli-client
client/target/release/signal-cli-client.exe

View file

@ -35,7 +35,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v3
# Override language selection by uncommenting this and choosing your languages # Override language selection by uncommenting this and choosing your languages
# with: # with:
# languages: go, javascript, csharp, python, cpp, java # languages: go, javascript, csharp, python, cpp, java
@ -43,7 +43,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@ -57,4 +57,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v3

View file

@ -182,7 +182,7 @@ jobs:
tar xf ./"${ARCHIVE_DIR}"/*.tar.gz tar xf ./"${ARCHIVE_DIR}"/*.tar.gz
rm -r signal-cli-archive-* signal-cli-native rm -r signal-cli-archive-* signal-cli-native
mkdir -p build/install/ mkdir -p build/install/
mv ./signal-cli-*/ build/install/signal-cli mv ./signal-cli-"${GITHUB_REF_NAME#v}"/ build/install/signal-cli
- name: Build Image - name: Build Image
id: build_image id: build_image

View file

@ -1,5 +1,86 @@
# Changelog # Changelog
## [Unreleased]
## [0.13.18] - 2025-07-16
Requires libsignal-client version 0.76.3.
### Added
- Added `--view-once` parameter to send command to send view once images
### Fixed
- Handle rate limit exception correctly when querying usernames
### Improved
- Shut down when dbus daemon connection goes away unexpectedly
- In daemon mode, exit immediately if account check fails at startup
- Improve behavior when sending to devices that have no available prekeys
## [0.13.17] - 2025-06-28
Requires libsignal-client version 0.76.0.
### Fixed
- Fix issue when loading an older inactive group
- Close attachment input streams after upload
- Fix storage sync behavior with unhandled fields
### Changed
- Improve behavior when pin data doesn't exist on the server
## [0.13.16] - 2025-06-07
Requires libsignal-client version 0.73.2.
### Changed
- Ensure every sent message gets a unique timestamp
## [0.13.15] - 2025-05-08
Requires libsignal-client version 0.70.0.
### Fixed
- Fix native access warning with Java 24
- Fix storage sync loop due to old removed e164 field
### Changed
- Increased compatibility of native build with older/virtual CPUs
## [0.13.14] - 2025-04-06
Requires libsignal-client version 0.68.1.
### Fixed
- Fix pre key import from old data files
### Changed
- Use websocket connection instead of HTTP for more requests
- Improve handling of messages with decryption error
## [0.13.13] - 2025-02-28
Requires libsignal-client version 0.66.2.
### Added
- Allow setting nickname and note with `updateContact` command
### Fixed
- Fix syncing nickname, note and expiration timer
- Fix check for registered users with a proxy
- Improve handling of storage records not yet supported by signal-cli
- Fix contact sync for networks requiring proxy
## [0.13.12] - 2025-01-18 ## [0.13.12] - 2025-01-18
Requires libsignal-client version 0.65.2. Requires libsignal-client version 0.65.2.

View file

@ -11,6 +11,10 @@ For this use-case, it has a daemon mode with JSON-RPC interface ([man page](http
and D-BUS interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)) . and D-BUS interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)) .
For the JSON-RPC interface there's also a simple [example client](https://github.com/AsamK/signal-cli/tree/master/client), written in Rust. For the JSON-RPC interface there's also a simple [example client](https://github.com/AsamK/signal-cli/tree/master/client), written in Rust.
signal-cli needs to be kept up-to-date to keep up with Signal-Server changes.
The official Signal clients expire after three months and then the Signal-Server can make incompatible changes.
So signal-cli releases older than three months may not work correctly.
## Installation ## Installation
You can [build signal-cli](#building) yourself or use You can [build signal-cli](#building) yourself or use
@ -55,8 +59,15 @@ of all country codes.)
signal-cli -a ACCOUNT register signal-cli -a ACCOUNT register
You can register Signal using a landline number. In this case you can skip SMS verification process and jump directly You can register Signal using a landline number. In this case, you need to follow the procedure below:
to the voice call verification by adding the `--voice` switch at the end of above register command. * Attempt a SMS verification process first (`signal-cli -a ACCOUNT register`)
* You will get an error `400 (InvalidTransportModeException)`, this is normal
* Wait 60 seconds
* Attempt a voice call verification by adding the `--voice` switch and wait for the call:
```sh
signal-cli -a ACCOUNT register --voice
```
Registering may require solving a CAPTCHA Registering may require solving a CAPTCHA
challenge: [Registration with captcha](https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha) challenge: [Registration with captcha](https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha)
@ -72,6 +83,12 @@ of all country codes.)
signal-cli -a ACCOUNT send -m "This is a message" RECIPIENT signal-cli -a ACCOUNT send -m "This is a message" RECIPIENT
``` ```
* Send a message to a username, usernames need to be prefixed with `u:`
```sh
signal-cli -a ACCOUNT send -m "This is a message" u:USERNAME.000
```
* Pipe the message content from another process. * Pipe the message content from another process.
uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT

View file

@ -3,12 +3,12 @@ plugins {
application application
eclipse eclipse
`check-lib-versions` `check-lib-versions`
id("org.graalvm.buildtools.native") version "0.10.4" id("org.graalvm.buildtools.native") version "0.10.6"
} }
allprojects { allprojects {
group = "org.asamk" group = "org.asamk"
version = "0.13.12" version = "0.13.19-SNAPSHOT"
} }
java { java {
@ -24,6 +24,7 @@ java {
application { application {
mainClass.set("org.asamk.signal.Main") mainClass.set("org.asamk.signal.Main")
applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED")
} }
graalvmNative { graalvmNative {
@ -32,6 +33,7 @@ graalvmNative {
buildArgs.add("--install-exit-handlers") buildArgs.add("--install-exit-handlers")
buildArgs.add("-Dfile.encoding=UTF-8") buildArgs.add("-Dfile.encoding=UTF-8")
buildArgs.add("-J-Dfile.encoding=UTF-8") buildArgs.add("-J-Dfile.encoding=UTF-8")
buildArgs.add("-march=compatibility")
resources.autodetect() resources.autodetect()
configurationFileDirectories.from(file("graalvm-config-dir")) configurationFileDirectories.from(file("graalvm-config-dir"))
if (System.getenv("GRAALVM_HOME") == null) { if (System.getenv("GRAALVM_HOME") == null) {
@ -46,7 +48,41 @@ graalvmNative {
} }
} }
val artifactType = Attribute.of("artifactType", String::class.java)
val minified = Attribute.of("minified", Boolean::class.javaObjectType)
dependencies { dependencies {
attributesSchema {
attribute(minified)
}
artifactTypes.getByName("jar") {
attributes.attribute(minified, false)
}
}
configurations.runtimeClasspath.configure {
attributes {
attribute(minified, true)
}
}
val excludePatterns = mapOf(
"libsignal-client" to setOf(
"libsignal_jni_testing_amd64.so",
"signal_jni_testing_amd64.dll",
"libsignal_jni_testing_amd64.dylib",
"libsignal_jni_testing_aarch64.dylib",
)
)
dependencies {
registerTransform(JarFileExcluder::class) {
from.attribute(minified, false).attribute(artifactType, "jar")
to.attribute(minified, true).attribute(artifactType, "jar")
parameters {
excludeFilesByArtifact = excludePatterns
}
}
implementation(libs.bouncycastle) implementation(libs.bouncycastle)
implementation(libs.jackson.databind) implementation(libs.jackson.databind)
implementation(libs.argparse4j) implementation(libs.argparse4j)
@ -78,12 +114,13 @@ tasks.withType<Jar> {
attributes( attributes(
"Implementation-Title" to project.name, "Implementation-Title" to project.name,
"Implementation-Version" to project.version, "Implementation-Version" to project.version,
"Main-Class" to application.mainClass.get() "Main-Class" to application.mainClass.get(),
"Enable-Native-Access" to "ALL-UNNAMED",
) )
} }
} }
task("fatJar", type = Jar::class) { tasks.register("fatJar", type = Jar::class) {
archiveBaseName.set("${project.name}-fat") archiveBaseName.set("${project.name}-fat")
exclude( exclude(
"META-INF/*.SF", "META-INF/*.SF",
@ -92,9 +129,11 @@ task("fatJar", type = Jar::class) {
"META-INF/NOTICE*", "META-INF/NOTICE*",
"META-INF/LICENSE*", "META-INF/LICENSE*",
"META-INF/INDEX.LIST", "META-INF/INDEX.LIST",
"**/module-info.class" "**/module-info.class",
) )
duplicatesStrategy = DuplicatesStrategy.WARN duplicatesStrategy = DuplicatesStrategy.WARN
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) doFirst {
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
}
with(tasks.jar.get()) with(tasks.jar.get())
} }

View file

@ -1,12 +1,10 @@
@file:Suppress("DEPRECATION") @file:Suppress("DEPRECATION")
import groovy.util.XmlSlurper
import groovy.util.slurpersupport.GPathResult
import org.codehaus.groovy.runtime.ResourceGroovyMethods
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.Task import org.gradle.api.Task
import org.gradle.api.artifacts.Dependency import org.gradle.api.artifacts.Dependency
import javax.xml.parsers.DocumentBuilderFactory
class CheckLibVersionsPlugin : Plugin<Project> { class CheckLibVersionsPlugin : Plugin<Project> {
override fun apply(project: Project) { override fun apply(project: Project) {
@ -28,10 +26,10 @@ class CheckLibVersionsPlugin : Plugin<Project> {
val name = dependency.name val name = dependency.name
val metaDataUrl = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml" val metaDataUrl = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml"
try { try {
val url = ResourceGroovyMethods.toURL(metaDataUrl) val dbf = DocumentBuilderFactory.newInstance()
val metaDataText = ResourceGroovyMethods.getText(url) val db = dbf.newDocumentBuilder()
val metadata = XmlSlurper().parseText(metaDataText) val doc = db.parse(metaDataUrl);
val newest = (metadata.getProperty("versioning") as GPathResult).getProperty("latest") val newest = doc.getElementsByTagName("latest").item(0).textContent
if (version != newest.toString()) { if (version != newest.toString()) {
println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}") println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}")
} }

View file

@ -0,0 +1,53 @@
import org.gradle.api.artifacts.transform.*
import org.gradle.api.file.FileSystemLocation
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
@CacheableTransform
abstract class JarFileExcluder : TransformAction<JarFileExcluder.Parameters> {
interface Parameters : TransformParameters {
@get:Input
var excludeFilesByArtifact: Map<String, Set<String>>
}
@get:PathSensitive(PathSensitivity.NAME_ONLY)
@get:InputArtifact
abstract val inputArtifact: Provider<FileSystemLocation>
override
fun transform(outputs: TransformOutputs) {
val fileName = inputArtifact.get().asFile.name
for (entry in parameters.excludeFilesByArtifact) {
if (fileName.startsWith(entry.key)) {
val nameWithoutExtension = fileName.substring(0, fileName.lastIndexOf("."))
excludeFiles(inputArtifact.get().asFile, entry.value, outputs.file("${nameWithoutExtension}.jar"))
return
}
}
outputs.file(inputArtifact)
}
private fun excludeFiles(artifact: File, excludeFiles: Set<String>, jarFile: File) {
ZipInputStream(FileInputStream(artifact)).use { input ->
ZipOutputStream(FileOutputStream(jarFile)).use { output ->
var entry = input.nextEntry
while (entry != null) {
if (!excludeFiles.contains(entry.name)) {
output.putNextEntry(entry)
input.copyTo(output)
output.closeEntry()
}
entry = input.nextEntry
}
}
}
}
}

798
client/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,17 @@
[package] [package]
name = "signal-cli-client" name = "signal-cli-client"
version = "0.0.1" version = "0.0.1"
edition = "2021" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
clap = { version = "4", features = ["cargo", "derive", "wrap_help"] } clap = { version = "4", features = ["cargo", "derive", "wrap_help"] }
log = "0.4"
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
tokio = { version = "1", features = ["rt", "macros", "net", "rt-multi-thread"] } tokio = { version = "1", features = ["rt", "macros", "net", "rt-multi-thread"] }
jsonrpsee = { version = "0.24", features = [ jsonrpsee = { version = "0.25", features = [
"macros", "macros",
"async-client", "async-client",
"http-client", "http-client",

View file

@ -15,6 +15,7 @@ pub struct Cli {
pub json_rpc_tcp: Option<Option<SocketAddr>>, pub json_rpc_tcp: Option<Option<SocketAddr>>,
/// UNIX socket address and port of signal-cli daemon /// UNIX socket address and port of signal-cli daemon
#[cfg(unix)]
#[arg(long, conflicts_with = "json_rpc_tcp")] #[arg(long, conflicts_with = "json_rpc_tcp")]
pub json_rpc_socket: Option<Option<OsString>>, pub json_rpc_socket: Option<Option<OsString>>,
@ -84,6 +85,8 @@ pub enum CliCommands {
}, },
GetUserStatus { GetUserStatus {
recipient: Vec<String>, recipient: Vec<String>,
#[arg(long)]
username: Vec<String>,
}, },
JoinGroup { JoinGroup {
#[arg(long)] #[arg(long)]
@ -176,6 +179,9 @@ pub enum CliCommands {
#[arg(short = 'a', long)] #[arg(short = 'a', long)]
attachment: Vec<String>, attachment: Vec<String>,
#[arg(long)]
view_once: bool,
#[arg(long)] #[arg(long)]
mention: Vec<String>, mention: Vec<String>,
@ -413,7 +419,7 @@ pub enum CliCommands {
#[arg(long = "about-emoji")] #[arg(long = "about-emoji")]
about_emoji: Option<String>, about_emoji: Option<String>,
#[arg(long = "mobile-coin-address")] #[arg(long = "mobile-coin-address", visible_alias = "mobilecoin-address")]
mobile_coin_address: Option<String>, mobile_coin_address: Option<String>,
#[arg(long)] #[arg(long)]

View file

@ -70,6 +70,7 @@ pub trait Rpc {
&self, &self,
account: Option<String>, account: Option<String>,
recipients: Vec<String>, recipients: Vec<String>,
usernames: Vec<String>,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "joinGroup", param_kind = map)] #[method(name = "joinGroup", param_kind = map)]
@ -182,6 +183,7 @@ pub trait Rpc {
endSession: bool, endSession: bool,
message: String, message: String,
attachments: Vec<String>, attachments: Vec<String>,
viewOnce: bool,
mentions: Vec<String>, mentions: Vec<String>,
textStyle: Vec<String>, textStyle: Vec<String>,
quoteTimestamp: Option<u64>, quoteTimestamp: Option<u64>,
@ -190,10 +192,10 @@ pub trait Rpc {
quoteMention: Vec<String>, quoteMention: Vec<String>,
quoteTextStyle: Vec<String>, quoteTextStyle: Vec<String>,
quoteAttachment: Vec<String>, quoteAttachment: Vec<String>,
preview_url: Option<String>, previewUrl: Option<String>,
preview_title: Option<String>, previewTitle: Option<String>,
preview_description: Option<String>, previewDescription: Option<String>,
preview_image: Option<String>, previewImage: Option<String>,
sticker: Option<String>, sticker: Option<String>,
storyTimestamp: Option<u64>, storyTimestamp: Option<u64>,
storyAuthor: Option<String>, storyAuthor: Option<String>,
@ -409,6 +411,7 @@ pub async fn connect_tcp(
Ok(ClientBuilder::default().build_with_tokio(sender, receiver)) Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
} }
#[cfg(unix)]
pub async fn connect_unix( pub async fn connect_unix(
socket_path: impl AsRef<Path>, socket_path: impl AsRef<Path>,
) -> Result<impl SubscriptionClientT, std::io::Error> { ) -> Result<impl SubscriptionClientT, std::io::Error> {
@ -417,6 +420,6 @@ pub async fn connect_unix(
Ok(ClientBuilder::default().build_with_tokio(sender, receiver)) Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
} }
pub async fn connect_http(uri: &str) -> Result<impl SubscriptionClientT, Error> { pub async fn connect_http(uri: &str) -> Result<impl SubscriptionClientT + use<>, Error> {
HttpClientBuilder::default().build(uri) HttpClientBuilder::default().build(uri)
} }

View file

@ -60,8 +60,13 @@ async fn handle_command(
.delete_local_account_data(cli.account, ignore_registered) .delete_local_account_data(cli.account, ignore_registered)
.await .await
} }
CliCommands::GetUserStatus { recipient } => { CliCommands::GetUserStatus {
client.get_user_status(cli.account, recipient).await recipient,
username,
} => {
client
.get_user_status(cli.account, recipient, username)
.await
} }
CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await, CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await,
CliCommands::Link { name } => { CliCommands::Link { name } => {
@ -70,7 +75,7 @@ async fn handle_command(
.await .await
.map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))? .map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))?
.device_link_uri; .device_link_uri;
println!("{}", url); println!("{url}");
client.finish_link(url, name).await client.finish_link(url, name).await
} }
CliCommands::ListAccounts => client.list_accounts().await, CliCommands::ListAccounts => client.list_accounts().await,
@ -139,6 +144,7 @@ async fn handle_command(
end_session, end_session,
message, message,
attachment, attachment,
view_once,
mention, mention,
text_style, text_style,
quote_timestamp, quote_timestamp,
@ -165,6 +171,7 @@ async fn handle_command(
end_session, end_session,
message.unwrap_or_default(), message.unwrap_or_default(),
attachment, attachment,
view_once,
mention, mention,
text_style, text_style,
quote_timestamp, quote_timestamp,
@ -477,23 +484,30 @@ async fn connect(cli: Cli) -> Result<Value, RpcError> {
handle_command(cli, client).await handle_command(cli, client).await
} else { } else {
let socket_path = cli #[cfg(windows)]
.json_rpc_socket {
.clone() Err(RpcError::Custom("Invalid socket".into()))
.unwrap_or(None) }
.or_else(|| { #[cfg(unix)]
std::env::var_os("XDG_RUNTIME_DIR").map(|runtime_dir| { {
PathBuf::from(runtime_dir) let socket_path = cli
.join(DEFAULT_SOCKET_SUFFIX) .json_rpc_socket
.into() .clone()
.unwrap_or(None)
.or_else(|| {
std::env::var_os("XDG_RUNTIME_DIR").map(|runtime_dir| {
PathBuf::from(runtime_dir)
.join(DEFAULT_SOCKET_SUFFIX)
.into()
})
}) })
}) .unwrap_or_else(|| ("/run".to_owned() + DEFAULT_SOCKET_SUFFIX).into());
.unwrap_or_else(|| ("/run".to_owned() + DEFAULT_SOCKET_SUFFIX).into()); let client = jsonrpc::connect_unix(socket_path)
let client = jsonrpc::connect_unix(socket_path) .await
.await .map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
handle_command(cli, client).await handle_command(cli, client).await
}
} }
} }

View file

@ -1,10 +1,8 @@
use futures_util::{stream::StreamExt, Sink, SinkExt, Stream}; use futures_util::{stream::StreamExt, Sink, SinkExt, Stream};
use jsonrpsee::core::{ use jsonrpsee::core::client::{ReceivedMessage, TransportReceiverT, TransportSenderT};
async_trait,
client::{ReceivedMessage, TransportReceiverT, TransportSenderT},
};
use thiserror::Error; use thiserror::Error;
#[cfg(unix)]
pub mod ipc; pub mod ipc;
mod stream_codec; mod stream_codec;
pub mod tcp; pub mod tcp;
@ -21,7 +19,6 @@ struct Sender<T: Send + Sink<String>> {
inner: T, inner: T,
} }
#[async_trait]
impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT
for Sender<T> for Sender<T>
{ {
@ -31,7 +28,7 @@ impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> T
self.inner self.inner
.send(body) .send(body)
.await .await
.map_err(|e| Errors::Other(format!("{:?}", e)))?; .map_err(|e| Errors::Other(format!("{e:?}")))?;
Ok(()) Ok(())
} }
@ -39,7 +36,7 @@ impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> T
self.inner self.inner
.close() .close()
.await .await
.map_err(|e| Errors::Other(format!("{:?}", e)))?; .map_err(|e| Errors::Other(format!("{e:?}")))?;
Ok(()) Ok(())
} }
} }
@ -48,7 +45,6 @@ struct Receiver<T: Send + Stream> {
inner: T, inner: T,
} }
#[async_trait]
impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static> TransportReceiverT impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static> TransportReceiverT
for Receiver<T> for Receiver<T>
{ {
@ -58,7 +54,7 @@ impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static>
match self.inner.next().await { match self.inner.next().await {
None => Err(Errors::Closed), None => Err(Errors::Closed),
Some(Ok(msg)) => Ok(ReceivedMessage::Text(msg)), Some(Ok(msg)) => Ok(ReceivedMessage::Text(msg)),
Some(Err(e)) => Err(Errors::Other(format!("{:?}", e))), Some(Err(e)) => Err(Errors::Other(format!("{e:?}"))),
} }
} }
} }

View file

@ -41,7 +41,7 @@ impl Decoder for StreamCodec {
match str::from_utf8(line.as_ref()) { match str::from_utf8(line.as_ref()) {
Ok(s) => Ok(Some(s.to_string())), Ok(s) => Ok(Some(s.to_string())),
Err(_) => Err(io::Error::new(io::ErrorKind::Other, "invalid UTF-8")), Err(_) => Err(io::Error::other("invalid UTF-8")),
} }
} else { } else {
Ok(None) Ok(None)

View file

@ -45,6 +45,24 @@
<content_attribute id="social-chat">intense</content_attribute> <content_attribute id="social-chat">intense</content_attribute>
</content_rating> </content_rating>
<releases> <releases>
<release version="0.13.18" date="2025-07-16">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.18</url>
</release>
<release version="0.13.17" date="2025-06-28">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.17</url>
</release>
<release version="0.13.16" date="2025-06-07">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.16</url>
</release>
<release version="0.13.15" date="2025-05-08">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.15</url>
</release>
<release version="0.13.14" date="2025-04-06">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.14</url>
</release>
<release version="0.13.13" date="2025-02-28">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.13</url>
</release>
<release version="0.13.12" date="2025-01-18"> <release version="0.13.12" date="2025-01-18">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.12</url> <url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.12</url>
</release> </release>

View file

@ -27,6 +27,10 @@
{ {
"name":"java.lang.ClassNotFoundException" "name":"java.lang.ClassNotFoundException"
}, },
{
"name":"java.lang.Enum",
"methods":[{"name":"ordinal","parameterTypes":[] }]
},
{ {
"name":"java.lang.IllegalArgumentException", "name":"java.lang.IllegalArgumentException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }] "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
@ -48,9 +52,13 @@
{ {
"name":"java.lang.String" "name":"java.lang.String"
}, },
{
"name":"java.lang.Thread",
"methods":[{"name":"currentThread","parameterTypes":[] }, {"name":"getStackTrace","parameterTypes":[] }]
},
{ {
"name":"java.lang.Throwable", "name":"java.lang.Throwable",
"methods":[{"name":"getMessage","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }] "methods":[{"name":"getMessage","parameterTypes":[] }, {"name":"setStackTrace","parameterTypes":["java.lang.StackTraceElement[]"] }, {"name":"toString","parameterTypes":[] }]
}, },
{ {
"name":"java.lang.UnsatisfiedLinkError", "name":"java.lang.UnsatisfiedLinkError",
@ -88,7 +96,11 @@
}, },
{ {
"name":"org.signal.libsignal.internal.CompletableFuture", "name":"org.signal.libsignal.internal.CompletableFuture",
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"complete","parameterTypes":["java.lang.Object"] }] "methods":[{"name":"<init>","parameterTypes":[] }, {"name":"complete","parameterTypes":["java.lang.Object"] }, {"name":"completeExceptionally","parameterTypes":["java.lang.Throwable"] }, {"name":"setCancellationId","parameterTypes":["long"] }]
},
{
"name":"org.signal.libsignal.internal.NativeHandleGuard$SimpleOwner",
"methods":[{"name":"unsafeNativeHandleWithoutGuard","parameterTypes":[] }]
}, },
{ {
"name":"org.signal.libsignal.net.CdsiLookupResponse", "name":"org.signal.libsignal.net.CdsiLookupResponse",
@ -110,6 +122,14 @@
{ {
"name":"org.signal.libsignal.net.ChatService$ResponseAndDebugInfo" "name":"org.signal.libsignal.net.ChatService$ResponseAndDebugInfo"
}, },
{
"name":"org.signal.libsignal.net.NetworkException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.net.RetryLaterException",
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{ {
"name":"org.signal.libsignal.protocol.DuplicateMessageException", "name":"org.signal.libsignal.protocol.DuplicateMessageException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }] "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
@ -187,6 +207,9 @@
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore$Direction", "name":"org.signal.libsignal.protocol.state.IdentityKeyStore$Direction",
"fields":[{"name":"RECEIVING"}, {"name":"SENDING"}] "fields":[{"name":"RECEIVING"}, {"name":"SENDING"}]
}, },
{
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore$IdentityChange"
},
{ {
"name":"org.signal.libsignal.protocol.state.KyberPreKeyRecord", "name":"org.signal.libsignal.protocol.state.KyberPreKeyRecord",
"fields":[{"name":"unsafeHandle"}] "fields":[{"name":"unsafeHandle"}]

View file

@ -39,6 +39,9 @@
{ {
"name":"[Ljava.sql.Statement;" "name":"[Ljava.sql.Statement;"
}, },
{
"name":"[Lorg.asamk.signal.commands.ListStickerPacksCommand$JsonStickerPack$JsonSticker;"
},
{ {
"name":"[Lorg.asamk.signal.json.JsonAttachment;" "name":"[Lorg.asamk.signal.json.JsonAttachment;"
}, },
@ -668,6 +671,10 @@
{ {
"name":"long[]" "name":"long[]"
}, },
{
"name":"okhttp3.internal.connection.RealConnectionPool",
"fields":[{"name":"addressStates"}]
},
{ {
"name":"okio.BufferedSink" "name":"okio.BufferedSink"
}, },
@ -1406,6 +1413,12 @@
"name":"org.asamk.signal.manager.storage.profiles.LegacyProfileStore$ProfileStoreDeserializer", "name":"org.asamk.signal.manager.storage.profiles.LegacyProfileStore$ProfileStoreDeserializer",
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }]
}, },
{
"name":"org.asamk.signal.manager.storage.profiles.LegacySignalProfile",
"allDeclaredFields":true,
"allDeclaredMethods":true,
"allDeclaredConstructors":true
},
{ {
"name":"org.asamk.signal.manager.storage.profiles.LegacySignalProfileEntry", "name":"org.asamk.signal.manager.storage.profiles.LegacySignalProfileEntry",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -1617,6 +1630,10 @@
"name":"org.bouncycastle.jcajce.provider.asymmetric.NTRU$Mappings", "name":"org.bouncycastle.jcajce.provider.asymmetric.NTRU$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }]
}, },
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.NoSig$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{ {
"name":"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings", "name":"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }]
@ -2037,7 +2054,10 @@
"name":"org.signal.libsignal.protocol.IdentityKey" "name":"org.signal.libsignal.protocol.IdentityKey"
}, },
{ {
"name":"org.signal.libsignal.protocol.ServiceId" "name":"org.signal.libsignal.protocol.ServiceId",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
}, },
{ {
"name":"org.signal.libsignal.protocol.SignalProtocolAddress" "name":"org.signal.libsignal.protocol.SignalProtocolAddress"
@ -2299,7 +2319,7 @@
"allDeclaredFields":true, "allDeclaredFields":true,
"allDeclaredMethods":true, "allDeclaredMethods":true,
"allDeclaredConstructors":true, "allDeclaredConstructors":true,
"methods":[{"name":"getAnnouncementGroup","parameterTypes":[] }, {"name":"getChangeNumber","parameterTypes":[] }, {"name":"getDeleteSync","parameterTypes":[] }, {"name":"getGiftBadges","parameterTypes":[] }, {"name":"getPaymentActivation","parameterTypes":[] }, {"name":"getPni","parameterTypes":[] }, {"name":"getSenderKey","parameterTypes":[] }, {"name":"getStorage","parameterTypes":[] }, {"name":"getStorageServiceEncryptionV2","parameterTypes":[] }, {"name":"getStories","parameterTypes":[] }, {"name":"getVersionedExpirationTimer","parameterTypes":[] }] "methods":[{"name":"getAnnouncementGroup","parameterTypes":[] }, {"name":"getAttachmentBackfill","parameterTypes":[] }, {"name":"getChangeNumber","parameterTypes":[] }, {"name":"getDeleteSync","parameterTypes":[] }, {"name":"getGiftBadges","parameterTypes":[] }, {"name":"getPaymentActivation","parameterTypes":[] }, {"name":"getPni","parameterTypes":[] }, {"name":"getSenderKey","parameterTypes":[] }, {"name":"getStorage","parameterTypes":[] }, {"name":"getStorageServiceEncryptionV2","parameterTypes":[] }, {"name":"getStories","parameterTypes":[] }, {"name":"getVersionedExpirationTimer","parameterTypes":[] }]
}, },
{ {
"name":"org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest", "name":"org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest",
@ -2326,6 +2346,13 @@
{ {
"name":"org.whispersystems.signalservice.api.groupsv2.TemporalCredential[]" "name":"org.whispersystems.signalservice.api.groupsv2.TemporalCredential[]"
}, },
{
"name":"org.whispersystems.signalservice.api.keys.OneTimePreKeyCounts",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{ {
"name":"org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse", "name":"org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -2393,7 +2420,14 @@
"name":"org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite", "name":"org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite",
"allDeclaredFields":true, "allDeclaredFields":true,
"allDeclaredMethods":true, "allDeclaredMethods":true,
"allDeclaredConstructors":true "allDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","byte[]","byte[]","byte[]","byte[]","byte[]","boolean","boolean","byte[]","java.util.List"] }, {"name":"getAbout","parameterTypes":[] }, {"name":"getAboutEmoji","parameterTypes":[] }, {"name":"getAvatar","parameterTypes":[] }, {"name":"getBadgeIds","parameterTypes":[] }, {"name":"getCommitment","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPaymentAddress","parameterTypes":[] }, {"name":"getPhoneNumberSharing","parameterTypes":[] }, {"name":"getSameAvatar","parameterTypes":[] }, {"name":"getVersion","parameterTypes":[] }]
},
{
"name":"org.whispersystems.signalservice.api.provisioning.ProvisioningMessage",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
}, },
{ {
"name":"org.whispersystems.signalservice.api.push.ServiceId", "name":"org.whispersystems.signalservice.api.push.ServiceId",
@ -2438,6 +2472,12 @@
"queryAllDeclaredConstructors":true, "queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String"] }] "methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String"] }]
}, },
{
"name":"org.whispersystems.signalservice.api.ratelimit.SubmitRecaptchaChallengePayload",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{ {
"name":"org.whispersystems.signalservice.api.storage.StorageAuthResponse", "name":"org.whispersystems.signalservice.api.storage.StorageAuthResponse",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -2916,7 +2956,28 @@
"allDeclaredFields":true, "allDeclaredFields":true,
"queryAllDeclaredMethods":true, "queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true, "queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }, {"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","java.lang.String","java.lang.String","org.whispersystems.signalservice.internal.push.WhoAmIResponse$Entitlements"] }, {"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","java.lang.String","java.lang.String","org.whispersystems.signalservice.internal.push.WhoAmIResponse$Entitlements","int","kotlin.jvm.internal.DefaultConstructorMarker"] }]
},
{
"name":"org.whispersystems.signalservice.internal.push.WhoAmIResponse$BackupEntitlement",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.lang.Long","java.lang.Long"] }]
},
{
"name":"org.whispersystems.signalservice.internal.push.WhoAmIResponse$BadgeEntitlement",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.Boolean","java.lang.Long"] }, {"name":"<init>","parameterTypes":["java.lang.String","java.lang.Boolean","java.lang.Long","int","kotlin.jvm.internal.DefaultConstructorMarker"] }]
},
{
"name":"org.whispersystems.signalservice.internal.push.WhoAmIResponse$Entitlements",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.util.List","org.whispersystems.signalservice.internal.push.WhoAmIResponse$BackupEntitlement"] }, {"name":"<init>","parameterTypes":["java.util.List","org.whispersystems.signalservice.internal.push.WhoAmIResponse$BackupEntitlement","int","kotlin.jvm.internal.DefaultConstructorMarker"] }]
}, },
{ {
"name":"org.whispersystems.signalservice.internal.serialize.protos.AddressProto", "name":"org.whispersystems.signalservice.internal.serialize.protos.AddressProto",
@ -2943,6 +3004,9 @@
"allDeclaredFields":true, "allDeclaredFields":true,
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }] "methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
}, },
{
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$BackupTierHistory"
},
{ {
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$Builder" "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$Builder"
}, },
@ -2952,6 +3016,9 @@
{ {
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$IAPSubscriberData" "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$IAPSubscriberData"
}, },
{
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$NotificationProfileManualOverride"
},
{ {
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$PhoneNumberSharingMode" "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$PhoneNumberSharingMode"
}, },
@ -2967,6 +3034,9 @@
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$UsernameLink", "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$UsernameLink",
"allDeclaredFields":true "allDeclaredFields":true
}, },
{
"name":"org.whispersystems.signalservice.internal.storage.protos.AvatarColor"
},
{ {
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord", "name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -3000,7 +3070,7 @@
{ {
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record", "name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record",
"allDeclaredFields":true, "allDeclaredFields":true,
"fields":[{"name":"archived"}, {"name":"blocked"}, {"name":"dontNotifyForMentionsIfMuted"}, {"name":"hideStory"}, {"name":"markedUnread"}, {"name":"masterKey"}, {"name":"mutedUntilTimestamp"}, {"name":"storySendMode"}, {"name":"whitelisted"}], "fields":[{"name":"archived"}, {"name":"avatarColor"}, {"name":"blocked"}, {"name":"dontNotifyForMentionsIfMuted"}, {"name":"hideStory"}, {"name":"markedUnread"}, {"name":"masterKey"}, {"name":"mutedUntilTimestamp"}, {"name":"storySendMode"}, {"name":"whitelisted"}],
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }] "methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
}, },
{ {

View file

@ -1,17 +1,17 @@
[versions] [versions]
slf4j = "2.0.16" slf4j = "2.0.17"
[libraries] [libraries]
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.80" bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.81"
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.18.2" jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.19.1"
argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0" argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0"
dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0" dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0"
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" } slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
logback = "ch.qos.logback:logback-classic:1.5.16" logback = "ch.qos.logback:logback-classic:1.5.18"
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_116" signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_127"
sqlite = "org.xerial:sqlite-jdbc:3.48.0.0" sqlite = "org.xerial:sqlite-jdbc:3.50.2.0"
hikari = "com.zaxxer:HikariCP:6.2.1" hikari = "com.zaxxer:HikariCP:6.3.0"
junit-jupiter = "org.junit.jupiter:junit-jupiter:5.11.4" junit-jupiter = "org.junit.jupiter:junit-jupiter:5.13.2"
junit-launcher = "org.junit.platform:junit-platform-launcher:1.11.4" junit-launcher = "org.junit.platform:junit-platform-launcher:1.13.2"

Binary file not shown.

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

11
gradlew vendored
View file

@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015-2021 the original authors. # Copyright © 2015 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@ -115,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@ -206,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command: # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped. # and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line. # treated as '${Hostname}' itself on the command line.
@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \ -classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.

4
gradlew.bat vendored
View file

@ -70,11 +70,11 @@ goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell

View file

@ -1,5 +1,7 @@
package org.asamk.signal.manager; package org.asamk.signal.manager;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import org.asamk.signal.manager.api.AlreadyReceivingException; import org.asamk.signal.manager.api.AlreadyReceivingException;
import org.asamk.signal.manager.api.AttachmentInvalidException; import org.asamk.signal.manager.api.AttachmentInvalidException;
import org.asamk.signal.manager.api.CaptchaRejectedException; import org.asamk.signal.manager.api.CaptchaRejectedException;
@ -28,6 +30,7 @@ import org.asamk.signal.manager.api.NotAGroupMemberException;
import org.asamk.signal.manager.api.NotPrimaryDeviceException; import org.asamk.signal.manager.api.NotPrimaryDeviceException;
import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.PendingAdminApprovalException; import org.asamk.signal.manager.api.PendingAdminApprovalException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.ReceiveConfig; import org.asamk.signal.manager.api.ReceiveConfig;
@ -49,7 +52,6 @@ import org.asamk.signal.manager.api.UsernameStatus;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException; import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import java.io.Closeable; import java.io.Closeable;
import java.io.File; import java.io.File;
@ -65,7 +67,7 @@ import java.util.Set;
public interface Manager extends Closeable { public interface Manager extends Closeable {
static boolean isValidNumber(final String e164Number, final String countryCode) { static boolean isValidNumber(final String e164Number, final String countryCode) {
return PhoneNumberFormatter.isValidNumber(e164Number, countryCode); return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode);
} }
static boolean isSignalClientAvailable() { static boolean isSignalClientAvailable() {
@ -94,7 +96,7 @@ public interface Manager extends Closeable {
*/ */
Map<String, UserStatus> getUserStatus(Set<String> numbers) throws IOException, RateLimitException; Map<String, UserStatus> getUserStatus(Set<String> numbers) throws IOException, RateLimitException;
Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames); Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) throws IOException;
void updateAccountAttributes( void updateAccountAttributes(
String deviceName, String deviceName,
@ -139,7 +141,7 @@ public interface Manager extends Closeable {
String newNumber, String newNumber,
String verificationCode, String verificationCode,
String pin String pin
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException; ) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException;
void unregister() throws IOException; void unregister() throws IOException;
@ -237,9 +239,12 @@ public interface Manager extends Closeable {
void deleteContact(RecipientIdentifier.Single recipient); void deleteContact(RecipientIdentifier.Single recipient);
void setContactName( void setContactName(
RecipientIdentifier.Single recipient, final RecipientIdentifier.Single recipient,
String givenName, final String givenName,
final String familyName final String familyName,
final String nickGivenName,
final String nickFamilyName,
final String note
) throws NotPrimaryDeviceException, UnregisteredRecipientException; ) throws NotPrimaryDeviceException, UnregisteredRecipientException;
void setContactsBlocked( void setContactsBlocked(

View file

@ -3,6 +3,7 @@ package org.asamk.signal.manager;
import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException; import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
@ -21,7 +22,7 @@ public interface RegistrationManager extends Closeable {
void verifyAccount( void verifyAccount(
String verificationCode, String verificationCode,
String pin String pin
) throws IOException, PinLockedException, IncorrectPinException; ) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException;
void deleteLocalAccountData() throws IOException; void deleteLocalAccountData() throws IOException;

View file

@ -2,6 +2,7 @@ package org.asamk.signal.manager;
import org.asamk.signal.manager.api.AccountCheckException; import org.asamk.signal.manager.api.AccountCheckException;
import org.asamk.signal.manager.api.NotRegisteredException; import org.asamk.signal.manager.api.NotRegisteredException;
import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.ServiceEnvironment; import org.asamk.signal.manager.api.ServiceEnvironment;
import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
@ -63,19 +64,28 @@ public class SignalAccountFiles {
return accountsStore.getAllNumbers(); return accountsStore.getAllNumbers();
} }
public MultiAccountManager initMultiAccountManager() throws IOException { public MultiAccountManager initMultiAccountManager() throws IOException, AccountCheckException {
final var managers = accountsStore.getAllAccounts().parallelStream().map(a -> { final var managerPairs = accountsStore.getAllAccounts().parallelStream().map(a -> {
try { try {
return initManager(a.number(), a.path()); return new Pair<Manager, Throwable>(initManager(a.number(), a.path()), null);
} catch (NotRegisteredException | IOException | AccountCheckException e) { } catch (NotRegisteredException e) {
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName()); logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
return null; return null;
} catch (Throwable e) { } catch (AccountCheckException | IOException e) {
logger.error("Failed to load {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName()); logger.error("Failed to load {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
throw e; return new Pair<Manager, Throwable>(null, e);
} }
}).filter(Objects::nonNull).toList(); }).filter(Objects::nonNull).toList();
for (final var pair : managerPairs) {
if (pair.second() instanceof IOException e) {
throw e;
} else if (pair.second() instanceof AccountCheckException e) {
throw e;
}
}
final var managers = managerPairs.stream().map(Pair::first).toList();
return new MultiAccountManagerImpl(managers, this); return new MultiAccountManagerImpl(managers, this);
} }

View file

@ -19,9 +19,7 @@ public class RenewSessionAction implements HandleAction {
@Override @Override
public void execute(Context context) throws Throwable { public void execute(Context context) throws Throwable {
context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId); context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId);
if (!recipientId.equals(context.getAccount().getSelfRecipientId())) { context.getSendHelper().sendNullMessage(recipientId);
context.getSendHelper().sendNullMessage(recipientId);
}
} }
@Override @Override

View file

@ -7,7 +7,6 @@ import org.signal.libsignal.metadata.ProtocolException;
import org.signal.libsignal.protocol.message.CiphertextMessage; import org.signal.libsignal.protocol.message.CiphertextMessage;
import org.signal.libsignal.protocol.message.DecryptionErrorMessage; import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.internal.push.Envelope; import org.whispersystems.signalservice.internal.push.Envelope;
import java.util.Optional; import java.util.Optional;
@ -15,29 +14,21 @@ import java.util.Optional;
public class SendRetryMessageRequestAction implements HandleAction { public class SendRetryMessageRequestAction implements HandleAction {
private final RecipientId recipientId; private final RecipientId recipientId;
private final ServiceId serviceId;
private final ProtocolException protocolException; private final ProtocolException protocolException;
private final SignalServiceEnvelope envelope; private final SignalServiceEnvelope envelope;
private final ServiceId accountId;
public SendRetryMessageRequestAction( public SendRetryMessageRequestAction(
final RecipientId recipientId, final RecipientId recipientId,
final ServiceId serviceId,
final ProtocolException protocolException, final ProtocolException protocolException,
final SignalServiceEnvelope envelope, final SignalServiceEnvelope envelope
final ServiceId accountId
) { ) {
this.recipientId = recipientId; this.recipientId = recipientId;
this.serviceId = serviceId;
this.protocolException = protocolException; this.protocolException = protocolException;
this.envelope = envelope; this.envelope = envelope;
this.accountId = accountId;
} }
@Override @Override
public void execute(Context context) throws Throwable { public void execute(Context context) throws Throwable {
context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId);
int senderDevice = protocolException.getSenderDevice(); int senderDevice = protocolException.getSenderDevice();
Optional<GroupId> groupId = protocolException.getGroupId().isPresent() ? Optional.of(GroupId.unknownVersion( Optional<GroupId> groupId = protocolException.getGroupId().isPresent() ? Optional.of(GroupId.unknownVersion(
protocolException.getGroupId().get())) : Optional.empty(); protocolException.getGroupId().get())) : Optional.empty();

View file

@ -49,8 +49,12 @@ public record Contact(
builder.givenName = copy.givenName(); builder.givenName = copy.givenName();
builder.familyName = copy.familyName(); builder.familyName = copy.familyName();
builder.nickName = copy.nickName(); builder.nickName = copy.nickName();
builder.nickNameGivenName = copy.nickNameGivenName();
builder.nickNameFamilyName = copy.nickNameFamilyName();
builder.note = copy.note();
builder.color = copy.color(); builder.color = copy.color();
builder.messageExpirationTime = copy.messageExpirationTime(); builder.messageExpirationTime = copy.messageExpirationTime();
builder.messageExpirationTimeVersion = copy.messageExpirationTimeVersion();
builder.muteUntil = copy.muteUntil(); builder.muteUntil = copy.muteUntil();
builder.hideStory = copy.hideStory(); builder.hideStory = copy.hideStory();
builder.isBlocked = copy.isBlocked(); builder.isBlocked = copy.isBlocked();

View file

@ -2,7 +2,6 @@ package org.asamk.signal.manager.api;
import org.asamk.signal.manager.util.Utils; import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.ecc.ECPublicKey;
import java.net.URI; import java.net.URI;
@ -37,7 +36,7 @@ public record DeviceLinkUrl(String deviceIdentifier, ECPublicKey deviceKey) {
} }
ECPublicKey deviceKey; ECPublicKey deviceKey;
try { try {
deviceKey = Curve.decodePoint(publicKeyBytes, 0); deviceKey = new ECPublicKey(publicKeyBytes);
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
throw new InvalidDeviceLinkException("Invalid device link", e); throw new InvalidDeviceLinkException("Invalid device link", e);
} }

View file

@ -2,7 +2,7 @@ package org.asamk.signal.manager.api;
public class InvalidNumberException extends Exception { public class InvalidNumberException extends Exception {
InvalidNumberException(String message) { public InvalidNumberException(String message) {
super(message); super(message);
} }

View file

@ -6,6 +6,7 @@ import java.util.Optional;
public record Message( public record Message(
String messageText, String messageText,
List<String> attachments, List<String> attachments,
boolean viewOnce,
List<Mention> mentions, List<Mention> mentions,
Optional<Quote> quote, Optional<Quote> quote,
Optional<Sticker> sticker, Optional<Sticker> sticker,

View file

@ -0,0 +1,3 @@
package org.asamk.signal.manager.api;
public class PinLockMissingException extends Exception {}

View file

@ -1,8 +1,8 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
import org.asamk.signal.manager.util.PhoneNumberFormatter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.UUID; import java.util.UUID;
@ -24,32 +24,28 @@ public sealed interface RecipientIdentifier {
sealed interface Single extends RecipientIdentifier { sealed interface Single extends RecipientIdentifier {
static Single fromString(String identifier, String localNumber) throws InvalidNumberException { static Single fromString(String identifier, String localNumber) throws InvalidNumberException {
try { if (UuidUtil.isUuid(identifier)) {
if (UuidUtil.isUuid(identifier)) { return new Uuid(UUID.fromString(identifier));
return new Uuid(UUID.fromString(identifier));
}
if (identifier.startsWith("PNI:")) {
final var pni = identifier.substring(4);
if (!UuidUtil.isUuid(pni)) {
throw new InvalidNumberException("Invalid PNI");
}
return new Pni(UUID.fromString(pni));
}
if (identifier.startsWith("u:")) {
return new Username(identifier.substring(2));
}
final var normalizedNumber = PhoneNumberFormatter.formatNumber(identifier, localNumber);
if (!normalizedNumber.equals(identifier)) {
final Logger logger = LoggerFactory.getLogger(RecipientIdentifier.class);
logger.debug("Normalized number {} to {}.", identifier, normalizedNumber);
}
return new Number(normalizedNumber);
} catch (org.whispersystems.signalservice.api.util.InvalidNumberException e) {
throw new InvalidNumberException(e.getMessage(), e);
} }
if (identifier.startsWith("PNI:")) {
final var pni = identifier.substring(4);
if (!UuidUtil.isUuid(pni)) {
throw new InvalidNumberException("Invalid PNI");
}
return new Pni(UUID.fromString(pni));
}
if (identifier.startsWith("u:")) {
return new Username(identifier.substring(2));
}
final var normalizedNumber = PhoneNumberFormatter.formatNumber(identifier, localNumber);
if (!normalizedNumber.equals(identifier)) {
final Logger logger = LoggerFactory.getLogger(RecipientIdentifier.class);
logger.debug("Normalized number {} to {}.", identifier, normalizedNumber);
}
return new Number(normalizedNumber);
} }
static Single fromAddress(RecipientAddress address) { static Single fromAddress(RecipientAddress address) {

View file

@ -2,9 +2,9 @@ package org.asamk.signal.manager.config;
import org.signal.libsignal.net.Network.Environment; import org.signal.libsignal.net.Network.Environment;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.HttpProxy;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl; import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
import org.whispersystems.signalservice.internal.configuration.SignalProxy; import org.whispersystems.signalservice.internal.configuration.SignalProxy;
@ -28,8 +28,9 @@ class LiveConfig {
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder() private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
.decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"); .decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF");
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57"; private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
private static final String SVR2_MRENCLAVE = "9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570"; private static final String SVR2_MRENCLAVE_LEGACY_LEGACY = "9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570";
private static final String SVR2_LEGACY_MRENCLAVE = "a6622ad4656e1abcd0bc0ff17c229477747d2ded0495c4ebee7ed35c1789fa97"; private static final String SVR2_MRENCLAVE_LEGACY = "093be9ea32405e85ae28dbb48eb668aebeb7dbe29517b9b86ad4bec4dfe0e6a6";
private static final String SVR2_MRENCLAVE = "29cd63c87bea751e3bfd0fbd401279192e2e5c99948b4ee9437eafc4968355fb";
private static final String URL = "https://chat.signal.org"; private static final String URL = "https://chat.signal.org";
private static final String CDN_URL = "https://cdn.signal.org"; private static final String CDN_URL = "https://cdn.signal.org";
@ -42,6 +43,7 @@ class LiveConfig {
private static final Optional<Dns> dns = Optional.empty(); private static final Optional<Dns> dns = Optional.empty();
private static final Optional<SignalProxy> proxy = Optional.empty(); private static final Optional<SignalProxy> proxy = Optional.empty();
private static final Optional<HttpProxy> systemProxy = Optional.empty();
private static final byte[] zkGroupServerPublicParams = Base64.getDecoder() private static final byte[] zkGroupServerPublicParams = Base64.getDecoder()
.decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw=="); .decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw==");
@ -69,6 +71,7 @@ class LiveConfig {
interceptors, interceptors,
dns, dns,
proxy, proxy,
systemProxy,
zkGroupServerPublicParams, zkGroupServerPublicParams,
genericServerPublicParams, genericServerPublicParams,
backupServerPublicParams, backupServerPublicParams,
@ -77,7 +80,7 @@ class LiveConfig {
static ECPublicKey getUnidentifiedSenderTrustRoot() { static ECPublicKey getUnidentifiedSenderTrustRoot() {
try { try {
return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0); return new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT);
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
@ -89,7 +92,7 @@ class LiveConfig {
createDefaultServiceConfiguration(interceptors), createDefaultServiceConfiguration(interceptors),
getUnidentifiedSenderTrustRoot(), getUnidentifiedSenderTrustRoot(),
CDSI_MRENCLAVE, CDSI_MRENCLAVE,
List.of(SVR2_MRENCLAVE, SVR2_LEGACY_MRENCLAVE)); List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_LEGACY_LEGACY));
} }
private LiveConfig() { private LiveConfig() {

View file

@ -30,7 +30,8 @@ public class ServiceConfig {
public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) { public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) {
final var deleteSync = !isPrimaryDevice; final var deleteSync = !isPrimaryDevice;
final var storageEncryptionV2 = !isPrimaryDevice; final var storageEncryptionV2 = !isPrimaryDevice;
return new AccountAttributes.Capabilities(true, deleteSync, true, storageEncryptionV2); final var attachmentBackfill = !isPrimaryDevice;
return new AccountAttributes.Capabilities(true, deleteSync, true, storageEncryptionV2, attachmentBackfill);
} }
public static ServiceEnvironmentConfig getServiceEnvironmentConfig( public static ServiceEnvironmentConfig getServiceEnvironmentConfig(

View file

@ -2,9 +2,9 @@ package org.asamk.signal.manager.config;
import org.signal.libsignal.net.Network; import org.signal.libsignal.net.Network;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.HttpProxy;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl; import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
import org.whispersystems.signalservice.internal.configuration.SignalProxy; import org.whispersystems.signalservice.internal.configuration.SignalProxy;
@ -28,8 +28,9 @@ class StagingConfig {
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder() private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
.decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx"); .decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx");
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57"; private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
private static final String SVR2_MRENCLAVE = "38e01eff4fe357dc0b0e8ef7a44b4abc5489fbccba3a78780f3872c277f62bf3"; private static final String SVR2_MRENCLAVE_LEGACY_LEGACY = "38e01eff4fe357dc0b0e8ef7a44b4abc5489fbccba3a78780f3872c277f62bf3";
private static final String SVR2_LEGACY_MRENCLAVE = "acb1973aa0bbbd14b3b4e06f145497d948fd4a98efc500fcce363b3b743ec482"; private static final String SVR2_MRENCLAVE_LEGACY = "2e8cefe6e3f389d8426adb24e9b7fb7adf10902c96f06f7bbcee36277711ed91";
private static final String SVR2_MRENCLAVE = "a75542d82da9f6914a1e31f8a7407053b99cc99a0e7291d8fbd394253e19b036";
private static final String URL = "https://chat.staging.signal.org"; private static final String URL = "https://chat.staging.signal.org";
private static final String CDN_URL = "https://cdn-staging.signal.org"; private static final String CDN_URL = "https://cdn-staging.signal.org";
@ -42,6 +43,7 @@ class StagingConfig {
private static final Optional<Dns> dns = Optional.empty(); private static final Optional<Dns> dns = Optional.empty();
private static final Optional<SignalProxy> proxy = Optional.empty(); private static final Optional<SignalProxy> proxy = Optional.empty();
private static final Optional<HttpProxy> systemProxy = Optional.empty();
private static final byte[] zkGroupServerPublicParams = Base64.getDecoder() private static final byte[] zkGroupServerPublicParams = Base64.getDecoder()
.decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ=="); .decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ==");
@ -69,6 +71,7 @@ class StagingConfig {
interceptors, interceptors,
dns, dns,
proxy, proxy,
systemProxy,
zkGroupServerPublicParams, zkGroupServerPublicParams,
genericServerPublicParams, genericServerPublicParams,
backupServerPublicParams, backupServerPublicParams,
@ -77,7 +80,7 @@ class StagingConfig {
static ECPublicKey getUnidentifiedSenderTrustRoot() { static ECPublicKey getUnidentifiedSenderTrustRoot() {
try { try {
return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0); return new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT);
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
@ -89,7 +92,7 @@ class StagingConfig {
createDefaultServiceConfiguration(interceptors), createDefaultServiceConfiguration(interceptors),
getUnidentifiedSenderTrustRoot(), getUnidentifiedSenderTrustRoot(),
CDSI_MRENCLAVE, CDSI_MRENCLAVE,
List.of(SVR2_MRENCLAVE, SVR2_LEGACY_MRENCLAVE)); List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_LEGACY_LEGACY));
} }
private StagingConfig() { private StagingConfig() {

View file

@ -4,6 +4,7 @@ import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.DeviceLinkUrl; import org.asamk.signal.manager.api.DeviceLinkUrl;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException; import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
@ -32,6 +33,7 @@ import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity; import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException; import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
@ -50,7 +52,7 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import okio.ByteString; import okio.ByteString;
@ -102,9 +104,9 @@ public class AccountHelper {
checkWhoAmiI(); checkWhoAmiI();
} }
if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) { if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
context.getSyncHelper().requestSyncPniIdentity(); throw new IOException("Missing PNI identity key, relinking required");
} }
if (account.getPreviousStorageVersion() < 4 if (account.getPreviousStorageVersion() < 10
&& account.isPrimaryDevice() && account.isPrimaryDevice()
&& account.getRegistrationLockPin() != null) { && account.getRegistrationLockPin() != null) {
migrateRegistrationPin(); migrateRegistrationPin();
@ -184,7 +186,7 @@ public class AccountHelper {
String newNumber, String newNumber,
String verificationCode, String verificationCode,
String pin String pin
) throws IncorrectPinException, PinLockedException, IOException { ) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
for (var attempts = 0; attempts < 5; attempts++) { for (var attempts = 0; attempts < 5; attempts++) {
try { try {
finishChangeNumberInternal(newNumber, verificationCode, pin); finishChangeNumberInternal(newNumber, verificationCode, pin);
@ -204,7 +206,7 @@ public class AccountHelper {
String newNumber, String newNumber,
String verificationCode, String verificationCode,
String pin String pin
) throws IncorrectPinException, PinLockedException, IOException { ) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
final var pniIdentity = KeyUtils.generateIdentityKeyPair(); final var pniIdentity = KeyUtils.generateIdentityKeyPair();
final var encryptedDeviceMessages = new ArrayList<OutgoingPushMessage>(); final var encryptedDeviceMessages = new ArrayList<OutgoingPushMessage>();
final var devicePniSignedPreKeys = new HashMap<Integer, SignedPreKeyEntity>(); final var devicePniSignedPreKeys = new HashMap<Integer, SignedPreKeyEntity>();
@ -289,12 +291,13 @@ public class AccountHelper {
context.getPinHelper(), context.getPinHelper(),
(sessionId1, verificationCode1, registrationLock) -> { (sessionId1, verificationCode1, registrationLock) -> {
final var registrationApi = dependencies.getRegistrationApi(); final var registrationApi = dependencies.getRegistrationApi();
final var accountApi = dependencies.getAccountApi();
try { try {
handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1)); handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1));
} catch (AlreadyVerifiedException e) { } catch (AlreadyVerifiedException e) {
// Already verified so can continue changing number // Already verified so can continue changing number
} }
return handleResponseException(registrationApi.changeNumber(new ChangePhoneNumberRequest(sessionId1, return handleResponseException(accountApi.changeNumber(new ChangePhoneNumberRequest(sessionId1,
null, null,
newNumber, newNumber,
registrationLock, registrationLock,
@ -378,7 +381,7 @@ public class AccountHelper {
candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash())); candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash()));
} }
final var response = dependencies.getAccountManager().reserveUsername(candidateHashes); final var response = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes));
final var hashIndex = candidateHashes.indexOf(response.getUsernameHash()); final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
if (hashIndex == -1) { if (hashIndex == -1) {
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes."); logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
@ -388,7 +391,7 @@ public class AccountHelper {
logger.debug("[reserveUsername] Successfully reserved username."); logger.debug("[reserveUsername] Successfully reserved username.");
final var username = candidates.get(hashIndex); final var username = candidates.get(hashIndex);
final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username); final var linkComponents = confirmUsernameAndCreateNewLink(username);
account.setUsername(username.getUsername()); account.setUsername(username.getUsername());
account.setUsernameLink(linkComponents); account.setUsernameLink(linkComponents);
account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress()); account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
@ -396,6 +399,40 @@ public class AccountHelper {
logger.debug("[confirmUsername] Successfully confirmed username."); logger.debug("[confirmUsername] Successfully confirmed username.");
} }
public UsernameLinkComponents createUsernameLink(Username username) throws IOException {
try {
Username.UsernameLink link = username.generateLink();
return handleResponseException(dependencies.getAccountApi().createUsernameLink(link));
} catch (BaseUsernameException e) {
throw new AssertionError(e);
}
}
private UsernameLinkComponents confirmUsernameAndCreateNewLink(Username username) throws IOException {
try {
Username.UsernameLink link = username.generateLink();
UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link));
return new UsernameLinkComponents(link.getEntropy(), serverId);
} catch (BaseUsernameException e) {
throw new AssertionError(e);
}
}
private UsernameLinkComponents reclaimUsernameAndLink(
Username username,
UsernameLinkComponents linkComponents
) throws IOException {
try {
Username.UsernameLink link = username.generateLink(linkComponents.getEntropy());
UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link));
return new UsernameLinkComponents(link.getEntropy(), serverId);
} catch (BaseUsernameException e) {
throw new AssertionError(e);
}
}
public void refreshCurrentUsername() throws IOException, BaseUsernameException { public void refreshCurrentUsername() throws IOException, BaseUsernameException {
final var localUsername = account.getUsername(); final var localUsername = account.getUsername();
if (localUsername == null) { if (localUsername == null) {
@ -438,14 +475,14 @@ public class AccountHelper {
final var usernameLink = account.getUsernameLink(); final var usernameLink = account.getUsernameLink();
if (usernameLink == null) { if (usernameLink == null) {
dependencies.getAccountManager() handleResponseException(dependencies.getAccountApi()
.reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash()))); .reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash()))));
logger.debug("[reserveUsername] Successfully reserved existing username."); logger.debug("[reserveUsername] Successfully reserved existing username.");
final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username); final var linkComponents = confirmUsernameAndCreateNewLink(username);
account.setUsernameLink(linkComponents); account.setUsernameLink(linkComponents);
logger.debug("[confirmUsername] Successfully confirmed existing username."); logger.debug("[confirmUsername] Successfully confirmed existing username.");
} else { } else {
final var linkComponents = dependencies.getAccountManager().reclaimUsernameAndLink(username, usernameLink); final var linkComponents = reclaimUsernameAndLink(username, usernameLink);
account.setUsernameLink(linkComponents); account.setUsernameLink(linkComponents);
logger.debug("[confirmUsername] Successfully reclaimed existing username and link."); logger.debug("[confirmUsername] Successfully reclaimed existing username and link.");
} }
@ -455,7 +492,7 @@ public class AccountHelper {
private void tryToSetUsernameLink(Username username) { private void tryToSetUsernameLink(Username username) {
for (var i = 1; i < 4; i++) { for (var i = 1; i < 4; i++) {
try { try {
final var linkComponents = dependencies.getAccountManager().createUsernameLink(username); final var linkComponents = createUsernameLink(username);
account.setUsernameLink(linkComponents); account.setUsernameLink(linkComponents);
break; break;
} catch (IOException e) { } catch (IOException e) {
@ -465,9 +502,8 @@ public class AccountHelper {
} }
public void deleteUsername() throws IOException { public void deleteUsername() throws IOException {
dependencies.getAccountManager().deleteUsernameLink(); handleResponseException(dependencies.getAccountApi().deleteUsername());
account.setUsernameLink(null); account.setUsernameLink(null);
dependencies.getAccountManager().deleteUsername();
account.setUsername(null); account.setUsername(null);
logger.debug("[deleteUsername] Successfully deleted the username."); logger.debug("[deleteUsername] Successfully deleted the username.");
} }
@ -479,7 +515,7 @@ public class AccountHelper {
} }
public void updateAccountAttributes() throws IOException { public void updateAccountAttributes() throws IOException {
dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null)); handleResponseException(dependencies.getAccountApi().setAccountAttributes(account.getAccountAttributes(null)));
} }
public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, org.asamk.signal.manager.api.DeviceLimitExceededException { public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, org.asamk.signal.manager.api.DeviceLimitExceededException {
@ -500,9 +536,9 @@ public class AccountHelper {
account.getAciIdentityKeyPair(), account.getAciIdentityKeyPair(),
account.getPniIdentityKeyPair(), account.getPniIdentityKeyPair(),
account.getProfileKey(), account.getProfileKey(),
account.getOrCreateAccountEntropyPool(),
account.getOrCreatePinMasterKey(), account.getOrCreatePinMasterKey(),
account.getOrCreateMediaRootBackupKey(), account.getOrCreateMediaRootBackupKey(),
account.getOrCreateAccountEntropyPool(),
verificationCode.getVerificationCode(), verificationCode.getVerificationCode(),
null)); null));
account.setMultiDevice(true); account.setMultiDevice(true);
@ -510,8 +546,8 @@ public class AccountHelper {
} }
public void removeLinkedDevices(int deviceId) throws IOException { public void removeLinkedDevices(int deviceId) throws IOException {
dependencies.getAccountManager().removeDevice(deviceId); handleResponseException(dependencies.getLinkDeviceApi().removeDevice(deviceId));
var devices = dependencies.getAccountManager().getDevices(); var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
account.setMultiDevice(devices.size() > 1); account.setMultiDevice(devices.size() > 1);
} }
@ -519,14 +555,16 @@ public class AccountHelper {
var masterKey = account.getOrCreatePinMasterKey(); var masterKey = account.getOrCreatePinMasterKey();
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey); context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
dependencies.getAccountManager().enableRegistrationLock(masterKey); handleResponseException(dependencies.getAccountApi()
.enableRegistrationLock(masterKey.deriveRegistrationLock()));
} }
public void setRegistrationPin(String pin) throws IOException { public void setRegistrationPin(String pin) throws IOException {
var masterKey = account.getOrCreatePinMasterKey(); var masterKey = account.getOrCreatePinMasterKey();
context.getPinHelper().setRegistrationLockPin(pin, masterKey); context.getPinHelper().setRegistrationLockPin(pin, masterKey);
dependencies.getAccountManager().enableRegistrationLock(masterKey); handleResponseException(dependencies.getAccountApi()
.enableRegistrationLock(masterKey.deriveRegistrationLock()));
account.setRegistrationLockPin(pin); account.setRegistrationLockPin(pin);
updateAccountAttributes(); updateAccountAttributes();
@ -535,7 +573,7 @@ public class AccountHelper {
public void removeRegistrationPin() throws IOException { public void removeRegistrationPin() throws IOException {
// Remove KBS Pin // Remove KBS Pin
context.getPinHelper().removeRegistrationLockPin(); context.getPinHelper().removeRegistrationLockPin();
dependencies.getAccountManager().disableRegistrationLock(); handleResponseException(dependencies.getAccountApi().disableRegistrationLock());
account.setRegistrationLockPin(null); account.setRegistrationLockPin(null);
} }
@ -544,7 +582,7 @@ public class AccountHelper {
// When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
// If this is the primary device, other users can't send messages to this number anymore. // If this is the primary device, other users can't send messages to this number anymore.
// If this is a linked device, other users can still send messages, but this device doesn't receive them anymore. // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
dependencies.getAccountManager().setGcmId(Optional.empty()); handleResponseException(dependencies.getAccountApi().clearFcmToken());
account.setRegistered(false); account.setRegistered(false);
unregisteredListener.call(); unregisteredListener.call();
@ -558,7 +596,7 @@ public class AccountHelper {
} }
account.setRegistrationLockPin(null); account.setRegistrationLockPin(null);
dependencies.getAccountManager().deleteAccount(); handleResponseException(dependencies.getAccountApi().deleteAccount());
account.setRegistered(false); account.setRegistered(false);
unregisteredListener.call(); unregisteredListener.call();

View file

@ -9,6 +9,7 @@ import org.asamk.signal.manager.util.IOUtils;
import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.protocol.InvalidMessageException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
@ -44,14 +45,20 @@ public class AttachmentHelper {
} }
public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException { public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
var attachmentStreams = createAttachmentStreams(attachments); final var attachmentStreams = createAttachmentStreams(attachments);
// Upload attachments here, so we only upload once even for multiple recipients try {
var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size()); // Upload attachments here, so we only upload once even for multiple recipients
for (var attachmentStream : attachmentStreams) { final var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size());
attachmentPointers.add(uploadAttachment(attachmentStream)); for (final var attachmentStream : attachmentStreams) {
attachmentPointers.add(uploadAttachment(attachmentStream));
}
return attachmentPointers;
} finally {
for (final var attachmentStream : attachmentStreams) {
attachmentStream.close();
}
} }
return attachmentPointers;
} }
private List<SignalServiceAttachmentStream> createAttachmentStreams(List<String> attachments) throws AttachmentInvalidException, IOException { private List<SignalServiceAttachmentStream> createAttachmentStreams(List<String> attachments) throws AttachmentInvalidException, IOException {
@ -132,9 +139,15 @@ public class AttachmentHelper {
SignalServiceAttachmentPointer pointer, SignalServiceAttachmentPointer pointer,
File tmpFile File tmpFile
) throws IOException { ) throws IOException {
if (pointer.getDigest().isEmpty()) {
throw new IOException("Attachment pointer has no digest.");
}
try { try {
return dependencies.getMessageReceiver() return dependencies.getMessageReceiver()
.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); .retrieveAttachment(pointer,
tmpFile,
ServiceConfig.MAX_ATTACHMENT_SIZE,
AttachmentCipherInputStream.IntegrityCheck.forEncryptedDigest(pointer.getDigest().get()));
} catch (MissingConfigurationException | InvalidMessageException e) { } catch (MissingConfigurationException | InvalidMessageException e) {
throw new IOException(e); throw new IOException(e);
} }

View file

@ -17,7 +17,14 @@ public class ContactHelper {
return sourceContact != null && sourceContact.isBlocked(); return sourceContact != null && sourceContact.isBlocked();
} }
public void setContactName(final RecipientId recipientId, final String givenName, final String familyName) { public void setContactName(
final RecipientId recipientId,
final String givenName,
final String familyName,
final String nickGivenName,
final String nickFamilyName,
final String note
) {
var contact = account.getContactStore().getContact(recipientId); var contact = account.getContactStore().getContact(recipientId);
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
builder.withIsHidden(false); builder.withIsHidden(false);
@ -27,6 +34,15 @@ public class ContactHelper {
if (familyName != null) { if (familyName != null) {
builder.withFamilyName(familyName); builder.withFamilyName(familyName);
} }
if (nickGivenName != null) {
builder.withNickNameGivenName(nickGivenName);
}
if (nickFamilyName != null) {
builder.withNickNameFamilyName(nickFamilyName);
}
if (note != null) {
builder.withNote(note);
}
account.getContactStore().storeContact(recipientId, builder.build()); account.getContactStore().storeContact(recipientId, builder.build());
} }

View file

@ -551,6 +551,9 @@ public class GroupHelper {
while (true) { while (true) {
final var page = context.getGroupV2Helper() final var page = context.getGroupV2Helper()
.getDecryptedGroupHistoryPage(groupSecretParams, fromRevision, sendEndorsementsExpirationMs); .getDecryptedGroupHistoryPage(groupSecretParams, fromRevision, sendEndorsementsExpirationMs);
if (page == null) {
break;
}
page.getChangeLogs() page.getChangeLogs()
.stream() .stream()
.map(DecryptedGroupChangeLog::getChange) .map(DecryptedGroupChangeLog::getChange)

View file

@ -44,6 +44,7 @@ import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -119,6 +120,8 @@ class GroupV2Helper {
groupsV2AuthorizationString, groupsV2AuthorizationString,
false, false,
sendEndorsementsExpirationMs); sendEndorsementsExpirationMs);
} catch (NotInGroupException e) {
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
} catch (NonSuccessfulResponseCodeException e) { } catch (NonSuccessfulResponseCodeException e) {
if (e.code == 403) { if (e.code == 403) {
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null); throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);

View file

@ -41,6 +41,7 @@ import org.signal.libsignal.metadata.ProtocolNoSessionException;
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
import org.signal.libsignal.metadata.SelfSendException; import org.signal.libsignal.metadata.SelfSendException;
import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.protocol.UsePqRatchet;
import org.signal.libsignal.protocol.groups.GroupSessionBuilder; import org.signal.libsignal.protocol.groups.GroupSessionBuilder;
import org.signal.libsignal.protocol.message.DecryptionErrorMessage; import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.InvalidInputException;
@ -105,7 +106,7 @@ public final class IncomingMessageHandler {
try { try {
final var cipherResult = dependencies.getCipher(destination == null final var cipherResult = dependencies.getCipher(destination == null
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp(), UsePqRatchet.NO);
content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp()); content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
if (content == null) { if (content == null) {
return new Pair<>(List.of(), null); return new Pair<>(List.of(), null);
@ -143,7 +144,7 @@ public final class IncomingMessageHandler {
try { try {
final var cipherResult = dependencies.getCipher(destination == null final var cipherResult = dependencies.getCipher(destination == null
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp(), UsePqRatchet.NO);
content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp()); content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
if (content == null) { if (content == null) {
return new Pair<>(List.of(), null); return new Pair<>(List.of(), null);
@ -157,6 +158,9 @@ public final class IncomingMessageHandler {
} catch (ProtocolInvalidKeyIdException | ProtocolInvalidKeyException | ProtocolNoSessionException | } catch (ProtocolInvalidKeyIdException | ProtocolInvalidKeyException | ProtocolNoSessionException |
ProtocolInvalidMessageException e) { ProtocolInvalidMessageException e) {
logger.debug("Failed to decrypt incoming message", e); logger.debug("Failed to decrypt incoming message", e);
if (e instanceof ProtocolInvalidKeyIdException) {
actions.add(RefreshPreKeysAction.create());
}
final var sender = account.getRecipientResolver().resolveRecipient(e.getSender()); final var sender = account.getRecipientResolver().resolveRecipient(e.getSender());
if (context.getContactHelper().isContactBlocked(sender)) { if (context.getContactHelper().isContactBlocked(sender)) {
logger.debug("Received invalid message from blocked contact, ignoring."); logger.debug("Received invalid message from blocked contact, ignoring.");
@ -165,12 +169,11 @@ public final class IncomingMessageHandler {
if (serviceId != null) { if (serviceId != null) {
final var isSelf = sender.equals(account.getSelfRecipientId()) final var isSelf = sender.equals(account.getSelfRecipientId())
&& e.getSenderDevice() == account.getDeviceId(); && e.getSenderDevice() == account.getDeviceId();
logger.debug("Received invalid message, queuing renew session action.");
actions.add(new RenewSessionAction(sender, serviceId, destination));
if (!isSelf) { if (!isSelf) {
logger.debug("Received invalid message, requesting message resend."); logger.debug("Received invalid message, requesting message resend.");
actions.add(new SendRetryMessageRequestAction(sender, serviceId, e, envelope, destination)); actions.add(new SendRetryMessageRequestAction(sender, e, envelope));
} else {
logger.debug("Received invalid message, queuing renew session action.");
actions.add(new RenewSessionAction(sender, serviceId, destination));
} }
} else { } else {
logger.debug("Received invalid message from invalid sender: {}", e.getSender()); logger.debug("Received invalid message from invalid sender: {}", e.getSender());

View file

@ -88,7 +88,11 @@ public class PinHelper {
IOException exception = null; IOException exception = null;
for (final var secureValueRecovery : secureValueRecoveries) { for (final var secureValueRecovery : secureValueRecoveries) {
try { try {
return getRegistrationLockData(secureValueRecovery, svr2Credentials, pin); final var lockData = getRegistrationLockData(secureValueRecovery, svr2Credentials, pin);
if (lockData == null) {
continue;
}
return lockData;
} catch (IOException e) { } catch (IOException e) {
exception = e; exception = e;
} }

View file

@ -11,17 +11,19 @@ import org.signal.libsignal.protocol.state.PreKeyRecord;
import org.signal.libsignal.protocol.state.SignedPreKeyRecord; import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.NetworkResultUtil;
import org.whispersystems.signalservice.api.account.PreKeyUpload; 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.ServiceIdType;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.internal.push.OneTimePreKeyCounts;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_STALE_AGE; import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_STALE_AGE;
import static org.asamk.signal.manager.config.ServiceConfig.SIGNED_PREKEY_ROTATE_AGE; import static org.asamk.signal.manager.config.ServiceConfig.SIGNED_PREKEY_ROTATE_AGE;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class PreKeyHelper { public class PreKeyHelper {
@ -82,7 +84,7 @@ public class PreKeyHelper {
) throws IOException { ) throws IOException {
OneTimePreKeyCounts preKeyCounts; OneTimePreKeyCounts preKeyCounts;
try { try {
preKeyCounts = dependencies.getAccountManager().getPreKeyCounts(serviceIdType); preKeyCounts = handleResponseException(dependencies.getKeysApi().getAvailablePreKeyCounts(serviceIdType));
} catch (AuthorizationFailedException e) { } catch (AuthorizationFailedException e) {
logger.debug("Failed to get pre key count, ignoring: " + e.getClass().getSimpleName()); logger.debug("Failed to get pre key count, ignoring: " + e.getClass().getSimpleName());
preKeyCounts = new OneTimePreKeyCounts(0, 0); preKeyCounts = new OneTimePreKeyCounts(0, 0);
@ -143,7 +145,7 @@ public class PreKeyHelper {
kyberPreKeyRecords); kyberPreKeyRecords);
var needsReset = false; var needsReset = false;
try { try {
dependencies.getAccountManager().setPreKeys(preKeyUpload); NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeys(preKeyUpload));
try { try {
if (preKeyRecords != null) { if (preKeyRecords != null) {
account.addPreKeys(serviceIdType, preKeyRecords); account.addPreKeys(serviceIdType, preKeyRecords);

View file

@ -23,6 +23,7 @@ import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.NetworkResultUtil;
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess; import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.profiles.AvatarUploadParams; import org.whispersystems.signalservice.api.profiles.AvatarUploadParams;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
@ -196,9 +197,10 @@ public final class ProfileHelper {
: avatar == null ? AvatarUploadParams.unchanged(true) : AvatarUploadParams.unchanged(false); : avatar == null ? AvatarUploadParams.unchanged(true) : AvatarUploadParams.unchanged(false);
final var paymentsAddress = Optional.ofNullable(newProfile.getMobileCoinAddress()) final var paymentsAddress = Optional.ofNullable(newProfile.getMobileCoinAddress())
.map(address -> PaymentUtils.signPaymentsAddress(address, .map(address -> PaymentUtils.signPaymentsAddress(address,
account.getAciIdentityKeyPair().getPrivateKey())); account.getAciIdentityKeyPair().getPrivateKey()))
.orElse(null);
logger.debug("Uploading new profile"); logger.debug("Uploading new profile");
final var avatarPath = dependencies.getAccountManager() final var avatarPath = NetworkResultUtil.toSetProfileLegacy(dependencies.getProfileApi()
.setVersionedProfile(account.getAci(), .setVersionedProfile(account.getAci(),
account.getProfileKey(), account.getProfileKey(),
newProfile.getInternalServiceName(), newProfile.getInternalServiceName(),
@ -208,9 +210,9 @@ public final class ProfileHelper {
avatarUploadParams, avatarUploadParams,
List.of(/* TODO implement support for badges */), List.of(/* TODO implement support for badges */),
account.getConfigurationStore().getPhoneNumberSharingMode() account.getConfigurationStore().getPhoneNumberSharingMode()
== PhoneNumberSharingMode.EVERYBODY); == PhoneNumberSharingMode.EVERYBODY));
if (!avatarUploadParams.keepTheSame) { if (!avatarUploadParams.keepTheSame) {
builder.withAvatarUrlPath(avatarPath.orElse(null)); builder.withAvatarUrlPath(avatarPath);
} }
newProfile = builder.build(); newProfile = builder.build();
} }

View file

@ -11,10 +11,10 @@ import org.asamk.signal.manager.storage.messageCache.CachedMessage;
import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalWebSocket;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState; import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException; import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException;
@ -28,7 +28,6 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
public class ReceiveHelper { public class ReceiveHelper {
@ -94,14 +93,14 @@ public class ReceiveHelper {
// Use a Map here because java Set doesn't have a get method ... // Use a Map here because java Set doesn't have a get method ...
Map<HandleAction, HandleAction> queuedActions = new HashMap<>(); Map<HandleAction, HandleAction> queuedActions = new HashMap<>();
final var signalWebSocket = dependencies.getSignalWebSocket(); final var signalWebSocket = dependencies.getAuthenticatedSignalWebSocket();
final var webSocketStateDisposable = Observable.merge(signalWebSocket.getUnidentifiedWebSocketState(), final var webSocketStateDisposable = signalWebSocket.getState()
signalWebSocket.getWebSocketState())
.subscribeOn(Schedulers.computation()) .subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation()) .observeOn(Schedulers.computation())
.distinctUntilChanged() .distinctUntilChanged()
.subscribe(this::onWebSocketStateChange); .subscribe(this::onWebSocketStateChange);
signalWebSocket.connect(); signalWebSocket.connect();
signalWebSocket.registerKeepAliveToken("receive");
try { try {
receiveMessagesInternal(signalWebSocket, timeout, returnOnTimeout, maxMessages, handler, queuedActions); receiveMessagesInternal(signalWebSocket, timeout, returnOnTimeout, maxMessages, handler, queuedActions);
@ -109,6 +108,7 @@ public class ReceiveHelper {
hasCaughtUpWithOldMessages = false; hasCaughtUpWithOldMessages = false;
handleQueuedActions(queuedActions.keySet()); handleQueuedActions(queuedActions.keySet());
queuedActions.clear(); queuedActions.clear();
signalWebSocket.removeKeepAliveToken("receive");
signalWebSocket.disconnect(); signalWebSocket.disconnect();
webSocketStateDisposable.dispose(); webSocketStateDisposable.dispose();
shouldStop = false; shouldStop = false;
@ -116,7 +116,7 @@ public class ReceiveHelper {
} }
private void receiveMessagesInternal( private void receiveMessagesInternal(
final SignalWebSocket signalWebSocket, final SignalWebSocket.AuthenticatedWebSocket signalWebSocket,
Duration timeout, Duration timeout,
boolean returnOnTimeout, boolean returnOnTimeout,
Integer maxMessages, Integer maxMessages,

View file

@ -10,13 +10,14 @@ import org.signal.libsignal.usernames.BaseUsernameException;
import org.signal.libsignal.usernames.Username; import org.signal.libsignal.usernames.Username;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.cds.CdsiV2Service;
import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.PNI; import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException; import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException;
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException; import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
import org.whispersystems.signalservice.api.services.CdsiV2Service; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import java.io.IOException; import java.io.IOException;
import java.util.Collection; import java.util.Collection;
@ -25,8 +26,10 @@ import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import static org.asamk.signal.manager.config.ServiceConfig.MAXIMUM_ONE_OFF_REQUEST_SIZE; import static org.asamk.signal.manager.config.ServiceConfig.MAXIMUM_ONE_OFF_REQUEST_SIZE;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class RecipientHelper { public class RecipientHelper {
@ -66,7 +69,7 @@ public class RecipientHelper {
.toSignalServiceAddress(); .toSignalServiceAddress();
} }
public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException { public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException, IOException {
final var recipientIds = new HashSet<RecipientId>(recipients.size()); final var recipientIds = new HashSet<RecipientId>(recipients.size());
for (var number : recipients) { for (var number : recipients) {
final var recipientId = resolveRecipient(number); final var recipientId = resolveRecipient(number);
@ -76,12 +79,11 @@ public class RecipientHelper {
} }
public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredRecipientException { public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) { if (recipient instanceof RecipientIdentifier.Uuid(UUID uuid)) {
return account.getRecipientResolver().resolveRecipient(ACI.from(uuidRecipient.uuid())); return account.getRecipientResolver().resolveRecipient(ACI.from(uuid));
} else if (recipient instanceof RecipientIdentifier.Pni pniRecipient) { } else if (recipient instanceof RecipientIdentifier.Pni(UUID pni)) {
return account.getRecipientResolver().resolveRecipient(PNI.from(pniRecipient.pni())); return account.getRecipientResolver().resolveRecipient(PNI.from(pni));
} else if (recipient instanceof RecipientIdentifier.Number numberRecipient) { } else if (recipient instanceof RecipientIdentifier.Number(String number)) {
final var number = numberRecipient.number();
return account.getRecipientStore().resolveRecipientByNumber(number, () -> { return account.getRecipientStore().resolveRecipientByNumber(number, () -> {
try { try {
return getRegisteredUserByNumber(number); return getRegisteredUserByNumber(number);
@ -89,9 +91,12 @@ public class RecipientHelper {
return null; return null;
} }
}); });
} else if (recipient instanceof RecipientIdentifier.Username usernameRecipient) { } else if (recipient instanceof RecipientIdentifier.Username(String username)) {
var username = usernameRecipient.username(); try {
return resolveRecipientByUsernameOrLink(username, false); return resolveRecipientByUsernameOrLink(username, false);
} catch (Exception e) {
return null;
}
} }
throw new AssertionError("Unexpected RecipientIdentifier: " + recipient); throw new AssertionError("Unexpected RecipientIdentifier: " + recipient);
} }
@ -99,7 +104,7 @@ public class RecipientHelper {
public RecipientId resolveRecipientByUsernameOrLink( public RecipientId resolveRecipientByUsernameOrLink(
String username, String username,
boolean forceRefresh boolean forceRefresh
) throws UnregisteredRecipientException { ) throws UnregisteredRecipientException, IOException {
final Username finalUsername; final Username finalUsername;
try { try {
finalUsername = getUsernameFromUsernameOrLink(username); finalUsername = getUsernameFromUsernameOrLink(username);
@ -108,18 +113,22 @@ public class RecipientHelper {
} }
if (forceRefresh) { if (forceRefresh) {
try { try {
final var aci = dependencies.getAccountManager().getAciByUsername(finalUsername); final var aci = handleResponseException(dependencies.getUsernameApi().getAciByUsername(finalUsername));
return account.getRecipientStore().resolveRecipientTrusted(aci, finalUsername.getUsername()); return account.getRecipientStore().resolveRecipientTrusted(aci, finalUsername.getUsername());
} catch (IOException e) { } catch (NonSuccessfulResponseCodeException e) {
throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null, if (e.code == 404) {
null, throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null,
null, null,
username)); null,
username));
}
logger.debug("Failed to get uuid for username: {}", username, e);
throw e;
} }
} }
return account.getRecipientStore().resolveRecipientByUsername(finalUsername.getUsername(), () -> { return account.getRecipientStore().resolveRecipientByUsername(finalUsername.getUsername(), () -> {
try { try {
return dependencies.getAccountManager().getAciByUsername(finalUsername); return handleResponseException(dependencies.getUsernameApi().getAciByUsername(finalUsername));
} catch (Exception e) { } catch (Exception e) {
return null; return null;
} }
@ -130,8 +139,8 @@ public class RecipientHelper {
try { try {
final var usernameLinkUrl = UsernameLinkUrl.fromUri(username); final var usernameLinkUrl = UsernameLinkUrl.fromUri(username);
final var components = usernameLinkUrl.getComponents(); final var components = usernameLinkUrl.getComponents();
final var encryptedUsername = dependencies.getAccountManager() final var encryptedUsername = handleResponseException(dependencies.getUsernameApi()
.getEncryptedUsernameFromLinkServerId(components.getServerId()); .getEncryptedUsernameFromLinkServerId(components.getServerId()));
final var link = new Username.UsernameLink(components.getEntropy(), encryptedUsername); final var link = new Username.UsernameLink(components.getEntropy(), encryptedUsername);
return Username.fromLink(link); return Username.fromLink(link);
@ -144,8 +153,8 @@ public class RecipientHelper {
try { try {
return Optional.of(resolveRecipient(recipient)); return Optional.of(resolveRecipient(recipient));
} catch (UnregisteredRecipientException e) { } catch (UnregisteredRecipientException e) {
if (recipient instanceof RecipientIdentifier.Number r) { if (recipient instanceof RecipientIdentifier.Number(String number)) {
return account.getRecipientStore().resolveRecipientByNumberOptional(r.number()); return account.getRecipientStore().resolveRecipientByNumberOptional(number);
} else { } else {
return Optional.empty(); return Optional.empty();
} }
@ -234,8 +243,8 @@ public class RecipientHelper {
final CdsiV2Service.Response response; final CdsiV2Service.Response response;
try { try {
response = dependencies.getAccountManager() response = handleResponseException(dependencies.getCdsApi()
.getRegisteredUsersWithCdsi(token.isEmpty() ? Set.of() : previousNumbers, .getRegisteredUsers(token.isEmpty() ? Set.of() : previousNumbers,
newNumbers, newNumbers,
account.getRecipientStore().getServiceIdToProfileKeyMap(), account.getRecipientStore().getServiceIdToProfileKeyMap(),
token, token,
@ -256,7 +265,7 @@ public class RecipientHelper {
account.setCdsiToken(newToken); account.setCdsiToken(newToken);
account.setLastRecipientsRefresh(System.currentTimeMillis()); account.setLastRecipientsRefresh(System.currentTimeMillis());
} }
}); }));
} catch (CdsiInvalidTokenException | CdsiInvalidArgumentException e) { } catch (CdsiInvalidTokenException | CdsiInvalidArgumentException e) {
account.setCdsiToken(null); account.setCdsiToken(null);
account.getCdsiStore().clearAll(); account.getCdsiStore().clearAll();

View file

@ -210,12 +210,6 @@ public class StorageHelper {
remoteOnlyRecords.size()); remoteOnlyRecords.size());
} }
final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords);
final var unknownDeletes = idDifference.localOnlyIds()
.stream()
.filter(id -> !KNOWN_TYPES.contains(id.getType()))
.toList();
if (!idDifference.localOnlyIds().isEmpty()) { if (!idDifference.localOnlyIds().isEmpty()) {
final var updated = account.getRecipientStore() final var updated = account.getRecipientStore()
.removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection, .removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection,
@ -228,6 +222,12 @@ public class StorageHelper {
} }
} }
final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords);
final var unknownDeletes = idDifference.localOnlyIds()
.stream()
.filter(id -> !KNOWN_TYPES.contains(id.getType()))
.toList();
logger.debug("Storage ids with unknown type: {} inserts, {} deletes", logger.debug("Storage ids with unknown type: {} inserts, {} deletes",
unknownInserts.size(), unknownInserts.size(),
unknownDeletes.size()); unknownDeletes.size());
@ -279,10 +279,22 @@ public class StorageHelper {
try (final var connection = account.getAccountDatabase().getConnection()) { try (final var connection = account.getAccountDatabase().getConnection()) {
connection.setAutoCommit(false); connection.setAutoCommit(false);
final var localStorageIds = getAllLocalStorageIds(connection); var localStorageIds = getAllLocalStorageIds(connection);
final var idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds); var idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds);
logger.debug("ID Difference :: {}", idDifference); logger.debug("ID Difference :: {}", idDifference);
final var unknownOnlyLocal = idDifference.localOnlyIds()
.stream()
.filter(id -> !KNOWN_TYPES.contains(id.getType()))
.toList();
if (!unknownOnlyLocal.isEmpty()) {
logger.debug("Storage ids with unknown type: {} to delete", unknownOnlyLocal.size());
account.getUnknownStorageIdStore().deleteUnknownStorageIds(connection, unknownOnlyLocal);
localStorageIds = getAllLocalStorageIds(connection);
idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds);
}
final var remoteDeletes = idDifference.remoteOnlyIds().stream().map(StorageId::getRaw).toList(); final var remoteDeletes = idDifference.remoteOnlyIds().stream().map(StorageId::getRaw).toList();
final var remoteInserts = buildLocalStorageRecords(connection, idDifference.localOnlyIds()); final var remoteInserts = buildLocalStorageRecords(connection, idDifference.localOnlyIds());
// TODO check if local storage record proto matches remote, then reset to remote storage_id // TODO check if local storage record proto matches remote, then reset to remote storage_id
@ -595,7 +607,7 @@ public class StorageHelper {
final var remote = remoteByRawId.get(rawId); final var remote = remoteByRawId.get(rawId);
final var local = localByRawId.get(rawId); final var local = localByRawId.get(rawId);
if (remote.getType() != local.getType() && local.getType() != 0) { if (remote.getType() != local.getType() && KNOWN_TYPES.contains(local.getType())) {
remoteOnlyRawIds.remove(rawId); remoteOnlyRawIds.remove(rawId);
localOnlyRawIds.remove(rawId); localOnlyRawIds.remove(rawId);
hasTypeMismatch = true; hasTypeMismatch = true;

View file

@ -70,17 +70,12 @@ public class SyncHelper {
requestSyncData(SyncMessage.Request.Type.BLOCKED); requestSyncData(SyncMessage.Request.Type.BLOCKED);
requestSyncData(SyncMessage.Request.Type.CONFIGURATION); requestSyncData(SyncMessage.Request.Type.CONFIGURATION);
requestSyncKeys(); requestSyncKeys();
requestSyncPniIdentity();
} }
public void requestSyncKeys() { public void requestSyncKeys() {
requestSyncData(SyncMessage.Request.Type.KEYS); requestSyncData(SyncMessage.Request.Type.KEYS);
} }
public void requestSyncPniIdentity() {
requestSyncData(SyncMessage.Request.Type.PNI_IDENTITY);
}
public SendMessageResult sendSyncFetchProfileMessage() { public SendMessageResult sendSyncFetchProfileMessage() {
return context.getSendHelper() return context.getSendHelper()
.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)); .sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
@ -165,7 +160,7 @@ public class SyncHelper {
final var contact = contactPair.second(); final var contact = contactPair.second();
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId); final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
final var deviceContact = getDeviceContact(address, recipientId, contact); final var deviceContact = getDeviceContact(address, contact);
out.write(deviceContact); out.write(deviceContact);
deviceContact.getAvatar().ifPresent(a -> { deviceContact.getAvatar().ifPresent(a -> {
try { try {
@ -180,7 +175,7 @@ public class SyncHelper {
final var address = account.getSelfRecipientAddress(); final var address = account.getSelfRecipientAddress();
final var recipientId = account.getSelfRecipientId(); final var recipientId = account.getSelfRecipientId();
final var contact = account.getContactStore().getContact(recipientId); final var contact = account.getContactStore().getContact(recipientId);
final var deviceContact = getDeviceContact(address, recipientId, contact); final var deviceContact = getDeviceContact(address, contact);
out.write(deviceContact); out.write(deviceContact);
deviceContact.getAvatar().ifPresent(a -> { deviceContact.getAvatar().ifPresent(a -> {
try { try {
@ -216,34 +211,14 @@ public class SyncHelper {
} }
@NotNull @NotNull
private DeviceContact getDeviceContact( private DeviceContact getDeviceContact(final RecipientAddress address, final Contact contact) throws IOException {
final RecipientAddress address,
final RecipientId recipientId,
final Contact contact
) throws IOException {
var currentIdentity = address.serviceId().isEmpty()
? null
: account.getIdentityKeyStore().getIdentityInfo(address.serviceId().get());
VerifiedMessage verifiedMessage = null;
if (currentIdentity != null) {
verifiedMessage = new VerifiedMessage(address.toSignalServiceAddress(),
currentIdentity.getIdentityKey(),
currentIdentity.getTrustLevel().toVerifiedState(),
currentIdentity.getDateAddedTimestamp());
}
var profileKey = account.getProfileStore().getProfileKey(recipientId);
return new DeviceContact(address.aci(), return new DeviceContact(address.aci(),
address.number(), address.number(),
Optional.ofNullable(contact == null ? null : contact.getName()), Optional.ofNullable(contact == null ? null : contact.getName()),
createContactAvatarAttachment(address), createContactAvatarAttachment(address),
Optional.ofNullable(contact == null ? null : contact.color()),
Optional.ofNullable(verifiedMessage),
Optional.ofNullable(profileKey),
Optional.ofNullable(contact == null ? null : contact.messageExpirationTime()), Optional.ofNullable(contact == null ? null : contact.messageExpirationTime()),
Optional.ofNullable(contact == null ? null : contact.messageExpirationTimeVersion()), Optional.ofNullable(contact == null ? null : contact.messageExpirationTimeVersion()),
Optional.empty(), Optional.empty());
contact != null && contact.isArchived());
} }
public SendMessageResult sendBlockedList() { public SendMessageResult sendBlockedList() {
@ -376,9 +351,6 @@ public class SyncHelper {
break; break;
} }
final var address = new RecipientAddress(c.getAci(), Optional.empty(), c.getE164(), Optional.empty()); final var address = new RecipientAddress(c.getAci(), Optional.empty(), c.getE164(), Optional.empty());
if (address.matches(account.getSelfRecipientAddress()) && c.getProfileKey().isPresent()) {
account.setProfileKey(c.getProfileKey().get());
}
final var recipientId = account.getRecipientTrustedResolver().resolveRecipientTrusted(address); final var recipientId = account.getRecipientTrustedResolver().resolveRecipientTrusted(address);
var contact = account.getContactStore().getContact(recipientId); var contact = account.getContactStore().getContact(recipientId);
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
@ -390,19 +362,6 @@ public class SyncHelper {
builder.withGivenName(c.getName().get()); builder.withGivenName(c.getName().get());
builder.withFamilyName(null); builder.withFamilyName(null);
} }
if (c.getColor().isPresent()) {
builder.withColor(c.getColor().get());
}
if (c.getProfileKey().isPresent()) {
account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get());
}
if (c.getVerified().isPresent()) {
final var verifiedMessage = c.getVerified().get();
account.getIdentityKeyStore()
.setIdentityTrustLevel(verifiedMessage.getDestination().getServiceId(),
verifiedMessage.getIdentityKey(),
TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
}
if (c.getExpirationTimer().isPresent()) { if (c.getExpirationTimer().isPresent()) {
if (c.getExpirationTimerVersion().isPresent() && ( if (c.getExpirationTimerVersion().isPresent() && (
contact == null || c.getExpirationTimerVersion().get() > contact.messageExpirationTimeVersion() contact == null || c.getExpirationTimerVersion().get() > contact.messageExpirationTimeVersion()
@ -417,7 +376,6 @@ public class SyncHelper {
contact == null ? 1 : contact.messageExpirationTimeVersion()); contact == null ? 1 : contact.messageExpirationTimeVersion());
} }
} }
builder.withIsArchived(c.isArchived());
account.getContactStore().storeContact(recipientId, builder.build()); account.getContactStore().storeContact(recipientId, builder.build());
if (c.getAvatar().isPresent()) { if (c.getAvatar().isPresent()) {

View file

@ -18,6 +18,8 @@ import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class UnidentifiedAccessHelper { public class UnidentifiedAccessHelper {
private static final Logger logger = LoggerFactory.getLogger(UnidentifiedAccessHelper.class); private static final Logger logger = LoggerFactory.getLogger(UnidentifiedAccessHelper.class);
@ -109,7 +111,8 @@ public class UnidentifiedAccessHelper {
return privacySenderCertificate.getSerialized(); return privacySenderCertificate.getSerialized();
} }
try { try {
final var certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy(); final var certificate = handleResponseException(dependencies.getCertificateApi()
.getSenderCertificateForPhoneNumberPrivacy());
privacySenderCertificate = new SenderCertificate(certificate); privacySenderCertificate = new SenderCertificate(certificate);
return certificate; return certificate;
} catch (IOException | InvalidCertificateException e) { } catch (IOException | InvalidCertificateException e) {
@ -125,7 +128,7 @@ public class UnidentifiedAccessHelper {
return senderCertificate.getSerialized(); return senderCertificate.getSerialized();
} }
try { try {
final var certificate = dependencies.getAccountManager().getSenderCertificate(); final var certificate = handleResponseException(dependencies.getCertificateApi().getSenderCertificate());
this.senderCertificate = new SenderCertificate(certificate); this.senderCertificate = new SenderCertificate(certificate);
return certificate; return certificate;
} catch (IOException | InvalidCertificateException e) { } catch (IOException | InvalidCertificateException e) {

View file

@ -35,6 +35,7 @@ import org.asamk.signal.manager.api.IdentityVerificationCode;
import org.asamk.signal.manager.api.InactiveGroupLinkException; import org.asamk.signal.manager.api.InactiveGroupLinkException;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.InvalidDeviceLinkException; import org.asamk.signal.manager.api.InvalidDeviceLinkException;
import org.asamk.signal.manager.api.InvalidNumberException;
import org.asamk.signal.manager.api.InvalidStickerException; import org.asamk.signal.manager.api.InvalidStickerException;
import org.asamk.signal.manager.api.InvalidUsernameException; import org.asamk.signal.manager.api.InvalidUsernameException;
import org.asamk.signal.manager.api.LastGroupAdminException; import org.asamk.signal.manager.api.LastGroupAdminException;
@ -47,6 +48,7 @@ import org.asamk.signal.manager.api.NotPrimaryDeviceException;
import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.PendingAdminApprovalException; import org.asamk.signal.manager.api.PendingAdminApprovalException;
import org.asamk.signal.manager.api.PhoneNumberSharingMode; import org.asamk.signal.manager.api.PhoneNumberSharingMode;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.Profile; import org.asamk.signal.manager.api.Profile;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
@ -87,12 +89,12 @@ import org.asamk.signal.manager.storage.stickers.StickerPack;
import org.asamk.signal.manager.util.AttachmentUtils; import org.asamk.signal.manager.util.AttachmentUtils;
import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.MimeUtils; import org.asamk.signal.manager.util.MimeUtils;
import org.asamk.signal.manager.util.PhoneNumberFormatter;
import org.asamk.signal.manager.util.StickerUtils; import org.asamk.signal.manager.util.StickerUtils;
import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.usernames.BaseUsernameException; import org.signal.libsignal.usernames.BaseUsernameException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServicePreview; import org.whispersystems.signalservice.api.messages.SignalServicePreview;
@ -106,8 +108,6 @@ import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhauste
import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException; import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException; import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.DeviceNameUtil;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.signalservice.internal.util.Util; import org.whispersystems.signalservice.internal.util.Util;
@ -132,7 +132,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -142,6 +142,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
import okio.Utf8; import okio.Utf8;
import static org.asamk.signal.manager.config.ServiceConfig.MAX_MESSAGE_SIZE_BYTES; import static org.asamk.signal.manager.config.ServiceConfig.MAX_MESSAGE_SIZE_BYTES;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
import static org.signal.core.util.StringExtensionsKt.splitByByteLength; import static org.signal.core.util.StringExtensionsKt.splitByByteLength;
public class ManagerImpl implements Manager { public class ManagerImpl implements Manager {
@ -161,6 +162,7 @@ public class ManagerImpl implements Manager {
private final List<Runnable> closedListeners = new ArrayList<>(); private final List<Runnable> closedListeners = new ArrayList<>();
private final List<Runnable> addressChangedListeners = new ArrayList<>(); private final List<Runnable> addressChangedListeners = new ArrayList<>();
private final CompositeDisposable disposable = new CompositeDisposable(); private final CompositeDisposable disposable = new CompositeDisposable();
private final AtomicLong lastMessageTimestamp = new AtomicLong();
public ManagerImpl( public ManagerImpl(
SignalAccount account, SignalAccount account,
@ -171,15 +173,7 @@ public class ManagerImpl implements Manager {
) { ) {
this.account = account; this.account = account;
final var sessionLock = new SignalSessionLock() { final var sessionLock = new ReentrantSignalSessionLock();
private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
@Override
public Lock acquire() {
LEGACY_LOCK.lock();
return LEGACY_LOCK::unlock;
}
};
this.dependencies = new SignalDependencies(serviceEnvironmentConfig, this.dependencies = new SignalDependencies(serviceEnvironmentConfig,
userAgent, userAgent,
account.getCredentialsProvider(), account.getCredentialsProvider(),
@ -291,7 +285,7 @@ public class ManagerImpl implements Manager {
} }
@Override @Override
public Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) { public Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) throws IOException {
final var registeredUsers = new HashMap<String, RecipientAddress>(); final var registeredUsers = new HashMap<String, RecipientAddress>();
for (final var username : usernames) { for (final var username : usernames) {
try { try {
@ -435,7 +429,7 @@ public class ManagerImpl implements Manager {
String newNumber, String newNumber,
String verificationCode, String verificationCode,
String pin String pin
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException { ) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException {
if (!account.isPrimaryDevice()) { if (!account.isPrimaryDevice()) {
throw new NotPrimaryDeviceException(); throw new NotPrimaryDeviceException();
} }
@ -457,10 +451,10 @@ public class ManagerImpl implements Manager {
String challenge, String challenge,
String captcha String captcha
) throws IOException, CaptchaRejectedException { ) throws IOException, CaptchaRejectedException {
captcha = captcha == null ? null : captcha.replace("signalcaptcha://", ""); captcha = captcha == null ? "" : captcha.replace("signalcaptcha://", "");
try { try {
dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha); handleResponseException(dependencies.getRateLimitChallengeApi().submitCaptchaChallenge(challenge, captcha));
} catch (org.whispersystems.signalservice.internal.push.exceptions.CaptchaRejectedException ignored) { } catch (org.whispersystems.signalservice.internal.push.exceptions.CaptchaRejectedException ignored) {
throw new CaptchaRejectedException(); throw new CaptchaRejectedException();
} }
@ -468,7 +462,7 @@ public class ManagerImpl implements Manager {
@Override @Override
public List<Device> getLinkedDevices() throws IOException { public List<Device> getLinkedDevices() throws IOException {
var devices = dependencies.getAccountManager().getDevices(); var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
account.setMultiDevice(devices.size() > 1); account.setMultiDevice(devices.size() > 1);
var identityKey = account.getAciIdentityKeyPair().getPrivateKey(); var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
return devices.stream().map(d -> { return devices.stream().map(d -> {
@ -607,6 +601,24 @@ public class ManagerImpl implements Manager {
return context.getGroupHelper().joinGroup(inviteLinkUrl); return context.getGroupHelper().joinGroup(inviteLinkUrl);
} }
private long getNextMessageTimestamp() {
while (true) {
final var last = lastMessageTimestamp.get();
final var timestamp = System.currentTimeMillis();
if (last == timestamp) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
continue;
}
if (lastMessageTimestamp.compareAndSet(last, timestamp)) {
return timestamp;
}
}
}
private SendMessageResults sendMessage( private SendMessageResults sendMessage(
SignalServiceDataMessage.Builder messageBuilder, SignalServiceDataMessage.Builder messageBuilder,
Set<RecipientIdentifier> recipients, Set<RecipientIdentifier> recipients,
@ -622,7 +634,7 @@ public class ManagerImpl implements Manager {
Optional<Long> editTargetTimestamp Optional<Long> editTargetTimestamp
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>(); var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
long timestamp = System.currentTimeMillis(); long timestamp = getNextMessageTimestamp();
messageBuilder.withTimestamp(timestamp); messageBuilder.withTimestamp(timestamp);
for (final var recipient : recipients) { for (final var recipient : recipients) {
if (recipient instanceof RecipientIdentifier.NoteToSelf || ( if (recipient instanceof RecipientIdentifier.NoteToSelf || (
@ -662,7 +674,7 @@ public class ManagerImpl implements Manager {
Set<RecipientIdentifier> recipients Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>(); var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
final var timestamp = System.currentTimeMillis(); final var timestamp = getNextMessageTimestamp();
for (var recipient : recipients) { for (var recipient : recipients) {
if (recipient instanceof RecipientIdentifier.Single single) { if (recipient instanceof RecipientIdentifier.Single single) {
final var message = new SignalServiceTypingMessage(action, timestamp, Optional.empty()); final var message = new SignalServiceTypingMessage(action, timestamp, Optional.empty());
@ -694,7 +706,7 @@ public class ManagerImpl implements Manager {
@Override @Override
public SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) { public SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) {
final var timestamp = System.currentTimeMillis(); final var timestamp = getNextMessageTimestamp();
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ,
messageIds, messageIds,
timestamp); timestamp);
@ -704,7 +716,7 @@ public class ManagerImpl implements Manager {
@Override @Override
public SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) { public SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) {
final var timestamp = System.currentTimeMillis(); final var timestamp = getNextMessageTimestamp();
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED,
messageIds, messageIds,
timestamp); timestamp);
@ -798,6 +810,7 @@ public class ManagerImpl implements Manager {
} else if (!additionalAttachments.isEmpty()) { } else if (!additionalAttachments.isEmpty()) {
messageBuilder.withAttachments(additionalAttachments); messageBuilder.withAttachments(additionalAttachments);
} }
messageBuilder.withViewOnce(message.viewOnce());
if (!message.mentions().isEmpty()) { if (!message.mentions().isEmpty()) {
messageBuilder.withMentions(resolveMentions(message.mentions())); messageBuilder.withMentions(resolveMentions(message.mentions()));
} }
@ -1049,15 +1062,23 @@ public class ManagerImpl implements Manager {
@Override @Override
public void setContactName( public void setContactName(
RecipientIdentifier.Single recipient, final RecipientIdentifier.Single recipient,
String givenName, final String givenName,
final String familyName final String familyName,
final String nickGivenName,
final String nickFamilyName,
final String note
) throws NotPrimaryDeviceException, UnregisteredRecipientException { ) throws NotPrimaryDeviceException, UnregisteredRecipientException {
if (!account.isPrimaryDevice()) { if (!account.isPrimaryDevice()) {
throw new NotPrimaryDeviceException(); throw new NotPrimaryDeviceException();
} }
context.getContactHelper() context.getContactHelper()
.setContactName(context.getRecipientHelper().resolveRecipient(recipient), givenName, familyName); .setContactName(context.getRecipientHelper().resolveRecipient(recipient),
givenName,
familyName,
nickGivenName,
nickFamilyName,
note);
syncRemoteStorage(); syncRemoteStorage();
} }
@ -1586,7 +1607,8 @@ public class ManagerImpl implements Manager {
context.close(); context.close();
executor.close(); executor.close();
dependencies.getSignalWebSocket().disconnect(); dependencies.getAuthenticatedSignalWebSocket().disconnect();
dependencies.getUnauthenticatedSignalWebSocket().disconnect();
dependencies.getPushServiceSocket().close(); dependencies.getPushServiceSocket().close();
disposable.dispose(); disposable.dispose();

View file

@ -29,12 +29,10 @@ import org.asamk.signal.manager.util.KeyUtils;
import org.signal.libsignal.protocol.IdentityKeyPair; import org.signal.libsignal.protocol.IdentityKeyPair;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.registration.ProvisioningApi;
import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.DeviceNameUtil;
import org.whispersystems.signalservice.internal.push.ProvisioningSocket; import org.whispersystems.signalservice.internal.push.ProvisioningSocket;
import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.push.PushServiceSocket;
@ -58,7 +56,7 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
private final Consumer<Manager> newManagerListener; private final Consumer<Manager> newManagerListener;
private final AccountsStore accountsStore; private final AccountsStore accountsStore;
private final SignalServiceAccountManager accountManager; private final ProvisioningApi provisioningApi;
private final IdentityKeyPair tempIdentityKey; private final IdentityKeyPair tempIdentityKey;
private final String password; private final String password;
@ -77,8 +75,6 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
tempIdentityKey = KeyUtils.generateIdentityKeyPair(); tempIdentityKey = KeyUtils.generateIdentityKeyPair();
password = KeyUtils.createPassword(); password = KeyUtils.createPassword();
final var clientZkOperations = ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration());
final var groupsV2Operations = new GroupsV2Operations(clientZkOperations, ServiceConfig.GROUP_MAX_SIZE);
final var credentialsProvider = new DynamicCredentialsProvider(null, final var credentialsProvider = new DynamicCredentialsProvider(null,
null, null,
null, null,
@ -87,23 +83,22 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
final var pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(), final var pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
credentialsProvider, credentialsProvider,
userAgent, userAgent,
clientZkOperations.getProfileOperations(),
ServiceConfig.AUTOMATIC_NETWORK_RETRY); ServiceConfig.AUTOMATIC_NETWORK_RETRY);
accountManager = new SignalServiceAccountManager(pushServiceSocket, final var provisioningSocket = new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(), userAgent), userAgent);
groupsV2Operations); this.provisioningApi = new ProvisioningApi(pushServiceSocket, provisioningSocket, credentialsProvider);
} }
@Override @Override
public URI getDeviceLinkUri() throws TimeoutException, IOException { public URI getDeviceLinkUri() throws TimeoutException, IOException {
var deviceUuid = accountManager.getNewDeviceUuid(); var deviceUuid = provisioningApi.getNewDeviceUuid();
return new DeviceLinkUrl(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri(); return new DeviceLinkUrl(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
} }
@Override @Override
public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException { public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException {
var ret = accountManager.getNewDeviceRegistration(tempIdentityKey); var ret = provisioningApi.getNewDeviceRegistration(tempIdentityKey);
var number = ret.getNumber(); var number = ret.getNumber();
var aci = ret.getAci(); var aci = ret.getAci();
var pni = ret.getPni(); var pni = ret.getPni();
@ -160,7 +155,7 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI)); final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
logger.debug("Finishing new device registration"); logger.debug("Finishing new device registration");
var deviceId = accountManager.finishNewDeviceRegistration(ret.getProvisioningCode(), var deviceId = provisioningApi.finishNewDeviceRegistration(ret.getProvisioningCode(),
account.getAccountAttributes(null), account.getAccountAttributes(null),
aciPreKeys, aciPreKeys,
pniPreKeys); pniPreKeys);

View file

@ -0,0 +1,16 @@
package org.asamk.signal.manager.internal;
import org.whispersystems.signalservice.api.SignalSessionLock;
import java.util.concurrent.locks.ReentrantLock;
class ReentrantSignalSessionLock implements SignalSessionLock {
private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
@Override
public Lock acquire() {
LEGACY_LOCK.lock();
return LEGACY_LOCK::unlock;
}
}

View file

@ -21,6 +21,7 @@ import org.asamk.signal.manager.RegistrationManager;
import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.UpdateProfile; import org.asamk.signal.manager.api.UpdateProfile;
@ -32,14 +33,11 @@ import org.asamk.signal.manager.helper.PinHelper;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.NumberVerificationUtils; import org.asamk.signal.manager.util.NumberVerificationUtils;
import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.usernames.BaseUsernameException; import org.signal.libsignal.usernames.BaseUsernameException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.account.PreKeyCollection; import org.whispersystems.signalservice.api.account.PreKeyCollection;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.PNI; import org.whispersystems.signalservice.api.push.ServiceId.PNI;
@ -48,13 +46,13 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException; import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
import org.whispersystems.signalservice.api.svr.SecureValueRecovery; import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import java.io.IOException; import java.io.IOException;
import java.util.function.Consumer; import java.util.function.Consumer;
import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType; import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class RegistrationManagerImpl implements RegistrationManager { public class RegistrationManagerImpl implements RegistrationManager {
@ -132,12 +130,15 @@ public class RegistrationManagerImpl implements RegistrationManager {
} }
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi(); final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
logger.trace("Creating verification session");
String sessionId = NumberVerificationUtils.handleVerificationSession(registrationApi, String sessionId = NumberVerificationUtils.handleVerificationSession(registrationApi,
account.getSessionId(account.getNumber()), account.getSessionId(account.getNumber()),
id -> account.setSessionId(account.getNumber(), id), id -> account.setSessionId(account.getNumber(), id),
voiceVerification, voiceVerification,
captcha); captcha);
logger.trace("Requesting verification code");
NumberVerificationUtils.requestVerificationCode(registrationApi, sessionId, voiceVerification); NumberVerificationUtils.requestVerificationCode(registrationApi, sessionId, voiceVerification);
logger.debug("Successfully requested verification code");
account.setRegistered(false); account.setRegistered(false);
} catch (DeprecatedVersionException e) { } catch (DeprecatedVersionException e) {
logger.debug("Signal-Server returned deprecated version exception", e); logger.debug("Signal-Server returned deprecated version exception", e);
@ -149,7 +150,7 @@ public class RegistrationManagerImpl implements RegistrationManager {
public void verifyAccount( public void verifyAccount(
String verificationCode, String verificationCode,
String pin String pin
) throws IOException, PinLockedException, IncorrectPinException { ) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException {
if (account.isRegistered()) { if (account.isRegistered()) {
throw new IOException("Account is already registered"); throw new IOException("Account is already registered");
} }
@ -199,7 +200,7 @@ public class RegistrationManagerImpl implements RegistrationManager {
final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI)); final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI));
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI)); final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi(); final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
final var response = Utils.handleResponseException(registrationApi.registerAccount(null, final var response = handleResponseException(registrationApi.registerAccount(null,
recoveryPassword, recoveryPassword,
account.getAccountAttributes(null), account.getAccountAttributes(null),
aciPreKeys, aciPreKeys,
@ -221,8 +222,14 @@ public class RegistrationManagerImpl implements RegistrationManager {
private boolean attemptReactivateAccount() { private boolean attemptReactivateAccount() {
try { try {
final var accountManager = createAuthenticatedSignalServiceAccountManager(); final var dependencies = new SignalDependencies(serviceEnvironmentConfig,
accountManager.setAccountAttributes(account.getAccountAttributes(null)); userAgent,
account.getCredentialsProvider(),
account.getSignalServiceDataStore(),
null,
new ReentrantSignalSessionLock());
handleResponseException(dependencies.getAccountApi()
.setAccountAttributes(account.getAccountAttributes(null)));
account.setRegistered(true); account.setRegistered(true);
logger.info("Reactivated existing account, verify is not necessary."); logger.info("Reactivated existing account, verify is not necessary.");
if (newManagerListener != null) { if (newManagerListener != null) {
@ -241,17 +248,6 @@ public class RegistrationManagerImpl implements RegistrationManager {
return false; return false;
} }
private SignalServiceAccountManager createAuthenticatedSignalServiceAccountManager() {
final var clientZkOperations = ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration());
final var pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
account.getCredentialsProvider(),
userAgent,
clientZkOperations.getProfileOperations(),
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
final var groupsV2Operations = new GroupsV2Operations(clientZkOperations, ServiceConfig.GROUP_MAX_SIZE);
return new SignalServiceAccountManager(pushServiceSocket, null, groupsV2Operations);
}
private VerifyAccountResponse verifyAccountWithCode( private VerifyAccountResponse verifyAccountWithCode(
final String sessionId, final String sessionId,
final String verificationCode, final String verificationCode,
@ -261,11 +257,11 @@ public class RegistrationManagerImpl implements RegistrationManager {
) throws IOException { ) throws IOException {
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi(); final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
try { try {
Utils.handleResponseException(registrationApi.verifyAccount(sessionId, verificationCode)); handleResponseException(registrationApi.verifyAccount(sessionId, verificationCode));
} catch (AlreadyVerifiedException e) { } catch (AlreadyVerifiedException e) {
// Already verified so can continue registering // Already verified so can continue registering
} }
return Utils.handleResponseException(registrationApi.registerAccount(sessionId, return handleResponseException(registrationApi.registerAccount(sessionId,
null, null,
account.getAccountAttributes(registrationLock), account.getAccountAttributes(registrationLock),
aciPreKeys, aciPreKeys,

View file

@ -2,42 +2,58 @@ package org.asamk.signal.manager.internal;
import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig; 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.metadata.certificate.CertificateValidator;
import org.signal.libsignal.net.Network; import org.signal.libsignal.net.Network;
import org.signal.libsignal.protocol.UsePqRatchet;
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceDataStore; import org.whispersystems.signalservice.api.SignalServiceDataStore;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.SignalWebSocket; import org.whispersystems.signalservice.api.account.AccountApi;
import org.whispersystems.signalservice.api.attachment.AttachmentApi;
import org.whispersystems.signalservice.api.cds.CdsApi;
import org.whispersystems.signalservice.api.certificate.CertificateApi;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.keys.KeysApi;
import org.whispersystems.signalservice.api.link.LinkDeviceApi; import org.whispersystems.signalservice.api.link.LinkDeviceApi;
import org.whispersystems.signalservice.api.message.MessageApi;
import org.whispersystems.signalservice.api.profiles.ProfileApi;
import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi;
import org.whispersystems.signalservice.api.registration.RegistrationApi; import org.whispersystems.signalservice.api.registration.RegistrationApi;
import org.whispersystems.signalservice.api.services.ProfileService; import org.whispersystems.signalservice.api.services.ProfileService;
import org.whispersystems.signalservice.api.storage.StorageServiceApi; import org.whispersystems.signalservice.api.storage.StorageServiceApi;
import org.whispersystems.signalservice.api.storage.StorageServiceRepository; import org.whispersystems.signalservice.api.storage.StorageServiceRepository;
import org.whispersystems.signalservice.api.svr.SecureValueRecovery; import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
import org.whispersystems.signalservice.api.username.UsernameApi;
import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
import org.whispersystems.signalservice.api.websocket.WebSocketFactory; import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
import org.whispersystems.signalservice.internal.push.ProvisioningSocket;
import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection; import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection;
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier; import java.util.function.Supplier;
public class SignalDependencies { public class SignalDependencies {
private static final Logger logger = LoggerFactory.getLogger(SignalDependencies.class);
private final Object LOCK = new Object(); private final Object LOCK = new Object();
private final ServiceEnvironmentConfig serviceEnvironmentConfig; private final ServiceEnvironmentConfig serviceEnvironmentConfig;
@ -50,22 +66,31 @@ public class SignalDependencies {
private boolean allowStories = true; private boolean allowStories = true;
private SignalServiceAccountManager accountManager; private SignalServiceAccountManager accountManager;
private AccountApi accountApi;
private RateLimitChallengeApi rateLimitChallengeApi;
private CdsApi cdsApi;
private UsernameApi usernameApi;
private GroupsV2Api groupsV2Api; private GroupsV2Api groupsV2Api;
private RegistrationApi registrationApi; private RegistrationApi registrationApi;
private LinkDeviceApi linkDeviceApi; private LinkDeviceApi linkDeviceApi;
private StorageServiceApi storageServiceApi; private StorageServiceApi storageServiceApi;
private CertificateApi certificateApi;
private AttachmentApi attachmentApi;
private MessageApi messageApi;
private KeysApi keysApi;
private GroupsV2Operations groupsV2Operations; private GroupsV2Operations groupsV2Operations;
private ClientZkOperations clientZkOperations; private ClientZkOperations clientZkOperations;
private PushServiceSocket pushServiceSocket; private PushServiceSocket pushServiceSocket;
private ProvisioningSocket provisioningSocket;
private Network libSignalNetwork; private Network libSignalNetwork;
private SignalWebSocket signalWebSocket; private SignalWebSocket.AuthenticatedWebSocket authenticatedSignalWebSocket;
private SignalWebSocket.UnauthenticatedWebSocket unauthenticatedSignalWebSocket;
private SignalServiceMessageReceiver messageReceiver; private SignalServiceMessageReceiver messageReceiver;
private SignalServiceMessageSender messageSender; private SignalServiceMessageSender messageSender;
private List<SecureValueRecovery> secureValueRecovery; private List<SecureValueRecovery> secureValueRecovery;
private ProfileService profileService; private ProfileService profileService;
private ProfileApi profileApi;
SignalDependencies( SignalDependencies(
final ServiceEnvironmentConfig serviceEnvironmentConfig, final ServiceEnvironmentConfig serviceEnvironmentConfig,
@ -95,7 +120,12 @@ public class SignalDependencies {
this.registrationApi = null; this.registrationApi = null;
this.secureValueRecovery = null; this.secureValueRecovery = null;
} }
getSignalWebSocket().forceNewWebSockets(); if (this.authenticatedSignalWebSocket != null) {
this.authenticatedSignalWebSocket.forceNewWebSocket();
}
if (this.unauthenticatedSignalWebSocket != null) {
this.unauthenticatedSignalWebSocket.forceNewWebSocket();
}
} }
/** /**
@ -118,25 +148,45 @@ public class SignalDependencies {
() -> pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(), () -> pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
credentialsProvider, credentialsProvider,
userAgent, userAgent,
getClientZkProfileOperations(),
ServiceConfig.AUTOMATIC_NETWORK_RETRY)); ServiceConfig.AUTOMATIC_NETWORK_RETRY));
} }
public ProvisioningSocket getProvisioningSocket() { public Network getLibSignalNetwork() {
return getOrCreate(() -> provisioningSocket, return getOrCreate(() -> libSignalNetwork, () -> {
() -> provisioningSocket = new ProvisioningSocket(getServiceEnvironmentConfig().signalServiceConfiguration(), libSignalNetwork = new Network(serviceEnvironmentConfig.netEnvironment(), userAgent);
userAgent)); setSignalNetworkProxy(libSignalNetwork);
});
} }
public Network getLibSignalNetwork() { private void setSignalNetworkProxy(Network libSignalNetwork) {
return getOrCreate(() -> libSignalNetwork, final var proxy = Utils.getHttpsProxy();
() -> libSignalNetwork = new Network(serviceEnvironmentConfig.netEnvironment(), userAgent)); if (proxy.address() instanceof InetSocketAddress addr) {
switch (proxy.type()) {
case Proxy.Type.DIRECT -> {
}
case Proxy.Type.HTTP -> {
try {
libSignalNetwork.setProxy("http", addr.getHostName(), addr.getPort(), null, null);
} catch (IOException e) {
logger.warn("Failed to set http proxy", e);
}
}
case Proxy.Type.SOCKS -> {
try {
libSignalNetwork.setProxy("socks", addr.getHostName(), addr.getPort(), null, null);
} catch (IOException e) {
logger.warn("Failed to set socks proxy", e);
}
}
}
}
} }
public SignalServiceAccountManager getAccountManager() { public SignalServiceAccountManager getAccountManager() {
return getOrCreate(() -> accountManager, return getOrCreate(() -> accountManager,
() -> accountManager = new SignalServiceAccountManager(getPushServiceSocket(), () -> accountManager = new SignalServiceAccountManager(getAuthenticatedSignalWebSocket(),
getProvisioningSocket(), getAccountApi(),
getPushServiceSocket(),
getGroupsV2Operations())); getGroupsV2Operations()));
} }
@ -152,6 +202,23 @@ public class SignalDependencies {
ServiceConfig.GROUP_MAX_SIZE); ServiceConfig.GROUP_MAX_SIZE);
} }
public AccountApi getAccountApi() {
return getOrCreate(() -> accountApi, () -> accountApi = new AccountApi(getAuthenticatedSignalWebSocket()));
}
public RateLimitChallengeApi getRateLimitChallengeApi() {
return getOrCreate(() -> rateLimitChallengeApi,
() -> rateLimitChallengeApi = new RateLimitChallengeApi(getAuthenticatedSignalWebSocket()));
}
public CdsApi getCdsApi() {
return getOrCreate(() -> cdsApi, () -> cdsApi = new CdsApi(getAuthenticatedSignalWebSocket()));
}
public UsernameApi getUsernameApi() {
return getOrCreate(() -> usernameApi, () -> usernameApi = new UsernameApi(getUnauthenticatedSignalWebSocket()));
}
public GroupsV2Api getGroupsV2Api() { public GroupsV2Api getGroupsV2Api() {
return getOrCreate(() -> groupsV2Api, () -> groupsV2Api = getAccountManager().getGroupsV2Api()); return getOrCreate(() -> groupsV2Api, () -> groupsV2Api = getAccountManager().getGroupsV2Api());
} }
@ -161,18 +228,41 @@ public class SignalDependencies {
} }
public LinkDeviceApi getLinkDeviceApi() { public LinkDeviceApi getLinkDeviceApi() {
return getOrCreate(() -> linkDeviceApi, () -> linkDeviceApi = new LinkDeviceApi(getPushServiceSocket())); return getOrCreate(() -> linkDeviceApi,
() -> linkDeviceApi = new LinkDeviceApi(getAuthenticatedSignalWebSocket()));
} }
private StorageServiceApi getStorageServiceApi() { private StorageServiceApi getStorageServiceApi() {
return getOrCreate(() -> storageServiceApi, return getOrCreate(() -> storageServiceApi,
() -> storageServiceApi = new StorageServiceApi(getPushServiceSocket())); () -> storageServiceApi = new StorageServiceApi(getAuthenticatedSignalWebSocket(),
getPushServiceSocket()));
} }
public StorageServiceRepository getStorageServiceRepository() { public StorageServiceRepository getStorageServiceRepository() {
return new StorageServiceRepository(getStorageServiceApi()); return new StorageServiceRepository(getStorageServiceApi());
} }
public CertificateApi getCertificateApi() {
return getOrCreate(() -> certificateApi,
() -> certificateApi = new CertificateApi(getAuthenticatedSignalWebSocket()));
}
public AttachmentApi getAttachmentApi() {
return getOrCreate(() -> attachmentApi,
() -> attachmentApi = new AttachmentApi(getAuthenticatedSignalWebSocket(), getPushServiceSocket()));
}
public MessageApi getMessageApi() {
return getOrCreate(() -> messageApi,
() -> messageApi = new MessageApi(getAuthenticatedSignalWebSocket(),
getUnauthenticatedSignalWebSocket()));
}
public KeysApi getKeysApi() {
return getOrCreate(() -> keysApi,
() -> keysApi = new KeysApi(getAuthenticatedSignalWebSocket(), getUnauthenticatedSignalWebSocket()));
}
public GroupsV2Operations getGroupsV2Operations() { public GroupsV2Operations getGroupsV2Operations() {
return getOrCreate(() -> groupsV2Operations, return getOrCreate(() -> groupsV2Operations,
() -> groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration()), () -> groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration()),
@ -189,33 +279,35 @@ public class SignalDependencies {
return clientZkOperations.getProfileOperations(); return clientZkOperations.getProfileOperations();
} }
public SignalWebSocket getSignalWebSocket() { public SignalWebSocket.AuthenticatedWebSocket getAuthenticatedSignalWebSocket() {
return getOrCreate(() -> signalWebSocket, () -> { return getOrCreate(() -> authenticatedSignalWebSocket, () -> {
final var timer = new UptimeSleepTimer(); final var timer = new UptimeSleepTimer();
final var healthMonitor = new SignalWebSocketHealthMonitor(timer); final var healthMonitor = new SignalWebSocketHealthMonitor(timer);
final var webSocketFactory = new WebSocketFactory() {
@Override
public WebSocketConnection createWebSocket() {
return new OkHttpWebSocketConnection("normal",
serviceEnvironmentConfig.signalServiceConfiguration(),
Optional.of(credentialsProvider),
userAgent,
healthMonitor,
allowStories);
}
@Override authenticatedSignalWebSocket = new SignalWebSocket.AuthenticatedWebSocket(() -> new OkHttpWebSocketConnection(
public WebSocketConnection createUnidentifiedWebSocket() { "normal",
return new OkHttpWebSocketConnection("unidentified", serviceEnvironmentConfig.signalServiceConfiguration(),
serviceEnvironmentConfig.signalServiceConfiguration(), Optional.of(credentialsProvider),
Optional.empty(), userAgent,
userAgent, healthMonitor,
healthMonitor, allowStories), () -> true, timer, TimeUnit.SECONDS.toMillis(10));
allowStories); healthMonitor.monitor(authenticatedSignalWebSocket);
} });
}; }
signalWebSocket = new SignalWebSocket(webSocketFactory);
healthMonitor.monitor(signalWebSocket); public SignalWebSocket.UnauthenticatedWebSocket getUnauthenticatedSignalWebSocket() {
return getOrCreate(() -> unauthenticatedSignalWebSocket, () -> {
final var timer = new UptimeSleepTimer();
final var healthMonitor = new SignalWebSocketHealthMonitor(timer);
unauthenticatedSignalWebSocket = new SignalWebSocket.UnauthenticatedWebSocket(() -> new OkHttpWebSocketConnection(
"unidentified",
serviceEnvironmentConfig.signalServiceConfiguration(),
Optional.empty(),
userAgent,
healthMonitor,
allowStories), () -> true, timer, TimeUnit.SECONDS.toMillis(10));
healthMonitor.monitor(unauthenticatedSignalWebSocket);
}); });
} }
@ -229,10 +321,14 @@ public class SignalDependencies {
() -> messageSender = new SignalServiceMessageSender(getPushServiceSocket(), () -> messageSender = new SignalServiceMessageSender(getPushServiceSocket(),
dataStore, dataStore,
sessionLock, sessionLock,
getSignalWebSocket(), getAttachmentApi(),
getMessageApi(),
getKeysApi(),
Optional.empty(), Optional.empty(),
executor, executor,
ServiceConfig.MAX_ENVELOPE_SIZE)); ServiceConfig.MAX_ENVELOPE_SIZE,
() -> true,
UsePqRatchet.NO));
} }
public List<SecureValueRecovery> getSecureValueRecovery() { public List<SecureValueRecovery> getSecureValueRecovery() {
@ -243,11 +339,19 @@ public class SignalDependencies {
.toList()); .toList());
} }
public ProfileApi getProfileApi() {
return getOrCreate(() -> profileApi,
() -> profileApi = new ProfileApi(getAuthenticatedSignalWebSocket(),
getUnauthenticatedSignalWebSocket(),
getPushServiceSocket(),
getClientZkProfileOperations()));
}
public ProfileService getProfileService() { public ProfileService getProfileService() {
return getOrCreate(() -> profileService, return getOrCreate(() -> profileService,
() -> profileService = new ProfileService(getClientZkProfileOperations(), () -> profileService = new ProfileService(getClientZkProfileOperations(),
getMessageReceiver(), getAuthenticatedSignalWebSocket(),
getSignalWebSocket())); getUnauthenticatedSignalWebSocket()));
} }
public SignalServiceCipher getCipher(ServiceIdType serviceIdType) { public SignalServiceCipher getCipher(ServiceIdType serviceIdType) {

View file

@ -2,195 +2,157 @@ package org.asamk.signal.manager.internal;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalWebSocket;
import org.whispersystems.signalservice.api.util.Preconditions; import org.whispersystems.signalservice.api.util.Preconditions;
import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.websocket.HealthMonitor; import org.whispersystems.signalservice.api.websocket.HealthMonitor;
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState; import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection; import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection;
import java.util.Arrays; import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import kotlin.Unit;
/**
* Monitors the health of the identified and unidentified WebSockets. If either one appears to be
* unhealthy, will trigger restarting both.
* <p>
* The monitor is also responsible for sending heartbeats/keep-alive messages to prevent
* timeouts.
*/
final class SignalWebSocketHealthMonitor implements HealthMonitor { final class SignalWebSocketHealthMonitor implements HealthMonitor {
private static final Logger logger = LoggerFactory.getLogger(SignalWebSocketHealthMonitor.class); private static final Logger logger = LoggerFactory.getLogger(SignalWebSocketHealthMonitor.class);
/**
* This is the amount of time in between sent keep alives. Must be greater than [KEEP_ALIVE_TIMEOUT]
*/
private static final long KEEP_ALIVE_SEND_CADENCE = TimeUnit.SECONDS.toMillis(OkHttpWebSocketConnection.KEEPALIVE_FREQUENCY_SECONDS); private static final long KEEP_ALIVE_SEND_CADENCE = TimeUnit.SECONDS.toMillis(OkHttpWebSocketConnection.KEEPALIVE_FREQUENCY_SECONDS);
private static final long MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE = KEEP_ALIVE_SEND_CADENCE * 3;
private SignalWebSocket signalWebSocket; /**
* This is the amount of time we will wait for a response to the keep alive before we consider the websockets dead.
* It is required that this value be less than [KEEP_ALIVE_SEND_CADENCE]
*/
private static final long KEEP_ALIVE_TIMEOUT = TimeUnit.SECONDS.toMillis(20);
private final Executor executor = Executors.newSingleThreadExecutor();
private final SleepTimer sleepTimer; private final SleepTimer sleepTimer;
private SignalWebSocket webSocket = null;
private volatile KeepAliveSender keepAliveSender; private volatile KeepAliveSender keepAliveSender = null;
private boolean needsKeepAlive = false;
private final HealthState identified = new HealthState(); private long lastKeepAliveReceived = 0;
private final HealthState unidentified = new HealthState();
public SignalWebSocketHealthMonitor(SleepTimer sleepTimer) { public SignalWebSocketHealthMonitor(SleepTimer sleepTimer) {
this.sleepTimer = sleepTimer; this.sleepTimer = sleepTimer;
} }
public void monitor(SignalWebSocket signalWebSocket) { void monitor(SignalWebSocket webSocket) {
Preconditions.checkNotNull(signalWebSocket); Preconditions.checkNotNull(webSocket);
Preconditions.checkArgument(this.signalWebSocket == null, "monitor can only be called once"); Preconditions.checkArgument(this.webSocket == null, "monitor can only be called once");
this.signalWebSocket = signalWebSocket; executor.execute(() -> {
//noinspection ResultOfMethodCallIgnored this.webSocket = webSocket;
signalWebSocket.getWebSocketState()
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation())
.distinctUntilChanged()
.subscribe(s -> onStateChange(s, identified));
//noinspection ResultOfMethodCallIgnored webSocket.getState()
signalWebSocket.getUnidentifiedWebSocketState() .subscribeOn(Schedulers.computation())
.subscribeOn(Schedulers.computation()) .observeOn(Schedulers.computation())
.observeOn(Schedulers.computation()) .distinctUntilChanged()
.distinctUntilChanged() .subscribe(this::onStateChanged);
.subscribe(s -> onStateChange(s, unidentified));
webSocket.addKeepAliveChangeListener(() -> {
executor.execute(this::updateKeepAliveSenderStatus);
return Unit.INSTANCE;
});
});
} }
private synchronized void onStateChange(WebSocketConnectionState connectionState, HealthState healthState) { private void onStateChanged(WebSocketConnectionState connectionState) {
switch (connectionState) { executor.execute(() -> {
case CONNECTED -> logger.debug("WebSocket is now connected"); needsKeepAlive = connectionState == WebSocketConnectionState.CONNECTED;
case AUTHENTICATION_FAILED -> logger.debug("WebSocket authentication failed");
case FAILED -> logger.debug("WebSocket connection failed");
}
healthState.needsKeepAlive = connectionState == WebSocketConnectionState.CONNECTED; updateKeepAliveSenderStatus();
});
}
if (keepAliveSender == null && isKeepAliveNecessary()) { @Override
public void onKeepAliveResponse(long sentTimestamp, boolean isIdentifiedWebSocket) {
final var keepAliveTime = System.currentTimeMillis();
executor.execute(() -> lastKeepAliveReceived = keepAliveTime);
}
@Override
public void onMessageError(int status, boolean isIdentifiedWebSocket) {
}
private void updateKeepAliveSenderStatus() {
if (keepAliveSender == null && sendKeepAlives()) {
keepAliveSender = new KeepAliveSender(); keepAliveSender = new KeepAliveSender();
keepAliveSender.start(); keepAliveSender.start();
} else if (keepAliveSender != null && !isKeepAliveNecessary()) { } else if (keepAliveSender != null && !sendKeepAlives()) {
keepAliveSender.shutdown(); keepAliveSender.shutdown();
keepAliveSender = null; keepAliveSender = null;
} }
} }
@Override private boolean sendKeepAlives() {
public void onKeepAliveResponse(long sentTimestamp, boolean isIdentifiedWebSocket) { return needsKeepAlive && webSocket != null && webSocket.shouldSendKeepAlives();
if (isIdentifiedWebSocket) {
identified.lastKeepAliveReceived = System.currentTimeMillis();
} else {
unidentified.lastKeepAliveReceived = System.currentTimeMillis();
}
}
@Override
public void onMessageError(int status, boolean isIdentifiedWebSocket) {
if (status == 409) {
HealthState healthState = (isIdentifiedWebSocket ? identified : unidentified);
if (healthState.mismatchErrorTracker.addSample(System.currentTimeMillis())) {
logger.warn("Received too many mismatch device errors, forcing new websockets.");
signalWebSocket.forceNewWebSockets();
signalWebSocket.connect();
}
}
}
private boolean isKeepAliveNecessary() {
return identified.needsKeepAlive || unidentified.needsKeepAlive;
}
private static class HealthState {
private final HttpErrorTracker mismatchErrorTracker = new HttpErrorTracker(5, TimeUnit.MINUTES.toMillis(1));
private volatile boolean needsKeepAlive;
private volatile long lastKeepAliveReceived;
} }
/** /**
* Sends periodic heartbeats/keep-alives over both WebSockets to prevent connection timeouts. If * Sends periodic heartbeats/keep-alives over the WebSocket to prevent connection timeouts. If
* either WebSocket fails 3 times to get a return heartbeat both are forced to be recreated. * the WebSocket fails to get a return heartbeat after [KEEP_ALIVE_TIMEOUT] seconds, it is forced to be recreated.
*/ */
private class KeepAliveSender extends Thread { private final class KeepAliveSender extends Thread {
private volatile boolean shouldKeepRunning = true; private volatile boolean shouldKeepRunning = true;
@Override
public void run() { public void run() {
identified.lastKeepAliveReceived = System.currentTimeMillis(); logger.debug("[KeepAliveSender({})] started", this.threadId());
unidentified.lastKeepAliveReceived = System.currentTimeMillis(); lastKeepAliveReceived = System.currentTimeMillis();
while (shouldKeepRunning && isKeepAliveNecessary()) { var keepAliveSendTime = System.currentTimeMillis();
while (shouldKeepRunning && sendKeepAlives()) {
try { try {
sleepTimer.sleep(KEEP_ALIVE_SEND_CADENCE); final var nextKeepAliveSendTime = keepAliveSendTime + KEEP_ALIVE_SEND_CADENCE;
sleepUntil(nextKeepAliveSendTime);
if (shouldKeepRunning && isKeepAliveNecessary()) { if (shouldKeepRunning && sendKeepAlives()) {
long keepAliveRequiredSinceTime = System.currentTimeMillis() keepAliveSendTime = System.currentTimeMillis();
- MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE; webSocket.sendKeepAlive();
}
if (identified.lastKeepAliveReceived < keepAliveRequiredSinceTime final var responseRequiredTime = keepAliveSendTime + KEEP_ALIVE_TIMEOUT;
|| unidentified.lastKeepAliveReceived < keepAliveRequiredSinceTime) { sleepUntil(responseRequiredTime);
logger.warn("Missed keep alives, identified last: "
+ identified.lastKeepAliveReceived if (shouldKeepRunning && sendKeepAlives()) {
+ " unidentified last: " if (lastKeepAliveReceived < keepAliveSendTime) {
+ unidentified.lastKeepAliveReceived logger.debug("Missed keep alive, last: {} needed by: {}",
+ " needed by: " lastKeepAliveReceived,
+ keepAliveRequiredSinceTime); responseRequiredTime);
signalWebSocket.forceNewWebSockets(); webSocket.forceNewWebSocket();
signalWebSocket.connect();
} else {
signalWebSocket.sendKeepAlive();
} }
} }
} catch (Throwable e) { } catch (Throwable e) {
logger.warn("Error occurred in KeepAliveSender, ignoring ...", e); logger.warn("Keep alive sender failed", e);
}
}
logger.debug("[KeepAliveSender({})] ended", threadId());
}
void sleepUntil(long timeMillis) {
while (System.currentTimeMillis() < timeMillis) {
final var waitTime = timeMillis - System.currentTimeMillis();
if (waitTime > 0) {
try {
sleepTimer.sleep(waitTime);
} catch (InterruptedException e) {
logger.warn("WebSocket health monitor interrupted", e);
}
} }
} }
} }
public void shutdown() { void shutdown() {
shouldKeepRunning = false; shouldKeepRunning = false;
} }
} }
private static final class HttpErrorTracker {
private final long[] timestamps;
private final long errorTimeRange;
public HttpErrorTracker(int samples, long errorTimeRange) {
this.timestamps = new long[samples];
this.errorTimeRange = errorTimeRange;
}
public synchronized boolean addSample(long now) {
long errorsMustBeAfter = now - errorTimeRange;
int count = 1;
int minIndex = 0;
for (int i = 0; i < timestamps.length; i++) {
if (timestamps[i] < errorsMustBeAfter) {
timestamps[i] = 0;
} else if (timestamps[i] != 0) {
count++;
}
if (timestamps[i] < timestamps[minIndex]) {
minIndex = i;
}
}
timestamps[minIndex] = now;
if (count >= timestamps.length) {
Arrays.fill(timestamps, 0);
return true;
}
return false;
}
}
} }

View file

@ -98,6 +98,9 @@ public abstract class Database implements AutoCloseable {
config.setJdbcUrl("jdbc:sqlite:" + databaseFile + "?foreign_keys=ON&journal_mode=wal"); config.setJdbcUrl("jdbc:sqlite:" + databaseFile + "?foreign_keys=ON&journal_mode=wal");
config.setDataSourceProperties(sqliteConfig.toProperties()); config.setDataSourceProperties(sqliteConfig.toProperties());
config.setMinimumIdle(1); config.setMinimumIdle(1);
config.setConnectionTimeout(90_000);
config.setMaximumPoolSize(50);
config.setMaxLifetime(0);
return new HikariDataSource(config); return new HikariDataSource(config);
} }
} }

View file

@ -116,7 +116,7 @@ public class SignalAccount implements Closeable {
private static final Logger logger = LoggerFactory.getLogger(SignalAccount.class); private static final Logger logger = LoggerFactory.getLogger(SignalAccount.class);
private static final int MINIMUM_STORAGE_VERSION = 1; private static final int MINIMUM_STORAGE_VERSION = 1;
private static final int CURRENT_STORAGE_VERSION = 9; private static final int CURRENT_STORAGE_VERSION = 10;
private final Object LOCK = new Object(); private final Object LOCK = new Object();
@ -827,6 +827,7 @@ public class SignalAccount implements Closeable {
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyPreKeyStore() != null) { if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyPreKeyStore() != null) {
logger.debug("Migrating legacy pre key store."); logger.debug("Migrating legacy pre key store.");
aciAccountData.getPreKeyStore().removeAllPreKeys();
for (var entry : legacySignalProtocolStore.getLegacyPreKeyStore().getPreKeys().entrySet()) { for (var entry : legacySignalProtocolStore.getLegacyPreKeyStore().getPreKeys().entrySet()) {
try { try {
aciAccountData.getPreKeyStore().storePreKey(entry.getKey(), new PreKeyRecord(entry.getValue())); aciAccountData.getPreKeyStore().storePreKey(entry.getKey(), new PreKeyRecord(entry.getValue()));
@ -838,6 +839,7 @@ public class SignalAccount implements Closeable {
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySignedPreKeyStore() != null) { if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySignedPreKeyStore() != null) {
logger.debug("Migrating legacy signed pre key store."); logger.debug("Migrating legacy signed pre key store.");
aciAccountData.getSignedPreKeyStore().removeAllSignedPreKeys();
for (var entry : legacySignalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) { for (var entry : legacySignalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) {
try { try {
aciAccountData.getSignedPreKeyStore() aciAccountData.getSignedPreKeyStore()

View file

@ -1,6 +1,7 @@
package org.asamk.signal.manager.storage.accounts; package org.asamk.signal.manager.storage.accounts;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.ServiceEnvironment; import org.asamk.signal.manager.api.ServiceEnvironment;
@ -10,7 +11,6 @@ import org.asamk.signal.manager.util.IOUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -181,7 +181,7 @@ public class AccountsStore {
return Arrays.stream(files) return Arrays.stream(files)
.filter(File::isFile) .filter(File::isFile)
.map(File::getName) .map(File::getName)
.filter(file -> PhoneNumberFormatter.isValidNumber(file, null)) .filter(file -> PhoneNumberUtil.getInstance().isPossibleNumber(file, null))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }

View file

@ -8,6 +8,7 @@ import org.asamk.signal.manager.storage.recipients.RecipientStore;
import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.state.IdentityKeyStore.Direction; import org.signal.libsignal.protocol.state.IdentityKeyStore.Direction;
import org.signal.libsignal.protocol.state.IdentityKeyStore.IdentityChange;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId;
@ -62,11 +63,11 @@ public class IdentityKeyStore {
return identityChanges; return identityChanges;
} }
public boolean saveIdentity(final ServiceId serviceId, final IdentityKey identityKey) { public IdentityChange saveIdentity(final ServiceId serviceId, final IdentityKey identityKey) {
return saveIdentity(serviceId.toString(), identityKey); return saveIdentity(serviceId.toString(), identityKey);
} }
public boolean saveIdentity( public IdentityChange saveIdentity(
final Connection connection, final Connection connection,
final ServiceId serviceId, final ServiceId serviceId,
final IdentityKey identityKey final IdentityKey identityKey
@ -74,9 +75,9 @@ public class IdentityKeyStore {
return saveIdentity(connection, serviceId.toString(), identityKey); return saveIdentity(connection, serviceId.toString(), identityKey);
} }
boolean saveIdentity(final String address, final IdentityKey identityKey) { IdentityChange saveIdentity(final String address, final IdentityKey identityKey) {
if (isRetryingDecryption) { if (isRetryingDecryption) {
return false; return IdentityChange.NEW_OR_UNCHANGED;
} }
try (final var connection = database.getConnection()) { try (final var connection = database.getConnection()) {
return saveIdentity(connection, address, identityKey); return saveIdentity(connection, address, identityKey);
@ -85,20 +86,24 @@ public class IdentityKeyStore {
} }
} }
private boolean saveIdentity( private IdentityChange saveIdentity(
final Connection connection, final Connection connection,
final String address, final String address,
final IdentityKey identityKey final IdentityKey identityKey
) throws SQLException { ) throws SQLException {
final var identityInfo = loadIdentity(connection, address); final var identityInfo = loadIdentity(connection, address);
if (identityInfo != null && identityInfo.getIdentityKey().equals(identityKey)) { if (identityInfo == null) {
saveNewIdentity(connection, address, identityKey, true);
return IdentityChange.NEW_OR_UNCHANGED;
}
if (identityInfo.getIdentityKey().equals(identityKey)) {
// Identity already exists, not updating the trust level // Identity already exists, not updating the trust level
logger.trace("Not storing new identity for recipient {}, identity already stored", address); logger.trace("Not storing new identity for recipient {}, identity already stored", address);
return false; return IdentityChange.NEW_OR_UNCHANGED;
} }
saveNewIdentity(connection, address, identityKey, identityInfo == null); saveNewIdentity(connection, address, identityKey, false);
return true; return IdentityChange.REPLACED_EXISTING;
} }
public void setRetryingDecryption(final boolean retryingDecryption) { public void setRetryingDecryption(final boolean retryingDecryption) {

View file

@ -33,7 +33,7 @@ public class SignalIdentityKeyStore implements org.signal.libsignal.protocol.sta
} }
@Override @Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { public IdentityChange saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return identityKeyStore.saveIdentity(address.getName(), identityKey); return identityKeyStore.saveIdentity(address.getName(), identityKey);
} }

View file

@ -10,6 +10,7 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Types; import java.sql.Types;
import java.util.HashMap;
import java.util.Objects; import java.util.Objects;
public class KeyValueStore { public class KeyValueStore {
@ -18,6 +19,7 @@ public class KeyValueStore {
private static final Logger logger = LoggerFactory.getLogger(KeyValueStore.class); private static final Logger logger = LoggerFactory.getLogger(KeyValueStore.class);
private final Database database; private final Database database;
private final HashMap<KeyValueEntry<?>, Object> cache = new HashMap<>();
public static void createSql(Connection connection) throws SQLException { public static void createSql(Connection connection) throws SQLException {
// When modifying the CREATE statement here, also add a migration in AccountDatabase.java // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
@ -36,11 +38,18 @@ public class KeyValueStore {
this.database = database; this.database = database;
} }
@SuppressWarnings("unchecked")
public <T> T getEntry(KeyValueEntry<T> key) { public <T> T getEntry(KeyValueEntry<T> key) {
synchronized (cache) {
if (cache.containsKey(key)) {
logger.trace("Got entry for key {} from cache", key.key());
return (T) cache.get(key);
}
}
try (final var connection = database.getConnection()) { try (final var connection = database.getConnection()) {
return getEntry(connection, key); return getEntry(connection, key);
} catch (SQLException e) { } catch (SQLException e) {
throw new RuntimeException("Failed read from pre_key store", e); throw new RuntimeException("Failed read from key_value store", e);
} }
} }
@ -63,11 +72,17 @@ public class KeyValueStore {
try (final var statement = connection.prepareStatement(sql)) { try (final var statement = connection.prepareStatement(sql)) {
statement.setString(1, key.key()); statement.setString(1, key.key());
final var result = Utils.executeQueryForOptional(statement, var result = Utils.executeQueryForOptional(statement, resultSet -> readValueFromResultSet(key, resultSet))
resultSet -> readValueFromResultSet(key, resultSet)).orElse(null); .orElse(null);
if (result == null) { if (result == null) {
return key.defaultValue(); logger.trace("Got entry for key {} from default value", key.key());
result = key.defaultValue();
} else {
logger.trace("Got entry for key {} from db", key.key());
}
synchronized (cache) {
cache.put(key, result);
} }
return result; return result;
} }
@ -95,6 +110,9 @@ public class KeyValueStore {
setParameterValue(statement, 2, key.clazz(), value); setParameterValue(statement, 2, key.clazz(), value);
statement.executeUpdate(); statement.executeUpdate();
} }
synchronized (cache) {
cache.put(key, value);
}
return true; return true;
} }

View file

@ -10,6 +10,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Objects; import java.util.Objects;
@ -75,7 +76,7 @@ public class MessageCache {
return cachedMessage; return cachedMessage;
} }
logger.debug("Moving cached message {} to {}", cachedMessage.getFile().toPath(), cacheFile.toPath()); logger.debug("Moving cached message {} to {}", cachedMessage.getFile().toPath(), cacheFile.toPath());
Files.move(cachedMessage.getFile().toPath(), cacheFile.toPath()); Files.move(cachedMessage.getFile().toPath(), cacheFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
return new CachedMessage(cacheFile); return new CachedMessage(cacheFile);
} }

View file

@ -4,8 +4,9 @@ import org.asamk.signal.manager.storage.Database;
import org.asamk.signal.manager.storage.Utils; import org.asamk.signal.manager.storage.Utils;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.InvalidKeyIdException; import org.signal.libsignal.protocol.InvalidKeyIdException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECKeyPair; import org.signal.libsignal.protocol.ecc.ECKeyPair;
import org.signal.libsignal.protocol.ecc.ECPrivateKey;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.signal.libsignal.protocol.state.PreKeyRecord; import org.signal.libsignal.protocol.state.PreKeyRecord;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -176,8 +177,8 @@ public class PreKeyStore implements SignalServicePreKeyStore {
private PreKeyRecord getPreKeyRecordFromResultSet(ResultSet resultSet) throws SQLException { private PreKeyRecord getPreKeyRecordFromResultSet(ResultSet resultSet) throws SQLException {
try { try {
final var keyId = resultSet.getInt("key_id"); final var keyId = resultSet.getInt("key_id");
final var publicKey = Curve.decodePoint(resultSet.getBytes("public_key"), 0); final var publicKey = new ECPublicKey(resultSet.getBytes("public_key"));
final var privateKey = Curve.decodePrivatePoint(resultSet.getBytes("private_key")); final var privateKey = new ECPrivateKey(resultSet.getBytes("private_key"));
return new PreKeyRecord(keyId, new ECKeyPair(publicKey, privateKey)); return new PreKeyRecord(keyId, new ECKeyPair(publicKey, privateKey));
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
return null; return null;

View file

@ -4,8 +4,9 @@ import org.asamk.signal.manager.storage.Database;
import org.asamk.signal.manager.storage.Utils; import org.asamk.signal.manager.storage.Utils;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.InvalidKeyIdException; import org.signal.libsignal.protocol.InvalidKeyIdException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECKeyPair; import org.signal.libsignal.protocol.ecc.ECKeyPair;
import org.signal.libsignal.protocol.ecc.ECPrivateKey;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.signal.libsignal.protocol.state.SignedPreKeyRecord; import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -238,8 +239,8 @@ public class SignedPreKeyStore implements org.signal.libsignal.protocol.state.Si
private SignedPreKeyRecord getSignedPreKeyRecordFromResultSet(ResultSet resultSet) throws SQLException { private SignedPreKeyRecord getSignedPreKeyRecordFromResultSet(ResultSet resultSet) throws SQLException {
try { try {
final var keyId = resultSet.getInt("key_id"); final var keyId = resultSet.getInt("key_id");
final var publicKey = Curve.decodePoint(resultSet.getBytes("public_key"), 0); final var publicKey = new ECPublicKey(resultSet.getBytes("public_key"));
final var privateKey = Curve.decodePrivatePoint(resultSet.getBytes("private_key")); final var privateKey = new ECPrivateKey(resultSet.getBytes("private_key"));
final var signature = resultSet.getBytes("signature"); final var signature = resultSet.getBytes("signature");
final var timestamp = resultSet.getLong("timestamp"); final var timestamp = resultSet.getLong("timestamp");
return new SignedPreKeyRecord(keyId, timestamp, new ECKeyPair(publicKey, privateKey), signature); return new SignedPreKeyRecord(keyId, timestamp, new ECKeyPair(publicKey, privateKey), signature);

View file

@ -65,7 +65,7 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
} }
@Override @Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { public IdentityChange saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return identityKeyStore.saveIdentity(address, identityKey); return identityKeyStore.saveIdentity(address, identityKey);
} }

View file

@ -83,10 +83,11 @@ public class MergeRecipientHelper {
recipientsToBeStripped.add(recipient); recipientsToBeStripped.add(recipient);
} }
logger.debug("Got separate recipients for high trust identifiers {}, need to merge ({}) and strip ({})", logger.debug("Got separate recipients for high trust identifiers {}, need to merge ({}, {}) and strip ({})",
address, address,
recipientsToBeMerged.stream().map(r -> r.id().toString()).collect(Collectors.joining(", ")), resultingRecipient.map(RecipientWithAddress::address),
recipientsToBeStripped.stream().map(r -> r.id().toString()).collect(Collectors.joining(", "))); recipientsToBeMerged.stream().map(r -> r.address().toString()).collect(Collectors.joining(", ")),
recipientsToBeStripped.stream().map(r -> r.address().toString()).collect(Collectors.joining(", ")));
RecipientAddress finalAddress = resultingRecipient.map(RecipientWithAddress::address).orElse(null); RecipientAddress finalAddress = resultingRecipient.map(RecipientWithAddress::address).orElse(null);
for (final var recipient : recipientsToBeMerged) { for (final var recipient : recipientsToBeMerged) {

View file

@ -994,7 +994,12 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
) throws SQLException { ) throws SQLException {
markUnregistered(connection, recipientId); markUnregistered(connection, recipientId);
final var address = resolveRecipientAddress(connection, recipientId); final var address = resolveRecipientAddress(connection, recipientId);
if (address.aci().isPresent() && address.pni().isPresent()) { final var needSplit = address.aci().isPresent() && address.pni().isPresent();
logger.trace("Marking unregistered recipient {} as unregistered (and split={}): {}",
recipientId,
needSplit,
address);
if (needSplit) {
final var numberAddress = new RecipientAddress(address.pni().get(), address.number().orElse(null)); final var numberAddress = new RecipientAddress(address.pni().get(), address.number().orElse(null));
updateRecipientAddress(connection, recipientId, address.removeIdentifiersFrom(numberAddress)); updateRecipientAddress(connection, recipientId, address.removeIdentifiersFrom(numberAddress));
addNewRecipient(connection, numberAddress); addNewRecipient(connection, numberAddress);

View file

@ -2,7 +2,6 @@ package org.asamk.signal.manager.syncStorage;
import org.asamk.signal.manager.api.Profile; import org.asamk.signal.manager.api.Profile;
import org.asamk.signal.manager.internal.JobExecutor; import org.asamk.signal.manager.internal.JobExecutor;
import org.asamk.signal.manager.jobs.CheckWhoAmIJob;
import org.asamk.signal.manager.jobs.DownloadProfileAvatarJob; import org.asamk.signal.manager.jobs.DownloadProfileAvatarJob;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.KeyUtils;
@ -112,7 +111,7 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
backupsPurchaseToken = IAPSubscriptionId.Companion.from(local.backupSubscriberData); backupsPurchaseToken = IAPSubscriptionId.Companion.from(local.backupSubscriberData);
} }
final var mergedBuilder = SignalAccountRecord.Companion.newBuilder(remote.unknownFields().toByteArray()) final var mergedBuilder = remote.newBuilder()
.givenName(givenName) .givenName(givenName)
.familyName(familyName) .familyName(familyName)
.avatarUrlPath(firstNonEmpty(remote.avatarUrlPath, local.avatarUrlPath)) .avatarUrlPath(firstNonEmpty(remote.avatarUrlPath, local.avatarUrlPath))
@ -146,7 +145,7 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
: remote.storyViewReceiptsEnabled) : remote.storyViewReceiptsEnabled)
.username(remote.username) .username(remote.username)
.usernameLink(remote.usernameLink) .usernameLink(remote.usernameLink)
.e164(account.isPrimaryDevice() ? local.e164 : remote.e164); .avatarColor(remote.avatarColor);
safeSetPayments(mergedBuilder, safeSetPayments(mergedBuilder,
payments != null && payments.enabled, payments != null && payments.enabled,
payments == null ? null : payments.entropy.toByteArray()); payments == null ? null : payments.entropy.toByteArray());
@ -179,10 +178,6 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
final var accountRecord = update.newRecord(); final var accountRecord = update.newRecord();
final var accountProto = accountRecord.getProto(); final var accountProto = accountRecord.getProto();
if (!accountProto.e164.equals(account.getNumber())) {
jobExecutor.enqueueJob(new CheckWhoAmIJob());
}
account.getConfigurationStore().setReadReceipts(connection, accountProto.readReceipts); account.getConfigurationStore().setReadReceipts(connection, accountProto.readReceipts);
account.getConfigurationStore().setTypingIndicators(connection, accountProto.typingIndicators); account.getConfigurationStore().setTypingIndicators(connection, accountProto.typingIndicators);
account.getConfigurationStore() account.getConfigurationStore()

View file

@ -37,7 +37,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
private static final Logger logger = LoggerFactory.getLogger(ContactRecordProcessor.class); private static final Logger logger = LoggerFactory.getLogger(ContactRecordProcessor.class);
private static final Pattern E164_PATTERN = Pattern.compile("^\\+[1-9]\\d{0,18}$"); private static final Pattern E164_PATTERN = Pattern.compile("^\\+[1-9]\\d{6,18}$");
private final ACI selfAci; private final ACI selfAci;
private final PNI selfPni; private final PNI selfPni;
@ -172,7 +172,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
e164 = firstNonEmpty(remote.e164, local.e164); e164 = firstNonEmpty(remote.e164, local.e164);
} }
final var mergedBuilder = SignalContactRecord.Companion.newBuilder(remote.unknownFields().toByteArray()) final var mergedBuilder = remote.newBuilder()
.aci(local.aci.isEmpty() ? remote.aci : local.aci) .aci(local.aci.isEmpty() ? remote.aci : local.aci)
.e164(e164) .e164(e164)
.pni(pni) .pni(pni)
@ -195,7 +195,8 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
.hidden(remote.hidden) .hidden(remote.hidden)
.pniSignatureVerified(remote.pniSignatureVerified || local.pniSignatureVerified) .pniSignatureVerified(remote.pniSignatureVerified || local.pniSignatureVerified)
.nickname(remote.nickname) .nickname(remote.nickname)
.note(remote.note); .note(remote.note)
.avatarColor(remote.avatarColor);
final var merged = mergedBuilder.build(); final var merged = mergedBuilder.build();
final var matchesRemote = doProtosMatch(merged, remote); final var matchesRemote = doProtosMatch(merged, remote);

View file

@ -74,7 +74,7 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
final var remote = remoteRecord.getProto(); final var remote = remoteRecord.getProto();
final var local = localRecord.getProto(); final var local = localRecord.getProto();
final var mergedBuilder = SignalGroupV1Record.Companion.newBuilder(remote.unknownFields().toByteArray()) final var mergedBuilder = remote.newBuilder()
.id(remote.id) .id(remote.id)
.blocked(remote.blocked) .blocked(remote.blocked)
.whitelisted(remote.whitelisted) .whitelisted(remote.whitelisted)

View file

@ -53,7 +53,7 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
final var remote = remoteRecord.getProto(); final var remote = remoteRecord.getProto();
final var local = localRecord.getProto(); final var local = localRecord.getProto();
final var mergedBuilder = SignalGroupV2Record.Companion.newBuilder(remote.unknownFields().toByteArray()) final var mergedBuilder = remote.newBuilder()
.masterKey(remote.masterKey) .masterKey(remote.masterKey)
.blocked(remote.blocked) .blocked(remote.blocked)
.whitelisted(remote.whitelisted) .whitelisted(remote.whitelisted)
@ -62,7 +62,8 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
.mutedUntilTimestamp(remote.mutedUntilTimestamp) .mutedUntilTimestamp(remote.mutedUntilTimestamp)
.dontNotifyForMentionsIfMuted(remote.dontNotifyForMentionsIfMuted) .dontNotifyForMentionsIfMuted(remote.dontNotifyForMentionsIfMuted)
.hideStory(remote.hideStory) .hideStory(remote.hideStory)
.storySendMode(remote.storySendMode); .storySendMode(remote.storySendMode)
.avatarColor(remote.avatarColor);
final var merged = mergedBuilder.build(); final var merged = mergedBuilder.build();
final var matchesRemote = doProtosMatch(merged, remote); final var matchesRemote = doProtosMatch(merged, remote);

View file

@ -74,7 +74,6 @@ public final class StorageSyncModels {
.phoneNumberSharingMode(Optional.ofNullable(configStore.getPhoneNumberSharingMode(connection)) .phoneNumberSharingMode(Optional.ofNullable(configStore.getPhoneNumberSharingMode(connection))
.map(StorageSyncModels::localToRemote) .map(StorageSyncModels::localToRemote)
.orElse(AccountRecord.PhoneNumberSharingMode.UNKNOWN)) .orElse(AccountRecord.PhoneNumberSharingMode.UNKNOWN))
.e164(self.getAddress().number().orElse(""))
.username(self.getAddress().username().orElse("")); .username(self.getAddress().username().orElse(""));
if (usernameLinkComponents != null) { if (usernameLinkComponents != null) {
final var linkColor = configStore.getUsernameLinkColor(connection); final var linkColor = configStore.getUsernameLinkColor(connection);

View file

@ -4,7 +4,7 @@ import org.asamk.signal.manager.storage.SignalAccount;
import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.IdentityKeyPair; import org.signal.libsignal.protocol.IdentityKeyPair;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve; import org.signal.libsignal.protocol.ecc.ECKeyPair;
import org.signal.libsignal.protocol.ecc.ECPrivateKey; import org.signal.libsignal.protocol.ecc.ECPrivateKey;
import org.signal.libsignal.protocol.kem.KEMKeyPair; import org.signal.libsignal.protocol.kem.KEMKeyPair;
import org.signal.libsignal.protocol.kem.KEMKeyType; import org.signal.libsignal.protocol.kem.KEMKeyType;
@ -33,8 +33,8 @@ public class KeyUtils {
public static IdentityKeyPair getIdentityKeyPair(byte[] publicKeyBytes, byte[] privateKeyBytes) { public static IdentityKeyPair getIdentityKeyPair(byte[] publicKeyBytes, byte[] privateKeyBytes) {
try { try {
IdentityKey publicKey = new IdentityKey(publicKeyBytes); final var publicKey = new IdentityKey(publicKeyBytes);
ECPrivateKey privateKey = Curve.decodePrivatePoint(privateKeyBytes); final var privateKey = new ECPrivateKey(privateKeyBytes);
return new IdentityKeyPair(publicKey, privateKey); return new IdentityKeyPair(publicKey, privateKey);
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
@ -43,7 +43,7 @@ public class KeyUtils {
} }
public static IdentityKeyPair generateIdentityKeyPair() { public static IdentityKeyPair generateIdentityKeyPair() {
var djbKeyPair = Curve.generateKeyPair(); var djbKeyPair = ECKeyPair.generate();
var djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey()); var djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());
var djbPrivateKey = djbKeyPair.getPrivateKey(); var djbPrivateKey = djbKeyPair.getPrivateKey();
@ -54,7 +54,7 @@ public class KeyUtils {
var records = new ArrayList<PreKeyRecord>(PREKEY_BATCH_SIZE); var records = new ArrayList<PreKeyRecord>(PREKEY_BATCH_SIZE);
for (var i = 0; i < PREKEY_BATCH_SIZE; i++) { for (var i = 0; i < PREKEY_BATCH_SIZE; i++) {
var preKeyId = (offset + i) % PREKEY_MAXIMUM_ID; var preKeyId = (offset + i) % PREKEY_MAXIMUM_ID;
var keyPair = Curve.generateKeyPair(); var keyPair = ECKeyPair.generate();
var record = new PreKeyRecord(preKeyId, keyPair); var record = new PreKeyRecord(preKeyId, keyPair);
records.add(record); records.add(record);
@ -66,13 +66,9 @@ public class KeyUtils {
final int signedPreKeyId, final int signedPreKeyId,
final ECPrivateKey privateKey final ECPrivateKey privateKey
) { ) {
var keyPair = Curve.generateKeyPair(); var keyPair = ECKeyPair.generate();
byte[] signature; byte[] signature;
try { signature = privateKey.calculateSignature(keyPair.getPublicKey().serialize());
signature = Curve.calculateSignature(privateKey, keyPair.getPublicKey().serialize());
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
return new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature); return new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
} }

View file

@ -4,6 +4,7 @@ import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException; import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
@ -47,38 +48,38 @@ public class NumberVerificationUtils {
} }
} }
sessionId = sessionResponse.getBody().getId(); sessionId = sessionResponse.getMetadata().getId();
sessionIdSaver.accept(sessionId); sessionIdSaver.accept(sessionId);
if (sessionResponse.getBody().getVerified()) { if (sessionResponse.getMetadata().getVerified()) {
return sessionId; return sessionId;
} }
if (sessionResponse.getBody().getAllowedToRequestCode()) { if (sessionResponse.getMetadata().getAllowedToRequestCode()) {
return sessionId; return sessionId;
} }
final var nextAttempt = voiceVerification final var nextAttempt = voiceVerification
? sessionResponse.getBody().getNextCall() ? sessionResponse.getMetadata().getNextCall()
: sessionResponse.getBody().getNextSms(); : sessionResponse.getMetadata().getNextSms();
if (nextAttempt == null) { if (nextAttempt == null) {
throw new VerificationMethodNotAvailableException(); throw new VerificationMethodNotAvailableException();
} else if (nextAttempt > 0) { } else if (nextAttempt > 0) {
final var timestamp = sessionResponse.getHeaders().getTimestamp() + nextAttempt * 1000; final var timestamp = sessionResponse.getClientReceivedAtMilliseconds() + nextAttempt * 1000;
throw new RateLimitException(timestamp); throw new RateLimitException(timestamp);
} }
final var nextVerificationAttempt = sessionResponse.getBody().getNextVerificationAttempt(); final var nextVerificationAttempt = sessionResponse.getMetadata().getNextVerificationAttempt();
if (nextVerificationAttempt != null && nextVerificationAttempt > 0) { if (nextVerificationAttempt != null && nextVerificationAttempt > 0) {
final var timestamp = sessionResponse.getHeaders().getTimestamp() + nextVerificationAttempt * 1000; final var timestamp = sessionResponse.getClientReceivedAtMilliseconds() + nextVerificationAttempt * 1000;
throw new CaptchaRequiredException(timestamp); throw new CaptchaRequiredException(timestamp);
} }
if (sessionResponse.getBody().getRequestedInformation().contains("captcha")) { if (sessionResponse.getMetadata().getRequestedInformation().contains("captcha")) {
if (captcha != null) { if (captcha != null) {
sessionResponse = submitCaptcha(registrationApi, sessionId, captcha); sessionResponse = submitCaptcha(registrationApi, sessionId, captcha);
} }
if (!sessionResponse.getBody().getAllowedToRequestCode()) { if (!sessionResponse.getMetadata().getAllowedToRequestCode()) {
throw new CaptchaRequiredException("Captcha Required"); throw new CaptchaRequiredException("Captcha Required");
} }
} }
@ -114,7 +115,7 @@ public class NumberVerificationUtils {
String pin, String pin,
PinHelper pinHelper, PinHelper pinHelper,
Verifier verifier Verifier verifier
) throws IOException, PinLockedException, IncorrectPinException { ) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException {
verificationCode = verificationCode.replace("-", ""); verificationCode = verificationCode.replace("-", "");
try { try {
final var response = verifier.verify(sessionId, verificationCode, null); final var response = verifier.verify(sessionId, verificationCode, null);
@ -127,7 +128,7 @@ public class NumberVerificationUtils {
final var registrationLockData = pinHelper.getRegistrationLockData(pin, e); final var registrationLockData = pinHelper.getRegistrationLockData(pin, e);
if (registrationLockData == null) { if (registrationLockData == null) {
throw e; throw new PinLockMissingException();
} }
var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock(); var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock();

View file

@ -23,8 +23,8 @@ public class PaymentUtils {
public static PaymentAddress signPaymentsAddress(byte[] publicAddressBytes, ECPrivateKey privateKey) { public static PaymentAddress signPaymentsAddress(byte[] publicAddressBytes, ECPrivateKey privateKey) {
byte[] signature = privateKey.calculateSignature(publicAddressBytes); byte[] signature = privateKey.calculateSignature(publicAddressBytes);
return new PaymentAddress.Builder().mobileCoinAddress(new PaymentAddress.MobileCoinAddress.Builder().address( return new PaymentAddress.Builder().mobileCoin(new PaymentAddress.MobileCoin.Builder().publicAddress(ByteString.of(
ByteString.of(publicAddressBytes)).signature(ByteString.of(signature)).build()).build(); publicAddressBytes)).signature(ByteString.of(signature)).build()).build();
} }
/** /**
@ -33,13 +33,15 @@ public class PaymentUtils {
* Returns the validated bytes if so, otherwise returns null. * Returns the validated bytes if so, otherwise returns null.
*/ */
public static byte[] verifyPaymentsAddress(PaymentAddress paymentAddress, ECPublicKey publicKey) { public static byte[] verifyPaymentsAddress(PaymentAddress paymentAddress, ECPublicKey publicKey) {
final var mobileCoinAddress = paymentAddress.mobileCoinAddress; final var mobileCoinAddress = paymentAddress.mobileCoin;
if (mobileCoinAddress == null || mobileCoinAddress.address == null || mobileCoinAddress.signature == null) { if (mobileCoinAddress == null
|| mobileCoinAddress.publicAddress == null
|| mobileCoinAddress.signature == null) {
logger.debug("Got payment address without mobile coin address, ignoring."); logger.debug("Got payment address without mobile coin address, ignoring.");
return null; return null;
} }
byte[] bytes = mobileCoinAddress.address.toByteArray(); byte[] bytes = mobileCoinAddress.publicAddress.toByteArray();
byte[] signature = mobileCoinAddress.signature.toByteArray(); byte[] signature = mobileCoinAddress.signature.toByteArray();
if (signature.length != 64 || !publicKey.verifySignature(bytes, signature)) { if (signature.length != 64 || !publicKey.verifySignature(bytes, signature)) {

View file

@ -0,0 +1,59 @@
package org.asamk.signal.manager.util;
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat;
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
import org.asamk.signal.manager.api.InvalidNumberException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PhoneNumberFormatter {
private static final Logger logger = LoggerFactory.getLogger(PhoneNumberFormatter.class);
private static String impreciseFormatNumber(String number, String localNumber) {
number = number.replaceAll("[^0-9+]", "");
if (number.charAt(0) == '+') return number;
if (localNumber.charAt(0) == '+') localNumber = localNumber.substring(1);
if (localNumber.length() == number.length() || number.length() > localNumber.length()) return "+" + number;
int difference = localNumber.length() - number.length();
return "+" + localNumber.substring(0, difference) + number;
}
public static String formatNumber(String number, String localNumber) throws InvalidNumberException {
if (number == null) {
throw new InvalidNumberException("Null String passed as number.");
}
if (number.contains("@")) {
throw new InvalidNumberException("Possible attempt to use email address.");
}
number = number.replaceAll("[^0-9+]", "");
if (number.isEmpty()) {
throw new InvalidNumberException("No valid characters found.");
}
try {
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
PhoneNumber localNumberObject = util.parse(localNumber, null);
String localCountryCode = util.getRegionCodeForNumber(localNumberObject);
logger.trace("Got local CC: {}", localCountryCode);
PhoneNumber numberObject = util.parse(number, localCountryCode);
return util.format(numberObject, PhoneNumberFormat.E164);
} catch (NumberParseException e) {
logger.debug("{}: {}", e.getClass().getSimpleName(), e.getMessage());
return impreciseFormatNumber(number, localNumber);
}
}
}

View file

@ -7,6 +7,7 @@ import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.NetworkResult; import org.whispersystems.signalservice.api.NetworkResult;
import org.whispersystems.signalservice.api.NetworkResultUtil;
import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.api.util.StreamDetails;
@ -15,6 +16,10 @@ import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
@ -150,15 +155,7 @@ public class Utils {
} }
public static <T> T handleResponseException(final NetworkResult<T> response) throws IOException { public static <T> T handleResponseException(final NetworkResult<T> response) throws IOException {
final var throwableOptional = response.getCause(); return NetworkResultUtil.toBasicLegacy(response);
if (throwableOptional != null) {
if (throwableOptional instanceof IOException ioException) {
throw ioException;
} else {
throw new IOException(throwableOptional);
}
}
return response.successOrThrow();
} }
public static ByteString firstNonEmpty(ByteString... strings) { public static ByteString firstNonEmpty(ByteString... strings) {
@ -202,4 +199,19 @@ public class Utils {
public static String nullIfEmpty(String string) { public static String nullIfEmpty(String string) {
return string == null || string.isEmpty() ? null : string; return string == null || string.isEmpty() ? null : string;
} }
public static Proxy getHttpsProxy() {
final URI uri;
try {
uri = new URI("https://example");
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
final var proxies = ProxySelector.getDefault().select(uri);
if (proxies.isEmpty()) {
return Proxy.NO_PROXY;
} else {
return proxies.getFirst();
}
}
} }

View file

@ -316,6 +316,11 @@ Data URI encoded attachments must follow the RFC 2397.
Additionally a file name can be added: Additionally a file name can be added:
e.g.: `data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>` e.g.: `data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>`
*--view-once*::
Send the message as a view once message.
A conformant client will only allow the receiver to view the message once.
View Once is only supported for messages that include an image attachment.
*--sticker* STICKER:: *--sticker* STICKER::
Send a sticker of a locally known sticker pack (syntax: stickerPackId:stickerId). Send a sticker of a locally known sticker pack (syntax: stickerPackId:stickerId).
Shouldn't be used together with `-m` as the official clients don't support this. Shouldn't be used together with `-m` as the official clients don't support this.
@ -657,7 +662,7 @@ Path to the new avatar image file.
*--remove-avatar*:: *--remove-avatar*::
Remove the avatar Remove the avatar
*--mobile-coin-address*:: *--mobile-coin-address*, **--mobilecoin-address**::
New MobileCoin address (Base64 encoded public address) New MobileCoin address (Base64 encoded public address)
=== updateContact === updateContact
@ -669,11 +674,20 @@ If the contact doesn't exist yet, it will be added.
RECIPIENT:: RECIPIENT::
Specify the recipient. Specify the recipient.
*--given-name* NAME, *--name* NAME:: *--given-name* GIVEN_NAME, *--name* NAME::
New (given) name. New system given name.
*--family-name* FAMILY_NAME:: *--family-name* FAMILY_NAME::
New family name. New system family name.
*--nick-given-name* NICK_GIVEN_NAME::
New nick given name.
*--nick-family-name* NICK_FAMILY_NAME::
New nick family name.
*--note* NOTE::
New note.
*-e*, *--expiration* EXPIRATION_SECONDS:: *-e*, *--expiration* EXPIRATION_SECONDS::
Set expiration time of messages (seconds). Set expiration time of messages (seconds).

View file

@ -141,6 +141,7 @@ exec 3> "$FIFO_FILE"
echo '{"jsonrpc":"2.0","id":"id","method":"listGroups"}' >&3 echo '{"jsonrpc":"2.0","id":"id","method":"listGroups"}' >&3
echo '{"jsonrpc":"2.0","id":"id","method":"listDevices"}' >&3 echo '{"jsonrpc":"2.0","id":"id","method":"listDevices"}' >&3
echo '{"jsonrpc":"2.0","id":"id","method":"listIdentities"}' >&3 echo '{"jsonrpc":"2.0","id":"id","method":"listIdentities"}' >&3
echo '{"jsonrpc":"2.0","id":"id","method":"listStickerPacks"}' >&3
echo '{"jsonrpc":"2.0","id":"id","method":"sendSyncRequest"}' >&3 echo '{"jsonrpc":"2.0","id":"id","method":"sendSyncRequest"}' >&3
echo '{"jsonrpc":"2.0","id":"id","method":"sendContacts"}' >&3 echo '{"jsonrpc":"2.0","id":"id","method":"sendContacts"}' >&3
echo '{"jsonrpc":"2.0","id":"id","method":"version"}' >&3 echo '{"jsonrpc":"2.0","id":"id","method":"version"}' >&3
@ -175,6 +176,17 @@ run_main -a "$NUMBER_2" receive
run_main -a "$NUMBER_2" send "$NUMBER_1" -m hi run_main -a "$NUMBER_2" send "$NUMBER_1" -m hi
run_main -a "$NUMBER_1" receive run_main -a "$NUMBER_1" receive
run_main -a "$NUMBER_2" receive run_main -a "$NUMBER_2" receive
run_main -a "$NUMBER_1" updateAccount --discoverable-by-number=true
run_main -a "$NUMBER_2" removeContact --forget "$NUMBER_1"
run_main -a "$NUMBER_2" send "$NUMBER_1" -m hi
run_main -a "$NUMBER_2" send "$NUMBER_1" -m hii
run_main -a "$NUMBER_1" updateAccount --discoverable-by-number=false
run_main -a "$NUMBER_1" receive
run_main -a "$NUMBER_2" receive
run_main -a "$NUMBER_2" send "$NUMBER_1" -m hi
run_main -a "$NUMBER_2" send "$NUMBER_1" -m hii
run_main -a "$NUMBER_1" receive
run_main -a "$NUMBER_2" receive
## Groups ## Groups
GROUP_ID=$(run_main -a "$NUMBER_1" --output=json updateGroup -n GRUPPE -a LICENSE -m "$NUMBER_1" | jq -r '.groupId') GROUP_ID=$(run_main -a "$NUMBER_1" --output=json updateGroup -n GRUPPE -a LICENSE -m "$NUMBER_1" | jq -r '.groupId')
run_main -a "$NUMBER_1" send "$NUMBER_2" -m first run_main -a "$NUMBER_1" send "$NUMBER_2" -m first
@ -237,7 +249,9 @@ for OUTPUT in "plain-text" "json"; do
run_linked -a "$NUMBER_1" --output="$OUTPUT" receive run_linked -a "$NUMBER_1" --output="$OUTPUT" receive
done done
run_main -a "$NUMBER_1" removeDevice -d 2 run_main -a "$NUMBER_1" --output="$OUTPUT" receive
run_main -a "$NUMBER_1" removeDevice -d 2 || true
run_main -a "$NUMBER_1" removeDevice -d 2 || true
## Unregister ## Unregister
if [ "$TEST_REGISTER" -eq 1 ]; then if [ "$TEST_REGISTER" -eq 1 ]; then

View file

@ -291,6 +291,8 @@ public class App {
commandHandler.handleMultiLocalCommand(command, multiAccountManager); commandHandler.handleMultiLocalCommand(command, multiAccountManager);
} catch (IOException e) { } catch (IOException e) {
throw new IOErrorException("Failed to load local accounts file", e); throw new IOErrorException("Failed to load local accounts file", e);
} catch (AccountCheckException e) {
throw new UnexpectedErrorException("Failed to load account file", e);
} }
} }

View file

@ -8,7 +8,7 @@ public class BaseConfig {
public static final String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion(); 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")) static final String USER_AGENT_SIGNAL_ANDROID = Optional.ofNullable(System.getenv("SIGNAL_CLI_USER_AGENT"))
.orElse("Signal-Android/7.30.1"); .orElse("Signal-Android/7.47.1");
static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null
? "signal-cli" ? "signal-cli"
: PROJECT_NAME + "/" + PROJECT_VERSION; : PROJECT_NAME + "/" + PROJECT_VERSION;

View file

@ -1,5 +1,6 @@
package org.asamk.signal; package org.asamk.signal;
import org.asamk.signal.commands.exceptions.CommandException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -11,7 +12,7 @@ import sun.misc.Signal;
public class Shutdown { public class Shutdown {
private static final Logger logger = LoggerFactory.getLogger(Shutdown.class); private static final Logger logger = LoggerFactory.getLogger(Shutdown.class);
private static final CompletableFuture<Void> shutdown = new CompletableFuture<>(); private static final CompletableFuture<Object> shutdown = new CompletableFuture<>();
private static final CompletableFuture<Void> shutdownComplete = new CompletableFuture<>(); private static final CompletableFuture<Void> shutdownComplete = new CompletableFuture<>();
private static boolean initialized = false; private static boolean initialized = false;
@ -43,9 +44,17 @@ public class Shutdown {
shutdown.complete(null); shutdown.complete(null);
} }
public static void waitForShutdown() throws InterruptedException { public static void triggerShutdown(CommandException exception) {
logger.debug("Triggering shutdown with exception.", exception);
shutdown.complete(exception);
}
public static void waitForShutdown() throws InterruptedException, CommandException {
try { try {
shutdown.get(); final var result = shutdown.get();
if (result instanceof CommandException e) {
throw e;
}
} catch (ExecutionException e) { } catch (ExecutionException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View file

@ -97,7 +97,7 @@ public class DaemonCommand implements MultiLocalCommand, LocalCommand {
final OutputWriter outputWriter final OutputWriter outputWriter
) throws CommandException { ) throws CommandException {
Shutdown.installHandler(); Shutdown.installHandler();
logger.info("Starting daemon in single-account mode for " + m.getSelfNumber()); logger.info("Starting daemon in single-account mode for {}", m.getSelfNumber());
final var noReceiveStdOut = Boolean.TRUE.equals(ns.getBoolean("no-receive-stdout")); final var noReceiveStdOut = Boolean.TRUE.equals(ns.getBoolean("no-receive-stdout"));
final var receiveMode = ns.<ReceiveMode>get("receive-mode"); final var receiveMode = ns.<ReceiveMode>get("receive-mode");
final var receiveConfig = getReceiveConfig(ns); final var receiveConfig = getReceiveConfig(ns);

View file

@ -9,6 +9,7 @@ import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NotPrimaryDeviceException; import org.asamk.signal.manager.api.NotPrimaryDeviceException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.output.OutputWriter; import org.asamk.signal.output.OutputWriter;
@ -50,6 +51,8 @@ public class FinishChangeNumberCommand implements JsonRpcLocalCommand {
+ "\nUse '--pin PIN_CODE' to specify the registration lock PIN"); + "\nUse '--pin PIN_CODE' to specify the registration lock PIN");
} catch (IncorrectPinException e) { } catch (IncorrectPinException e) {
throw new UserErrorException("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining()); throw new UserErrorException("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining());
} catch (PinLockMissingException e) {
throw new UserErrorException("Account is pin locked, but pin data has been deleted on the server.");
} catch (NotPrimaryDeviceException e) { } catch (NotPrimaryDeviceException e) {
throw new UserErrorException("This command doesn't work on linked devices."); throw new UserErrorException("This command doesn't work on linked devices.");
} catch (IOException e) { } catch (IOException e) {

View file

@ -64,9 +64,16 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand {
} }
final var usernames = ns.<String>getList("username"); final var usernames = ns.<String>getList("username");
final var registeredUsernames = usernames == null final Map<String, UsernameStatus> registeredUsernames;
? Map.<String, UsernameStatus>of() try {
: m.getUsernameStatus(new HashSet<>(usernames)); registeredUsernames = usernames == null ? Map.of() : m.getUsernameStatus(new HashSet<>(usernames));
} catch (IOException e) {
throw new IOErrorException("Unable to check if users are registered: "
+ e.getMessage()
+ " ("
+ e.getClass().getSimpleName()
+ ")", e);
}
// Output // Output
switch (outputWriter) { switch (outputWriter) {

View file

@ -66,6 +66,9 @@ public class SendCommand implements JsonRpcLocalCommand {
.help("Add an attachment. " .help("Add an attachment. "
+ "Can be either a file path or a data URI. Data URI encoded attachments must follow the RFC 2397. Additionally a file name can be added, e.g. " + "Can be either a file path or a data URI. Data URI encoded attachments must follow the RFC 2397. Additionally a file name can be added, e.g. "
+ "data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>."); + "data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>.");
subparser.addArgument("--view-once")
.action(Arguments.storeTrue())
.help("Send the message as a view once message");
subparser.addArgument("-e", "--end-session", "--endsession") subparser.addArgument("-e", "--end-session", "--endsession")
.help("Clear session state and send end session message.") .help("Clear session state and send end session message.")
.action(Arguments.storeTrue()); .action(Arguments.storeTrue());
@ -164,6 +167,7 @@ public class SendCommand implements JsonRpcLocalCommand {
if (attachments == null) { if (attachments == null) {
attachments = List.of(); attachments = List.of();
} }
final var viewOnce = Boolean.TRUE.equals(ns.getBoolean("view-once"));
final var selfNumber = m.getSelfNumber(); final var selfNumber = m.getSelfNumber();
@ -179,6 +183,9 @@ public class SendCommand implements JsonRpcLocalCommand {
final var quoteTimestamp = ns.getLong("quote-timestamp"); final var quoteTimestamp = ns.getLong("quote-timestamp");
if (quoteTimestamp != null) { if (quoteTimestamp != null) {
final var quoteAuthor = ns.getString("quote-author"); final var quoteAuthor = ns.getString("quote-author");
if (quoteAuthor == null) {
throw new UserErrorException("Quote author parameter is missing");
}
final var quoteMessage = ns.getString("quote-message"); final var quoteMessage = ns.getString("quote-message");
final var quoteMentionStrings = ns.<String>getList("quote-mention"); final var quoteMentionStrings = ns.<String>getList("quote-mention");
final var quoteMentions = quoteMentionStrings == null final var quoteMentions = quoteMentionStrings == null
@ -236,6 +243,7 @@ public class SendCommand implements JsonRpcLocalCommand {
try { try {
final var message = new Message(messageText, final var message = new Message(messageText,
attachments, attachments,
viewOnce,
mentions, mentions,
Optional.ofNullable(quote), Optional.ofNullable(quote),
Optional.ofNullable(sticker), Optional.ofNullable(sticker),
@ -247,8 +255,14 @@ public class SendCommand implements JsonRpcLocalCommand {
: m.sendMessage(message, recipientIdentifiers, notifySelf); : m.sendMessage(message, recipientIdentifiers, notifySelf);
outputResult(outputWriter, results); outputResult(outputWriter, results);
} catch (AttachmentInvalidException | IOException e) { } catch (AttachmentInvalidException | IOException e) {
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() if (e instanceof IOException io && io.getMessage().contains("No prekeys available")) {
.getSimpleName() + ")", e); throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
.getSimpleName() + "), maybe one of the devices of the recipient wasn't online for a while.",
e);
} else {
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
.getSimpleName() + ")", e);
}
} catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
throw new UserErrorException(e.getMessage()); throw new UserErrorException(e.getMessage());
} catch (UnregisteredRecipientException e) { } catch (UnregisteredRecipientException e) {

View file

@ -26,8 +26,11 @@ public class UpdateContactCommand implements JsonRpcLocalCommand {
subparser.help("Update the details of a given contact"); subparser.help("Update the details of a given contact");
subparser.addArgument("recipient").help("Contact number"); subparser.addArgument("recipient").help("Contact number");
subparser.addArgument("-n", "--name").help("New contact name"); subparser.addArgument("-n", "--name").help("New contact name");
subparser.addArgument("--given-name").help("New contact given name"); subparser.addArgument("--given-name").help("New system given name");
subparser.addArgument("--family-name").help("New contact family name"); subparser.addArgument("--family-name").help("New system family name");
subparser.addArgument("--nick-given-name").help("New nick given name");
subparser.addArgument("--nick-family-name").help("New nick family name");
subparser.addArgument("--note").help("New note");
subparser.addArgument("-e", "--expiration").type(int.class).help("Set expiration time of messages (seconds)"); subparser.addArgument("-e", "--expiration").type(int.class).help("Set expiration time of messages (seconds)");
} }
@ -54,8 +57,15 @@ public class UpdateContactCommand implements JsonRpcLocalCommand {
familyName = ""; familyName = "";
} }
} }
if (givenName != null || familyName != null) { var nickGivenName = ns.getString("nick-given-name");
m.setContactName(recipient, givenName, familyName); var nickFamilyName = ns.getString("nick-family-name");
var note = ns.getString("note");
if (givenName != null
|| familyName != null
|| nickGivenName != null
|| nickFamilyName != null
|| note != null) {
m.setContactName(recipient, givenName, familyName, nickGivenName, nickFamilyName, note);
} }
} catch (IOException e) { } catch (IOException e) {
throw new IOErrorException("Update contact error: " + e.getMessage(), e); throw new IOErrorException("Update contact error: " + e.getMessage(), e);

View file

@ -27,7 +27,8 @@ public class UpdateProfileCommand implements JsonRpcLocalCommand {
subparser.addArgument("--family-name").help("New profile family name (optional)"); subparser.addArgument("--family-name").help("New profile family name (optional)");
subparser.addArgument("--about").help("New profile about text"); subparser.addArgument("--about").help("New profile about text");
subparser.addArgument("--about-emoji").help("New profile about emoji"); subparser.addArgument("--about-emoji").help("New profile about emoji");
subparser.addArgument("--mobile-coin-address").help("New MobileCoin address (Base64 encoded public address)"); subparser.addArgument("--mobile-coin-address", "--mobilecoin-address")
.help("New MobileCoin address (Base64 encoded public address)");
final var avatarOptions = subparser.addMutuallyExclusiveGroup(); final var avatarOptions = subparser.addMutuallyExclusiveGroup();
avatarOptions.addArgument("--avatar").help("Path to new profile avatar"); avatarOptions.addArgument("--avatar").help("Path to new profile avatar");

View file

@ -11,6 +11,7 @@ import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.RegistrationManager; import org.asamk.signal.manager.RegistrationManager;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.output.JsonWriter; import org.asamk.signal.output.JsonWriter;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -76,6 +77,8 @@ public class VerifyCommand implements RegistrationCommand, JsonRpcRegistrationCo
+ "\nUse '--pin PIN_CODE' to specify the registration lock PIN"); + "\nUse '--pin PIN_CODE' to specify the registration lock PIN");
} catch (IncorrectPinException e) { } catch (IncorrectPinException e) {
throw new UserErrorException("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining()); throw new UserErrorException("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining());
} catch (PinLockMissingException e) {
throw new UserErrorException("Account is pin locked, but pin data has been deleted on the server.");
} catch (IOException e) { } catch (IOException e) {
throw new IOErrorException("Verify error: " + e.getMessage(), e); throw new IOErrorException("Verify error: " + e.getMessage(), e);
} }

View file

@ -1,17 +1,21 @@
package org.asamk.signal.dbus; package org.asamk.signal.dbus;
import org.asamk.signal.DbusConfig; import org.asamk.signal.DbusConfig;
import org.asamk.signal.Shutdown;
import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.MultiAccountManager; import org.asamk.signal.manager.MultiAccountManager;
import org.freedesktop.dbus.connections.IDisconnectCallback;
import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder;
import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -94,7 +98,9 @@ public class DbusHandler implements AutoCloseable {
final var busType = isDbusSystem ? DBusConnection.DBusBusType.SYSTEM : DBusConnection.DBusBusType.SESSION; final var busType = isDbusSystem ? DBusConnection.DBusBusType.SYSTEM : DBusConnection.DBusBusType.SESSION;
logger.debug("Starting DBus server on {} bus: {}", busType, busname); logger.debug("Starting DBus server on {} bus: {}", busType, busname);
try { try {
dBusConnection = DBusConnectionBuilder.forType(busType).build(); dBusConnection = DBusConnectionBuilder.forType(busType)
.withDisconnectCallback(new DisconnectCallback())
.build();
dbusRunner.run(dBusConnection); dbusRunner.run(dBusConnection);
} catch (DBusException e) { } catch (DBusException e) {
throw new UnexpectedErrorException("Dbus command failed: " + e.getMessage(), e); throw new UnexpectedErrorException("Dbus command failed: " + e.getMessage(), e);
@ -141,4 +147,13 @@ public class DbusHandler implements AutoCloseable {
void run(DBusConnection connection) throws DBusException; void run(DBusConnection connection) throws DBusException;
} }
private static final class DisconnectCallback implements IDisconnectCallback {
@Override
public void disconnectOnError(IOException ex) {
logger.debug("DBus daemon disconnected unexpectedly, shutting down");
Shutdown.triggerShutdown(new IOErrorException("Unexpected dbus daemon disconnect", ex));
}
}
} }

View file

@ -516,7 +516,10 @@ public class DbusManagerImpl implements Manager {
public void setContactName( public void setContactName(
final RecipientIdentifier.Single recipient, final RecipientIdentifier.Single recipient,
final String givenName, final String givenName,
final String familyName final String familyName,
final String nickGivenName,
final String nickFamilyName,
final String note
) throws NotPrimaryDeviceException { ) throws NotPrimaryDeviceException {
signal.setContactName(recipient.getIdentifier(), givenName); signal.setContactName(recipient.getIdentifier(), givenName);
} }

View file

@ -10,6 +10,7 @@ import org.asamk.signal.manager.RegistrationManager;
import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.UserAlreadyExistsException; import org.asamk.signal.manager.api.UserAlreadyExistsException;
@ -105,6 +106,8 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl {
+ (e.getTimeRemaining() / 1000 / 60 / 60)); + (e.getTimeRemaining() / 1000 / 60 / 60));
} catch (IncorrectPinException e) { } catch (IncorrectPinException e) {
throw new Error.Failure("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining()); throw new Error.Failure("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining());
} catch (PinLockMissingException e) {
throw new Error.Failure("Account is pin locked, but pin data has been deleted on the server.");
} }
} }

View file

@ -236,6 +236,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
try { try {
final var message = new Message(messageText, final var message = new Message(messageText,
attachments, attachments,
false,
List.of(), List.of(),
Optional.empty(), Optional.empty(),
Optional.empty(), Optional.empty(),
@ -399,6 +400,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
try { try {
final var message = new Message(messageText, final var message = new Message(messageText,
attachments, attachments,
false,
List.of(), List.of(),
Optional.empty(), Optional.empty(),
Optional.empty(), Optional.empty(),
@ -444,6 +446,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
try { try {
final var message = new Message(messageText, final var message = new Message(messageText,
attachments, attachments,
false,
List.of(), List.of(),
Optional.empty(), Optional.empty(),
Optional.empty(), Optional.empty(),
@ -531,7 +534,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
@Override @Override
public void setContactName(final String number, final String name) { public void setContactName(final String number, final String name) {
try { try {
m.setContactName(getSingleRecipientIdentifier(number, m.getSelfNumber()), name, ""); m.setContactName(getSingleRecipientIdentifier(number, m.getSelfNumber()), name, "", null, null, null);
} catch (NotPrimaryDeviceException e) { } catch (NotPrimaryDeviceException e) {
throw new Error.Failure("This command doesn't work on linked devices."); throw new Error.Failure("This command doesn't work on linked devices.");
} catch (UnregisteredRecipientException e) { } catch (UnregisteredRecipientException e) {

View file

@ -151,6 +151,13 @@ public class JsonRpcReader {
} }
private JsonRpcMessage parseJsonRpcMessage(final String input) { private JsonRpcMessage parseJsonRpcMessage(final String input) {
if (input.trim().isEmpty()) {
jsonRpcSender.sendResponse(JsonRpcResponse.forError(new JsonRpcResponse.Error(JsonRpcResponse.Error.PARSE_ERROR,
"Empty input line",
null), null));
return null;
}
final JsonNode jsonNode; final JsonNode jsonNode;
try { try {
jsonNode = objectMapper.readTree(input); jsonNode = objectMapper.readTree(input);