Compare commits

...

130 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
AsamK
f14c204764 Bump version to 0.13.12 2025-01-18 20:53:46 +01:00
AsamK
71d3b83a1c Update user agent 2025-01-18 20:24:01 +01:00
AsamK
148bf7dee2 Add man page to build tar file
Fixes #1660
2025-01-18 20:07:41 +01:00
AsamK
2d1ba6b4ca Extend man Makefile 2025-01-18 19:56:23 +01:00
AsamK
055a8ee8b9 Add general description for global/subcommand arguments
Fixes #1649
2025-01-18 16:59:25 +01:00
AsamK
407a20d4bb Update client dependencies 2025-01-18 16:53:09 +01:00
AsamK
05cd6aee6a Add version and group to all modules 2025-01-18 16:30:28 +01:00
AsamK
a1378507b2 Rename lib to libsignal-cli 2025-01-18 16:30:07 +01:00
AsamK
78cd0b13de Update dependencies 2025-01-18 16:08:43 +01:00
AsamK
c25468a71e Update reflect-config.json 2025-01-17 16:09:39 +01:00
AsamK
a5d2e1ea23 Use getRawQuery to prevent double decoding the query
Fixes #1682
2025-01-17 16:03:00 +01:00
AsamK
6acf16ef4e Improve tests 2025-01-14 23:12:45 +01:00
AsamK
e11e093020 Enable sqlite WAL journal_mode
Related #1670
2025-01-14 22:35:45 +01:00
AsamK
74c2604dc8 Set sqlite PRAGMA via Url 2025-01-14 22:31:36 +01:00
AsamK
e4af0be0ad Use existing connection to read configuration during storage sync 2025-01-14 21:33:12 +01:00
AsamK
5ac5938c8b Reduce log level of invalid sync contact address
Fixes #1663
2025-01-14 20:41:04 +01:00
AsamK
94269744ad Improve final address when merging recipients 2025-01-14 20:30:06 +01:00
AsamK
7a25ae5b9c Fix reading nickname from storage record
Fixes #1664
2025-01-14 20:30:06 +01:00
AsamK
cbd92654cf Store pni correctly in storage record 2025-01-14 20:30:06 +01:00
AsamK
bd95373a70 Parse unregisteredAtTimestamp correctly
Fixes #1651
Fixes #1646
2025-01-14 20:30:06 +01:00
AsamK
d982633215 Update dependencies 2025-01-14 20:30:06 +01:00
AsamK
f91ca82902 Prepare next release 2025-01-14 20:30:06 +01:00
Slayer
c55ee85c5c Fixing RW connections deadlock on SQLite
Without this change we're getting a connection in the same thread we hold one already.
2025-01-14 12:21:12 +01:00
AsamK
a3776c88bd Bump version to 0.13.11 2024-12-26 20:31:28 +01:00
AsamK
4a781656b4 Update dependencies 2024-12-26 20:23:08 +01:00
AsamK
11d38f29ef Improve splitting of long message bodies 2024-12-26 20:15:19 +01:00
AsamK
22a0ff976a Update libsignal-service 2024-12-26 20:14:53 +01:00
AsamK
c05b47e4d0 Delete storage id of unregistered recipients after remote update 2024-12-25 17:19:33 +01:00
AsamK
ac145e6a27 Ignore destination if it's an empty uuid
Fixes #1643
2024-12-25 16:23:36 +01:00
AsamK
f00b8523d9 Update dependencies 2024-12-15 21:18:02 +01:00
AsamK
c3f8d68ceb Create account entropy pool instead of master key 2024-12-15 21:14:40 +01:00
AsamK
9d92a3e06b Update libsignal-service 2024-12-15 21:13:59 +01:00
AsamK
f2df600d38 Update CHANGELOG.md
Fixes #1641
2024-12-01 10:12:46 +01:00
AsamK
24d344fda4 Prepare next release 2024-11-30 16:32:31 +01:00
105 changed files with 2235 additions and 1108 deletions

View file

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ '21', '23' ]
java: [ '21', '24' ]
steps:
- uses: actions/checkout@v4
@ -26,11 +26,22 @@ jobs:
distribution: 'zulu'
java-version: ${{ matrix.java }}
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
uses: gradle/actions/setup-gradle@v4
with:
dependency-graph: generate-and-submit
- name: Install asciidoc
run: sudo apt update && sudo apt --no-install-recommends install -y asciidoc-base
- name: Build with Gradle
run: ./gradlew --no-daemon build
- name: Build man page
run: |
cd man
make install
- name: Add man page to archive
run: |
version=$(tar tf build/distributions/signal-cli-*.tar | head -n1 | sed 's|signal-cli-\([^/]*\)/.*|\1|')
echo $version
tar --transform="flags=r;s|man|signal-cli-${version}/man|" -rf build/distributions/signal-cli-${version}.tar man/man{1,5}
- name: Compress archive
run: gzip -n -9 build/distributions/signal-cli-*.tar
- name: Archive production artifacts
@ -58,3 +69,28 @@ jobs:
with:
name: signal-cli-native
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.
- 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
# with:
# languages: go, javascript, csharp, python, cpp, java
@ -43,7 +43,7 @@ jobs:
# 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)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -57,4 +57,4 @@ jobs:
# make release
- 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
rm -r signal-cli-archive-* signal-cli-native
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
id: build_image

View file

@ -1,7 +1,112 @@
# 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
Requires libsignal-client version 0.65.2.
### Fixed
- Fix sync of contact nick name
- Fix incorrectly marking recipients as unregistered after sync
- Fix cause of database deadlock (Thanks @dukhaSlayer)
- Fix parsing of account query param in events http endpoint
### Changed
- Enable sqlite WAL journal\_mode for improved performance
## [0.13.11] - 2024-12-26
Requires libsignal-client version 0.64.0.
### Fixed
- Fix issue with receiving messages that have an invalid destination
## [0.13.10] - 2024-11-30
Requires libsignal-client version 0.62.0.
### Fixed
- Fix receiving some unusual contact sync messages

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)) .
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
You can [build signal-cli](#building) yourself or use
@ -55,8 +59,15 @@ of all country codes.)
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
to the voice call verification by adding the `--voice` switch at the end of above register command.
You can register Signal using a landline number. In this case, you need to follow the procedure below:
* 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
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
```
* 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.
uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT

View file

@ -3,10 +3,13 @@ plugins {
application
eclipse
`check-lib-versions`
id("org.graalvm.buildtools.native") version "0.10.3"
id("org.graalvm.buildtools.native") version "0.10.6"
}
version = "0.13.10"
allprojects {
group = "org.asamk"
version = "0.13.19-SNAPSHOT"
}
java {
sourceCompatibility = JavaVersion.VERSION_21
@ -21,6 +24,7 @@ java {
application {
mainClass.set("org.asamk.signal.Main")
applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED")
}
graalvmNative {
@ -29,6 +33,7 @@ graalvmNative {
buildArgs.add("--install-exit-handlers")
buildArgs.add("-Dfile.encoding=UTF-8")
buildArgs.add("-J-Dfile.encoding=UTF-8")
buildArgs.add("-march=compatibility")
resources.autodetect()
configurationFileDirectories.from(file("graalvm-config-dir"))
if (System.getenv("GRAALVM_HOME") == null) {
@ -43,7 +48,41 @@ graalvmNative {
}
}
val artifactType = Attribute.of("artifactType", String::class.java)
val minified = Attribute.of("minified", Boolean::class.javaObjectType)
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.jackson.databind)
implementation(libs.argparse4j)
@ -51,7 +90,7 @@ dependencies {
implementation(libs.slf4j.api)
implementation(libs.slf4j.jul)
implementation(libs.logback)
implementation(project(":lib"))
implementation(project(":libsignal-cli"))
}
configurations {
@ -75,12 +114,13 @@ tasks.withType<Jar> {
attributes(
"Implementation-Title" to project.name,
"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")
exclude(
"META-INF/*.SF",
@ -89,9 +129,11 @@ task("fatJar", type = Jar::class) {
"META-INF/NOTICE*",
"META-INF/LICENSE*",
"META-INF/INDEX.LIST",
"**/module-info.class"
"**/module-info.class",
)
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())
}

View file

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

1026
client/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -60,8 +60,13 @@ async fn handle_command(
.delete_local_account_data(cli.account, ignore_registered)
.await
}
CliCommands::GetUserStatus { recipient } => {
client.get_user_status(cli.account, recipient).await
CliCommands::GetUserStatus {
recipient,
username,
} => {
client
.get_user_status(cli.account, recipient, username)
.await
}
CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await,
CliCommands::Link { name } => {
@ -70,7 +75,7 @@ async fn handle_command(
.await
.map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))?
.device_link_uri;
println!("{}", url);
println!("{url}");
client.finish_link(url, name).await
}
CliCommands::ListAccounts => client.list_accounts().await,
@ -139,6 +144,7 @@ async fn handle_command(
end_session,
message,
attachment,
view_once,
mention,
text_style,
quote_timestamp,
@ -165,6 +171,7 @@ async fn handle_command(
end_session,
message.unwrap_or_default(),
attachment,
view_once,
mention,
text_style,
quote_timestamp,
@ -477,23 +484,30 @@ async fn connect(cli: Cli) -> Result<Value, RpcError> {
handle_command(cli, client).await
} else {
let socket_path = cli
.json_rpc_socket
.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()
#[cfg(windows)]
{
Err(RpcError::Custom("Invalid socket".into()))
}
#[cfg(unix)]
{
let socket_path = cli
.json_rpc_socket
.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());
let client = jsonrpc::connect_unix(socket_path)
.await
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
.unwrap_or_else(|| ("/run".to_owned() + DEFAULT_SOCKET_SUFFIX).into());
let client = jsonrpc::connect_unix(socket_path)
.await
.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 jsonrpsee::core::{
async_trait,
client::{ReceivedMessage, TransportReceiverT, TransportSenderT},
};
use jsonrpsee::core::client::{ReceivedMessage, TransportReceiverT, TransportSenderT};
use thiserror::Error;
#[cfg(unix)]
pub mod ipc;
mod stream_codec;
pub mod tcp;
@ -21,7 +19,6 @@ struct Sender<T: Send + Sink<String>> {
inner: T,
}
#[async_trait]
impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT
for Sender<T>
{
@ -31,7 +28,7 @@ impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> T
self.inner
.send(body)
.await
.map_err(|e| Errors::Other(format!("{:?}", e)))?;
.map_err(|e| Errors::Other(format!("{e:?}")))?;
Ok(())
}
@ -39,7 +36,7 @@ impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> T
self.inner
.close()
.await
.map_err(|e| Errors::Other(format!("{:?}", e)))?;
.map_err(|e| Errors::Other(format!("{e:?}")))?;
Ok(())
}
}
@ -48,7 +45,6 @@ struct Receiver<T: Send + Stream> {
inner: T,
}
#[async_trait]
impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static> TransportReceiverT
for Receiver<T>
{
@ -58,7 +54,7 @@ impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static>
match self.inner.next().await {
None => Err(Errors::Closed),
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()) {
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 {
Ok(None)

View file

@ -45,6 +45,30 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<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">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.12</url>
</release>
<release version="0.13.11" date="2024-12-26">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.11</url>
</release>
<release version="0.13.10" date="2024-11-30">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.10</url>
</release>

View file

@ -27,6 +27,10 @@
{
"name":"java.lang.ClassNotFoundException"
},
{
"name":"java.lang.Enum",
"methods":[{"name":"ordinal","parameterTypes":[] }]
},
{
"name":"java.lang.IllegalArgumentException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
@ -48,9 +52,13 @@
{
"name":"java.lang.String"
},
{
"name":"java.lang.Thread",
"methods":[{"name":"currentThread","parameterTypes":[] }, {"name":"getStackTrace","parameterTypes":[] }]
},
{
"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",
@ -88,7 +96,11 @@
},
{
"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",
@ -110,6 +122,14 @@
{
"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",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
@ -187,6 +207,9 @@
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore$Direction",
"fields":[{"name":"RECEIVING"}, {"name":"SENDING"}]
},
{
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore$IdentityChange"
},
{
"name":"org.signal.libsignal.protocol.state.KyberPreKeyRecord",
"fields":[{"name":"unsafeHandle"}]

View file

@ -39,6 +39,9 @@
{
"name":"[Ljava.sql.Statement;"
},
{
"name":"[Lorg.asamk.signal.commands.ListStickerPacksCommand$JsonStickerPack$JsonSticker;"
},
{
"name":"[Lorg.asamk.signal.json.JsonAttachment;"
},
@ -75,6 +78,9 @@
{
"name":"[Lorg.asamk.signal.manager.storage.accounts.AccountsStorage$Account;"
},
{
"name":"[Lorg.asamk.signal.manager.storage.stickerPacks.JsonStickerPack$JsonSticker;"
},
{
"name":"[Lorg.whispersystems.signalservice.api.groupsv2.TemporalCredential;"
},
@ -665,6 +671,10 @@
{
"name":"long[]"
},
{
"name":"okhttp3.internal.connection.RealConnectionPool",
"fields":[{"name":"addressStates"}]
},
{
"name":"okio.BufferedSink"
},
@ -1403,6 +1413,12 @@
"name":"org.asamk.signal.manager.storage.profiles.LegacyProfileStore$ProfileStoreDeserializer",
"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",
"allDeclaredFields":true,
@ -1614,6 +1630,10 @@
"name":"org.bouncycastle.jcajce.provider.asymmetric.NTRU$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.NoSig$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
@ -2034,7 +2054,10 @@
"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"
@ -2296,7 +2319,7 @@
"allDeclaredFields":true,
"allDeclaredMethods":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",
@ -2323,6 +2346,13 @@
{
"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",
"allDeclaredFields":true,
@ -2390,7 +2420,14 @@
"name":"org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite",
"allDeclaredFields":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",
@ -2435,6 +2472,12 @@
"queryAllDeclaredConstructors":true,
"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",
"allDeclaredFields":true,
@ -2913,7 +2956,28 @@
"allDeclaredFields":true,
"queryAllDeclaredMethods":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",
@ -2940,12 +3004,21 @@
"allDeclaredFields":true,
"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$Companion"
},
{
"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"
},
@ -2961,6 +3034,9 @@
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$UsernameLink",
"allDeclaredFields":true
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.AvatarColor"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord",
"allDeclaredFields":true,
@ -2994,6 +3070,7 @@
{
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record",
"allDeclaredFields":true,
"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":[] }]
},
{

View file

@ -1,17 +1,17 @@
[versions]
slf4j = "2.0.16"
slf4j = "2.0.17"
[libraries]
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.79"
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.18.1"
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.81"
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.19.1"
argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0"
dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0"
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
logback = "ch.qos.logback:logback-classic:1.5.12"
logback = "ch.qos.logback:logback-classic:1.5.18"
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_112"
sqlite = "org.xerial:sqlite-jdbc:3.47.0.0"
hikari = "com.zaxxer:HikariCP:6.2.1"
junit-jupiter = "org.junit.jupiter:junit-jupiter:5.11.3"
junit-launcher = "org.junit.platform:junit-platform-launcher:1.11.3"
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_127"
sqlite = "org.xerial:sqlite-jdbc:3.50.2.0"
hikari = "com.zaxxer:HikariCP:6.3.0"
junit-jupiter = "org.junit.jupiter:junit-jupiter:5.13.2"
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
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
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

11
gradlew vendored
View file

@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# 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
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -115,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@ -206,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# 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.
# * 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.
@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.

4
gradlew.bat vendored
View file

@ -70,11 +70,11 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set CLASSPATH=
@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
@rem End local scope for the variables with windows NT shell

View file

@ -1,5 +1,7 @@
package org.asamk.signal.manager;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import org.asamk.signal.manager.api.AlreadyReceivingException;
import org.asamk.signal.manager.api.AttachmentInvalidException;
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.Pair;
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.RateLimitException;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import java.io.Closeable;
import java.io.File;
@ -65,7 +67,7 @@ import java.util.Set;
public interface Manager extends Closeable {
static boolean isValidNumber(final String e164Number, final String countryCode) {
return PhoneNumberFormatter.isValidNumber(e164Number, countryCode);
return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode);
}
static boolean isSignalClientAvailable() {
@ -94,7 +96,7 @@ public interface Manager extends Closeable {
*/
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(
String deviceName,
@ -139,7 +141,7 @@ public interface Manager extends Closeable {
String newNumber,
String verificationCode,
String pin
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException;
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException;
void unregister() throws IOException;
@ -237,9 +239,12 @@ public interface Manager extends Closeable {
void deleteContact(RecipientIdentifier.Single recipient);
void setContactName(
RecipientIdentifier.Single recipient,
String givenName,
final String familyName
final RecipientIdentifier.Single recipient,
final String givenName,
final String familyName,
final String nickGivenName,
final String nickFamilyName,
final String note
) throws NotPrimaryDeviceException, UnregisteredRecipientException;
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.IncorrectPinException;
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.RateLimitException;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
@ -21,7 +22,7 @@ public interface RegistrationManager extends Closeable {
void verifyAccount(
String verificationCode,
String pin
) throws IOException, PinLockedException, IncorrectPinException;
) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException;
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.NotRegisteredException;
import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.ServiceEnvironment;
import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
@ -63,19 +64,28 @@ public class SignalAccountFiles {
return accountsStore.getAllNumbers();
}
public MultiAccountManager initMultiAccountManager() throws IOException {
final var managers = accountsStore.getAllAccounts().parallelStream().map(a -> {
public MultiAccountManager initMultiAccountManager() throws IOException, AccountCheckException {
final var managerPairs = accountsStore.getAllAccounts().parallelStream().map(a -> {
try {
return initManager(a.number(), a.path());
} catch (NotRegisteredException | IOException | AccountCheckException e) {
return new Pair<Manager, Throwable>(initManager(a.number(), a.path()), null);
} catch (NotRegisteredException e) {
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
return null;
} catch (Throwable e) {
} catch (AccountCheckException | IOException e) {
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();
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);
}

View file

@ -19,9 +19,7 @@ public class RenewSessionAction implements HandleAction {
@Override
public void execute(Context context) throws Throwable {
context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId);
if (!recipientId.equals(context.getAccount().getSelfRecipientId())) {
context.getSendHelper().sendNullMessage(recipientId);
}
context.getSendHelper().sendNullMessage(recipientId);
}
@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.DecryptionErrorMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.internal.push.Envelope;
import java.util.Optional;
@ -15,29 +14,21 @@ import java.util.Optional;
public class SendRetryMessageRequestAction implements HandleAction {
private final RecipientId recipientId;
private final ServiceId serviceId;
private final ProtocolException protocolException;
private final SignalServiceEnvelope envelope;
private final ServiceId accountId;
public SendRetryMessageRequestAction(
final RecipientId recipientId,
final ServiceId serviceId,
final ProtocolException protocolException,
final SignalServiceEnvelope envelope,
final ServiceId accountId
final SignalServiceEnvelope envelope
) {
this.recipientId = recipientId;
this.serviceId = serviceId;
this.protocolException = protocolException;
this.envelope = envelope;
this.accountId = accountId;
}
@Override
public void execute(Context context) throws Throwable {
context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId);
int senderDevice = protocolException.getSenderDevice();
Optional<GroupId> groupId = protocolException.getGroupId().isPresent() ? Optional.of(GroupId.unknownVersion(
protocolException.getGroupId().get())) : Optional.empty();

View file

@ -49,8 +49,12 @@ public record Contact(
builder.givenName = copy.givenName();
builder.familyName = copy.familyName();
builder.nickName = copy.nickName();
builder.nickNameGivenName = copy.nickNameGivenName();
builder.nickNameFamilyName = copy.nickNameFamilyName();
builder.note = copy.note();
builder.color = copy.color();
builder.messageExpirationTime = copy.messageExpirationTime();
builder.messageExpirationTimeVersion = copy.messageExpirationTimeVersion();
builder.muteUntil = copy.muteUntil();
builder.hideStory = copy.hideStory();
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.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import java.net.URI;
@ -37,7 +36,7 @@ public record DeviceLinkUrl(String deviceIdentifier, ECPublicKey deviceKey) {
}
ECPublicKey deviceKey;
try {
deviceKey = Curve.decodePoint(publicKeyBytes, 0);
deviceKey = new ECPublicKey(publicKeyBytes);
} catch (InvalidKeyException 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 {
InvalidNumberException(String message) {
public InvalidNumberException(String message) {
super(message);
}

View file

@ -6,6 +6,7 @@ import java.util.Optional;
public record Message(
String messageText,
List<String> attachments,
boolean viewOnce,
List<Mention> mentions,
Optional<Quote> quote,
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;
import org.asamk.signal.manager.util.PhoneNumberFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.UUID;
@ -24,32 +24,28 @@ public sealed interface RecipientIdentifier {
sealed interface Single extends RecipientIdentifier {
static Single fromString(String identifier, String localNumber) throws InvalidNumberException {
try {
if (UuidUtil.isUuid(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 (UuidUtil.isUuid(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);
}
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.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
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.SignalCdsiUrl;
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
@ -28,8 +28,9 @@ class LiveConfig {
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
.decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF");
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
private static final String SVR2_MRENCLAVE = "9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570";
private static final String SVR2_LEGACY_MRENCLAVE = "a6622ad4656e1abcd0bc0ff17c229477747d2ded0495c4ebee7ed35c1789fa97";
private static final String SVR2_MRENCLAVE_LEGACY_LEGACY = "9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570";
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 CDN_URL = "https://cdn.signal.org";
@ -42,6 +43,7 @@ class LiveConfig {
private static final Optional<Dns> dns = 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()
.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,
dns,
proxy,
systemProxy,
zkGroupServerPublicParams,
genericServerPublicParams,
backupServerPublicParams,
@ -77,7 +80,7 @@ class LiveConfig {
static ECPublicKey getUnidentifiedSenderTrustRoot() {
try {
return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0);
return new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
@ -89,7 +92,7 @@ class LiveConfig {
createDefaultServiceConfiguration(interceptors),
getUnidentifiedSenderTrustRoot(),
CDSI_MRENCLAVE,
List.of(SVR2_MRENCLAVE, SVR2_LEGACY_MRENCLAVE));
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_LEGACY_LEGACY));
}
private LiveConfig() {

View file

@ -20,7 +20,7 @@ public class ServiceConfig {
public static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
public static final long MAX_ENVELOPE_SIZE = 0;
public static final int MAX_MESSAGE_BODY_SIZE = 2000;
public static final int MAX_MESSAGE_SIZE_BYTES = 2000;
public static final long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024;
public static final boolean AUTOMATIC_NETWORK_RETRY = true;
public static final int GROUP_MAX_SIZE = 1001;
@ -30,7 +30,8 @@ public class ServiceConfig {
public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) {
final var deleteSync = !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(

View file

@ -2,9 +2,9 @@ package org.asamk.signal.manager.config;
import org.signal.libsignal.net.Network;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
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.SignalCdsiUrl;
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
@ -28,8 +28,9 @@ class StagingConfig {
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
.decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx");
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
private static final String SVR2_MRENCLAVE = "38e01eff4fe357dc0b0e8ef7a44b4abc5489fbccba3a78780f3872c277f62bf3";
private static final String SVR2_LEGACY_MRENCLAVE = "acb1973aa0bbbd14b3b4e06f145497d948fd4a98efc500fcce363b3b743ec482";
private static final String SVR2_MRENCLAVE_LEGACY_LEGACY = "38e01eff4fe357dc0b0e8ef7a44b4abc5489fbccba3a78780f3872c277f62bf3";
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 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<SignalProxy> proxy = Optional.empty();
private static final Optional<HttpProxy> systemProxy = Optional.empty();
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==");
@ -69,6 +71,7 @@ class StagingConfig {
interceptors,
dns,
proxy,
systemProxy,
zkGroupServerPublicParams,
genericServerPublicParams,
backupServerPublicParams,
@ -77,7 +80,7 @@ class StagingConfig {
static ECPublicKey getUnidentifiedSenderTrustRoot() {
try {
return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0);
return new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
@ -89,7 +92,7 @@ class StagingConfig {
createDefaultServiceConfiguration(interceptors),
getUnidentifiedSenderTrustRoot(),
CDSI_MRENCLAVE,
List.of(SVR2_MRENCLAVE, SVR2_LEGACY_MRENCLAVE));
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_LEGACY_LEGACY));
}
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.IncorrectPinException;
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.RateLimitException;
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.SignalServiceAddress;
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.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
@ -50,7 +52,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import okio.ByteString;
@ -102,9 +104,9 @@ public class AccountHelper {
checkWhoAmiI();
}
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.getRegistrationLockPin() != null) {
migrateRegistrationPin();
@ -184,7 +186,7 @@ public class AccountHelper {
String newNumber,
String verificationCode,
String pin
) throws IncorrectPinException, PinLockedException, IOException {
) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
for (var attempts = 0; attempts < 5; attempts++) {
try {
finishChangeNumberInternal(newNumber, verificationCode, pin);
@ -204,7 +206,7 @@ public class AccountHelper {
String newNumber,
String verificationCode,
String pin
) throws IncorrectPinException, PinLockedException, IOException {
) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
final var pniIdentity = KeyUtils.generateIdentityKeyPair();
final var encryptedDeviceMessages = new ArrayList<OutgoingPushMessage>();
final var devicePniSignedPreKeys = new HashMap<Integer, SignedPreKeyEntity>();
@ -289,12 +291,13 @@ public class AccountHelper {
context.getPinHelper(),
(sessionId1, verificationCode1, registrationLock) -> {
final var registrationApi = dependencies.getRegistrationApi();
final var accountApi = dependencies.getAccountApi();
try {
handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1));
} catch (AlreadyVerifiedException e) {
// Already verified so can continue changing number
}
return handleResponseException(registrationApi.changeNumber(new ChangePhoneNumberRequest(sessionId1,
return handleResponseException(accountApi.changeNumber(new ChangePhoneNumberRequest(sessionId1,
null,
newNumber,
registrationLock,
@ -378,7 +381,7 @@ public class AccountHelper {
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());
if (hashIndex == -1) {
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.");
final var username = candidates.get(hashIndex);
final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username);
final var linkComponents = confirmUsernameAndCreateNewLink(username);
account.setUsername(username.getUsername());
account.setUsernameLink(linkComponents);
account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
@ -396,6 +399,40 @@ public class AccountHelper {
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 {
final var localUsername = account.getUsername();
if (localUsername == null) {
@ -438,14 +475,14 @@ public class AccountHelper {
final var usernameLink = account.getUsernameLink();
if (usernameLink == null) {
dependencies.getAccountManager()
.reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash())));
handleResponseException(dependencies.getAccountApi()
.reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash()))));
logger.debug("[reserveUsername] Successfully reserved existing username.");
final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username);
final var linkComponents = confirmUsernameAndCreateNewLink(username);
account.setUsernameLink(linkComponents);
logger.debug("[confirmUsername] Successfully confirmed existing username.");
} else {
final var linkComponents = dependencies.getAccountManager().reclaimUsernameAndLink(username, usernameLink);
final var linkComponents = reclaimUsernameAndLink(username, usernameLink);
account.setUsernameLink(linkComponents);
logger.debug("[confirmUsername] Successfully reclaimed existing username and link.");
}
@ -455,7 +492,7 @@ public class AccountHelper {
private void tryToSetUsernameLink(Username username) {
for (var i = 1; i < 4; i++) {
try {
final var linkComponents = dependencies.getAccountManager().createUsernameLink(username);
final var linkComponents = createUsernameLink(username);
account.setUsernameLink(linkComponents);
break;
} catch (IOException e) {
@ -465,9 +502,8 @@ public class AccountHelper {
}
public void deleteUsername() throws IOException {
dependencies.getAccountManager().deleteUsernameLink();
handleResponseException(dependencies.getAccountApi().deleteUsername());
account.setUsernameLink(null);
dependencies.getAccountManager().deleteUsername();
account.setUsername(null);
logger.debug("[deleteUsername] Successfully deleted the username.");
}
@ -479,7 +515,7 @@ public class AccountHelper {
}
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 {
@ -500,9 +536,9 @@ public class AccountHelper {
account.getAciIdentityKeyPair(),
account.getPniIdentityKeyPair(),
account.getProfileKey(),
account.getOrCreateAccountEntropyPool(),
account.getOrCreatePinMasterKey(),
account.getOrCreateMediaRootBackupKey(),
account.getOrCreateAccountEntropyPool(),
verificationCode.getVerificationCode(),
null));
account.setMultiDevice(true);
@ -510,8 +546,8 @@ public class AccountHelper {
}
public void removeLinkedDevices(int deviceId) throws IOException {
dependencies.getAccountManager().removeDevice(deviceId);
var devices = dependencies.getAccountManager().getDevices();
handleResponseException(dependencies.getLinkDeviceApi().removeDevice(deviceId));
var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
account.setMultiDevice(devices.size() > 1);
}
@ -519,14 +555,16 @@ public class AccountHelper {
var masterKey = account.getOrCreatePinMasterKey();
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
dependencies.getAccountManager().enableRegistrationLock(masterKey);
handleResponseException(dependencies.getAccountApi()
.enableRegistrationLock(masterKey.deriveRegistrationLock()));
}
public void setRegistrationPin(String pin) throws IOException {
var masterKey = account.getOrCreatePinMasterKey();
context.getPinHelper().setRegistrationLockPin(pin, masterKey);
dependencies.getAccountManager().enableRegistrationLock(masterKey);
handleResponseException(dependencies.getAccountApi()
.enableRegistrationLock(masterKey.deriveRegistrationLock()));
account.setRegistrationLockPin(pin);
updateAccountAttributes();
@ -535,7 +573,7 @@ public class AccountHelper {
public void removeRegistrationPin() throws IOException {
// Remove KBS Pin
context.getPinHelper().removeRegistrationLockPin();
dependencies.getAccountManager().disableRegistrationLock();
handleResponseException(dependencies.getAccountApi().disableRegistrationLock());
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.
// 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.
dependencies.getAccountManager().setGcmId(Optional.empty());
handleResponseException(dependencies.getAccountApi().clearFcmToken());
account.setRegistered(false);
unregisteredListener.call();
@ -558,7 +596,7 @@ public class AccountHelper {
}
account.setRegistrationLockPin(null);
dependencies.getAccountManager().deleteAccount();
handleResponseException(dependencies.getAccountApi().deleteAccount());
account.setRegistered(false);
unregisteredListener.call();

View file

@ -9,6 +9,7 @@ import org.asamk.signal.manager.util.IOUtils;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
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 {
var attachmentStreams = createAttachmentStreams(attachments);
final var attachmentStreams = createAttachmentStreams(attachments);
// Upload attachments here, so we only upload once even for multiple recipients
var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size());
for (var attachmentStream : attachmentStreams) {
attachmentPointers.add(uploadAttachment(attachmentStream));
try {
// Upload attachments here, so we only upload once even for multiple recipients
final var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size());
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 {
@ -132,9 +139,15 @@ public class AttachmentHelper {
SignalServiceAttachmentPointer pointer,
File tmpFile
) throws IOException {
if (pointer.getDigest().isEmpty()) {
throw new IOException("Attachment pointer has no digest.");
}
try {
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) {
throw new IOException(e);
}

View file

@ -17,7 +17,14 @@ public class ContactHelper {
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);
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
builder.withIsHidden(false);
@ -27,6 +34,15 @@ public class ContactHelper {
if (familyName != null) {
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());
}

View file

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

View file

@ -28,6 +28,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.groupsv2.DecryptChangeVerificationMode;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupResponse;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
@ -43,6 +44,7 @@ import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
import java.io.IOException;
import java.util.ArrayList;
@ -118,6 +120,8 @@ class GroupV2Helper {
groupsV2AuthorizationString,
false,
sendEndorsementsExpirationMs);
} catch (NotInGroupException e) {
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
} catch (NonSuccessfulResponseCodeException e) {
if (e.code == 403) {
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
@ -652,11 +656,13 @@ class GroupV2Helper {
DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) {
if (signedGroupChange != null) {
var groupOperations = dependencies.getGroupsV2Operations()
.forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
final var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
final var groupId = groupSecretParams.getPublicParams().getGroupIdentifier();
try {
return groupOperations.decryptChange(GroupChange.ADAPTER.decode(signedGroupChange), true).orElse(null);
return groupOperations.decryptChange(GroupChange.ADAPTER.decode(signedGroupChange),
DecryptChangeVerificationMode.verify(groupId)).orElse(null);
} catch (VerificationFailedException | InvalidGroupStateException | IOException e) {
return null;
}

View file

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

View file

@ -88,7 +88,11 @@ public class PinHelper {
IOException exception = null;
for (final var secureValueRecovery : secureValueRecoveries) {
try {
return getRegistrationLockData(secureValueRecovery, svr2Credentials, pin);
final var lockData = getRegistrationLockData(secureValueRecovery, svr2Credentials, pin);
if (lockData == null) {
continue;
}
return lockData;
} catch (IOException 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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.NetworkResultUtil;
import org.whispersystems.signalservice.api.account.PreKeyUpload;
import org.whispersystems.signalservice.api.keys.OneTimePreKeyCounts;
import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.internal.push.OneTimePreKeyCounts;
import java.io.IOException;
import java.util.List;
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.util.Utils.handleResponseException;
public class PreKeyHelper {
@ -82,7 +84,7 @@ public class PreKeyHelper {
) throws IOException {
OneTimePreKeyCounts preKeyCounts;
try {
preKeyCounts = dependencies.getAccountManager().getPreKeyCounts(serviceIdType);
preKeyCounts = handleResponseException(dependencies.getKeysApi().getAvailablePreKeyCounts(serviceIdType));
} catch (AuthorizationFailedException e) {
logger.debug("Failed to get pre key count, ignoring: " + e.getClass().getSimpleName());
preKeyCounts = new OneTimePreKeyCounts(0, 0);
@ -143,7 +145,7 @@ public class PreKeyHelper {
kyberPreKeyRecords);
var needsReset = false;
try {
dependencies.getAccountManager().setPreKeys(preKeyUpload);
NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeys(preKeyUpload));
try {
if (preKeyRecords != null) {
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.NetworkResultUtil;
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.profiles.AvatarUploadParams;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
@ -196,9 +197,10 @@ public final class ProfileHelper {
: avatar == null ? AvatarUploadParams.unchanged(true) : AvatarUploadParams.unchanged(false);
final var paymentsAddress = Optional.ofNullable(newProfile.getMobileCoinAddress())
.map(address -> PaymentUtils.signPaymentsAddress(address,
account.getAciIdentityKeyPair().getPrivateKey()));
account.getAciIdentityKeyPair().getPrivateKey()))
.orElse(null);
logger.debug("Uploading new profile");
final var avatarPath = dependencies.getAccountManager()
final var avatarPath = NetworkResultUtil.toSetProfileLegacy(dependencies.getProfileApi()
.setVersionedProfile(account.getAci(),
account.getProfileKey(),
newProfile.getInternalServiceName(),
@ -208,9 +210,9 @@ public final class ProfileHelper {
avatarUploadParams,
List.of(/* TODO implement support for badges */),
account.getConfigurationStore().getPhoneNumberSharingMode()
== PhoneNumberSharingMode.EVERYBODY);
== PhoneNumberSharingMode.EVERYBODY));
if (!avatarUploadParams.keepTheSame) {
builder.withAvatarUrlPath(avatarPath.orElse(null));
builder.withAvatarUrlPath(avatarPath);
}
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalWebSocket;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.ServiceId;
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.WebSocketUnavailableException;
@ -28,7 +28,6 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class ReceiveHelper {
@ -94,14 +93,14 @@ public class ReceiveHelper {
// Use a Map here because java Set doesn't have a get method ...
Map<HandleAction, HandleAction> queuedActions = new HashMap<>();
final var signalWebSocket = dependencies.getSignalWebSocket();
final var webSocketStateDisposable = Observable.merge(signalWebSocket.getUnidentifiedWebSocketState(),
signalWebSocket.getWebSocketState())
final var signalWebSocket = dependencies.getAuthenticatedSignalWebSocket();
final var webSocketStateDisposable = signalWebSocket.getState()
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation())
.distinctUntilChanged()
.subscribe(this::onWebSocketStateChange);
signalWebSocket.connect();
signalWebSocket.registerKeepAliveToken("receive");
try {
receiveMessagesInternal(signalWebSocket, timeout, returnOnTimeout, maxMessages, handler, queuedActions);
@ -109,6 +108,7 @@ public class ReceiveHelper {
hasCaughtUpWithOldMessages = false;
handleQueuedActions(queuedActions.keySet());
queuedActions.clear();
signalWebSocket.removeKeepAliveToken("receive");
signalWebSocket.disconnect();
webSocketStateDisposable.dispose();
shouldStop = false;
@ -116,7 +116,7 @@ public class ReceiveHelper {
}
private void receiveMessagesInternal(
final SignalWebSocket signalWebSocket,
final SignalWebSocket.AuthenticatedWebSocket signalWebSocket,
Duration timeout,
boolean returnOnTimeout,
Integer maxMessages,

View file

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

View file

@ -198,17 +198,6 @@ public class StorageHelper {
logger.debug("Pre-Merge ID Difference :: {}", idDifference);
if (!idDifference.localOnlyIds().isEmpty()) {
final var updated = account.getRecipientStore()
.removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection, idDifference.localOnlyIds());
if (updated > 0) {
logger.warn(
"Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.",
updated);
}
}
if (!idDifference.isEmpty()) {
final var remoteOnlyRecords = getSignalStorageRecords(storageKey,
remoteManifest,
@ -221,6 +210,18 @@ public class StorageHelper {
remoteOnlyRecords.size());
}
if (!idDifference.localOnlyIds().isEmpty()) {
final var updated = account.getRecipientStore()
.removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection,
idDifference.localOnlyIds());
if (updated > 0) {
logger.warn(
"Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.",
updated);
}
}
final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords);
final var unknownDeletes = idDifference.localOnlyIds()
.stream()
@ -278,10 +279,22 @@ public class StorageHelper {
try (final var connection = account.getAccountDatabase().getConnection()) {
connection.setAutoCommit(false);
final var localStorageIds = getAllLocalStorageIds(connection);
final var idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds);
var localStorageIds = getAllLocalStorageIds(connection);
var idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds);
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 remoteInserts = buildLocalStorageRecords(connection, idDifference.localOnlyIds());
// TODO check if local storage record proto matches remote, then reset to remote storage_id
@ -352,7 +365,8 @@ public class StorageHelper {
final var storageId = newContactStorageIds.get(recipientId);
if (storageId.getType() == ManifestRecord.Identifier.Type.ACCOUNT.getValue()) {
final var recipient = account.getRecipientStore().getRecipient(connection, recipientId);
final var accountRecord = StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
final var accountRecord = StorageSyncModels.localToRemoteRecord(connection,
account.getConfigurationStore(),
recipient,
account.getUsernameLink());
newStorageRecords.add(new SignalStorageRecord(storageId,
@ -550,7 +564,8 @@ public class StorageHelper {
final var selfRecipient = account.getRecipientStore()
.getRecipient(connection, account.getSelfRecipientId());
final var record = StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
final var record = StorageSyncModels.localToRemoteRecord(connection,
account.getConfigurationStore(),
selfRecipient,
account.getUsernameLink());
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().account(record).build());
@ -592,7 +607,7 @@ public class StorageHelper {
final var remote = remoteByRawId.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);
localOnlyRawIds.remove(rawId);
hasTypeMismatch = true;

View file

@ -70,17 +70,12 @@ public class SyncHelper {
requestSyncData(SyncMessage.Request.Type.BLOCKED);
requestSyncData(SyncMessage.Request.Type.CONFIGURATION);
requestSyncKeys();
requestSyncPniIdentity();
}
public void requestSyncKeys() {
requestSyncData(SyncMessage.Request.Type.KEYS);
}
public void requestSyncPniIdentity() {
requestSyncData(SyncMessage.Request.Type.PNI_IDENTITY);
}
public SendMessageResult sendSyncFetchProfileMessage() {
return context.getSendHelper()
.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
@ -165,7 +160,7 @@ public class SyncHelper {
final var contact = contactPair.second();
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
final var deviceContact = getDeviceContact(address, recipientId, contact);
final var deviceContact = getDeviceContact(address, contact);
out.write(deviceContact);
deviceContact.getAvatar().ifPresent(a -> {
try {
@ -180,7 +175,7 @@ public class SyncHelper {
final var address = account.getSelfRecipientAddress();
final var recipientId = account.getSelfRecipientId();
final var contact = account.getContactStore().getContact(recipientId);
final var deviceContact = getDeviceContact(address, recipientId, contact);
final var deviceContact = getDeviceContact(address, contact);
out.write(deviceContact);
deviceContact.getAvatar().ifPresent(a -> {
try {
@ -216,34 +211,14 @@ public class SyncHelper {
}
@NotNull
private DeviceContact getDeviceContact(
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);
private DeviceContact getDeviceContact(final RecipientAddress address, final Contact contact) throws IOException {
return new DeviceContact(address.aci(),
address.number(),
Optional.ofNullable(contact == null ? null : contact.getName()),
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.messageExpirationTimeVersion()),
Optional.empty(),
contact != null && contact.isArchived());
Optional.empty());
}
public SendMessageResult sendBlockedList() {
@ -366,7 +341,7 @@ public class SyncHelper {
c = s.read();
} catch (IOException e) {
if (e.getMessage() != null && e.getMessage().contains("Missing contact address!")) {
logger.warn("Sync contacts contained invalid contact, ignoring: {}", e.getMessage());
logger.debug("Sync contacts contained invalid contact, ignoring: {}", e.getMessage());
continue;
} else {
throw e;
@ -376,9 +351,6 @@ public class SyncHelper {
break;
}
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);
var contact = account.getContactStore().getContact(recipientId);
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
@ -390,19 +362,6 @@ public class SyncHelper {
builder.withGivenName(c.getName().get());
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.getExpirationTimerVersion().isPresent() && (
contact == null || c.getExpirationTimerVersion().get() > contact.messageExpirationTimeVersion()
@ -417,7 +376,6 @@ public class SyncHelper {
contact == null ? 1 : contact.messageExpirationTimeVersion());
}
}
builder.withIsArchived(c.isArchived());
account.getContactStore().storeContact(recipientId, builder.build());
if (c.getAvatar().isPresent()) {

View file

@ -18,6 +18,8 @@ import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class UnidentifiedAccessHelper {
private static final Logger logger = LoggerFactory.getLogger(UnidentifiedAccessHelper.class);
@ -109,7 +111,8 @@ public class UnidentifiedAccessHelper {
return privacySenderCertificate.getSerialized();
}
try {
final var certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy();
final var certificate = handleResponseException(dependencies.getCertificateApi()
.getSenderCertificateForPhoneNumberPrivacy());
privacySenderCertificate = new SenderCertificate(certificate);
return certificate;
} catch (IOException | InvalidCertificateException e) {
@ -125,7 +128,7 @@ public class UnidentifiedAccessHelper {
return senderCertificate.getSerialized();
}
try {
final var certificate = dependencies.getAccountManager().getSenderCertificate();
final var certificate = handleResponseException(dependencies.getCertificateApi().getSenderCertificate());
this.senderCertificate = new SenderCertificate(certificate);
return certificate;
} 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.IncorrectPinException;
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.InvalidUsernameException;
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.PendingAdminApprovalException;
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.Profile;
import org.asamk.signal.manager.api.RateLimitException;
@ -68,7 +70,6 @@ import org.asamk.signal.manager.api.UserStatus;
import org.asamk.signal.manager.api.UsernameLinkUrl;
import org.asamk.signal.manager.api.UsernameStatus;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.helper.AccountFileUpdater;
import org.asamk.signal.manager.helper.Context;
@ -88,12 +89,12 @@ import org.asamk.signal.manager.storage.stickers.StickerPack;
import org.asamk.signal.manager.util.AttachmentUtils;
import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.MimeUtils;
import org.asamk.signal.manager.util.PhoneNumberFormatter;
import org.asamk.signal.manager.util.StickerUtils;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.usernames.BaseUsernameException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServicePreview;
@ -107,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.UsernameTakenException;
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.internal.util.Hex;
import org.whispersystems.signalservice.internal.util.Util;
@ -133,13 +132,18 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
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.stream.Collectors;
import java.util.stream.Stream;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import okio.Utf8;
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;
public class ManagerImpl implements Manager {
@ -158,6 +162,7 @@ public class ManagerImpl implements Manager {
private final List<Runnable> closedListeners = new ArrayList<>();
private final List<Runnable> addressChangedListeners = new ArrayList<>();
private final CompositeDisposable disposable = new CompositeDisposable();
private final AtomicLong lastMessageTimestamp = new AtomicLong();
public ManagerImpl(
SignalAccount account,
@ -168,15 +173,7 @@ public class ManagerImpl implements Manager {
) {
this.account = account;
final var sessionLock = new SignalSessionLock() {
private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
@Override
public Lock acquire() {
LEGACY_LOCK.lock();
return LEGACY_LOCK::unlock;
}
};
final var sessionLock = new ReentrantSignalSessionLock();
this.dependencies = new SignalDependencies(serviceEnvironmentConfig,
userAgent,
account.getCredentialsProvider(),
@ -288,7 +285,7 @@ public class ManagerImpl implements Manager {
}
@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>();
for (final var username : usernames) {
try {
@ -432,7 +429,7 @@ public class ManagerImpl implements Manager {
String newNumber,
String verificationCode,
String pin
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException {
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException {
if (!account.isPrimaryDevice()) {
throw new NotPrimaryDeviceException();
}
@ -454,10 +451,10 @@ public class ManagerImpl implements Manager {
String challenge,
String captcha
) throws IOException, CaptchaRejectedException {
captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
captcha = captcha == null ? "" : captcha.replace("signalcaptcha://", "");
try {
dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha);
handleResponseException(dependencies.getRateLimitChallengeApi().submitCaptchaChallenge(challenge, captcha));
} catch (org.whispersystems.signalservice.internal.push.exceptions.CaptchaRejectedException ignored) {
throw new CaptchaRejectedException();
}
@ -465,7 +462,7 @@ public class ManagerImpl implements Manager {
@Override
public List<Device> getLinkedDevices() throws IOException {
var devices = dependencies.getAccountManager().getDevices();
var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
account.setMultiDevice(devices.size() > 1);
var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
return devices.stream().map(d -> {
@ -604,6 +601,24 @@ public class ManagerImpl implements Manager {
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(
SignalServiceDataMessage.Builder messageBuilder,
Set<RecipientIdentifier> recipients,
@ -619,7 +634,7 @@ public class ManagerImpl implements Manager {
Optional<Long> editTargetTimestamp
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
long timestamp = System.currentTimeMillis();
long timestamp = getNextMessageTimestamp();
messageBuilder.withTimestamp(timestamp);
for (final var recipient : recipients) {
if (recipient instanceof RecipientIdentifier.NoteToSelf || (
@ -659,7 +674,7 @@ public class ManagerImpl implements Manager {
Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
final var timestamp = System.currentTimeMillis();
final var timestamp = getNextMessageTimestamp();
for (var recipient : recipients) {
if (recipient instanceof RecipientIdentifier.Single single) {
final var message = new SignalServiceTypingMessage(action, timestamp, Optional.empty());
@ -691,7 +706,7 @@ public class ManagerImpl implements Manager {
@Override
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,
messageIds,
timestamp);
@ -701,7 +716,7 @@ public class ManagerImpl implements Manager {
@Override
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,
messageIds,
timestamp);
@ -763,17 +778,24 @@ public class ManagerImpl implements Manager {
final Message message
) throws AttachmentInvalidException, IOException, UnregisteredRecipientException, InvalidStickerException {
final var additionalAttachments = new ArrayList<SignalServiceAttachment>();
if (message.messageText().length() > ServiceConfig.MAX_MESSAGE_BODY_SIZE) {
final var messageBytes = message.messageText().getBytes(StandardCharsets.UTF_8);
final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
final var streamDetails = new StreamDetails(new ByteArrayInputStream(messageBytes),
MimeUtils.LONG_TEXT,
messageBytes.length);
final var textAttachment = AttachmentUtils.createAttachmentStream(streamDetails,
Optional.empty(),
uploadSpec);
messageBuilder.withBody(message.messageText().substring(0, ServiceConfig.MAX_MESSAGE_BODY_SIZE));
additionalAttachments.add(context.getAttachmentHelper().uploadAttachment(textAttachment));
if (Utf8.size(message.messageText()) > MAX_MESSAGE_SIZE_BYTES) {
final var result = splitByByteLength(message.messageText(), MAX_MESSAGE_SIZE_BYTES);
final var trimmed = result.getFirst();
final var remainder = result.getSecond();
if (remainder != null) {
final var messageBytes = message.messageText().getBytes(StandardCharsets.UTF_8);
final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
final var streamDetails = new StreamDetails(new ByteArrayInputStream(messageBytes),
MimeUtils.LONG_TEXT,
messageBytes.length);
final var textAttachment = AttachmentUtils.createAttachmentStream(streamDetails,
Optional.empty(),
uploadSpec);
messageBuilder.withBody(trimmed);
additionalAttachments.add(context.getAttachmentHelper().uploadAttachment(textAttachment));
} else {
messageBuilder.withBody(message.messageText());
}
} else {
messageBuilder.withBody(message.messageText());
}
@ -788,6 +810,7 @@ public class ManagerImpl implements Manager {
} else if (!additionalAttachments.isEmpty()) {
messageBuilder.withAttachments(additionalAttachments);
}
messageBuilder.withViewOnce(message.viewOnce());
if (!message.mentions().isEmpty()) {
messageBuilder.withMentions(resolveMentions(message.mentions()));
}
@ -1039,15 +1062,23 @@ public class ManagerImpl implements Manager {
@Override
public void setContactName(
RecipientIdentifier.Single recipient,
String givenName,
final String familyName
final RecipientIdentifier.Single recipient,
final String givenName,
final String familyName,
final String nickGivenName,
final String nickFamilyName,
final String note
) throws NotPrimaryDeviceException, UnregisteredRecipientException {
if (!account.isPrimaryDevice()) {
throw new NotPrimaryDeviceException();
}
context.getContactHelper()
.setContactName(context.getRecipientHelper().resolveRecipient(recipient), givenName, familyName);
.setContactName(context.getRecipientHelper().resolveRecipient(recipient),
givenName,
familyName,
nickGivenName,
nickFamilyName,
note);
syncRemoteStorage();
}
@ -1576,7 +1607,8 @@ public class ManagerImpl implements Manager {
context.close();
executor.close();
dependencies.getSignalWebSocket().disconnect();
dependencies.getAuthenticatedSignalWebSocket().disconnect();
dependencies.getUnauthenticatedSignalWebSocket().disconnect();
dependencies.getPushServiceSocket().close();
disposable.dispose();

View file

@ -29,12 +29,10 @@ import org.asamk.signal.manager.util.KeyUtils;
import org.signal.libsignal.protocol.IdentityKeyPair;
import org.slf4j.Logger;
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.SignalServiceAddress;
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.internal.push.ProvisioningSocket;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
@ -58,7 +56,7 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
private final Consumer<Manager> newManagerListener;
private final AccountsStore accountsStore;
private final SignalServiceAccountManager accountManager;
private final ProvisioningApi provisioningApi;
private final IdentityKeyPair tempIdentityKey;
private final String password;
@ -77,8 +75,6 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
tempIdentityKey = KeyUtils.generateIdentityKeyPair();
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,
null,
null,
@ -87,23 +83,22 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
final var pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
credentialsProvider,
userAgent,
clientZkOperations.getProfileOperations(),
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
accountManager = new SignalServiceAccountManager(pushServiceSocket,
new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(), userAgent),
groupsV2Operations);
final var provisioningSocket = new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
userAgent);
this.provisioningApi = new ProvisioningApi(pushServiceSocket, provisioningSocket, credentialsProvider);
}
@Override
public URI getDeviceLinkUri() throws TimeoutException, IOException {
var deviceUuid = accountManager.getNewDeviceUuid();
var deviceUuid = provisioningApi.getNewDeviceUuid();
return new DeviceLinkUrl(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
}
@Override
public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException {
var ret = accountManager.getNewDeviceRegistration(tempIdentityKey);
var ret = provisioningApi.getNewDeviceRegistration(tempIdentityKey);
var number = ret.getNumber();
var aci = ret.getAci();
var pni = ret.getPni();
@ -160,7 +155,7 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
logger.debug("Finishing new device registration");
var deviceId = accountManager.finishNewDeviceRegistration(ret.getProvisioningCode(),
var deviceId = provisioningApi.finishNewDeviceRegistration(ret.getProvisioningCode(),
account.getAccountAttributes(null),
aciPreKeys,
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.IncorrectPinException;
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.RateLimitException;
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.util.KeyUtils;
import org.asamk.signal.manager.util.NumberVerificationUtils;
import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.usernames.BaseUsernameException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
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.push.ServiceId.ACI;
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.DeprecatedVersionException;
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import java.io.IOException;
import java.util.function.Consumer;
import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class RegistrationManagerImpl implements RegistrationManager {
@ -132,12 +130,15 @@ public class RegistrationManagerImpl implements RegistrationManager {
}
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
logger.trace("Creating verification session");
String sessionId = NumberVerificationUtils.handleVerificationSession(registrationApi,
account.getSessionId(account.getNumber()),
id -> account.setSessionId(account.getNumber(), id),
voiceVerification,
captcha);
logger.trace("Requesting verification code");
NumberVerificationUtils.requestVerificationCode(registrationApi, sessionId, voiceVerification);
logger.debug("Successfully requested verification code");
account.setRegistered(false);
} catch (DeprecatedVersionException e) {
logger.debug("Signal-Server returned deprecated version exception", e);
@ -149,7 +150,7 @@ public class RegistrationManagerImpl implements RegistrationManager {
public void verifyAccount(
String verificationCode,
String pin
) throws IOException, PinLockedException, IncorrectPinException {
) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException {
if (account.isRegistered()) {
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 pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
final var response = Utils.handleResponseException(registrationApi.registerAccount(null,
final var response = handleResponseException(registrationApi.registerAccount(null,
recoveryPassword,
account.getAccountAttributes(null),
aciPreKeys,
@ -221,8 +222,14 @@ public class RegistrationManagerImpl implements RegistrationManager {
private boolean attemptReactivateAccount() {
try {
final var accountManager = createAuthenticatedSignalServiceAccountManager();
accountManager.setAccountAttributes(account.getAccountAttributes(null));
final var dependencies = new SignalDependencies(serviceEnvironmentConfig,
userAgent,
account.getCredentialsProvider(),
account.getSignalServiceDataStore(),
null,
new ReentrantSignalSessionLock());
handleResponseException(dependencies.getAccountApi()
.setAccountAttributes(account.getAccountAttributes(null)));
account.setRegistered(true);
logger.info("Reactivated existing account, verify is not necessary.");
if (newManagerListener != null) {
@ -241,17 +248,6 @@ public class RegistrationManagerImpl implements RegistrationManager {
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(
final String sessionId,
final String verificationCode,
@ -261,11 +257,11 @@ public class RegistrationManagerImpl implements RegistrationManager {
) throws IOException {
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
try {
Utils.handleResponseException(registrationApi.verifyAccount(sessionId, verificationCode));
handleResponseException(registrationApi.verifyAccount(sessionId, verificationCode));
} catch (AlreadyVerifiedException e) {
// Already verified so can continue registering
}
return Utils.handleResponseException(registrationApi.registerAccount(sessionId,
return handleResponseException(registrationApi.registerAccount(sessionId,
null,
account.getAccountAttributes(registrationLock),
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.ServiceEnvironmentConfig;
import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.signal.libsignal.net.Network;
import org.signal.libsignal.protocol.UsePqRatchet;
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.SignalServiceDataStore;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
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.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.keys.KeysApi;
import org.whispersystems.signalservice.api.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.SignalServiceAddress;
import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi;
import org.whispersystems.signalservice.api.registration.RegistrationApi;
import org.whispersystems.signalservice.api.services.ProfileService;
import org.whispersystems.signalservice.api.storage.StorageServiceApi;
import org.whispersystems.signalservice.api.storage.StorageServiceRepository;
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.UptimeSleepTimer;
import org.whispersystems.signalservice.api.websocket.WebSocketFactory;
import org.whispersystems.signalservice.internal.push.ProvisioningSocket;
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
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.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
public class SignalDependencies {
private static final Logger logger = LoggerFactory.getLogger(SignalDependencies.class);
private final Object LOCK = new Object();
private final ServiceEnvironmentConfig serviceEnvironmentConfig;
@ -50,22 +66,31 @@ public class SignalDependencies {
private boolean allowStories = true;
private SignalServiceAccountManager accountManager;
private AccountApi accountApi;
private RateLimitChallengeApi rateLimitChallengeApi;
private CdsApi cdsApi;
private UsernameApi usernameApi;
private GroupsV2Api groupsV2Api;
private RegistrationApi registrationApi;
private LinkDeviceApi linkDeviceApi;
private StorageServiceApi storageServiceApi;
private CertificateApi certificateApi;
private AttachmentApi attachmentApi;
private MessageApi messageApi;
private KeysApi keysApi;
private GroupsV2Operations groupsV2Operations;
private ClientZkOperations clientZkOperations;
private PushServiceSocket pushServiceSocket;
private ProvisioningSocket provisioningSocket;
private Network libSignalNetwork;
private SignalWebSocket signalWebSocket;
private SignalWebSocket.AuthenticatedWebSocket authenticatedSignalWebSocket;
private SignalWebSocket.UnauthenticatedWebSocket unauthenticatedSignalWebSocket;
private SignalServiceMessageReceiver messageReceiver;
private SignalServiceMessageSender messageSender;
private List<SecureValueRecovery> secureValueRecovery;
private ProfileService profileService;
private ProfileApi profileApi;
SignalDependencies(
final ServiceEnvironmentConfig serviceEnvironmentConfig,
@ -95,7 +120,12 @@ public class SignalDependencies {
this.registrationApi = 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(),
credentialsProvider,
userAgent,
getClientZkProfileOperations(),
ServiceConfig.AUTOMATIC_NETWORK_RETRY));
}
public ProvisioningSocket getProvisioningSocket() {
return getOrCreate(() -> provisioningSocket,
() -> provisioningSocket = new ProvisioningSocket(getServiceEnvironmentConfig().signalServiceConfiguration(),
userAgent));
public Network getLibSignalNetwork() {
return getOrCreate(() -> libSignalNetwork, () -> {
libSignalNetwork = new Network(serviceEnvironmentConfig.netEnvironment(), userAgent);
setSignalNetworkProxy(libSignalNetwork);
});
}
public Network getLibSignalNetwork() {
return getOrCreate(() -> libSignalNetwork,
() -> libSignalNetwork = new Network(serviceEnvironmentConfig.netEnvironment(), userAgent));
private void setSignalNetworkProxy(Network libSignalNetwork) {
final var proxy = Utils.getHttpsProxy();
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() {
return getOrCreate(() -> accountManager,
() -> accountManager = new SignalServiceAccountManager(getPushServiceSocket(),
getProvisioningSocket(),
() -> accountManager = new SignalServiceAccountManager(getAuthenticatedSignalWebSocket(),
getAccountApi(),
getPushServiceSocket(),
getGroupsV2Operations()));
}
@ -152,6 +202,23 @@ public class SignalDependencies {
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() {
return getOrCreate(() -> groupsV2Api, () -> groupsV2Api = getAccountManager().getGroupsV2Api());
}
@ -161,18 +228,41 @@ public class SignalDependencies {
}
public LinkDeviceApi getLinkDeviceApi() {
return getOrCreate(() -> linkDeviceApi, () -> linkDeviceApi = new LinkDeviceApi(getPushServiceSocket()));
return getOrCreate(() -> linkDeviceApi,
() -> linkDeviceApi = new LinkDeviceApi(getAuthenticatedSignalWebSocket()));
}
private StorageServiceApi getStorageServiceApi() {
return getOrCreate(() -> storageServiceApi,
() -> storageServiceApi = new StorageServiceApi(getPushServiceSocket()));
() -> storageServiceApi = new StorageServiceApi(getAuthenticatedSignalWebSocket(),
getPushServiceSocket()));
}
public StorageServiceRepository getStorageServiceRepository() {
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() {
return getOrCreate(() -> groupsV2Operations,
() -> groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration()),
@ -189,33 +279,35 @@ public class SignalDependencies {
return clientZkOperations.getProfileOperations();
}
public SignalWebSocket getSignalWebSocket() {
return getOrCreate(() -> signalWebSocket, () -> {
public SignalWebSocket.AuthenticatedWebSocket getAuthenticatedSignalWebSocket() {
return getOrCreate(() -> authenticatedSignalWebSocket, () -> {
final var timer = new UptimeSleepTimer();
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
public WebSocketConnection createUnidentifiedWebSocket() {
return new OkHttpWebSocketConnection("unidentified",
serviceEnvironmentConfig.signalServiceConfiguration(),
Optional.empty(),
userAgent,
healthMonitor,
allowStories);
}
};
signalWebSocket = new SignalWebSocket(webSocketFactory);
healthMonitor.monitor(signalWebSocket);
authenticatedSignalWebSocket = new SignalWebSocket.AuthenticatedWebSocket(() -> new OkHttpWebSocketConnection(
"normal",
serviceEnvironmentConfig.signalServiceConfiguration(),
Optional.of(credentialsProvider),
userAgent,
healthMonitor,
allowStories), () -> true, timer, TimeUnit.SECONDS.toMillis(10));
healthMonitor.monitor(authenticatedSignalWebSocket);
});
}
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(),
dataStore,
sessionLock,
getSignalWebSocket(),
getAttachmentApi(),
getMessageApi(),
getKeysApi(),
Optional.empty(),
executor,
ServiceConfig.MAX_ENVELOPE_SIZE));
ServiceConfig.MAX_ENVELOPE_SIZE,
() -> true,
UsePqRatchet.NO));
}
public List<SecureValueRecovery> getSecureValueRecovery() {
@ -243,11 +339,19 @@ public class SignalDependencies {
.toList());
}
public ProfileApi getProfileApi() {
return getOrCreate(() -> profileApi,
() -> profileApi = new ProfileApi(getAuthenticatedSignalWebSocket(),
getUnauthenticatedSignalWebSocket(),
getPushServiceSocket(),
getClientZkProfileOperations()));
}
public ProfileService getProfileService() {
return getOrCreate(() -> profileService,
() -> profileService = new ProfileService(getClientZkProfileOperations(),
getMessageReceiver(),
getSignalWebSocket()));
getAuthenticatedSignalWebSocket(),
getUnauthenticatedSignalWebSocket()));
}
public SignalServiceCipher getCipher(ServiceIdType serviceIdType) {

View file

@ -2,195 +2,157 @@ package org.asamk.signal.manager.internal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalWebSocket;
import org.whispersystems.signalservice.api.util.Preconditions;
import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.websocket.HealthMonitor;
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
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 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 {
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 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 volatile KeepAliveSender keepAliveSender;
private final HealthState identified = new HealthState();
private final HealthState unidentified = new HealthState();
private SignalWebSocket webSocket = null;
private volatile KeepAliveSender keepAliveSender = null;
private boolean needsKeepAlive = false;
private long lastKeepAliveReceived = 0;
public SignalWebSocketHealthMonitor(SleepTimer sleepTimer) {
this.sleepTimer = sleepTimer;
}
public void monitor(SignalWebSocket signalWebSocket) {
Preconditions.checkNotNull(signalWebSocket);
Preconditions.checkArgument(this.signalWebSocket == null, "monitor can only be called once");
void monitor(SignalWebSocket webSocket) {
Preconditions.checkNotNull(webSocket);
Preconditions.checkArgument(this.webSocket == null, "monitor can only be called once");
this.signalWebSocket = signalWebSocket;
executor.execute(() -> {
//noinspection ResultOfMethodCallIgnored
signalWebSocket.getWebSocketState()
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation())
.distinctUntilChanged()
.subscribe(s -> onStateChange(s, identified));
this.webSocket = webSocket;
//noinspection ResultOfMethodCallIgnored
signalWebSocket.getUnidentifiedWebSocketState()
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation())
.distinctUntilChanged()
.subscribe(s -> onStateChange(s, unidentified));
webSocket.getState()
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation())
.distinctUntilChanged()
.subscribe(this::onStateChanged);
webSocket.addKeepAliveChangeListener(() -> {
executor.execute(this::updateKeepAliveSenderStatus);
return Unit.INSTANCE;
});
});
}
private synchronized void onStateChange(WebSocketConnectionState connectionState, HealthState healthState) {
switch (connectionState) {
case CONNECTED -> logger.debug("WebSocket is now connected");
case AUTHENTICATION_FAILED -> logger.debug("WebSocket authentication failed");
case FAILED -> logger.debug("WebSocket connection failed");
}
private void onStateChanged(WebSocketConnectionState connectionState) {
executor.execute(() -> {
needsKeepAlive = connectionState == WebSocketConnectionState.CONNECTED;
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.start();
} else if (keepAliveSender != null && !isKeepAliveNecessary()) {
} else if (keepAliveSender != null && !sendKeepAlives()) {
keepAliveSender.shutdown();
keepAliveSender = null;
}
}
@Override
public void onKeepAliveResponse(long sentTimestamp, boolean isIdentifiedWebSocket) {
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;
private boolean sendKeepAlives() {
return needsKeepAlive && webSocket != null && webSocket.shouldSendKeepAlives();
}
/**
* Sends periodic heartbeats/keep-alives over both WebSockets to prevent connection timeouts. If
* either WebSocket fails 3 times to get a return heartbeat both are forced to be recreated.
* Sends periodic heartbeats/keep-alives over the WebSocket to prevent connection timeouts. If
* the WebSocket fails to get a return heartbeat after [KEEP_ALIVE_TIMEOUT] seconds, it is forced to be recreated.
*/
private class KeepAliveSender extends Thread {
private final class KeepAliveSender extends Thread {
private volatile boolean shouldKeepRunning = true;
@Override
public void run() {
identified.lastKeepAliveReceived = System.currentTimeMillis();
unidentified.lastKeepAliveReceived = System.currentTimeMillis();
logger.debug("[KeepAliveSender({})] started", this.threadId());
lastKeepAliveReceived = System.currentTimeMillis();
while (shouldKeepRunning && isKeepAliveNecessary()) {
var keepAliveSendTime = System.currentTimeMillis();
while (shouldKeepRunning && sendKeepAlives()) {
try {
sleepTimer.sleep(KEEP_ALIVE_SEND_CADENCE);
final var nextKeepAliveSendTime = keepAliveSendTime + KEEP_ALIVE_SEND_CADENCE;
sleepUntil(nextKeepAliveSendTime);
if (shouldKeepRunning && isKeepAliveNecessary()) {
long keepAliveRequiredSinceTime = System.currentTimeMillis()
- MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE;
if (shouldKeepRunning && sendKeepAlives()) {
keepAliveSendTime = System.currentTimeMillis();
webSocket.sendKeepAlive();
}
if (identified.lastKeepAliveReceived < keepAliveRequiredSinceTime
|| unidentified.lastKeepAliveReceived < keepAliveRequiredSinceTime) {
logger.warn("Missed keep alives, identified last: "
+ identified.lastKeepAliveReceived
+ " unidentified last: "
+ unidentified.lastKeepAliveReceived
+ " needed by: "
+ keepAliveRequiredSinceTime);
signalWebSocket.forceNewWebSockets();
signalWebSocket.connect();
} else {
signalWebSocket.sendKeepAlive();
final var responseRequiredTime = keepAliveSendTime + KEEP_ALIVE_TIMEOUT;
sleepUntil(responseRequiredTime);
if (shouldKeepRunning && sendKeepAlives()) {
if (lastKeepAliveReceived < keepAliveSendTime) {
logger.debug("Missed keep alive, last: {} needed by: {}",
lastKeepAliveReceived,
responseRequiredTime);
webSocket.forceNewWebSocket();
}
}
} 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;
}
}
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

@ -95,10 +95,12 @@ public abstract class Database implements AutoCloseable {
sqliteConfig.setTransactionMode(SQLiteConfig.TransactionMode.IMMEDIATE);
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:sqlite:" + databaseFile);
config.setJdbcUrl("jdbc:sqlite:" + databaseFile + "?foreign_keys=ON&journal_mode=wal");
config.setDataSourceProperties(sqliteConfig.toProperties());
config.setMinimumIdle(1);
config.setConnectionInitSql("PRAGMA foreign_keys=ON");
config.setConnectionTimeout(90_000);
config.setMaximumPoolSize(50);
config.setMaxLifetime(0);
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 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();
@ -827,6 +827,7 @@ public class SignalAccount implements Closeable {
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyPreKeyStore() != null) {
logger.debug("Migrating legacy pre key store.");
aciAccountData.getPreKeyStore().removeAllPreKeys();
for (var entry : legacySignalProtocolStore.getLegacyPreKeyStore().getPreKeys().entrySet()) {
try {
aciAccountData.getPreKeyStore().storePreKey(entry.getKey(), new PreKeyRecord(entry.getValue()));
@ -838,6 +839,7 @@ public class SignalAccount implements Closeable {
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySignedPreKeyStore() != null) {
logger.debug("Migrating legacy signed pre key store.");
aciAccountData.getSignedPreKeyStore().removeAllSignedPreKeys();
for (var entry : legacySignalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) {
try {
aciAccountData.getSignedPreKeyStore()
@ -1550,9 +1552,7 @@ public class SignalAccount implements Closeable {
return key;
}
pinMasterKey = KeyUtils.createMasterKey();
save();
return pinMasterKey;
return getOrCreateAccountEntropyPool().deriveMasterKey();
}
private MasterKey getMasterKey() {

View file

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

View file

@ -36,6 +36,10 @@ public class ConfigurationStore {
return keyValueStore.getEntry(readReceipts);
}
public Boolean getReadReceipts(final Connection connection) throws SQLException {
return keyValueStore.getEntry(connection, readReceipts);
}
public void setReadReceipts(final boolean value) {
if (keyValueStore.storeEntry(readReceipts, value)) {
recipientStore.rotateSelfStorageId();
@ -52,6 +56,10 @@ public class ConfigurationStore {
return keyValueStore.getEntry(unidentifiedDeliveryIndicators);
}
public Boolean getUnidentifiedDeliveryIndicators(final Connection connection) throws SQLException {
return keyValueStore.getEntry(connection, unidentifiedDeliveryIndicators);
}
public void setUnidentifiedDeliveryIndicators(final boolean value) {
if (keyValueStore.storeEntry(unidentifiedDeliveryIndicators, value)) {
recipientStore.rotateSelfStorageId();
@ -71,6 +79,10 @@ public class ConfigurationStore {
return keyValueStore.getEntry(typingIndicators);
}
public Boolean getTypingIndicators(final Connection connection) throws SQLException {
return keyValueStore.getEntry(connection, typingIndicators);
}
public void setTypingIndicators(final boolean value) {
if (keyValueStore.storeEntry(typingIndicators, value)) {
recipientStore.rotateSelfStorageId();
@ -87,6 +99,10 @@ public class ConfigurationStore {
return keyValueStore.getEntry(linkPreviews);
}
public Boolean getLinkPreviews(final Connection connection) throws SQLException {
return keyValueStore.getEntry(connection, linkPreviews);
}
public void setLinkPreviews(final boolean value) {
if (keyValueStore.storeEntry(linkPreviews, value)) {
recipientStore.rotateSelfStorageId();
@ -103,6 +119,10 @@ public class ConfigurationStore {
return keyValueStore.getEntry(phoneNumberUnlisted);
}
public Boolean getPhoneNumberUnlisted(final Connection connection) throws SQLException {
return keyValueStore.getEntry(connection, phoneNumberUnlisted);
}
public void setPhoneNumberUnlisted(final boolean value) {
if (keyValueStore.storeEntry(phoneNumberUnlisted, value)) {
recipientStore.rotateSelfStorageId();
@ -119,6 +139,10 @@ public class ConfigurationStore {
return keyValueStore.getEntry(phoneNumberSharingMode);
}
public PhoneNumberSharingMode getPhoneNumberSharingMode(final Connection connection) throws SQLException {
return keyValueStore.getEntry(connection, phoneNumberSharingMode);
}
public void setPhoneNumberSharingMode(final PhoneNumberSharingMode value) {
if (keyValueStore.storeEntry(phoneNumberSharingMode, value)) {
recipientStore.rotateSelfStorageId();
@ -138,6 +162,10 @@ public class ConfigurationStore {
return keyValueStore.getEntry(usernameLinkColor);
}
public String getUsernameLinkColor(final Connection connection) throws SQLException {
return keyValueStore.getEntry(connection, usernameLinkColor);
}
public void setUsernameLinkColor(final String color) {
if (keyValueStore.storeEntry(usernameLinkColor, color)) {
recipientStore.rotateSelfStorageId();

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.InvalidKeyException;
import org.signal.libsignal.protocol.state.IdentityKeyStore.Direction;
import org.signal.libsignal.protocol.state.IdentityKeyStore.IdentityChange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.ServiceId;
@ -62,11 +63,11 @@ public class IdentityKeyStore {
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);
}
public boolean saveIdentity(
public IdentityChange saveIdentity(
final Connection connection,
final ServiceId serviceId,
final IdentityKey identityKey
@ -74,9 +75,9 @@ public class IdentityKeyStore {
return saveIdentity(connection, serviceId.toString(), identityKey);
}
boolean saveIdentity(final String address, final IdentityKey identityKey) {
IdentityChange saveIdentity(final String address, final IdentityKey identityKey) {
if (isRetryingDecryption) {
return false;
return IdentityChange.NEW_OR_UNCHANGED;
}
try (final var connection = database.getConnection()) {
return saveIdentity(connection, address, identityKey);
@ -85,20 +86,24 @@ public class IdentityKeyStore {
}
}
private boolean saveIdentity(
private IdentityChange saveIdentity(
final Connection connection,
final String address,
final IdentityKey identityKey
) throws SQLException {
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
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);
return true;
saveNewIdentity(connection, address, identityKey, false);
return IdentityChange.REPLACED_EXISTING;
}
public void setRetryingDecryption(final boolean retryingDecryption) {

View file

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

View file

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

View file

@ -10,6 +10,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Collections;
import java.util.Objects;
@ -75,7 +76,7 @@ public class MessageCache {
return cachedMessage;
}
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);
}

View file

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

View file

@ -4,8 +4,9 @@ import org.asamk.signal.manager.storage.Database;
import org.asamk.signal.manager.storage.Utils;
import org.signal.libsignal.protocol.InvalidKeyException;
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.ECPrivateKey;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
import org.slf4j.Logger;
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 {
try {
final var keyId = resultSet.getInt("key_id");
final var publicKey = Curve.decodePoint(resultSet.getBytes("public_key"), 0);
final var privateKey = Curve.decodePrivatePoint(resultSet.getBytes("private_key"));
final var publicKey = new ECPublicKey(resultSet.getBytes("public_key"));
final var privateKey = new ECPrivateKey(resultSet.getBytes("private_key"));
final var signature = resultSet.getBytes("signature");
final var timestamp = resultSet.getLong("timestamp");
return new SignedPreKeyRecord(keyId, timestamp, new ECKeyPair(publicKey, privateKey), signature);

View file

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

View file

@ -39,7 +39,7 @@ public class MergeRecipientHelper {
)
) || recipient.address().aci().equals(address.aci())) {
logger.debug("Got existing recipient {}, updating with high trust address", recipient.id());
store.updateRecipientAddress(recipient.id(), recipient.address().withIdentifiersFrom(address));
store.updateRecipientAddress(recipient.id(), address.withOtherIdentifiersFrom(recipient.address()));
return new Pair<>(recipient.id(), List.of());
}
@ -83,24 +83,25 @@ public class MergeRecipientHelper {
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,
recipientsToBeMerged.stream().map(r -> r.id().toString()).collect(Collectors.joining(", ")),
recipientsToBeStripped.stream().map(r -> r.id().toString()).collect(Collectors.joining(", ")));
resultingRecipient.map(RecipientWithAddress::address),
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);
for (final var recipient : recipientsToBeMerged) {
if (finalAddress == null) {
finalAddress = recipient.address();
} else {
finalAddress = finalAddress.withIdentifiersFrom(recipient.address());
finalAddress = finalAddress.withOtherIdentifiersFrom(recipient.address());
}
store.removeRecipientAddress(recipient.id());
}
if (finalAddress == null) {
finalAddress = address;
} else {
finalAddress = finalAddress.withIdentifiersFrom(address);
finalAddress = address.withOtherIdentifiersFrom(finalAddress);
}
for (final var recipient : recipientsToBeStripped) {

View file

@ -79,11 +79,11 @@ public record RecipientAddress(
this(Optional.of(serviceId), Optional.empty());
}
public RecipientAddress withIdentifiersFrom(RecipientAddress address) {
return new RecipientAddress(address.aci.or(this::aci),
address.pni.or(this::pni),
address.number.or(this::number),
address.username.or(this::username));
public RecipientAddress withOtherIdentifiersFrom(RecipientAddress address) {
return new RecipientAddress(this.aci.or(address::aci),
this.pni.or(address::pni),
this.number.or(address::number),
this.username.or(address::username));
}
public RecipientAddress removeIdentifiersFrom(RecipientAddress address) {

View file

@ -934,7 +934,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
public void markUndiscoverablePossiblyUnregistered(final Set<String> numbers) {
logger.debug("Marking {} numbers as unregistered", numbers.size());
logger.debug("Marking {} numbers as undiscoverable", numbers.size());
try (final var connection = database.getConnection()) {
connection.setAutoCommit(false);
for (final var number : numbers) {
@ -994,7 +994,12 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
) throws SQLException {
markUnregistered(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));
updateRecipientAddress(connection, recipientId, address.removeIdentifiersFrom(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.internal.JobExecutor;
import org.asamk.signal.manager.jobs.CheckWhoAmIJob;
import org.asamk.signal.manager.jobs.DownloadProfileAvatarJob;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.util.KeyUtils;
@ -11,6 +10,7 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId;
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.api.util.UuidUtil;
@ -22,8 +22,12 @@ import java.sql.SQLException;
import java.util.Arrays;
import java.util.Optional;
import okio.ByteString;
import static org.asamk.signal.manager.util.Utils.firstNonEmpty;
import static org.asamk.signal.manager.util.Utils.firstNonNull;
import static org.whispersystems.signalservice.api.storage.AccountRecordExtensionsKt.safeSetBackupsSubscriber;
import static org.whispersystems.signalservice.api.storage.AccountRecordExtensionsKt.safeSetPayments;
import static org.whispersystems.signalservice.api.storage.AccountRecordExtensionsKt.safeSetSubscriber;
/**
* Processes {@link SignalAccountRecord}s.
@ -48,7 +52,8 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
final var recipient = account.getRecipientStore().getRecipient(connection, selfRecipientId);
final var storageId = account.getRecipientStore().getSelfStorageId(connection);
this.localAccountRecord = new SignalAccountRecord(storageId,
StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
StorageSyncModels.localToRemoteRecord(connection,
account.getConfigurationStore(),
recipient,
account.getUsernameLink()));
}
@ -77,7 +82,36 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
familyName = local.familyName;
}
final var mergedBuilder = SignalAccountRecord.Companion.newBuilder(remote.unknownFields().toByteArray())
final var payments = remote.payments != null && remote.payments.entropy.size() > 0
? remote.payments
: local.payments;
final ByteString donationSubscriberId;
final String donationSubscriberCurrencyCode;
if (remote.subscriberId.size() > 0) {
donationSubscriberId = remote.subscriberId;
donationSubscriberCurrencyCode = remote.subscriberCurrencyCode;
} else {
donationSubscriberId = local.subscriberId;
donationSubscriberCurrencyCode = local.subscriberCurrencyCode;
}
final ByteString backupsSubscriberId;
final IAPSubscriptionId backupsPurchaseToken;
final var remoteBackupSubscriberData = remote.backupSubscriberData;
if (remoteBackupSubscriberData != null && remoteBackupSubscriberData.subscriberId.size() > 0) {
backupsSubscriberId = remoteBackupSubscriberData.subscriberId;
backupsPurchaseToken = IAPSubscriptionId.Companion.from(remoteBackupSubscriberData);
} else {
backupsSubscriberId = local.backupSubscriberData != null
? local.backupSubscriberData.subscriberId
: ByteString.EMPTY;
backupsPurchaseToken = IAPSubscriptionId.Companion.from(local.backupSubscriberData);
}
final var mergedBuilder = remote.newBuilder()
.givenName(givenName)
.familyName(familyName)
.avatarUrlPath(firstNonEmpty(remote.avatarUrlPath, local.avatarUrlPath))
@ -96,9 +130,6 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
.preferredReactionEmoji(firstNonEmpty(remote.preferredReactionEmoji, local.preferredReactionEmoji))
.subscriberId(firstNonEmpty(remote.subscriberId, local.subscriberId))
.subscriberCurrencyCode(firstNonEmpty(remote.subscriberCurrencyCode, local.subscriberCurrencyCode))
.backupsSubscriberId(firstNonEmpty(remote.backupsSubscriberId, local.backupsSubscriberId))
.backupsSubscriberCurrencyCode(firstNonEmpty(remote.backupsSubscriberCurrencyCode,
local.backupsSubscriberCurrencyCode))
.displayBadgesOnProfile(remote.displayBadgesOnProfile)
.subscriptionManuallyCancelled(remote.subscriptionManuallyCancelled)
.keepMutedChatsArchived(remote.keepMutedChatsArchived)
@ -114,10 +145,13 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
: remote.storyViewReceiptsEnabled)
.username(remote.username)
.usernameLink(remote.usernameLink)
.e164(account.isPrimaryDevice() ? local.e164 : remote.e164);
if (firstNonNull(remote.payments, local.payments) != null) {
mergedBuilder.payments(firstNonNull(remote.payments, local.payments));
}
.avatarColor(remote.avatarColor);
safeSetPayments(mergedBuilder,
payments != null && payments.enabled,
payments == null ? null : payments.entropy.toByteArray());
safeSetSubscriber(mergedBuilder, donationSubscriberId, donationSubscriberCurrencyCode);
safeSetBackupsSubscriber(mergedBuilder, backupsSubscriberId, backupsPurchaseToken);
final var merged = mergedBuilder.build();
final var matchesRemote = doProtosMatch(merged, remote);
@ -144,10 +178,6 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
final var accountRecord = update.newRecord();
final var accountProto = accountRecord.getProto();
if (!accountProto.e164.equals(account.getNumber())) {
jobExecutor.enqueueJob(new CheckWhoAmIJob());
}
account.getConfigurationStore().setReadReceipts(connection, accountProto.readReceipts);
account.getConfigurationStore().setTypingIndicators(connection, accountProto.typingIndicators);
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 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 PNI selfPni;
@ -172,7 +172,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
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)
.e164(e164)
.pni(pni)
@ -195,7 +195,8 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
.hidden(remote.hidden)
.pniSignatureVerified(remote.pniSignatureVerified || local.pniSignatureVerified)
.nickname(remote.nickname)
.note(remote.note);
.note(remote.note)
.avatarColor(remote.avatarColor);
final var merged = mergedBuilder.build();
final var matchesRemote = doProtosMatch(merged, remote);
@ -267,10 +268,16 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
.withGivenName(nullIfEmpty(contactProto.systemGivenName))
.withFamilyName(nullIfEmpty(contactProto.systemFamilyName))
.withNickName(nullIfEmpty(contactProto.systemNickname))
.withNickNameGivenName(nullIfEmpty(contactProto.givenName))
.withNickNameFamilyName(nullIfEmpty(contactProto.familyName))
.withNickNameGivenName(nullIfEmpty(contactProto.nickname == null
? null
: contactProto.nickname.given))
.withNickNameFamilyName(nullIfEmpty(contactProto.nickname == null
? null
: contactProto.nickname.family))
.withNote(nullIfEmpty(contactProto.note))
.withUnregisteredTimestamp(contactProto.unregisteredAtTimestamp);
.withUnregisteredTimestamp(contactProto.unregisteredAtTimestamp == 0
? null
: contactProto.unregisteredAtTimestamp);
account.getRecipientStore().storeContact(connection, recipientId, newContact.build());
}

View file

@ -49,7 +49,7 @@ abstract class DefaultStorageRecordProcessor<E extends SignalRecord<?>> implemen
final var local = getMatching(remote);
if (local.isEmpty()) {
debug(remote.getId(), remote, "No matching local record. Inserting.");
debug(remote.getId(), remote, "[Local Insert] No matching local record. Inserting.");
insertLocal(remote);
return;
}
@ -70,7 +70,7 @@ abstract class DefaultStorageRecordProcessor<E extends SignalRecord<?>> implemen
if (!merged.equals(local.get())) {
final var update = new StorageRecordUpdate<>(local.get(), merged);
debug(remote.getId(), remote, "[Local Update] " + update);
debug(remote.getId(), remote, "[Local Update] " + local.get().describeDiff(merged));
updateLocal(update);
}
}

View file

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

View file

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

View file

@ -8,7 +8,7 @@ import java.sql.SQLException;
* Handles processing a remote record, which involves applying any local changes that need to be
* made based on the remote records.
*/
interface StorageRecordProcessor<E extends SignalRecord> {
interface StorageRecordProcessor<E extends SignalRecord<?>> {
void process(E remoteRecord) throws SQLException;
}

View file

@ -5,7 +5,7 @@ import org.whispersystems.signalservice.api.storage.SignalRecord;
/**
* Represents a pair of records: one old, and one new. The new record should replace the old.
*/
record StorageRecordUpdate<E extends SignalRecord>(E oldRecord, E newRecord) {
record StorageRecordUpdate<E extends SignalRecord<?>>(E oldRecord, E newRecord) {
@Override
public String toString() {

View file

@ -22,6 +22,8 @@ import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.Id
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Optional;
import okio.ByteString;
@ -49,10 +51,11 @@ public final class StorageSyncModels {
}
public static AccountRecord localToRemoteRecord(
final Connection connection,
ConfigurationStore configStore,
Recipient self,
UsernameLinkComponents usernameLinkComponents
) {
) throws SQLException {
final var builder = SignalAccountRecord.Companion.newBuilder(self.getStorageRecord());
if (self.getProfileKey() != null) {
builder.profileKey(ByteString.of(self.getProfileKey().serialize()));
@ -62,19 +65,18 @@ public final class StorageSyncModels {
.familyName(emptyIfNull(self.getProfile().getFamilyName()))
.avatarUrlPath(emptyIfNull(self.getProfile().getAvatarUrlPath()));
}
builder.typingIndicators(Optional.ofNullable(configStore.getTypingIndicators()).orElse(true))
.readReceipts(Optional.ofNullable(configStore.getReadReceipts()).orElse(true))
.sealedSenderIndicators(Optional.ofNullable(configStore.getUnidentifiedDeliveryIndicators())
builder.typingIndicators(Optional.ofNullable(configStore.getTypingIndicators(connection)).orElse(true))
.readReceipts(Optional.ofNullable(configStore.getReadReceipts(connection)).orElse(true))
.sealedSenderIndicators(Optional.ofNullable(configStore.getUnidentifiedDeliveryIndicators(connection))
.orElse(true))
.linkPreviews(Optional.ofNullable(configStore.getLinkPreviews()).orElse(true))
.unlistedPhoneNumber(Optional.ofNullable(configStore.getPhoneNumberUnlisted()).orElse(false))
.phoneNumberSharingMode(Optional.ofNullable(configStore.getPhoneNumberSharingMode())
.linkPreviews(Optional.ofNullable(configStore.getLinkPreviews(connection)).orElse(true))
.unlistedPhoneNumber(Optional.ofNullable(configStore.getPhoneNumberUnlisted(connection)).orElse(false))
.phoneNumberSharingMode(Optional.ofNullable(configStore.getPhoneNumberSharingMode(connection))
.map(StorageSyncModels::localToRemote)
.orElse(AccountRecord.PhoneNumberSharingMode.UNKNOWN))
.e164(self.getAddress().number().orElse(""))
.username(self.getAddress().username().orElse(""));
if (usernameLinkComponents != null) {
final var linkColor = configStore.getUsernameLinkColor();
final var linkColor = configStore.getUsernameLinkColor(connection);
builder.usernameLink(new UsernameLink.Builder().entropy(ByteString.of(usernameLinkComponents.getEntropy()))
.serverId(UuidUtil.toByteString(usernameLinkComponents.getServerId()))
.color(linkColor == null ? UsernameLink.Color.UNKNOWN : UsernameLink.Color.valueOf(linkColor))
@ -89,7 +91,7 @@ public final class StorageSyncModels {
final var builder = SignalContactRecord.Companion.newBuilder(recipient.getStorageRecord())
.aci(address.aci().map(ACI::toString).orElse(""))
.e164(address.number().orElse(""))
.pni(address.pni().map(PNI::toString).orElse(""))
.pni(address.pni().map(PNI::toStringWithoutPrefix).orElse(""))
.username(address.username().orElse(""))
.profileKey(recipient.getProfileKey() == null
? ByteString.EMPTY

View file

@ -4,7 +4,7 @@ import org.asamk.signal.manager.storage.SignalAccount;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.IdentityKeyPair;
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.kem.KEMKeyPair;
import org.signal.libsignal.protocol.kem.KEMKeyType;
@ -15,7 +15,6 @@ import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.account.PreKeyCollection;
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import java.security.SecureRandom;
import java.util.ArrayList;
@ -34,8 +33,8 @@ public class KeyUtils {
public static IdentityKeyPair getIdentityKeyPair(byte[] publicKeyBytes, byte[] privateKeyBytes) {
try {
IdentityKey publicKey = new IdentityKey(publicKeyBytes);
ECPrivateKey privateKey = Curve.decodePrivatePoint(privateKeyBytes);
final var publicKey = new IdentityKey(publicKeyBytes);
final var privateKey = new ECPrivateKey(privateKeyBytes);
return new IdentityKeyPair(publicKey, privateKey);
} catch (InvalidKeyException e) {
@ -44,7 +43,7 @@ public class KeyUtils {
}
public static IdentityKeyPair generateIdentityKeyPair() {
var djbKeyPair = Curve.generateKeyPair();
var djbKeyPair = ECKeyPair.generate();
var djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());
var djbPrivateKey = djbKeyPair.getPrivateKey();
@ -55,7 +54,7 @@ public class KeyUtils {
var records = new ArrayList<PreKeyRecord>(PREKEY_BATCH_SIZE);
for (var i = 0; i < PREKEY_BATCH_SIZE; i++) {
var preKeyId = (offset + i) % PREKEY_MAXIMUM_ID;
var keyPair = Curve.generateKeyPair();
var keyPair = ECKeyPair.generate();
var record = new PreKeyRecord(preKeyId, keyPair);
records.add(record);
@ -67,13 +66,9 @@ public class KeyUtils {
final int signedPreKeyId,
final ECPrivateKey privateKey
) {
var keyPair = Curve.generateKeyPair();
var keyPair = ECKeyPair.generate();
byte[] signature;
try {
signature = Curve.calculateSignature(privateKey, keyPair.getPublicKey().serialize());
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
signature = privateKey.calculateSignature(keyPair.getPublicKey().serialize());
return new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
}
@ -109,10 +104,6 @@ public class KeyUtils {
return getSecretBytes(32);
}
public static MasterKey createMasterKey() {
return MasterKey.createNew(secureRandom);
}
public static MediaRootBackupKey createMediaRootBackupKey() {
return new MediaRootBackupKey(getSecretBytes(32));
}

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.NonNormalizedPhoneNumberException;
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.RateLimitException;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
@ -11,9 +12,9 @@ import org.asamk.signal.manager.helper.PinHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.exceptions.ChallengeRequiredException;
import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.PushChallengeRequiredException;
import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException;
import org.whispersystems.signalservice.api.registration.RegistrationApi;
import org.whispersystems.signalservice.internal.push.LockedException;
@ -39,8 +40,7 @@ public class NumberVerificationUtils {
RegistrationSessionMetadataResponse sessionResponse;
try {
sessionResponse = getValidSession(registrationApi, sessionId);
} catch (PushChallengeRequiredException |
org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException e) {
} catch (ChallengeRequiredException e) {
if (captcha != null) {
sessionResponse = submitCaptcha(registrationApi, sessionId, captcha);
} else {
@ -48,38 +48,38 @@ public class NumberVerificationUtils {
}
}
sessionId = sessionResponse.getBody().getId();
sessionId = sessionResponse.getMetadata().getId();
sessionIdSaver.accept(sessionId);
if (sessionResponse.getBody().getVerified()) {
if (sessionResponse.getMetadata().getVerified()) {
return sessionId;
}
if (sessionResponse.getBody().getAllowedToRequestCode()) {
if (sessionResponse.getMetadata().getAllowedToRequestCode()) {
return sessionId;
}
final var nextAttempt = voiceVerification
? sessionResponse.getBody().getNextCall()
: sessionResponse.getBody().getNextSms();
? sessionResponse.getMetadata().getNextCall()
: sessionResponse.getMetadata().getNextSms();
if (nextAttempt == null) {
throw new VerificationMethodNotAvailableException();
} else if (nextAttempt > 0) {
final var timestamp = sessionResponse.getHeaders().getTimestamp() + nextAttempt * 1000;
final var timestamp = sessionResponse.getClientReceivedAtMilliseconds() + nextAttempt * 1000;
throw new RateLimitException(timestamp);
}
final var nextVerificationAttempt = sessionResponse.getBody().getNextVerificationAttempt();
final var nextVerificationAttempt = sessionResponse.getMetadata().getNextVerificationAttempt();
if (nextVerificationAttempt != null && nextVerificationAttempt > 0) {
final var timestamp = sessionResponse.getHeaders().getTimestamp() + nextVerificationAttempt * 1000;
final var timestamp = sessionResponse.getClientReceivedAtMilliseconds() + nextVerificationAttempt * 1000;
throw new CaptchaRequiredException(timestamp);
}
if (sessionResponse.getBody().getRequestedInformation().contains("captcha")) {
if (sessionResponse.getMetadata().getRequestedInformation().contains("captcha")) {
if (captcha != null) {
sessionResponse = submitCaptcha(registrationApi, sessionId, captcha);
}
if (!sessionResponse.getBody().getAllowedToRequestCode()) {
if (!sessionResponse.getMetadata().getAllowedToRequestCode()) {
throw new CaptchaRequiredException("Captcha Required");
}
}
@ -99,7 +99,7 @@ public class NumberVerificationUtils {
voiceVerification ? VerificationCodeTransport.VOICE : VerificationCodeTransport.SMS);
try {
Utils.handleResponseException(response);
} catch (org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException e) {
} catch (ChallengeRequiredException e) {
throw new CaptchaRequiredException(e.getMessage(), e);
} catch (org.whispersystems.signalservice.api.push.exceptions.NonNormalizedPhoneNumberException e) {
throw new NonNormalizedPhoneNumberException("Phone number is not normalized ("
@ -115,7 +115,7 @@ public class NumberVerificationUtils {
String pin,
PinHelper pinHelper,
Verifier verifier
) throws IOException, PinLockedException, IncorrectPinException {
) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException {
verificationCode = verificationCode.replace("-", "");
try {
final var response = verifier.verify(sessionId, verificationCode, null);
@ -128,7 +128,7 @@ public class NumberVerificationUtils {
final var registrationLockData = pinHelper.getRegistrationLockData(pin, e);
if (registrationLockData == null) {
throw e;
throw new PinLockMissingException();
}
var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock();
@ -179,9 +179,7 @@ public class NumberVerificationUtils {
captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
try {
return Utils.handleResponseException(registrationApi.submitCaptchaToken(sessionId, captcha));
} catch (PushChallengeRequiredException |
org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException |
TokenNotAcceptedException _e) {
} catch (ChallengeRequiredException | TokenNotAcceptedException _e) {
throw new CaptchaRequiredException("Captcha not accepted");
} catch (NonSuccessfulResponseCodeException e) {
if (e.code == 400) {

View file

@ -23,8 +23,8 @@ public class PaymentUtils {
public static PaymentAddress signPaymentsAddress(byte[] publicAddressBytes, ECPrivateKey privateKey) {
byte[] signature = privateKey.calculateSignature(publicAddressBytes);
return new PaymentAddress.Builder().mobileCoinAddress(new PaymentAddress.MobileCoinAddress.Builder().address(
ByteString.of(publicAddressBytes)).signature(ByteString.of(signature)).build()).build();
return new PaymentAddress.Builder().mobileCoin(new PaymentAddress.MobileCoin.Builder().publicAddress(ByteString.of(
publicAddressBytes)).signature(ByteString.of(signature)).build()).build();
}
/**
@ -33,13 +33,15 @@ public class PaymentUtils {
* Returns the validated bytes if so, otherwise returns null.
*/
public static byte[] verifyPaymentsAddress(PaymentAddress paymentAddress, ECPublicKey publicKey) {
final var mobileCoinAddress = paymentAddress.mobileCoinAddress;
if (mobileCoinAddress == null || mobileCoinAddress.address == null || mobileCoinAddress.signature == null) {
final var mobileCoinAddress = paymentAddress.mobileCoin;
if (mobileCoinAddress == null
|| mobileCoinAddress.publicAddress == null
|| mobileCoinAddress.signature == null) {
logger.debug("Got payment address without mobile coin address, ignoring.");
return null;
}
byte[] bytes = mobileCoinAddress.address.toByteArray();
byte[] bytes = mobileCoinAddress.publicAddress.toByteArray();
byte[] signature = mobileCoinAddress.signature.toByteArray();
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.LoggerFactory;
import org.whispersystems.signalservice.api.NetworkResult;
import org.whispersystems.signalservice.api.NetworkResultUtil;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.util.StreamDetails;
@ -15,6 +16,10 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
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.nio.charset.StandardCharsets;
import java.util.HashMap;
@ -150,15 +155,7 @@ public class Utils {
}
public static <T> T handleResponseException(final NetworkResult<T> response) throws IOException {
final var throwableOptional = response.getCause();
if (throwableOptional != null) {
if (throwableOptional instanceof IOException ioException) {
throw ioException;
} else {
throw new IOException(throwableOptional);
}
}
return response.successOrThrow();
return NetworkResultUtil.toBasicLegacy(response);
}
public static ByteString firstNonEmpty(ByteString... strings) {
@ -202,4 +199,19 @@ public class Utils {
public static String nullIfEmpty(String 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

@ -111,18 +111,20 @@ class MergeRecipientHelperTest {
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI), rec(3, ADDR_A.NUM)),
ADDR_A.FULL,
Set.of(rec(1, ADDR_A.FULL))),
new T(Set.of(rec(1, ADDR_A.ACI.withIdentifiersFrom(ADDR_B.PNI)), rec(2, ADDR_A.PNI), rec(3, ADDR_A.NUM)),
ADDR_A.FULL,
Set.of(rec(1, ADDR_A.FULL))),
new T(Set.of(rec(1, ADDR_A.ACI.withIdentifiersFrom(ADDR_B.NUM)), rec(2, ADDR_A.PNI), rec(3, ADDR_A.NUM)),
ADDR_A.FULL,
Set.of(rec(1, ADDR_A.FULL))),
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI), rec(3, ADDR_A.NUM.withIdentifiersFrom(ADDR_B.ACI))),
new T(Set.of(rec(1, ADDR_B.PNI.withOtherIdentifiersFrom(ADDR_A.ACI)),
rec(2, ADDR_A.PNI),
rec(3, ADDR_A.NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
new T(Set.of(rec(1, ADDR_B.NUM.withOtherIdentifiersFrom(ADDR_A.ACI)),
rec(2, ADDR_A.PNI),
rec(3, ADDR_A.NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
new T(Set.of(rec(1, ADDR_A.ACI),
rec(2, ADDR_A.PNI),
rec(3, ADDR_B.ACI.withOtherIdentifiersFrom(ADDR_A.NUM))),
ADDR_A.FULL,
Set.of(rec(1, ADDR_A.FULL), rec(3, ADDR_B.ACI))),
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI.withIdentifiersFrom(ADDR_B.ACI)), rec(3, ADDR_A.NUM)),
ADDR_A.FULL,
Set.of(rec(1, ADDR_A.FULL), rec(2, ADDR_B.ACI))),
new T(Set.of(rec(1, ADDR_A.ACI),
rec(2, ADDR_B.ACI.withOtherIdentifiersFrom(ADDR_A.PNI)),
rec(3, ADDR_A.NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL), rec(2, ADDR_B.ACI))),
};
@ParameterizedTest

View file

@ -1,4 +1,6 @@
A2X = a2x
MKDIR = mkdir
GZIP = gzip
MANPAGESRC = signal-cli.1 signal-cli-dbus.5 signal-cli-jsonrpc.5
@ -8,3 +10,13 @@ all: $(MANPAGESRC)
%: %.adoc
@echo "Generating manpage for $@"
$(A2X) --no-xmllint --doctype manpage --format manpage "$^"
.PHONY: install
install: all
$(MKDIR) -p man1 man5
for f in *.1; do $(GZIP) < "$$f" > man1/"$$f".gz ; done
for f in *.5; do $(GZIP) < "$$f" > man5/"$$f".gz ; done
.PHONY: clean
clean:
rm -rf *.1 *.5 man1 man5

View file

@ -316,6 +316,11 @@ 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>`
*--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::
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.
@ -657,7 +662,7 @@ Path to the new avatar image file.
*--remove-avatar*::
Remove the avatar
*--mobile-coin-address*::
*--mobile-coin-address*, **--mobilecoin-address**::
New MobileCoin address (Base64 encoded public address)
=== updateContact
@ -669,11 +674,20 @@ If the contact doesn't exist yet, it will be added.
RECIPIENT::
Specify the recipient.
*--given-name* NAME, *--name* NAME::
New (given) name.
*--given-name* GIVEN_NAME, *--name* NAME::
New system given 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::
Set expiration time of messages (seconds).

View file

@ -1,4 +1,4 @@
#!/bin/sh
#!/bin/bash
if [ $# -ne 2 ]; then
echo "Usage: $0 NUMBER_1 NUMBER_2"
exit 1
@ -133,7 +133,7 @@ mkfifo "$FIFO_FILE"
run_main -a "$NUMBER_1" send "$NUMBER_2" -m hi
run_main -a "$NUMBER_2" jsonRpc < "$FIFO_FILE" &
exec 3<> "$FIFO_FILE"
exec 3> "$FIFO_FILE"
echo '{"jsonrpc":"2.0","id":"id","method":"updateContact","params":{"recipient":"'"$NUMBER_1"'","name":"NUMBER_1","expiration":10}}' >&3
echo '{"jsonrpc":"2.0","id":5,"method":"block","params":{"recipient":"'"$NUMBER_1"'"}}' >&3
echo '{"jsonrpc":"2.0","id":null,"method":"unblock","params":{"recipient":"'"$NUMBER_1"'"}}' >&3
@ -141,6 +141,7 @@ exec 3<> "$FIFO_FILE"
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":"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":"sendContacts"}' >&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_1" 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
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
@ -237,7 +249,9 @@ for OUTPUT in "plain-text" "json"; do
run_linked -a "$NUMBER_1" --output="$OUTPUT" receive
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
if [ "$TEST_REGISTER" -eq 1 ]; then

View file

@ -6,4 +6,6 @@ dependencyResolutionManagement {
}
rootProject.name = "signal-cli"
include("lib")
include("libsignal-cli")
project(":libsignal-cli").projectDir = file("lib")

View file

@ -99,6 +99,9 @@ public class App {
.help("Disable message send log (for resending messages that recipient couldn't decrypt)")
.action(Arguments.storeTrue());
parser.epilog(
"The global arguments are shown with 'signal-cli -h' and need to come before the subcommand, while the subcommand-specific arguments (shown with 'signal-cli SUBCOMMAND -h') need to be given after the subcommand.");
var subparsers = parser.addSubparsers().title("subcommands").dest("command");
Commands.getCommandSubparserAttachers().forEach((key, value) -> {
@ -288,6 +291,8 @@ public class App {
commandHandler.handleMultiLocalCommand(command, multiAccountManager);
} catch (IOException 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();
static final String USER_AGENT_SIGNAL_ANDROID = Optional.ofNullable(System.getenv("SIGNAL_CLI_USER_AGENT"))
.orElse("Signal-Android/7.26.1");
.orElse("Signal-Android/7.47.1");
static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null
? "signal-cli"
: PROJECT_NAME + "/" + PROJECT_VERSION;

View file

@ -1,5 +1,6 @@
package org.asamk.signal;
import org.asamk.signal.commands.exceptions.CommandException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -11,7 +12,7 @@ import sun.misc.Signal;
public class Shutdown {
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 boolean initialized = false;
@ -43,9 +44,17 @@ public class Shutdown {
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 {
shutdown.get();
final var result = shutdown.get();
if (result instanceof CommandException e) {
throw e;
}
} catch (ExecutionException e) {
throw new RuntimeException(e);
}

View file

@ -97,7 +97,7 @@ public class DaemonCommand implements MultiLocalCommand, LocalCommand {
final OutputWriter outputWriter
) throws CommandException {
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 receiveMode = ns.<ReceiveMode>get("receive-mode");
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.api.IncorrectPinException;
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.output.OutputWriter;
@ -50,6 +51,8 @@ public class FinishChangeNumberCommand implements JsonRpcLocalCommand {
+ "\nUse '--pin PIN_CODE' to specify the registration lock PIN");
} catch (IncorrectPinException e) {
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) {
throw new UserErrorException("This command doesn't work on linked devices.");
} catch (IOException e) {

View file

@ -64,9 +64,16 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand {
}
final var usernames = ns.<String>getList("username");
final var registeredUsernames = usernames == null
? Map.<String, UsernameStatus>of()
: m.getUsernameStatus(new HashSet<>(usernames));
final Map<String, UsernameStatus> registeredUsernames;
try {
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
switch (outputWriter) {

View file

@ -66,6 +66,9 @@ public class SendCommand implements JsonRpcLocalCommand {
.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. "
+ "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")
.help("Clear session state and send end session message.")
.action(Arguments.storeTrue());
@ -164,6 +167,7 @@ public class SendCommand implements JsonRpcLocalCommand {
if (attachments == null) {
attachments = List.of();
}
final var viewOnce = Boolean.TRUE.equals(ns.getBoolean("view-once"));
final var selfNumber = m.getSelfNumber();
@ -179,6 +183,9 @@ public class SendCommand implements JsonRpcLocalCommand {
final var quoteTimestamp = ns.getLong("quote-timestamp");
if (quoteTimestamp != null) {
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 quoteMentionStrings = ns.<String>getList("quote-mention");
final var quoteMentions = quoteMentionStrings == null
@ -236,6 +243,7 @@ public class SendCommand implements JsonRpcLocalCommand {
try {
final var message = new Message(messageText,
attachments,
viewOnce,
mentions,
Optional.ofNullable(quote),
Optional.ofNullable(sticker),
@ -247,8 +255,14 @@ public class SendCommand implements JsonRpcLocalCommand {
: m.sendMessage(message, recipientIdentifiers, notifySelf);
outputResult(outputWriter, results);
} catch (AttachmentInvalidException | IOException e) {
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
.getSimpleName() + ")", e);
if (e instanceof IOException io && io.getMessage().contains("No prekeys available")) {
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) {
throw new UserErrorException(e.getMessage());
} catch (UnregisteredRecipientException e) {

View file

@ -26,8 +26,11 @@ public class UpdateContactCommand implements JsonRpcLocalCommand {
subparser.help("Update the details of a given contact");
subparser.addArgument("recipient").help("Contact number");
subparser.addArgument("-n", "--name").help("New contact name");
subparser.addArgument("--given-name").help("New contact given name");
subparser.addArgument("--family-name").help("New contact family name");
subparser.addArgument("--given-name").help("New system given 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)");
}
@ -54,8 +57,15 @@ public class UpdateContactCommand implements JsonRpcLocalCommand {
familyName = "";
}
}
if (givenName != null || familyName != null) {
m.setContactName(recipient, givenName, familyName);
var nickGivenName = ns.getString("nick-given-name");
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) {
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("--about").help("New profile about text");
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();
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.manager.RegistrationManager;
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.output.JsonWriter;
import org.slf4j.Logger;
@ -76,6 +77,8 @@ public class VerifyCommand implements RegistrationCommand, JsonRpcRegistrationCo
+ "\nUse '--pin PIN_CODE' to specify the registration lock PIN");
} catch (IncorrectPinException e) {
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) {
throw new IOErrorException("Verify error: " + e.getMessage(), e);
}

View file

@ -1,17 +1,21 @@
package org.asamk.signal.dbus;
import org.asamk.signal.DbusConfig;
import org.asamk.signal.Shutdown;
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.UserErrorException;
import org.asamk.signal.manager.Manager;
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.DBusConnectionBuilder;
import org.freedesktop.dbus.exceptions.DBusException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@ -94,7 +98,9 @@ public class DbusHandler implements AutoCloseable {
final var busType = isDbusSystem ? DBusConnection.DBusBusType.SYSTEM : DBusConnection.DBusBusType.SESSION;
logger.debug("Starting DBus server on {} bus: {}", busType, busname);
try {
dBusConnection = DBusConnectionBuilder.forType(busType).build();
dBusConnection = DBusConnectionBuilder.forType(busType)
.withDisconnectCallback(new DisconnectCallback())
.build();
dbusRunner.run(dBusConnection);
} catch (DBusException 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;
}
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));
}
}
}

Some files were not shown because too many files have changed in this diff Show more