Compare commits

...

144 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
AsamK
0a296e77a0 Bump version to 0.13.10 2024-11-30 16:14:53 +01:00
AsamK
ba147a48f8 Update reflect config 2024-11-30 15:45:26 +01:00
AsamK
77a5c454b7 Improve documentation for recipient arguments
Fixes #1639
2024-11-29 21:10:46 +01:00
AsamK
2c68b5a9e1 Add support for using PNI as recipient 2024-11-29 21:10:46 +01:00
AsamK
68c9d84d19 Update libsignal-service
Fixes #1633
2024-11-24 13:04:04 +01:00
AsamK
fe752e0c79 Add simplification for single recipient reactions 2024-11-24 11:51:54 +01:00
AsamK
26b5a4c582 Small code improvement 2024-11-23 23:57:23 +01:00
AsamK
10ee295ea3 Fix receiving shared contacts in graalvm mode
Fixes #1629
2024-11-23 23:57:23 +01:00
AsamK
6a5ea5fc01 Workaround issue with invalid address in recipient store 2024-11-23 23:57:23 +01:00
AsamK
ff6cb5262a Update libsignal-service
Support for storage encryption v2 and account entropy pool

Fixes #1632
2024-11-23 23:57:23 +01:00
AsamK
f2005593ec Reformat files 2024-11-23 22:35:06 +01:00
AsamK
3533500b73 Update dependencies 2024-11-23 22:35:06 +01:00
AsamK
e5251ae158 Switch to using toml version catalogs 2024-11-21 21:17:33 +01:00
AsamK
a5e272be3f Prepare next release 2024-10-28 23:45:03 +01:00
207 changed files with 4354 additions and 2326 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,5 +1,120 @@
# 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
- Fix receiving expiration timer updates
### Improved
- Add support for new storage encryption scheme
## [0.13.9] - 2024-10-28
### Fixed

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.9"
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,33 @@
<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>
<release version="0.13.9" date="2024-10-28">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.9</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"}]
@ -228,6 +251,10 @@
"name":"org.signal.libsignal.usernames.CannotBeEmptyException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.usernames.DiscriminatorCannotBeZeroException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.usernames.MissingSeparatorException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]

View file

@ -39,9 +39,24 @@
{
"name":"[Ljava.sql.Statement;"
},
{
"name":"[Lorg.asamk.signal.commands.ListStickerPacksCommand$JsonStickerPack$JsonSticker;"
},
{
"name":"[Lorg.asamk.signal.json.JsonAttachment;"
},
{
"name":"[Lorg.asamk.signal.json.JsonCallMessage$IceUpdate;"
},
{
"name":"[Lorg.asamk.signal.json.JsonContactAddress;"
},
{
"name":"[Lorg.asamk.signal.json.JsonContactEmail;"
},
{
"name":"[Lorg.asamk.signal.json.JsonContactPhone;"
},
{
"name":"[Lorg.asamk.signal.json.JsonMention;"
},
@ -51,6 +66,9 @@
{
"name":"[Lorg.asamk.signal.json.JsonQuotedAttachment;"
},
{
"name":"[Lorg.asamk.signal.json.JsonSharedContact;"
},
{
"name":"[Lorg.asamk.signal.json.JsonSyncReadMessage;"
},
@ -60,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;"
},
@ -124,6 +145,13 @@
"name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.squareup.wire.Message",
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
},
{
"name":"com.squareup.wire.ProtoAdapter"
},
{
"name":"com.squareup.wire.internal.ImmutableList",
"allDeclaredFields":true,
@ -209,9 +237,14 @@
{
"name":"java.io.FilePermission"
},
{
"name":"java.io.OutputStream"
},
{
"name":"java.io.Serializable",
"allDeclaredMethods":true
"allDeclaredFields":true,
"allDeclaredMethods":true,
"allDeclaredClasses":true
},
{
"name":"java.lang.Boolean",
@ -426,6 +459,12 @@
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"java.util.ImmutableCollections$List12",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{
"name":"java.util.ImmutableCollections$ListN",
"allDeclaredFields":true,
@ -577,6 +616,9 @@
{
"name":"kotlin.String"
},
{
"name":"kotlin.Unit"
},
{
"name":"kotlin.collections.AbstractCollection",
"allDeclaredFields":true,
@ -629,6 +671,13 @@
{
"name":"long[]"
},
{
"name":"okhttp3.internal.connection.RealConnectionPool",
"fields":[{"name":"addressStates"}]
},
{
"name":"okio.BufferedSink"
},
{
"name":"okio.ByteString"
},
@ -990,7 +1039,7 @@
"allDeclaredFields":true,
"allDeclaredMethods":true,
"allDeclaredConstructors":true,
"methods":[{"name":"display","parameterTypes":[] }, {"name":"family","parameterTypes":[] }, {"name":"given","parameterTypes":[] }, {"name":"middle","parameterTypes":[] }, {"name":"prefix","parameterTypes":[] }, {"name":"suffix","parameterTypes":[] }]
"methods":[{"name":"display","parameterTypes":[] }, {"name":"family","parameterTypes":[] }, {"name":"given","parameterTypes":[] }, {"name":"middle","parameterTypes":[] }, {"name":"nickname","parameterTypes":[] }, {"name":"prefix","parameterTypes":[] }, {"name":"suffix","parameterTypes":[] }]
},
{
"name":"org.asamk.signal.json.JsonContactPhone",
@ -1025,7 +1074,7 @@
"allDeclaredFields":true,
"allDeclaredMethods":true,
"allDeclaredConstructors":true,
"methods":[{"name":"groupId","parameterTypes":[] }, {"name":"type","parameterTypes":[] }]
"methods":[{"name":"groupId","parameterTypes":[] }, {"name":"groupName","parameterTypes":[] }, {"name":"revision","parameterTypes":[] }, {"name":"type","parameterTypes":[] }]
},
{
"name":"org.asamk.signal.json.JsonMention",
@ -1247,7 +1296,7 @@
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["int","long","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"<init>","parameterTypes":["int","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"<init>","parameterTypes":["int","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"aciAccountData","parameterTypes":[] }, {"name":"deviceId","parameterTypes":[] }, {"name":"encryptedDeviceName","parameterTypes":[] }, {"name":"isMultiDevice","parameterTypes":[] }, {"name":"number","parameterTypes":[] }, {"name":"password","parameterTypes":[] }, {"name":"pinMasterKey","parameterTypes":[] }, {"name":"pniAccountData","parameterTypes":[] }, {"name":"profileKey","parameterTypes":[] }, {"name":"registered","parameterTypes":[] }, {"name":"registrationLockPin","parameterTypes":[] }, {"name":"serviceEnvironment","parameterTypes":[] }, {"name":"storageKey","parameterTypes":[] }, {"name":"timestamp","parameterTypes":[] }, {"name":"username","parameterTypes":[] }, {"name":"usernameLinkEntropy","parameterTypes":[] }, {"name":"usernameLinkServerId","parameterTypes":[] }, {"name":"version","parameterTypes":[] }]
"methods":[{"name":"<init>","parameterTypes":["int","long","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"<init>","parameterTypes":["int","long","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"<init>","parameterTypes":["int","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"<init>","parameterTypes":["int","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"accountEntropyPool","parameterTypes":[] }, {"name":"aciAccountData","parameterTypes":[] }, {"name":"deviceId","parameterTypes":[] }, {"name":"encryptedDeviceName","parameterTypes":[] }, {"name":"isMultiDevice","parameterTypes":[] }, {"name":"mediaRootBackupKey","parameterTypes":[] }, {"name":"number","parameterTypes":[] }, {"name":"password","parameterTypes":[] }, {"name":"pinMasterKey","parameterTypes":[] }, {"name":"pniAccountData","parameterTypes":[] }, {"name":"profileKey","parameterTypes":[] }, {"name":"registered","parameterTypes":[] }, {"name":"registrationLockPin","parameterTypes":[] }, {"name":"serviceEnvironment","parameterTypes":[] }, {"name":"storageKey","parameterTypes":[] }, {"name":"timestamp","parameterTypes":[] }, {"name":"username","parameterTypes":[] }, {"name":"usernameLinkEntropy","parameterTypes":[] }, {"name":"usernameLinkServerId","parameterTypes":[] }, {"name":"version","parameterTypes":[] }]
},
{
"name":"org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData",
@ -1364,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,
@ -1499,6 +1554,10 @@
"name":"org.bouncycastle.jcajce.provider.asymmetric.COMPOSITE$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.CONTEXT$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.CompositeSignatures$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
@ -1559,14 +1618,30 @@
"name":"org.bouncycastle.jcajce.provider.asymmetric.LMS$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.MLDSA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.MLKEM$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"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":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.SLHDSA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.SPHINCSPlus$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
@ -1979,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"
@ -2241,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":"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",
@ -2268,6 +2346,20 @@
{
"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,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","int","kotlin.jvm.internal.DefaultConstructorMarker"] }]
},
{
"name":"org.whispersystems.signalservice.api.messages.calls.HangupMessage",
"allDeclaredFields":true,
@ -2328,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",
@ -2373,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,
@ -2851,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",
@ -2875,7 +3001,26 @@
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord",
"allDeclaredFields":true
"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"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$PinnedConversation",
@ -2889,9 +3034,22 @@
"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
"allDeclaredFields":true,
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$Builder"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$Companion"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$IdentityState"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$Name",
@ -2899,11 +3057,30 @@
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record",
"allDeclaredFields":true
"allDeclaredFields":true,
"fields":[{"name":"archived"}, {"name":"blocked"}, {"name":"id"}, {"name":"markedUnread"}, {"name":"mutedUntilTimestamp"}, {"name":"whitelisted"}],
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record$Builder"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record$Companion"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record",
"allDeclaredFields":true
"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":[] }]
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record$Builder"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record$Companion"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record$StorySendMode"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord",
@ -2913,6 +3090,9 @@
"name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord$Identifier",
"fields":[{"name":"raw_"}, {"name":"type_"}]
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.OptionalBool"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.Payments",
"allDeclaredFields":true

17
gradle/libs.versions.toml Normal file
View file

@ -0,0 +1,17 @@
[versions]
slf4j = "2.0.17"
[libraries]
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.18"
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,
@ -130,19 +132,24 @@ public interface Manager extends Closeable {
void deleteUsername() throws IOException;
void startChangeNumber(
String newNumber, boolean voiceVerification, String captcha
String newNumber,
boolean voiceVerification,
String captcha
) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, NotPrimaryDeviceException, VerificationMethodNotAvailableException;
void finishChangeNumber(
String newNumber, String verificationCode, String pin
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException;
String newNumber,
String verificationCode,
String pin
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException;
void unregister() throws IOException;
void deleteAccount() throws IOException;
void submitRateLimitRecaptchaChallenge(
String challenge, String captcha
String challenge,
String captcha
) throws IOException, CaptchaRejectedException;
List<Device> getLinkedDevices() throws IOException;
@ -156,17 +163,21 @@ public interface Manager extends Closeable {
List<Group> getGroups();
SendGroupMessageResults quitGroup(
GroupId groupId, Set<RecipientIdentifier.Single> groupAdmins
GroupId groupId,
Set<RecipientIdentifier.Single> groupAdmins
) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException;
void deleteGroup(GroupId groupId) throws IOException;
Pair<GroupId, SendGroupMessageResults> createGroup(
String name, Set<RecipientIdentifier.Single> members, String avatarFile
String name,
Set<RecipientIdentifier.Single> members,
String avatarFile
) throws IOException, AttachmentInvalidException, UnregisteredRecipientException;
SendGroupMessageResults updateGroup(
final GroupId groupId, final UpdateGroup updateGroup
final GroupId groupId,
final UpdateGroup updateGroup
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException;
Pair<GroupId, SendGroupMessageResults> joinGroup(
@ -174,27 +185,29 @@ public interface Manager extends Closeable {
) throws IOException, InactiveGroupLinkException, PendingAdminApprovalException;
SendMessageResults sendTypingMessage(
TypingAction action, Set<RecipientIdentifier> recipients
TypingAction action,
Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
SendMessageResults sendReadReceipt(
RecipientIdentifier.Single sender, List<Long> messageIds
);
SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List<Long> messageIds);
SendMessageResults sendViewedReceipt(
RecipientIdentifier.Single sender, List<Long> messageIds
);
SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List<Long> messageIds);
SendMessageResults sendMessage(
Message message, Set<RecipientIdentifier> recipients, boolean notifySelf
Message message,
Set<RecipientIdentifier> recipients,
boolean notifySelf
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
SendMessageResults sendEditMessage(
Message message, Set<RecipientIdentifier> recipients, long editTargetTimestamp
Message message,
Set<RecipientIdentifier> recipients,
long editTargetTimestamp
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
SendMessageResults sendRemoteDeleteMessage(
long targetSentTimestamp, Set<RecipientIdentifier> recipients
long targetSentTimestamp,
Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
SendMessageResults sendMessageReaction(
@ -207,13 +220,16 @@ public interface Manager extends Closeable {
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
SendMessageResults sendPaymentNotificationMessage(
byte[] receipt, String note, RecipientIdentifier.Single recipient
byte[] receipt,
String note,
RecipientIdentifier.Single recipient
) throws IOException;
SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException;
SendMessageResults sendMessageRequestResponse(
MessageEnvelope.Sync.MessageRequestResponse.Type type, Set<RecipientIdentifier> recipientIdentifiers
MessageEnvelope.Sync.MessageRequestResponse.Type type,
Set<RecipientIdentifier> recipientIdentifiers
);
void hideRecipient(RecipientIdentifier.Single recipient);
@ -223,22 +239,30 @@ 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(
Collection<RecipientIdentifier.Single> recipient, boolean blocked
Collection<RecipientIdentifier.Single> recipient,
boolean blocked
) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException;
void setGroupsBlocked(
Collection<GroupId> groupId, boolean blocked
Collection<GroupId> groupId,
boolean blocked
) throws GroupNotFoundException, IOException, NotPrimaryDeviceException;
/**
* Change the expiration timer for a contact
*/
void setExpirationTimer(
RecipientIdentifier.Single recipient, int messageExpirationTimer
RecipientIdentifier.Single recipient,
int messageExpirationTimer
) throws IOException, UnregisteredRecipientException;
/**
@ -277,7 +301,9 @@ public interface Manager extends Closeable {
* Receive new messages from server, returns if no new message arrive in a timespan of timeout.
*/
void receiveMessages(
Optional<Duration> timeout, Optional<Integer> maxMessages, ReceiveMessageHandler handler
Optional<Duration> timeout,
Optional<Integer> maxMessages,
ReceiveMessageHandler handler
) throws IOException, AlreadyReceivingException;
void stopReceiveMessages();
@ -309,7 +335,8 @@ public interface Manager extends Closeable {
* @param recipient account of the identity
*/
boolean trustIdentityVerified(
RecipientIdentifier.Single recipient, IdentityVerificationCode verificationCode
RecipientIdentifier.Single recipient,
IdentityVerificationCode verificationCode
) throws UnregisteredRecipientException;
/**

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;
@ -13,12 +14,15 @@ import java.io.IOException;
public interface RegistrationManager extends Closeable {
void register(
boolean voiceVerification, String captcha, final boolean forceRegister
boolean voiceVerification,
String captcha,
final boolean forceRegister
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException;
void verifyAccount(
String verificationCode, String pin
) throws IOException, PinLockedException, IncorrectPinException;
String verificationCode,
String pin
) 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);
}
@ -85,7 +95,8 @@ public class SignalAccountFiles {
}
private Manager initManager(
String number, String accountPath
String number,
String accountPath
) throws IOException, NotRegisteredException, AccountCheckException {
if (accountPath == null) {
throw new NotRegisteredException();
@ -152,7 +163,8 @@ public class SignalAccountFiles {
}
public RegistrationManager initRegistrationManager(
String number, Consumer<Manager> newManagerListener
String number,
Consumer<Manager> newManagerListener
) throws IOException {
final var accountPath = accountsStore.getPathByNumber(number);
if (accountPath == null || !SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) {

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

@ -13,7 +13,9 @@ public class ResendMessageAction implements HandleAction {
private final MessageSendLogEntry messageSendLogEntry;
public ResendMessageAction(
final RecipientId recipientId, final long timestamp, final MessageSendLogEntry messageSendLogEntry
final RecipientId recipientId,
final long timestamp,
final MessageSendLogEntry messageSendLogEntry
) {
this.recipientId = recipientId;
this.timestamp = timestamp;

View file

@ -15,7 +15,9 @@ public class SendReceiptAction implements HandleAction {
private final List<Long> timestamps = new ArrayList<>();
public SendReceiptAction(
final RecipientId recipientId, final SignalServiceReceiptMessage.Type type, final long timestamp
final RecipientId recipientId,
final SignalServiceReceiptMessage.Type type,
final long timestamp
) {
this.recipientId = recipientId;
this.type = type;

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

@ -27,7 +27,9 @@ public record Group(
) {
public static Group from(
final GroupInfo groupInfo, final RecipientAddressResolver recipientStore, final RecipientId selfRecipientId
final GroupInfo groupInfo,
final RecipientAddressResolver recipientStore,
final RecipientId selfRecipientId
) {
return new Group(groupInfo.getGroupId(),
groupInfo.getTitle(),

View file

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

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

@ -338,7 +338,8 @@ public record MessageEnvelope(
}
static Attachment from(
SignalServiceDataMessage.Quote.QuotedAttachment a, final AttachmentFileProvider fileProvider
SignalServiceDataMessage.Quote.QuotedAttachment a,
final AttachmentFileProvider fileProvider
) {
return new Attachment(Optional.empty(),
Optional.empty(),
@ -510,9 +511,7 @@ public record MessageEnvelope(
public record Preview(String title, String description, long date, String url, Optional<Attachment> image) {
static Preview from(
SignalServicePreview preview, final AttachmentFileProvider fileProvider
) {
static Preview from(SignalServicePreview preview, final AttachmentFileProvider fileProvider) {
return new Preview(preview.getTitle(),
preview.getDescription(),
preview.getDate(),
@ -612,11 +611,12 @@ public record MessageEnvelope(
RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver
) {
return new Blocked(blockedListMessage.getAddresses()
.stream()
.map(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(d))
.toApiRecipientAddress())
.toList(), blockedListMessage.getGroupIds().stream().map(GroupId::unknownVersion).toList());
return new Blocked(blockedListMessage.individuals.stream()
.map(d -> new RecipientAddress(d.getAci() == null ? null : d.getAci().toString(),
null,
d.getE164(),
null))
.toList(), blockedListMessage.groupIds.stream().map(GroupId::unknownVersion).toList());
}
}
@ -832,9 +832,7 @@ public record MessageEnvelope(
Optional<TextAttachment> textAttachment
) {
public static Story from(
SignalServiceStoryMessage storyMessage, final AttachmentFileProvider fileProvider
) {
public static Story from(SignalServiceStoryMessage storyMessage, final AttachmentFileProvider fileProvider) {
return new Story(storyMessage.getAllowsReplies().orElse(false),
storyMessage.getGroupContext().map(c -> GroupUtils.getGroupIdV2(c.getMasterKey())),
storyMessage.getFileAttachment().map(f -> Data.Attachment.from(f, fileProvider)),
@ -852,7 +850,8 @@ public record MessageEnvelope(
) {
static TextAttachment from(
SignalServiceTextAttachment textAttachment, final AttachmentFileProvider fileProvider
SignalServiceTextAttachment textAttachment,
final AttachmentFileProvider fileProvider
) {
return new TextAttachment(textAttachment.getText(),
textAttachment.getStyle().map(Style::from),

View file

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

View file

@ -161,7 +161,8 @@ public class Profile {
}
public enum Capability {
storage;
storage,
storageServiceEncryptionV2Capability;
public static Capability valueOfOrNull(String value) {
try {

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,24 +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("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) {
@ -50,7 +54,7 @@ public sealed interface RecipientIdentifier {
} else if (address.aci().isPresent()) {
return new Uuid(UUID.fromString(address.aci().get()));
} else if (address.pni().isPresent()) {
return new Pni(address.pni().get());
return new Pni(UUID.fromString(address.pni().get().substring(4)));
} else if (address.username().isPresent()) {
return new Username(address.username().get());
}
@ -73,16 +77,16 @@ public sealed interface RecipientIdentifier {
}
}
record Pni(String pni) implements Single {
record Pni(UUID pni) implements Single {
@Override
public String getIdentifier() {
return pni;
return "PNI:" + pni.toString();
}
@Override
public RecipientAddress toPartialRecipientAddress() {
return new RecipientAddress(null, pni, null, null);
return new RecipientAddress(null, getIdentifier(), null, null);
}
}

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==");
@ -51,7 +53,7 @@ class LiveConfig {
private static final byte[] backupServerPublicParams = Base64.getDecoder()
.decode("AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O");
private static Environment LIBSIGNAL_NET_ENV = Environment.PRODUCTION;
private static final Environment LIBSIGNAL_NET_ENV = Environment.PRODUCTION;
static SignalServiceConfiguration createDefaultServiceConfiguration(
final List<Interceptor> interceptors
@ -69,14 +71,16 @@ class LiveConfig {
interceptors,
dns,
proxy,
systemProxy,
zkGroupServerPublicParams,
genericServerPublicParams,
backupServerPublicParams);
backupServerPublicParams,
false);
}
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);
}
@ -88,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;
@ -29,11 +29,14 @@ public class ServiceConfig {
public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) {
final var deleteSync = !isPrimaryDevice;
return new AccountAttributes.Capabilities(true, deleteSync, true);
final var storageEncryptionV2 = !isPrimaryDevice;
final var attachmentBackfill = !isPrimaryDevice;
return new AccountAttributes.Capabilities(true, deleteSync, true, storageEncryptionV2, attachmentBackfill);
}
public static ServiceEnvironmentConfig getServiceEnvironmentConfig(
ServiceEnvironment serviceEnvironment, String userAgent
ServiceEnvironment serviceEnvironment,
String userAgent
) {
final Interceptor userAgentInterceptor = chain -> chain.proceed(chain.request()
.newBuilder()

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==");
@ -51,7 +53,7 @@ class StagingConfig {
private static final byte[] backupServerPublicParams = Base64.getDecoder()
.decode("AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8");
private static Network.Environment LIBSIGNAL_NET_ENV = Network.Environment.STAGING;
private static final Network.Environment LIBSIGNAL_NET_ENV = Network.Environment.STAGING;
static SignalServiceConfiguration createDefaultServiceConfiguration(
final List<Interceptor> interceptors
@ -69,14 +71,16 @@ class StagingConfig {
interceptors,
dns,
proxy,
systemProxy,
zkGroupServerPublicParams,
genericServerPublicParams,
backupServerPublicParams);
backupServerPublicParams,
false);
}
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);
}
@ -88,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

@ -18,7 +18,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
public class GroupUtils {
public static void setGroupContext(
final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo groupInfo
final SignalServiceDataMessage.Builder messageBuilder,
final GroupInfo groupInfo
) {
if (groupInfo instanceof GroupInfoV1) {
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)

View file

@ -3,8 +3,8 @@ package org.asamk.signal.manager.helper;
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.InvalidDeviceLinkException;
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;
@ -27,11 +27,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
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,12 +52,13 @@ 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;
import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
public class AccountHelper {
@ -101,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();
@ -165,7 +168,9 @@ public class AccountHelper {
}
public void startChangeNumber(
String newNumber, boolean voiceVerification, String captcha
String newNumber,
boolean voiceVerification,
String captcha
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException {
final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword());
final var registrationApi = accountManager.getRegistrationApi();
@ -178,8 +183,10 @@ public class AccountHelper {
}
public void finishChangeNumber(
String newNumber, String verificationCode, String pin
) throws IncorrectPinException, PinLockedException, IOException {
String newNumber,
String verificationCode,
String pin
) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
for (var attempts = 0; attempts < 5; attempts++) {
try {
finishChangeNumberInternal(newNumber, verificationCode, pin);
@ -196,8 +203,10 @@ public class AccountHelper {
}
private void finishChangeNumberInternal(
String newNumber, String verificationCode, String pin
) throws IncorrectPinException, PinLockedException, IOException {
String newNumber,
String verificationCode,
String pin
) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
final var pniIdentity = KeyUtils.generateIdentityKeyPair();
final var encryptedDeviceMessages = new ArrayList<OutgoingPushMessage>();
final var devicePniSignedPreKeys = new HashMap<Integer, SignedPreKeyEntity>();
@ -282,13 +291,13 @@ public class AccountHelper {
context.getPinHelper(),
(sessionId1, verificationCode1, registrationLock) -> {
final var registrationApi = dependencies.getRegistrationApi();
final var accountApi = dependencies.getAccountApi();
try {
Utils.handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1));
handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1));
} catch (AlreadyVerifiedException e) {
// Already verified so can continue changing number
}
return Utils.handleResponseException(registrationApi.changeNumber(new ChangePhoneNumberRequest(
sessionId1,
return handleResponseException(accountApi.changeNumber(new ChangePhoneNumberRequest(sessionId1,
null,
newNumber,
registrationLock,
@ -308,9 +317,7 @@ public class AccountHelper {
handlePniChangeNumberMessage(selfChangeNumber, updatePni);
}
public void handlePniChangeNumberMessage(
final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni
) {
public void handlePniChangeNumberMessage(final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni) {
if (pniChangeNumber.identityKeyPair != null
&& pniChangeNumber.registrationId != null
&& pniChangeNumber.signedPreKey != null) {
@ -374,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.");
@ -384,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());
@ -392,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) {
@ -434,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.");
}
@ -451,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) {
@ -461,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.");
}
@ -475,36 +515,39 @@ 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, InvalidDeviceLinkException, org.asamk.signal.manager.api.DeviceLimitExceededException {
String verificationCode;
public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, org.asamk.signal.manager.api.DeviceLimitExceededException {
final var linkDeviceApi = dependencies.getLinkDeviceApi();
final LinkedDeviceVerificationCodeResponse verificationCode;
try {
verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode();
verificationCode = handleResponseException(linkDeviceApi.getDeviceVerificationCode());
} catch (DeviceLimitExceededException e) {
throw new org.asamk.signal.manager.api.DeviceLimitExceededException("Too many linked devices", e);
}
try {
dependencies.getAccountManager()
.addDevice(deviceLinkInfo.deviceIdentifier(),
deviceLinkInfo.deviceKey(),
account.getAciIdentityKeyPair(),
account.getPniIdentityKeyPair(),
account.getProfileKey(),
account.getOrCreatePinMasterKey(),
verificationCode);
} catch (InvalidKeyException e) {
throw new InvalidDeviceLinkException("Invalid device link", e);
}
handleResponseException(dependencies.getLinkDeviceApi()
.linkDevice(account.getNumber(),
account.getAci(),
account.getPni(),
deviceLinkInfo.deviceIdentifier(),
deviceLinkInfo.deviceKey(),
account.getAciIdentityKeyPair(),
account.getPniIdentityKeyPair(),
account.getProfileKey(),
account.getOrCreateAccountEntropyPool(),
account.getOrCreatePinMasterKey(),
account.getOrCreateMediaRootBackupKey(),
verificationCode.getVerificationCode(),
null));
account.setMultiDevice(true);
context.getJobExecutor().enqueueJob(new SyncStorageJob());
}
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);
}
@ -512,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();
@ -528,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);
}
@ -537,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();
@ -551,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 {
@ -104,9 +111,7 @@ public class AttachmentHelper {
retrieveAttachment(attachment, input -> IOUtils.copyStream(input, outputStream));
}
public void retrieveAttachment(
SignalServiceAttachment attachment, AttachmentHandler consumer
) throws IOException {
public void retrieveAttachment(SignalServiceAttachment attachment, AttachmentHandler consumer) throws IOException {
if (attachment.isStream()) {
var input = attachment.asStream().getInputStream();
// don't close input stream here, it might be reused later (e.g. with contact sync messages ...)
@ -131,11 +136,18 @@ public class AttachmentHelper {
}
private InputStream retrieveAttachmentAsStream(
SignalServiceAttachmentPointer pointer, File tmpFile
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());
}
@ -49,7 +65,9 @@ public class ContactHelper {
}
public void setExpirationTimer(
RecipientId recipientId, int messageExpirationTimer, int messageExpirationTimerVersion
RecipientId recipientId,
int messageExpirationTimer,
int messageExpirationTimerVersion
) {
var contact = account.getContactStore().getContact(recipientId);
if (contact != null && (

View file

@ -118,7 +118,9 @@ public class GroupHelper {
}
public GroupInfoV2 getOrMigrateGroup(
final GroupMasterKey groupMasterKey, final int revision, final byte[] signedGroupChange
final GroupMasterKey groupMasterKey,
final int revision,
final byte[] signedGroupChange
) {
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
@ -166,7 +168,8 @@ public class GroupHelper {
}
private DecryptedGroup handleDecryptedGroupResponse(
GroupInfoV2 groupInfoV2, final DecryptedGroupResponse decryptedGroupResponse
GroupInfoV2 groupInfoV2,
final DecryptedGroupResponse decryptedGroupResponse
) {
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
ReceivedGroupSendEndorsements groupSendEndorsements = dependencies.getGroupsV2Operations()
@ -181,7 +184,8 @@ public class GroupHelper {
}
private GroupChange handleGroupChangeResponse(
final GroupInfoV2 groupInfoV2, final GroupChangeResponse groupChangeResponse
final GroupInfoV2 groupInfoV2,
final GroupChangeResponse groupChangeResponse
) {
ReceivedGroupSendEndorsements groupSendEndorsements = dependencies.getGroupsV2Operations()
.forGroup(GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()))
@ -195,7 +199,9 @@ public class GroupHelper {
}
public Pair<GroupId, SendGroupMessageResults> createGroup(
String name, Set<RecipientId> members, String avatarFile
String name,
Set<RecipientId> members,
String avatarFile
) throws IOException, AttachmentInvalidException {
final var selfRecipientId = account.getSelfRecipientId();
if (members != null && members.contains(selfRecipientId)) {
@ -363,7 +369,8 @@ public class GroupHelper {
}
public SendGroupMessageResults quitGroup(
final GroupId groupId, final Set<RecipientId> newAdmins
final GroupId groupId,
final Set<RecipientId> newAdmins
) throws IOException, LastGroupAdminException, NotAGroupMemberException, GroupNotFoundException {
var group = getGroupForUpdating(groupId);
if (group instanceof GroupInfoV1) {
@ -396,9 +403,7 @@ public class GroupHelper {
context.getJobExecutor().enqueueJob(new SyncStorageJob());
}
public SendGroupMessageResults sendGroupInfoRequest(
GroupIdV1 groupId, RecipientId recipientId
) throws IOException {
public SendGroupMessageResults sendGroupInfoRequest(GroupIdV1 groupId, RecipientId recipientId) throws IOException {
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize());
var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build());
@ -408,7 +413,8 @@ public class GroupHelper {
}
public SendGroupMessageResults sendGroupInfoMessage(
GroupIdV1 groupId, RecipientId recipientId
GroupIdV1 groupId,
RecipientId recipientId
) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
GroupInfoV1 g;
var group = getGroupForUpdating(groupId);
@ -480,7 +486,9 @@ public class GroupHelper {
}
private void retrieveGroupV2Avatar(
GroupSecretParams groupSecretParams, String cdnKey, OutputStream outputStream
GroupSecretParams groupSecretParams,
String cdnKey,
OutputStream outputStream
) throws IOException {
var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
@ -543,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)
@ -583,7 +594,10 @@ public class GroupHelper {
}
private SendGroupMessageResults updateGroupV1(
final GroupInfoV1 gv1, final String name, final Set<RecipientId> members, final byte[] avatarFile
final GroupInfoV1 gv1,
final String name,
final Set<RecipientId> members,
final byte[] avatarFile
) throws IOException, AttachmentInvalidException {
updateGroupV1Details(gv1, name, members, avatarFile);
@ -596,7 +610,10 @@ public class GroupHelper {
}
private void updateGroupV1Details(
final GroupInfoV1 g, final String name, final Collection<RecipientId> members, final byte[] avatarFile
final GroupInfoV1 g,
final String name,
final Collection<RecipientId> members,
final byte[] avatarFile
) throws IOException {
if (name != null) {
g.name = name;
@ -615,7 +632,8 @@ public class GroupHelper {
* Change the expiration timer for a group
*/
private void setExpirationTimer(
GroupInfoV1 groupInfoV1, int messageExpirationTimer
GroupInfoV1 groupInfoV1,
int messageExpirationTimer
) throws NotAGroupMemberException, GroupNotFoundException, IOException, GroupSendingNotAllowedException {
groupInfoV1.messageExpirationTime = messageExpirationTimer;
account.getGroupStore().updateGroup(groupInfoV1);
@ -828,7 +846,8 @@ public class GroupHelper {
}
private SendGroupMessageResults quitGroupV2(
final GroupInfoV2 groupInfoV2, final Set<RecipientId> newAdmins
final GroupInfoV2 groupInfoV2,
final Set<RecipientId> newAdmins
) throws LastGroupAdminException, IOException {
final var currentAdmins = groupInfoV2.getAdminMembers();
newAdmins.removeAll(currentAdmins);
@ -882,7 +901,9 @@ public class GroupHelper {
}
private SendGroupMessageResults sendUpdateGroupV2Message(
GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
GroupInfoV2 group,
DecryptedGroup newDecryptedGroup,
GroupChange groupChange
) throws IOException {
final var selfRecipientId = account.getSelfRecipientId();
final var members = group.getMembersIncludingPendingWithout(selfRecipientId);

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;
@ -82,7 +84,7 @@ class GroupV2Helper {
final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
return dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupsV2AuthorizationString);
} catch (NonSuccessfulResponseCodeException e) {
if (e.getCode() == 403) {
if (e.code == 403) {
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
}
logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
@ -94,7 +96,8 @@ class GroupV2Helper {
}
DecryptedGroupJoinInfo getDecryptedGroupJoinInfo(
GroupMasterKey groupMasterKey, GroupLinkPassword password
GroupMasterKey groupMasterKey,
GroupLinkPassword password
) throws IOException, GroupLinkNotActiveException {
var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
@ -105,7 +108,9 @@ class GroupV2Helper {
}
GroupHistoryPage getDecryptedGroupHistoryPage(
final GroupSecretParams groupSecretParams, int fromRevision, long sendEndorsementsExpirationMs
final GroupSecretParams groupSecretParams,
int fromRevision,
long sendEndorsementsExpirationMs
) throws NotAGroupMemberException {
try {
final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
@ -115,8 +120,10 @@ class GroupV2Helper {
groupsV2AuthorizationString,
false,
sendEndorsementsExpirationMs);
} catch (NotInGroupException e) {
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
} catch (NonSuccessfulResponseCodeException e) {
if (e.getCode() == 403) {
if (e.code == 403) {
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
}
logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage());
@ -138,9 +145,7 @@ class GroupV2Helper {
return partialDecryptedGroup.revision;
}
Pair<GroupInfoV2, DecryptedGroupResponse> createGroup(
String name, Set<RecipientId> members, byte[] avatarFile
) {
Pair<GroupInfoV2, DecryptedGroupResponse> createGroup(String name, Set<RecipientId> members, byte[] avatarFile) {
final var newGroup = buildNewGroup(name, members, avatarFile);
if (newGroup == null) {
return null;
@ -170,9 +175,7 @@ class GroupV2Helper {
return new Pair<>(g, response);
}
private GroupsV2Operations.NewGroup buildNewGroup(
String name, Set<RecipientId> members, byte[] avatar
) {
private GroupsV2Operations.NewGroup buildNewGroup(String name, Set<RecipientId> members, byte[] avatar) {
final var profileKeyCredential = context.getProfileHelper()
.getExpiringProfileKeyCredential(context.getAccount().getSelfRecipientId());
if (profileKeyCredential == null) {
@ -202,7 +205,10 @@ class GroupV2Helper {
}
Pair<DecryptedGroup, GroupChangeResponse> updateGroup(
GroupInfoV2 groupInfoV2, String name, String description, byte[] avatarFile
GroupInfoV2 groupInfoV2,
String name,
String description,
byte[] avatarFile
) throws IOException {
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
@ -225,7 +231,8 @@ class GroupV2Helper {
}
Pair<DecryptedGroup, GroupChangeResponse> addMembers(
GroupInfoV2 groupInfoV2, Set<RecipientId> newMembers
GroupInfoV2 groupInfoV2,
Set<RecipientId> newMembers
) throws IOException {
GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
@ -251,7 +258,8 @@ class GroupV2Helper {
}
Pair<DecryptedGroup, GroupChangeResponse> leaveGroup(
GroupInfoV2 groupInfoV2, Set<RecipientId> membersToMakeAdmin
GroupInfoV2 groupInfoV2,
Set<RecipientId> membersToMakeAdmin
) throws IOException {
var pendingMembersList = groupInfoV2.getGroup().pendingMembers;
final var selfAci = getSelfAci();
@ -271,7 +279,8 @@ class GroupV2Helper {
}
Pair<DecryptedGroup, GroupChangeResponse> removeMembers(
GroupInfoV2 groupInfoV2, Set<RecipientId> members
GroupInfoV2 groupInfoV2,
Set<RecipientId> members
) throws IOException {
final var memberUuids = members.stream()
.map(context.getRecipientHelper()::resolveSignalServiceAddress)
@ -283,7 +292,8 @@ class GroupV2Helper {
}
Pair<DecryptedGroup, GroupChangeResponse> approveJoinRequestMembers(
GroupInfoV2 groupInfoV2, Set<RecipientId> members
GroupInfoV2 groupInfoV2,
Set<RecipientId> members
) throws IOException {
final var memberUuids = members.stream()
.map(context.getRecipientHelper()::resolveSignalServiceAddress)
@ -294,7 +304,8 @@ class GroupV2Helper {
}
Pair<DecryptedGroup, GroupChangeResponse> refuseJoinRequestMembers(
GroupInfoV2 groupInfoV2, Set<RecipientId> members
GroupInfoV2 groupInfoV2,
Set<RecipientId> members
) throws IOException {
final var memberUuids = members.stream()
.map(context.getRecipientHelper()::resolveSignalServiceAddress)
@ -304,7 +315,8 @@ class GroupV2Helper {
}
Pair<DecryptedGroup, GroupChangeResponse> revokeInvitedMembers(
GroupInfoV2 groupInfoV2, Set<RecipientId> members
GroupInfoV2 groupInfoV2,
Set<RecipientId> members
) throws IOException {
var pendingMembersList = groupInfoV2.getGroup().pendingMembers;
final var memberUuids = members.stream()
@ -318,7 +330,8 @@ class GroupV2Helper {
}
Pair<DecryptedGroup, GroupChangeResponse> banMembers(
GroupInfoV2 groupInfoV2, Set<RecipientId> block
GroupInfoV2 groupInfoV2,
Set<RecipientId> block
) throws IOException {
GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
@ -336,7 +349,8 @@ class GroupV2Helper {
}
Pair<DecryptedGroup, GroupChangeResponse> unbanMembers(
GroupInfoV2 groupInfoV2, Set<RecipientId> block
GroupInfoV2 groupInfoV2,
Set<RecipientId> block
) throws IOException {
GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
@ -359,7 +373,8 @@ class GroupV2Helper {
}
Pair<DecryptedGroup, GroupChangeResponse> setGroupLinkState(
GroupInfoV2 groupInfoV2, GroupLinkState state
GroupInfoV2 groupInfoV2,
GroupLinkState state
) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
@ -374,7 +389,8 @@ class GroupV2Helper {
}
Pair<DecryptedGroup, GroupChangeResponse> setEditDetailsPermission(
GroupInfoV2 groupInfoV2, GroupPermission permission
GroupInfoV2 groupInfoV2,
GroupPermission permission
) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
@ -384,7 +400,8 @@ class GroupV2Helper {
}
Pair<DecryptedGroup, GroupChangeResponse> setAddMemberPermission(
GroupInfoV2 groupInfoV2, GroupPermission permission
GroupInfoV2 groupInfoV2,
GroupPermission permission
) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
@ -468,7 +485,9 @@ class GroupV2Helper {
}
Pair<DecryptedGroup, GroupChangeResponse> setMemberAdmin(
GroupInfoV2 groupInfoV2, RecipientId recipientId, boolean admin
GroupInfoV2 groupInfoV2,
RecipientId recipientId,
boolean admin
) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
@ -482,7 +501,8 @@ class GroupV2Helper {
}
Pair<DecryptedGroup, GroupChangeResponse> setMessageExpirationTimer(
GroupInfoV2 groupInfoV2, int messageExpirationTimer
GroupInfoV2 groupInfoV2,
int messageExpirationTimer
) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
final var change = groupOperations.createModifyGroupTimerChange(messageExpirationTimer);
@ -490,7 +510,8 @@ class GroupV2Helper {
}
Pair<DecryptedGroup, GroupChangeResponse> setIsAnnouncementGroup(
GroupInfoV2 groupInfoV2, boolean isAnnouncementGroup
GroupInfoV2 groupInfoV2,
boolean isAnnouncementGroup
) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
final var change = groupOperations.createAnnouncementGroupChange(isAnnouncementGroup);
@ -518,7 +539,8 @@ class GroupV2Helper {
}
private Pair<DecryptedGroup, GroupChangeResponse> revokeInvites(
GroupInfoV2 groupInfoV2, Set<DecryptedPendingMember> pendingMembers
GroupInfoV2 groupInfoV2,
Set<DecryptedPendingMember> pendingMembers
) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
final var uuidCipherTexts = pendingMembers.stream().map(member -> {
@ -532,28 +554,32 @@ class GroupV2Helper {
}
private Pair<DecryptedGroup, GroupChangeResponse> approveJoinRequest(
GroupInfoV2 groupInfoV2, Set<UUID> uuids
GroupInfoV2 groupInfoV2,
Set<UUID> uuids
) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
return commitChange(groupInfoV2, groupOperations.createApproveGroupJoinRequest(uuids));
}
private Pair<DecryptedGroup, GroupChangeResponse> refuseJoinRequest(
GroupInfoV2 groupInfoV2, Set<ServiceId> serviceIds
GroupInfoV2 groupInfoV2,
Set<ServiceId> serviceIds
) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
return commitChange(groupInfoV2, groupOperations.createRefuseGroupJoinRequest(serviceIds, false, List.of()));
}
private Pair<DecryptedGroup, GroupChangeResponse> ejectMembers(
GroupInfoV2 groupInfoV2, Set<ACI> members
GroupInfoV2 groupInfoV2,
Set<ACI> members
) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(members, false, List.of()));
}
private Pair<DecryptedGroup, GroupChangeResponse> commitChange(
GroupInfoV2 groupInfoV2, GroupChange.Actions.Builder change
GroupInfoV2 groupInfoV2,
GroupChange.Actions.Builder change
) throws IOException {
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
final var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
@ -630,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;
}
@ -676,7 +704,8 @@ class GroupV2Helper {
}
private GroupsV2AuthorizationString getAuthorizationString(
final GroupSecretParams groupSecretParams, final long todaySeconds
final GroupSecretParams groupSecretParams,
final long todaySeconds
) throws VerificationFailedException {
var authCredentialResponse = groupApiCredentials.get(todaySeconds);
final var aci = getSelfAci();

View file

@ -66,9 +66,7 @@ public class IdentityHelper {
return fingerprint == null ? null : fingerprint.getScannableFingerprint();
}
private Fingerprint computeSafetyNumberFingerprint(
final ServiceId serviceId, final IdentityKey theirIdentityKey
) {
private Fingerprint computeSafetyNumberFingerprint(final ServiceId serviceId, final IdentityKey theirIdentityKey) {
if (!serviceId.isUnknown()) {
return Utils.computeSafetyNumberForUuid(account.getAci(),
account.getAciIdentityKeyPair().getPublicKey(),
@ -89,7 +87,9 @@ public class IdentityHelper {
}
private boolean trustIdentity(
RecipientId recipientId, BiFunction<ServiceId, IdentityKey, Boolean> verifier, TrustLevel trustLevel
RecipientId recipientId,
BiFunction<ServiceId, IdentityKey, Boolean> verifier,
TrustLevel trustLevel
) {
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
final var serviceId = address.serviceId().orElse(null);

View file

@ -31,6 +31,7 @@ import org.asamk.signal.manager.internal.SignalDependencies;
import org.asamk.signal.manager.jobs.RetrieveStickerPackJob;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.stickers.StickerPack;
import org.signal.libsignal.metadata.ProtocolInvalidKeyException;
@ -40,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;
@ -104,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);
@ -142,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);
@ -156,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.");
@ -164,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());
@ -190,7 +194,9 @@ public final class IncomingMessageHandler {
}
private SignalServiceContent validate(
Envelope envelope, SignalServiceCipherResult cipherResult, long serverDeliveredTimestamp
Envelope envelope,
SignalServiceCipherResult cipherResult,
long serverDeliveredTimestamp
) throws ProtocolInvalidKeyException, ProtocolInvalidMessageException, UnsupportedDataMessageException, InvalidMessageStructureException {
final var content = cipherResult.getContent();
final var envelopeMetadata = cipherResult.getMetadata();
@ -280,7 +286,9 @@ public final class IncomingMessageHandler {
}
public List<HandleAction> handleMessage(
SignalServiceEnvelope envelope, SignalServiceContent content, ReceiveConfig receiveConfig
SignalServiceEnvelope envelope,
SignalServiceContent content,
ReceiveConfig receiveConfig
) {
var actions = new ArrayList<HandleAction>();
final var senderDeviceAddress = getSender(envelope, content);
@ -381,7 +389,8 @@ public final class IncomingMessageHandler {
}
private boolean handlePniSignatureMessage(
final SignalServicePniSignatureMessage message, final SignalServiceAddress senderAddress
final SignalServicePniSignatureMessage message,
final SignalServiceAddress senderAddress
) {
final var aci = senderAddress.getServiceId();
final var aciIdentity = account.getIdentityKeyStore().getIdentityInfo(aci);
@ -520,12 +529,12 @@ public final class IncomingMessageHandler {
}
if (syncMessage.getBlockedList().isPresent()) {
final var blockedListMessage = syncMessage.getBlockedList().get();
for (var address : blockedListMessage.getAddresses()) {
context.getContactHelper()
.setContactBlocked(account.getRecipientResolver().resolveRecipient(address), true);
for (var individual : blockedListMessage.individuals) {
final var address = new RecipientAddress(individual.getAci(), individual.getE164());
final var recipientId = account.getRecipientResolver().resolveRecipient(address);
context.getContactHelper().setContactBlocked(recipientId, true);
}
for (var groupId : blockedListMessage.getGroupIds()
.stream()
for (var groupId : blockedListMessage.groupIds.stream()
.map(GroupId::unknownVersion)
.collect(Collectors.toSet())) {
try {
@ -580,14 +589,22 @@ public final class IncomingMessageHandler {
}
if (syncMessage.getKeys().isPresent()) {
final var keysMessage = syncMessage.getKeys().get();
if (keysMessage.getStorageService().isPresent()) {
final var storageKey = keysMessage.getStorageService().get();
if (keysMessage.getAccountEntropyPool() != null) {
final var aep = keysMessage.getAccountEntropyPool();
account.setAccountEntropyPool(aep);
actions.add(SyncStorageDataAction.create());
} else if (keysMessage.getMaster() != null) {
final var masterKey = keysMessage.getMaster();
account.setMasterKey(masterKey);
actions.add(SyncStorageDataAction.create());
} else if (keysMessage.getStorageService() != null) {
final var storageKey = keysMessage.getStorageService();
account.setStorageKey(storageKey);
actions.add(SyncStorageDataAction.create());
}
if (keysMessage.getMaster().isPresent()) {
final var masterKey = keysMessage.getMaster().get();
account.setMasterKey(masterKey);
if (keysMessage.getMediaRootBackupKey() != null) {
final var mrb = keysMessage.getMediaRootBackupKey();
account.setMediaRootBackupKey(mrb);
actions.add(SyncStorageDataAction.create());
}
}
@ -865,7 +882,9 @@ public final class IncomingMessageHandler {
}
private List<HandleAction> handleSignalServiceStoryMessage(
SignalServiceStoryMessage message, RecipientId source, boolean ignoreAttachments
SignalServiceStoryMessage message,
RecipientId source,
boolean ignoreAttachments
) {
var actions = new ArrayList<HandleAction>();
if (message.getGroupContext().isPresent()) {
@ -946,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

@ -21,9 +21,7 @@ public class PinHelper {
this.secureValueRecoveries = secureValueRecoveries;
}
public void setRegistrationLockPin(
String pin, MasterKey masterKey
) throws IOException {
public void setRegistrationLockPin(String pin, MasterKey masterKey) throws IOException {
IOException exception = null;
for (final var secureValueRecovery : secureValueRecoveries) {
try {
@ -82,14 +80,19 @@ public class PinHelper {
}
public SecureValueRecovery.RestoreResponse.Success getRegistrationLockData(
String pin, LockedException lockedException
String pin,
LockedException lockedException
) throws IOException, IncorrectPinException {
var svr2Credentials = lockedException.getSvr2Credentials();
if (svr2Credentials != null) {
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;
}
@ -103,7 +106,9 @@ public class PinHelper {
}
public SecureValueRecovery.RestoreResponse.Success getRegistrationLockData(
SecureValueRecovery secureValueRecovery, AuthCredentials authCredentials, String pin
SecureValueRecovery secureValueRecovery,
AuthCredentials authCredentials,
String pin
) throws IOException, IncorrectPinException {
final var restoreResponse = secureValueRecovery.restoreDataPreRegistration(authCredentials, null, pin);

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 {
@ -30,9 +32,7 @@ public class PreKeyHelper {
private final SignalAccount account;
private final SignalDependencies dependencies;
public PreKeyHelper(
final SignalAccount account, final SignalDependencies dependencies
) {
public PreKeyHelper(final SignalAccount account, final SignalDependencies dependencies) {
this.account = account;
this.dependencies = dependencies;
}
@ -79,11 +79,12 @@ public class PreKeyHelper {
}
private boolean refreshPreKeysIfNecessary(
final ServiceIdType serviceIdType, final IdentityKeyPair identityKeyPair
final ServiceIdType serviceIdType,
final IdentityKeyPair identityKeyPair
) 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);
@ -144,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);
@ -173,7 +174,7 @@ public class PreKeyHelper {
// This can happen when the primary device has changed phone number
logger.warn("Failed to updated pre keys: {}", e.getMessage());
} catch (NonSuccessfulResponseCodeException e) {
if (serviceIdType != ServiceIdType.PNI || e.getCode() != 422) {
if (serviceIdType != ServiceIdType.PNI || e.code != 422) {
throw e;
}
logger.warn("Failed to set PNI pre keys, ignoring for now. Account needs to be reregistered to fix this.");
@ -221,7 +222,8 @@ public class PreKeyHelper {
}
private List<KyberPreKeyRecord> generateKyberPreKeys(
ServiceIdType serviceIdType, final IdentityKeyPair identityKeyPair
ServiceIdType serviceIdType,
final IdentityKeyPair identityKeyPair
) {
final var accountData = account.getAccountData(serviceIdType);
final var offset = accountData.getPreKeyMetadata().getNextKyberPreKeyId();
@ -246,7 +248,9 @@ public class PreKeyHelper {
}
private KyberPreKeyRecord generateLastResortKyberPreKey(
ServiceIdType serviceIdType, IdentityKeyPair identityKeyPair, final int offset
ServiceIdType serviceIdType,
IdentityKeyPair identityKeyPair,
final int offset
) {
final var accountData = account.getAccountData(serviceIdType);
final var signedPreKeyId = accountData.getPreKeyMetadata().getNextKyberPreKeyId() + offset;

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();
}
@ -271,7 +273,9 @@ public final class ProfileHelper {
}
private Profile decryptProfileAndDownloadAvatar(
final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
final RecipientId recipientId,
final ProfileKey profileKey,
final SignalServiceProfile encryptedProfile
) {
final var avatarPath = encryptedProfile.getAvatar();
downloadProfileAvatar(recipientId, avatarPath, profileKey);
@ -280,7 +284,9 @@ public final class ProfileHelper {
}
public void downloadProfileAvatar(
final RecipientId recipientId, final String avatarPath, final ProfileKey profileKey
final RecipientId recipientId,
final String avatarPath,
final ProfileKey profileKey
) {
var profile = account.getProfileStore().getProfile(recipientId);
if (profile == null || !Objects.equals(avatarPath, profile.getAvatarUrlPath())) {
@ -308,7 +314,8 @@ public final class ProfileHelper {
}
private Single<ProfileAndCredential> retrieveProfile(
RecipientId recipientId, SignalServiceProfile.RequestType requestType
RecipientId recipientId,
SignalServiceProfile.RequestType requestType
) {
var unidentifiedAccess = getUnidentifiedAccess(recipientId);
var profileKey = Optional.ofNullable(account.getProfileStore().getProfileKey(recipientId));
@ -331,13 +338,6 @@ public final class ProfileHelper {
final var profile = account.getProfileStore().getProfile(recipientId);
if (recipientId.equals(account.getSelfRecipientId())) {
final var isUnrestricted = encryptedProfile.isUnrestrictedUnidentifiedAccess();
if (account.isUnrestrictedUnidentifiedAccess() != isUnrestricted) {
account.setUnrestrictedUnidentifiedAccess(isUnrestricted);
}
}
Profile newProfile = null;
if (profileKey.isPresent()) {
logger.trace("Decrypting profile");
@ -353,6 +353,18 @@ public final class ProfileHelper {
.build();
}
if (recipientId.equals(account.getSelfRecipientId())) {
final var isUnrestricted = encryptedProfile.isUnrestrictedUnidentifiedAccess();
if (account.isUnrestrictedUnidentifiedAccess() != isUnrestricted) {
account.setUnrestrictedUnidentifiedAccess(isUnrestricted);
}
if (account.isPrimaryDevice() && profile != null && newProfile.getCapabilities()
.contains(Profile.Capability.storageServiceEncryptionV2Capability) && !profile.getCapabilities()
.contains(Profile.Capability.storageServiceEncryptionV2Capability)) {
context.getJobExecutor().enqueueJob(new SyncStorageJob(true));
}
}
try {
logger.trace("Storing identity");
final var identityKey = new IdentityKey(Base64.getDecoder().decode(encryptedProfile.getIdentityKey()));
@ -408,9 +420,7 @@ public final class ProfileHelper {
});
}
private void downloadProfileAvatar(
RecipientAddress address, String avatarPath, ProfileKey profileKey
) {
private void downloadProfileAvatar(RecipientAddress address, String avatarPath, ProfileKey profileKey) {
if (avatarPath == null) {
try {
context.getAvatarStore().deleteProfileAvatar(address);
@ -430,7 +440,9 @@ public final class ProfileHelper {
}
private void retrieveProfileAvatar(
String avatarPath, ProfileKey profileKey, OutputStream outputStream
String avatarPath,
ProfileKey profileKey,
OutputStream outputStream
) throws IOException {
var tmpFile = IOUtils.createTempFile();
try (var input = dependencies.getMessageReceiver()

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 {
@ -83,7 +82,10 @@ public class ReceiveHelper {
}
public void receiveMessages(
Duration timeout, boolean returnOnTimeout, Integer maxMessages, Manager.ReceiveMessageHandler handler
Duration timeout,
boolean returnOnTimeout,
Integer maxMessages,
Manager.ReceiveMessageHandler handler
) throws IOException {
account.setNeedsToRetryFailedMessages(true);
hasCaughtUpWithOldMessages = false;
@ -91,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);
@ -106,6 +108,7 @@ public class ReceiveHelper {
hasCaughtUpWithOldMessages = false;
handleQueuedActions(queuedActions.keySet());
queuedActions.clear();
signalWebSocket.removeKeepAliveToken("receive");
signalWebSocket.disconnect();
webSocketStateDisposable.dispose();
shouldStop = false;
@ -113,7 +116,7 @@ public class ReceiveHelper {
}
private void receiveMessagesInternal(
final SignalWebSocket signalWebSocket,
final SignalWebSocket.AuthenticatedWebSocket signalWebSocket,
Duration timeout,
boolean returnOnTimeout,
Integer maxMessages,
@ -264,7 +267,8 @@ public class ReceiveHelper {
}
private List<HandleAction> retryFailedReceivedMessage(
final Manager.ReceiveMessageHandler handler, final CachedMessage cachedMessage
final Manager.ReceiveMessageHandler handler,
final CachedMessage cachedMessage
) {
var envelope = cachedMessage.loadEnvelope();
if (envelope == null) {

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.parseOrThrow(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,16 +91,20 @@ 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);
}
public RecipientId resolveRecipientByUsernameOrLink(
String username, boolean forceRefresh
) throws UnregisteredRecipientException {
String username,
boolean forceRefresh
) throws UnregisteredRecipientException, IOException {
final Username finalUsername;
try {
finalUsername = getUsernameFromUsernameOrLink(username);
@ -107,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;
}
@ -129,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);
@ -143,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();
}
@ -180,7 +190,8 @@ public class RecipientHelper {
}
private Map<String, RegisteredUser> getRegisteredUsers(
final Set<String> numbers, final boolean isPartialRefresh
final Set<String> numbers,
final boolean isPartialRefresh
) throws IOException {
Map<String, RegisteredUser> registeredUsers = getRegisteredUsersV2(numbers, isPartialRefresh);
@ -211,7 +222,8 @@ public class RecipientHelper {
}
private Map<String, RegisteredUser> getRegisteredUsersV2(
final Set<String> numbers, boolean isPartialRefresh
final Set<String> numbers,
boolean isPartialRefresh
) throws IOException {
final var previousNumbers = isPartialRefresh ? Set.<String>of() : account.getCdsiStore().getAllNumbers();
final var newNumbers = new HashSet<>(numbers) {{
@ -231,12 +243,11 @@ 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,
dependencies.getServiceEnvironmentConfig().cdsiMrenclave(),
null,
dependencies.getLibSignalNetwork(),
newToken -> {
@ -254,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

@ -125,7 +125,8 @@ public class SendHelper {
}
public SendMessageResult sendReceiptMessage(
final SignalServiceReceiptMessage receiptMessage, final RecipientId recipientId
final SignalServiceReceiptMessage receiptMessage,
final RecipientId recipientId
) {
final var messageSendLogStore = account.getMessageSendLogStore();
final var result = handleSendMessage(recipientId,
@ -157,7 +158,9 @@ public class SendHelper {
}
public SendMessageResult sendRetryReceipt(
DecryptionErrorMessage errorMessage, RecipientId recipientId, Optional<GroupId> groupId
DecryptionErrorMessage errorMessage,
RecipientId recipientId,
Optional<GroupId> groupId
) {
logger.debug("Sending retry receipt for {} to {}, device: {}",
errorMessage.getTimestamp(),
@ -183,7 +186,8 @@ public class SendHelper {
}
public SendMessageResult sendSelfMessage(
SignalServiceDataMessage.Builder messageBuilder, Optional<Long> editTargetTimestamp
SignalServiceDataMessage.Builder messageBuilder,
Optional<Long> editTargetTimestamp
) {
final var recipientId = account.getSelfRecipientId();
final var contact = account.getContactStore().getContact(recipientId);
@ -214,9 +218,7 @@ public class SendHelper {
}
}
public SendMessageResult sendTypingMessage(
SignalServiceTypingMessage message, RecipientId recipientId
) {
public SendMessageResult sendTypingMessage(SignalServiceTypingMessage message, RecipientId recipientId) {
final var result = handleSendMessage(recipientId,
(messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendTyping(List.of(
address), List.of(unidentifiedAccess), message, null).getFirst());
@ -225,7 +227,8 @@ public class SendHelper {
}
public List<SendMessageResult> sendGroupTypingMessage(
SignalServiceTypingMessage message, GroupId groupId
SignalServiceTypingMessage message,
GroupId groupId
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
final var g = getGroupForSending(groupId);
if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
@ -238,7 +241,9 @@ public class SendHelper {
}
public SendMessageResult resendMessage(
final RecipientId recipientId, final long timestamp, final MessageSendLogEntry messageSendLogEntry
final RecipientId recipientId,
final long timestamp,
final MessageSendLogEntry messageSendLogEntry
) {
logger.trace("Resending message {} to {}", timestamp, recipientId);
if (messageSendLogEntry.groupId().isEmpty()) {
@ -552,7 +557,9 @@ public class SendHelper {
}
private List<SendMessageResult> sendGroupMessageInternalWithLegacy(
final LegacySenderHandler sender, final Set<RecipientId> recipientIds, final boolean isRecipientUpdate
final LegacySenderHandler sender,
final Set<RecipientId> recipientIds,
final boolean isRecipientUpdate
) throws IOException {
final var recipientIdList = new ArrayList<>(recipientIds);
final var addresses = recipientIdList.stream()
@ -644,7 +651,9 @@ public class SendHelper {
}
private SendMessageResult sendMessage(
SignalServiceDataMessage message, RecipientId recipientId, Optional<Long> editTargetTimestamp
SignalServiceDataMessage message,
RecipientId recipientId,
Optional<Long> editTargetTimestamp
) {
final var messageSendLogStore = account.getMessageSendLogStore();
final var urgent = true;

View file

@ -30,7 +30,9 @@ public class StickerHelper {
}
public StickerPack addOrUpdateStickerPack(
final StickerPackId stickerPackId, final byte[] stickerPackKey, final boolean installed
final StickerPackId stickerPackId,
final byte[] stickerPackKey,
final boolean installed
) {
final var sticker = account.getStickerStore().getStickerPack(stickerPackId);
if (sticker != null) {
@ -50,7 +52,8 @@ public class StickerHelper {
}
public JsonStickerPack getOrRetrieveStickerPack(
StickerPackId packId, byte[] packKey
StickerPackId packId,
byte[] packKey
) throws InvalidStickerException {
try {
retrieveStickerPack(packId, packKey);

View file

@ -2,6 +2,7 @@ package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.api.GroupIdV1;
import org.asamk.signal.manager.api.GroupIdV2;
import org.asamk.signal.manager.api.Profile;
import org.asamk.signal.manager.internal.SignalDependencies;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.recipients.RecipientId;
@ -17,11 +18,17 @@ import org.signal.core.util.SetUtil;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.storage.RecordIkm;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.api.storage.StorageKey;
import org.whispersystems.signalservice.api.storage.StorageRecordConvertersKt;
import org.whispersystems.signalservice.api.storage.StorageServiceRepository;
import org.whispersystems.signalservice.api.storage.StorageServiceRepository.ManifestIfDifferentVersionResult;
import org.whispersystems.signalservice.api.storage.StorageServiceRepository.WriteStorageRecordsResult;
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
import java.io.IOException;
import java.sql.Connection;
@ -32,9 +39,10 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class StorageHelper {
private static final Logger logger = LoggerFactory.getLogger(StorageHelper.class);
@ -54,7 +62,7 @@ public class StorageHelper {
}
public void syncDataWithStorage() throws IOException {
final var storageKey = account.getOrCreateStorageKey();
var storageKey = account.getOrCreateStorageKey();
if (storageKey == null) {
if (!account.isPrimaryDevice()) {
logger.debug("Storage key unknown, requesting from primary device.");
@ -65,52 +73,76 @@ public class StorageHelper {
logger.trace("Reading manifest from remote storage");
final var localManifestVersion = account.getStorageManifestVersion();
final var localManifest = account.getStorageManifest().orElse(SignalStorageManifest.EMPTY);
SignalStorageManifest remoteManifest;
try {
remoteManifest = dependencies.getAccountManager()
.getStorageManifestIfDifferentVersion(storageKey, localManifestVersion)
.orElse(localManifest);
} catch (InvalidKeyException e) {
logger.warn("Manifest couldn't be decrypted.");
if (account.isPrimaryDevice()) {
try {
forcePushToStorage(storageKey);
} catch (RetryLaterException rle) {
// TODO retry later
return;
}
}
return;
}
logger.trace("Manifest versions: local {}, remote {}", localManifestVersion, remoteManifest.getVersion());
final var localManifest = account.getStorageManifest().orElse(SignalStorageManifest.Companion.getEMPTY());
final var storageServiceRepository = dependencies.getStorageServiceRepository();
final var result = storageServiceRepository.getStorageManifestIfDifferentVersion(storageKey,
localManifestVersion);
var needsForcePush = false;
if (remoteManifest.getVersion() > localManifestVersion) {
logger.trace("Remote version was newer, reading records.");
needsForcePush = readDataFromStorage(storageKey, localManifest, remoteManifest);
} else if (remoteManifest.getVersion() < localManifest.getVersion()) {
logger.debug("Remote storage manifest version was older. User might have switched accounts.");
}
logger.trace("Done reading data from remote storage");
final var remoteManifest = switch (result) {
case ManifestIfDifferentVersionResult.DifferentVersion diff -> {
final var manifest = diff.getManifest();
storeManifestLocally(manifest);
yield manifest;
}
case ManifestIfDifferentVersionResult.DecryptionError ignore -> {
logger.warn("Manifest couldn't be decrypted.");
if (account.isPrimaryDevice()) {
needsForcePush = true;
} else {
context.getSyncHelper().requestSyncKeys();
}
yield null;
}
case ManifestIfDifferentVersionResult.SameVersion ignored -> localManifest;
case ManifestIfDifferentVersionResult.NetworkError e -> throw e.getException();
case ManifestIfDifferentVersionResult.StatusCodeError e -> throw e.getException();
default -> throw new RuntimeException("Unhandled ManifestIfDifferentVersionResult type");
};
if (localManifest != remoteManifest) {
storeManifestLocally(remoteManifest);
}
if (remoteManifest != null) {
logger.trace("Manifest versions: local {}, remote {}", localManifestVersion, remoteManifest.version);
readRecordsWithPreviouslyUnknownTypes(storageKey);
if (remoteManifest.version > localManifestVersion) {
logger.trace("Remote version was newer, reading records.");
needsForcePush = readDataFromStorage(storageKey, localManifest, remoteManifest);
} else if (remoteManifest.version < localManifest.version) {
logger.debug("Remote storage manifest version was older. User might have switched accounts.");
}
logger.trace("Done reading data from remote storage");
readRecordsWithPreviouslyUnknownTypes(storageKey, remoteManifest);
}
logger.trace("Adding missing storageIds to local data");
account.getRecipientStore().setMissingStorageIds();
account.getGroupStore().setMissingStorageIds();
var needsMultiDeviceSync = false;
try {
needsMultiDeviceSync = writeToStorage(storageKey, remoteManifest, needsForcePush);
} catch (RetryLaterException e) {
// TODO retry later
return;
if (account.needsStorageKeyMigration()) {
logger.debug("Storage needs force push due to new account entropy pool");
// Set new aep and reset previous master key and storage key
account.setAccountEntropyPool(account.getOrCreateAccountEntropyPool());
storageKey = account.getOrCreateStorageKey();
context.getSyncHelper().sendKeysMessage();
needsForcePush = true;
} else if (remoteManifest == null) {
if (account.isPrimaryDevice()) {
needsForcePush = true;
}
} else if (remoteManifest.recordIkm == null && account.getSelfRecipientProfile()
.getCapabilities()
.contains(Profile.Capability.storageServiceEncryptionV2Capability)) {
logger.debug("The SSRE2 capability is supported, but no recordIkm is set! Force pushing.");
needsForcePush = true;
} else {
try {
needsMultiDeviceSync = writeToStorage(storageKey, remoteManifest, needsForcePush);
} catch (RetryLaterException e) {
// TODO retry later
return;
}
}
if (needsForcePush) {
@ -131,6 +163,23 @@ public class StorageHelper {
logger.debug("Done syncing data with remote storage");
}
public void forcePushToStorage() throws IOException {
if (!account.isPrimaryDevice()) {
return;
}
final var storageKey = account.getOrCreateStorageKey();
if (storageKey == null) {
return;
}
try {
forcePushToStorage(storageKey);
} catch (RetryLaterException e) {
// TODO retry later
}
}
private boolean readDataFromStorage(
final StorageKey storageKey,
final SignalStorageManifest localManifest,
@ -140,36 +189,37 @@ public class StorageHelper {
try (final var connection = account.getAccountDatabase().getConnection()) {
connection.setAutoCommit(false);
var idDifference = findIdDifference(remoteManifest.getStorageIds(), localManifest.getStorageIds());
var idDifference = findIdDifference(remoteManifest.storageIds, localManifest.storageIds);
if (idDifference.hasTypeMismatches() && account.isPrimaryDevice()) {
logger.debug("Found type mismatches in the ID sets! Scheduling a force push after this sync completes.");
needsForcePush = true;
}
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);
}
}
logger.debug("Pre-Merge ID Difference :: {}", idDifference);
if (!idDifference.isEmpty()) {
final var remoteOnlyRecords = getSignalStorageRecords(storageKey, idDifference.remoteOnlyIds());
final var remoteOnlyRecords = getSignalStorageRecords(storageKey,
remoteManifest,
idDifference.remoteOnlyIds());
if (remoteOnlyRecords.size() != idDifference.remoteOnlyIds().size()) {
logger.debug("Could not find all remote-only records! Requested: "
+ idDifference.remoteOnlyIds()
.size()
+ ", Found: "
+ remoteOnlyRecords.size()
+ ". These stragglers should naturally get deleted during the sync.");
logger.debug(
"Could not find all remote-only records! Requested: {}, Found: {}. These stragglers should naturally get deleted during the sync.",
idDifference.remoteOnlyIds().size(),
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);
@ -194,18 +244,21 @@ public class StorageHelper {
return needsForcePush;
}
private void readRecordsWithPreviouslyUnknownTypes(final StorageKey storageKey) throws IOException {
private void readRecordsWithPreviouslyUnknownTypes(
final StorageKey storageKey,
final SignalStorageManifest remoteManifest
) throws IOException {
try (final var connection = account.getAccountDatabase().getConnection()) {
connection.setAutoCommit(false);
final var knownUnknownIds = account.getUnknownStorageIdStore()
.getUnknownStorageIds(connection, KNOWN_TYPES);
if (!knownUnknownIds.isEmpty()) {
logger.debug("We have " + knownUnknownIds.size() + " unknown records that we can now process.");
logger.debug("We have {} unknown records that we can now process.", knownUnknownIds.size());
final var remote = getSignalStorageRecords(storageKey, knownUnknownIds);
final var remote = getSignalStorageRecords(storageKey, remoteManifest, knownUnknownIds);
logger.debug("Found " + remote.size() + " of the known-unknowns remotely.");
logger.debug("Found {} of the known-unknowns remotely.", remote.size());
processKnownRecords(connection, remote);
account.getUnknownStorageIdStore()
@ -218,22 +271,37 @@ public class StorageHelper {
}
private boolean writeToStorage(
final StorageKey storageKey, final SignalStorageManifest remoteManifest, final boolean needsForcePush
final StorageKey storageKey,
final SignalStorageManifest remoteManifest,
final boolean needsForcePush
) throws IOException, RetryLaterException {
final WriteOperationResult remoteWriteOperation;
try (final var connection = account.getAccountDatabase().getConnection()) {
connection.setAutoCommit(false);
final var localStorageIds = getAllLocalStorageIds(connection);
final var idDifference = findIdDifference(remoteManifest.getStorageIds(), localStorageIds);
logger.debug("ID Difference :: " + idDifference);
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
remoteWriteOperation = new WriteOperationResult(new SignalStorageManifest(remoteManifest.getVersion() + 1,
remoteWriteOperation = new WriteOperationResult(new SignalStorageManifest(remoteManifest.version + 1,
account.getDeviceId(),
remoteManifest.recordIkm,
localStorageIds), remoteInserts, remoteDeletes);
connection.commit();
@ -242,39 +310,37 @@ public class StorageHelper {
}
if (remoteWriteOperation.isEmpty()) {
logger.debug("No remote writes needed. Still at version: " + remoteManifest.getVersion());
logger.debug("No remote writes needed. Still at version: {}", remoteManifest.version);
return false;
}
logger.debug("We have something to write remotely.");
logger.debug("WriteOperationResult :: " + remoteWriteOperation);
logger.debug("WriteOperationResult :: {}", remoteWriteOperation);
StorageSyncValidations.validate(remoteWriteOperation,
remoteManifest,
needsForcePush,
account.getSelfRecipientAddress());
final Optional<SignalStorageManifest> conflict;
try {
conflict = dependencies.getAccountManager()
.writeStorageRecords(storageKey,
remoteWriteOperation.manifest(),
remoteWriteOperation.inserts(),
remoteWriteOperation.deletes());
} catch (InvalidKeyException e) {
logger.warn("Failed to decrypt conflicting storage manifest: {}", e.getMessage());
throw new IOException(e);
final var result = dependencies.getStorageServiceRepository()
.writeStorageRecords(storageKey,
remoteWriteOperation.manifest(),
remoteWriteOperation.inserts(),
remoteWriteOperation.deletes());
switch (result) {
case WriteStorageRecordsResult.ConflictError ignored -> {
logger.debug("Hit a conflict when trying to resolve the conflict! Retrying.");
throw new RetryLaterException();
}
case WriteStorageRecordsResult.NetworkError networkError -> throw networkError.getException();
case WriteStorageRecordsResult.StatusCodeError statusCodeError -> throw statusCodeError.getException();
case WriteStorageRecordsResult.Success ignored -> {
logger.debug("Saved new manifest. Now at version: {}", remoteWriteOperation.manifest().version);
storeManifestLocally(remoteWriteOperation.manifest());
return true;
}
default -> throw new IllegalStateException("Unexpected value: " + result);
}
if (conflict.isPresent()) {
logger.debug("Hit a conflict when trying to resolve the conflict! Retrying.");
throw new RetryLaterException();
}
logger.debug("Saved new manifest. Now at version: " + remoteWriteOperation.manifest().getVersion());
storeManifestLocally(remoteWriteOperation.manifest());
return true;
}
private void forcePushToStorage(
@ -282,7 +348,8 @@ public class StorageHelper {
) throws IOException, RetryLaterException {
logger.debug("Force pushing local state to remote storage");
final var currentVersion = dependencies.getAccountManager().getStorageManifestVersion();
final var currentVersion = handleResponseException(dependencies.getStorageServiceRepository()
.getManifestVersion());
final var newVersion = currentVersion + 1;
final var newStorageRecords = new ArrayList<SignalStorageRecord>();
final Map<RecipientId, StorageId> newContactStorageIds;
@ -298,17 +365,19 @@ 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(),
storageId.getRaw());
newStorageRecords.add(accountRecord);
account.getUsernameLink());
newStorageRecords.add(new SignalStorageRecord(storageId,
new StorageRecord.Builder().account(accountRecord).build()));
} else {
final var recipient = account.getRecipientStore().getRecipient(connection, recipientId);
final var address = recipient.getAddress().getIdentifier();
final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, address);
final var record = StorageSyncModels.localToRemoteRecord(recipient, identity, storageId.getRaw());
newStorageRecords.add(record);
final var record = StorageSyncModels.localToRemoteRecord(recipient, identity);
newStorageRecords.add(new SignalStorageRecord(storageId,
new StorageRecord.Builder().contact(record).build()));
}
}
@ -317,8 +386,9 @@ public class StorageHelper {
for (final var groupId : groupV1Ids) {
final var storageId = newGroupV1StorageIds.get(groupId);
final var group = account.getGroupStore().getGroup(connection, groupId);
final var record = StorageSyncModels.localToRemoteRecord(group, storageId.getRaw());
newStorageRecords.add(record);
final var record = StorageSyncModels.localToRemoteRecord(group);
newStorageRecords.add(new SignalStorageRecord(storageId,
new StorageRecord.Builder().groupV1(record).build()));
}
final var groupV2Ids = account.getGroupStore().getGroupV2Ids(connection);
@ -326,8 +396,9 @@ public class StorageHelper {
for (final var groupId : groupV2Ids) {
final var storageId = newGroupV2StorageIds.get(groupId);
final var group = account.getGroupStore().getGroup(connection, groupId);
final var record = StorageSyncModels.localToRemoteRecord(group, storageId.getRaw());
newStorageRecords.add(record);
final var record = StorageSyncModels.localToRemoteRecord(group);
newStorageRecords.add(new SignalStorageRecord(storageId,
new StorageRecord.Builder().groupV2(record).build()));
}
connection.commit();
@ -336,34 +407,46 @@ public class StorageHelper {
}
final var newStorageIds = newStorageRecords.stream().map(SignalStorageRecord::getId).toList();
final var manifest = new SignalStorageManifest(newVersion, account.getDeviceId(), newStorageIds);
final RecordIkm recordIkm;
if (account.getSelfRecipientProfile()
.getCapabilities()
.contains(Profile.Capability.storageServiceEncryptionV2Capability)) {
logger.debug("Generating and including a new recordIkm.");
recordIkm = RecordIkm.Companion.generate();
} else {
logger.debug("SSRE2 not yet supported. Not including recordIkm.");
recordIkm = null;
}
final var manifest = new SignalStorageManifest(newVersion, account.getDeviceId(), recordIkm, newStorageIds);
StorageSyncValidations.validateForcePush(manifest, newStorageRecords, account.getSelfRecipientAddress());
final Optional<SignalStorageManifest> conflict;
try {
if (newVersion > 1) {
logger.trace("Force-pushing data. Inserting {} IDs.", newStorageRecords.size());
conflict = dependencies.getAccountManager()
.resetStorageRecords(storageServiceKey, manifest, newStorageRecords);
} else {
logger.trace("First version, normal push. Inserting {} IDs.", newStorageRecords.size());
conflict = dependencies.getAccountManager()
.writeStorageRecords(storageServiceKey, manifest, newStorageRecords, Collections.emptyList());
final WriteStorageRecordsResult result;
if (newVersion > 1) {
logger.trace("Force-pushing data. Inserting {} IDs.", newStorageRecords.size());
result = dependencies.getStorageServiceRepository()
.resetAndWriteStorageRecords(storageServiceKey, manifest, newStorageRecords);
} else {
logger.trace("First version, normal push. Inserting {} IDs.", newStorageRecords.size());
result = dependencies.getStorageServiceRepository()
.writeStorageRecords(storageServiceKey, manifest, newStorageRecords, Collections.emptyList());
}
switch (result) {
case WriteStorageRecordsResult.ConflictError ignored -> {
logger.debug("Hit a conflict. Trying again.");
throw new RetryLaterException();
}
} catch (InvalidKeyException e) {
logger.debug("Hit an invalid key exception, which likely indicates a conflict.", e);
throw new RetryLaterException();
case WriteStorageRecordsResult.NetworkError networkError -> throw networkError.getException();
case WriteStorageRecordsResult.StatusCodeError statusCodeError -> throw statusCodeError.getException();
case WriteStorageRecordsResult.Success ignored -> {
logger.debug("Force push succeeded. Updating local manifest version to: {}", manifest.version);
storeManifestLocally(manifest);
}
default -> throw new IllegalStateException("Unexpected value: " + result);
}
if (conflict.isPresent()) {
logger.debug("Hit a conflict. Trying again.");
throw new RetryLaterException();
}
logger.debug("Force push succeeded. Updating local manifest version to: " + manifest.getVersion());
storeManifestLocally(manifest);
try (final var connection = account.getAccountDatabase().getConnection()) {
connection.setAutoCommit(false);
account.getRecipientStore().updateStorageIds(connection, newContactStorageIds);
@ -403,21 +486,35 @@ public class StorageHelper {
private void storeManifestLocally(
final SignalStorageManifest remoteManifest
) {
account.setStorageManifestVersion(remoteManifest.getVersion());
account.setStorageManifestVersion(remoteManifest.version);
account.setStorageManifest(remoteManifest);
}
private List<SignalStorageRecord> getSignalStorageRecords(
final StorageKey storageKey, final List<StorageId> storageIds
final StorageKey storageKey,
final SignalStorageManifest manifest,
final List<StorageId> storageIds
) throws IOException {
List<SignalStorageRecord> records;
try {
records = dependencies.getAccountManager().readStorageRecords(storageKey, storageIds);
} catch (InvalidKeyException e) {
logger.warn("Failed to read storage records, ignoring.");
return List.of();
}
return records;
final var result = dependencies.getStorageServiceRepository()
.readStorageRecords(storageKey, manifest.recordIkm, storageIds);
return switch (result) {
case StorageServiceRepository.StorageRecordResult.DecryptionError decryptionError -> {
if (decryptionError.getException() instanceof InvalidKeyException) {
logger.warn("Failed to read storage records, ignoring.");
yield List.of();
} else if (decryptionError.getException() instanceof IOException ioe) {
throw ioe;
} else {
throw new IOException(decryptionError.getException());
}
}
case StorageServiceRepository.StorageRecordResult.NetworkError networkError ->
throw networkError.getException();
case StorageServiceRepository.StorageRecordResult.StatusCodeError statusCodeError ->
throw statusCodeError.getException();
case StorageServiceRepository.StorageRecordResult.Success success -> success.getRecords();
default -> throw new IllegalStateException("Unexpected value: " + result);
};
}
private List<StorageId> getAllLocalStorageIds(final Connection connection) throws SQLException {
@ -430,45 +527,52 @@ public class StorageHelper {
}
private List<SignalStorageRecord> buildLocalStorageRecords(
final Connection connection, final List<StorageId> storageIds
final Connection connection,
final List<StorageId> storageIds
) throws SQLException {
final var records = new ArrayList<SignalStorageRecord>();
final var records = new ArrayList<SignalStorageRecord>(storageIds.size());
for (final var storageId : storageIds) {
final var record = buildLocalStorageRecord(connection, storageId);
if (record != null) {
records.add(record);
}
records.add(record);
}
return records;
}
private SignalStorageRecord buildLocalStorageRecord(
Connection connection, StorageId storageId
Connection connection,
StorageId storageId
) throws SQLException {
return switch (ManifestRecord.Identifier.Type.fromValue(storageId.getType())) {
case ManifestRecord.Identifier.Type.CONTACT -> {
final var recipient = account.getRecipientStore().getRecipient(connection, storageId);
final var address = recipient.getAddress().getIdentifier();
final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, address);
yield StorageSyncModels.localToRemoteRecord(recipient, identity, storageId.getRaw());
final var record = StorageSyncModels.localToRemoteRecord(recipient, identity);
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().contact(record).build());
}
case ManifestRecord.Identifier.Type.GROUPV1 -> {
final var groupV1 = account.getGroupStore().getGroupV1(connection, storageId);
yield StorageSyncModels.localToRemoteRecord(groupV1, storageId.getRaw());
final var record = StorageSyncModels.localToRemoteRecord(groupV1);
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().groupV1(record).build());
}
case ManifestRecord.Identifier.Type.GROUPV2 -> {
final var groupV2 = account.getGroupStore().getGroupV2(connection, storageId);
yield StorageSyncModels.localToRemoteRecord(groupV2, storageId.getRaw());
final var record = StorageSyncModels.localToRemoteRecord(groupV2);
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().groupV2(record).build());
}
case ManifestRecord.Identifier.Type.ACCOUNT -> {
final var selfRecipient = account.getRecipientStore()
.getRecipient(connection, account.getSelfRecipientId());
yield StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
final var record = StorageSyncModels.localToRemoteRecord(connection,
account.getConfigurationStore(),
selfRecipient,
account.getUsernameLink(),
storageId.getRaw());
account.getUsernameLink());
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().account(record).build());
}
case null, default -> {
throw new AssertionError("Got unknown local storage record type: " + storageId);
}
case null, default -> throw new AssertionError("Got unknown local storage record type: " + storageId);
};
}
@ -484,7 +588,8 @@ public class StorageHelper {
* exclusive to the local data set.
*/
private static IdDifferenceResult findIdDifference(
Collection<StorageId> remoteIds, Collection<StorageId> localIds
Collection<StorageId> remoteIds,
Collection<StorageId> localIds
) {
final var base64Encoder = Base64.getEncoder();
final var remoteByRawId = remoteIds.stream()
@ -502,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;
@ -520,7 +625,8 @@ public class StorageHelper {
}
private List<StorageId> processKnownRecords(
final Connection connection, List<SignalStorageRecord> records
final Connection connection,
List<SignalStorageRecord> records
) throws SQLException {
final var unknownRecords = new ArrayList<StorageId>();
@ -530,13 +636,24 @@ public class StorageHelper {
final var groupV2RecordProcessor = new GroupV2RecordProcessor(account, connection);
for (final var record : records) {
logger.debug("Reading record of type {}", record.getType());
switch (ManifestRecord.Identifier.Type.fromValue(record.getType())) {
case ACCOUNT -> accountRecordProcessor.process(record.getAccount().get());
case GROUPV1 -> groupV1RecordProcessor.process(record.getGroupV1().get());
case GROUPV2 -> groupV2RecordProcessor.process(record.getGroupV2().get());
case CONTACT -> contactRecordProcessor.process(record.getContact().get());
case null, default -> unknownRecords.add(record.getId());
if (record.getProto().account != null) {
logger.debug("Reading record {} of type account", record.getId());
accountRecordProcessor.process(StorageRecordConvertersKt.toSignalAccountRecord(record.getProto().account,
record.getId()));
} else if (record.getProto().groupV1 != null) {
logger.debug("Reading record {} of type groupV1", record.getId());
groupV1RecordProcessor.process(StorageRecordConvertersKt.toSignalGroupV1Record(record.getProto().groupV1,
record.getId()));
} else if (record.getProto().groupV2 != null) {
logger.debug("Reading record {} of type groupV2", record.getId());
groupV2RecordProcessor.process(StorageRecordConvertersKt.toSignalGroupV2Record(record.getProto().groupV2,
record.getId()));
} else if (record.getProto().contact != null) {
logger.debug("Reading record {} of type contact", record.getId());
contactRecordProcessor.process(StorageRecordConvertersKt.toSignalContactRecord(record.getProto().contact,
record.getId()));
} else {
unknownRecords.add(record.getId());
}
}

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,39 +211,25 @@ 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() {
var addresses = new ArrayList<SignalServiceAddress>();
var addresses = new ArrayList<BlockedListMessage.Individual>();
for (var record : account.getContactStore().getContacts()) {
if (record.second().isBlocked()) {
addresses.add(context.getRecipientHelper().resolveSignalServiceAddress(record.first()));
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(record.first());
if (address.aci().isPresent() || address.number().isPresent()) {
addresses.add(new BlockedListMessage.Individual(address.aci().orElse(null),
address.number().orElse(null)));
}
}
}
var groupIds = new ArrayList<byte[]>();
@ -262,7 +243,9 @@ public class SyncHelper {
}
public SendMessageResult sendVerifiedMessage(
SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel
SignalServiceAddress destination,
IdentityKey identityKey,
TrustLevel trustLevel
) {
var verifiedMessage = new VerifiedMessage(destination,
identityKey,
@ -272,13 +255,16 @@ public class SyncHelper {
}
public SendMessageResult sendKeysMessage() {
var keysMessage = new KeysMessage(Optional.ofNullable(account.getOrCreateStorageKey()),
Optional.ofNullable(account.getOrCreatePinMasterKey()));
var keysMessage = new KeysMessage(account.getOrCreateStorageKey(),
account.getOrCreatePinMasterKey(),
account.getOrCreateAccountEntropyPool(),
account.getOrCreateMediaRootBackupKey());
return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage));
}
public SendMessageResult sendStickerOperationsMessage(
List<StickerPack> installStickers, List<StickerPack> removeStickers
List<StickerPack> installStickers,
List<StickerPack> removeStickers
) {
var installStickerMessages = installStickers.stream().map(s -> getStickerPackOperationMessage(s, true));
var removeStickerMessages = removeStickers.stream().map(s -> getStickerPackOperationMessage(s, false));
@ -288,7 +274,8 @@ public class SyncHelper {
}
private static StickerPackOperationMessage getStickerPackOperationMessage(
final StickerPack s, final boolean installed
final StickerPack s,
final boolean installed
) {
return new StickerPackOperationMessage(s.packId().serialize(),
s.packKey(),
@ -354,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;
@ -364,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);
@ -378,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()
@ -399,13 +370,12 @@ public class SyncHelper {
builder.withMessageExpirationTimeVersion(c.getExpirationTimerVersion().get());
} else {
logger.debug(
"[ContactSync] {} was synced with an old expiration timer. Ignoring. Received: {} Current: ${}",
"[ContactSync] {} was synced with an old expiration timer. Ignoring. Received: {} Current: {}",
recipientId,
c.getExpirationTimerVersion(),
contact == null ? 1 : contact.messageExpirationTimeVersion());
}
}
builder.withIsArchived(c.isArchived());
account.getContactStore().storeContact(recipientId, builder.build());
if (c.getAvatar().isPresent()) {
@ -414,15 +384,14 @@ public class SyncHelper {
}
}
public SendMessageResult sendMessageRequestResponse(
final MessageRequestResponse.Type type, final GroupId groupId
) {
public SendMessageResult sendMessageRequestResponse(final MessageRequestResponse.Type type, final GroupId groupId) {
final var response = MessageRequestResponseMessage.forGroup(groupId.serialize(), localToRemoteType(type));
return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response));
}
public SendMessageResult sendMessageRequestResponse(
final MessageRequestResponse.Type type, final RecipientId recipientId
final MessageRequestResponse.Type type,
final RecipientId recipientId
) {
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
if (address.serviceId().isEmpty()) {

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) {
@ -158,7 +161,8 @@ public class UnidentifiedAccessHelper {
}
private static byte[] getTargetUnidentifiedAccessKey(
final Profile targetProfile, final ProfileKey theirProfileKey
final Profile targetProfile,
final ProfileKey theirProfileKey
) {
return switch (targetProfile.getUnidentifiedAccessMode()) {
case ENABLED -> theirProfileKey == null ? null : UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);

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 {
@ -417,7 +414,9 @@ public class ManagerImpl implements Manager {
@Override
public void startChangeNumber(
String newNumber, boolean voiceVerification, String captcha
String newNumber,
boolean voiceVerification,
String captcha
) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, NotPrimaryDeviceException, VerificationMethodNotAvailableException {
if (!account.isPrimaryDevice()) {
throw new NotPrimaryDeviceException();
@ -427,8 +426,10 @@ public class ManagerImpl implements Manager {
@Override
public void finishChangeNumber(
String newNumber, String verificationCode, String pin
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException {
String newNumber,
String verificationCode,
String pin
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException {
if (!account.isPrimaryDevice()) {
throw new NotPrimaryDeviceException();
}
@ -447,12 +448,13 @@ public class ManagerImpl implements Manager {
@Override
public void submitRateLimitRecaptchaChallenge(
String challenge, String captcha
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();
}
@ -460,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 -> {
@ -527,7 +529,8 @@ public class ManagerImpl implements Manager {
@Override
public SendGroupMessageResults quitGroup(
GroupId groupId, Set<RecipientIdentifier.Single> groupAdmins
GroupId groupId,
Set<RecipientIdentifier.Single> groupAdmins
) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException {
final var newAdmins = context.getRecipientHelper().resolveRecipients(groupAdmins);
return context.getGroupHelper().quitGroup(groupId, newAdmins);
@ -545,7 +548,9 @@ public class ManagerImpl implements Manager {
@Override
public Pair<GroupId, SendGroupMessageResults> createGroup(
String name, Set<RecipientIdentifier.Single> members, String avatarFile
String name,
Set<RecipientIdentifier.Single> members,
String avatarFile
) throws IOException, AttachmentInvalidException, UnregisteredRecipientException {
return context.getGroupHelper()
.createGroup(name,
@ -555,7 +560,8 @@ public class ManagerImpl implements Manager {
@Override
public SendGroupMessageResults updateGroup(
final GroupId groupId, final UpdateGroup updateGroup
final GroupId groupId,
final UpdateGroup updateGroup
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException {
return context.getGroupHelper()
.updateGroup(groupId,
@ -595,8 +601,28 @@ 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, boolean notifySelf
SignalServiceDataMessage.Builder messageBuilder,
Set<RecipientIdentifier> recipients,
boolean notifySelf
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
return sendMessage(messageBuilder, recipients, notifySelf, Optional.empty());
}
@ -608,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 || (
@ -644,10 +670,11 @@ public class ManagerImpl implements Manager {
}
private SendMessageResults sendTypingMessage(
SignalServiceTypingMessage.Action action, Set<RecipientIdentifier> recipients
SignalServiceTypingMessage.Action action,
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());
@ -671,16 +698,15 @@ public class ManagerImpl implements Manager {
@Override
public SendMessageResults sendTypingMessage(
TypingAction action, Set<RecipientIdentifier> recipients
TypingAction action,
Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
return sendTypingMessage(action.toSignalService(), recipients);
}
@Override
public SendMessageResults sendReadReceipt(
RecipientIdentifier.Single sender, List<Long> messageIds
) {
final var timestamp = System.currentTimeMillis();
public SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) {
final var timestamp = getNextMessageTimestamp();
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ,
messageIds,
timestamp);
@ -689,10 +715,8 @@ public class ManagerImpl implements Manager {
}
@Override
public SendMessageResults sendViewedReceipt(
RecipientIdentifier.Single sender, List<Long> messageIds
) {
final var timestamp = System.currentTimeMillis();
public SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) {
final var timestamp = getNextMessageTimestamp();
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED,
messageIds,
timestamp);
@ -724,7 +748,9 @@ public class ManagerImpl implements Manager {
@Override
public SendMessageResults sendMessage(
Message message, Set<RecipientIdentifier> recipients, boolean notifySelf
Message message,
Set<RecipientIdentifier> recipients,
boolean notifySelf
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
final var selfProfile = context.getProfileHelper().getSelfProfile();
if (selfProfile == null || selfProfile.getDisplayName().isEmpty()) {
@ -738,7 +764,9 @@ public class ManagerImpl implements Manager {
@Override
public SendMessageResults sendEditMessage(
Message message, Set<RecipientIdentifier> recipients, long editTargetTimestamp
Message message,
Set<RecipientIdentifier> recipients,
long editTargetTimestamp
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
final var messageBuilder = SignalServiceDataMessage.newBuilder();
applyMessage(messageBuilder, message);
@ -746,20 +774,28 @@ public class ManagerImpl implements Manager {
}
private void applyMessage(
final SignalServiceDataMessage.Builder messageBuilder, final Message message
final SignalServiceDataMessage.Builder messageBuilder,
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());
}
@ -774,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()));
}
@ -863,7 +900,8 @@ public class ManagerImpl implements Manager {
@Override
public SendMessageResults sendRemoteDeleteMessage(
long targetSentTimestamp, Set<RecipientIdentifier> recipients
long targetSentTimestamp,
Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp);
final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
@ -873,7 +911,7 @@ public class ManagerImpl implements Manager {
.deleteEntryForRecipientNonGroup(targetSentTimestamp, ACI.from(u.uuid()));
} else if (recipient instanceof RecipientIdentifier.Pni pni) {
account.getMessageSendLogStore()
.deleteEntryForRecipientNonGroup(targetSentTimestamp, PNI.parseOrThrow(pni.pni()));
.deleteEntryForRecipientNonGroup(targetSentTimestamp, PNI.from(pni.pni()));
} else if (recipient instanceof RecipientIdentifier.Single r) {
try {
final var recipientId = context.getRecipientHelper().resolveRecipient(r);
@ -915,7 +953,9 @@ public class ManagerImpl implements Manager {
@Override
public SendMessageResults sendPaymentNotificationMessage(
byte[] receipt, String note, RecipientIdentifier.Single recipient
byte[] receipt,
String note,
RecipientIdentifier.Single recipient
) throws IOException {
final var paymentNotification = new SignalServiceDataMessage.PaymentNotification(receipt, note);
final var payment = new SignalServiceDataMessage.Payment(paymentNotification, null);
@ -958,7 +998,8 @@ public class ManagerImpl implements Manager {
@Override
public SendMessageResults sendMessageRequestResponse(
final MessageRequestResponse.Type type, final Set<RecipientIdentifier> recipients
final MessageRequestResponse.Type type,
final Set<RecipientIdentifier> recipients
) {
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
for (final var recipient : recipients) {
@ -1021,19 +1062,30 @@ 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();
}
@Override
public void setContactsBlocked(
Collection<RecipientIdentifier.Single> recipients, boolean blocked
Collection<RecipientIdentifier.Single> recipients,
boolean blocked
) throws IOException, UnregisteredRecipientException {
if (recipients.isEmpty()) {
return;
@ -1067,7 +1119,8 @@ public class ManagerImpl implements Manager {
@Override
public void setGroupsBlocked(
final Collection<GroupId> groupIds, final boolean blocked
final Collection<GroupId> groupIds,
final boolean blocked
) throws GroupNotFoundException, IOException {
if (groupIds.isEmpty()) {
return;
@ -1093,7 +1146,8 @@ public class ManagerImpl implements Manager {
@Override
public void setExpirationTimer(
RecipientIdentifier.Single recipient, int messageExpirationTimer
RecipientIdentifier.Single recipient,
int messageExpirationTimer
) throws IOException, UnregisteredRecipientException {
var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
context.getContactHelper().setExpirationTimer(recipientId, messageExpirationTimer);
@ -1255,7 +1309,9 @@ public class ManagerImpl implements Manager {
@Override
public void receiveMessages(
Optional<Duration> timeout, Optional<Integer> maxMessages, ReceiveMessageHandler handler
Optional<Duration> timeout,
Optional<Integer> maxMessages,
ReceiveMessageHandler handler
) throws IOException, AlreadyReceivingException {
receiveMessages(timeout.orElse(Duration.ofMinutes(1)), timeout.isPresent(), maxMessages.orElse(null), handler);
}
@ -1275,7 +1331,10 @@ public class ManagerImpl implements Manager {
}
private void receiveMessages(
Duration timeout, boolean returnOnTimeout, Integer maxMessages, ReceiveMessageHandler handler
Duration timeout,
boolean returnOnTimeout,
Integer maxMessages,
ReceiveMessageHandler handler
) throws IOException, AlreadyReceivingException {
synchronized (messageHandlers) {
if (isReceiving()) {
@ -1431,7 +1490,8 @@ public class ManagerImpl implements Manager {
@Override
public boolean trustIdentityVerified(
RecipientIdentifier.Single recipient, IdentityVerificationCode verificationCode
RecipientIdentifier.Single recipient,
IdentityVerificationCode verificationCode
) throws UnregisteredRecipientException {
return switch (verificationCode) {
case IdentityVerificationCode.Fingerprint fingerprint -> trustIdentity(recipient,
@ -1450,7 +1510,8 @@ public class ManagerImpl implements Manager {
}
private boolean trustIdentity(
RecipientIdentifier.Single recipient, Function<RecipientId, Boolean> trustMethod
RecipientIdentifier.Single recipient,
Function<RecipientId, Boolean> trustMethod
) throws UnregisteredRecipientException {
final var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
final var updated = trustMethod.apply(recipientId);
@ -1546,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();
@ -150,7 +145,9 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
ret.getAciIdentity(),
ret.getPniIdentity(),
profileKey,
ret.getMasterKey());
ret.getMasterKey(),
ret.getAccountEntropyPool(),
ret.getMediaRootBackupKey());
account.getConfigurationStore().setReadReceipts(ret.isReadReceipts());
@ -158,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 {
@ -105,7 +103,9 @@ public class RegistrationManagerImpl implements RegistrationManager {
@Override
public void register(
boolean voiceVerification, String captcha, final boolean forceRegister
boolean voiceVerification,
String captcha,
final boolean forceRegister
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException {
if (account.isRegistered()
&& account.getServiceEnvironment() != null
@ -130,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);
@ -145,8 +148,9 @@ public class RegistrationManagerImpl implements RegistrationManager {
@Override
public void verifyAccount(
String verificationCode, String pin
) throws IOException, PinLockedException, IncorrectPinException {
String verificationCode,
String pin
) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException {
if (account.isRegistered()) {
throw new IOException("Account is already registered");
}
@ -196,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,
@ -218,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) {
@ -238,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,
@ -258,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,39 +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;
@ -47,20 +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,
@ -90,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();
}
}
/**
@ -113,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()));
}
@ -147,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());
}
@ -155,6 +227,42 @@ public class SignalDependencies {
return getOrCreate(() -> registrationApi, () -> registrationApi = getAccountManager().getRegistrationApi());
}
public LinkDeviceApi getLinkDeviceApi() {
return getOrCreate(() -> linkDeviceApi,
() -> linkDeviceApi = new LinkDeviceApi(getAuthenticatedSignalWebSocket()));
}
private StorageServiceApi getStorageServiceApi() {
return getOrCreate(() -> storageServiceApi,
() -> 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()),
@ -171,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);
});
}
@ -211,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() {
@ -225,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

@ -8,13 +8,27 @@ import java.io.IOException;
public class SyncStorageJob implements Job {
private final boolean forcePush;
private static final Logger logger = LoggerFactory.getLogger(SyncStorageJob.class);
public SyncStorageJob() {
this.forcePush = false;
}
public SyncStorageJob(final boolean forcePush) {
this.forcePush = forcePush;
}
@Override
public void run(Context context) {
logger.trace("Running storage sync job");
try {
context.getStorageHelper().syncDataWithStorage();
if (forcePush) {
context.getStorageHelper().forcePushToStorage();
} else {
context.getStorageHelper().syncDataWithStorage();
}
} catch (IOException e) {
logger.warn("Failed to sync storage data", e);
}

View file

@ -611,7 +611,8 @@ public class AccountDatabase extends Database {
}
private static void createUuidMappingTable(
final Connection connection, final Statement statement
final Connection connection,
final Statement statement
) throws SQLException {
statement.executeUpdate("""
CREATE TABLE tmp_mapping_table (

View file

@ -22,7 +22,8 @@ public class AttachmentStore {
}
public void storeAttachmentPreview(
final SignalServiceAttachmentPointer pointer, final AttachmentStorer storer
final SignalServiceAttachmentPointer pointer,
final AttachmentStorer storer
) throws IOException {
storeAttachment(getAttachmentPreviewFile(pointer.getRemoteId(),
pointer.getFileName(),
@ -30,7 +31,8 @@ public class AttachmentStore {
}
public void storeAttachment(
final SignalServiceAttachmentPointer pointer, final AttachmentStorer storer
final SignalServiceAttachmentPointer pointer,
final AttachmentStorer storer
) throws IOException {
storeAttachment(getAttachmentFile(pointer), storer);
}
@ -54,22 +56,24 @@ public class AttachmentStore {
}
private File getAttachmentPreviewFile(
SignalServiceAttachmentRemoteId attachmentId, Optional<String> filename, Optional<String> contentType
SignalServiceAttachmentRemoteId attachmentId,
Optional<String> filename,
Optional<String> contentType
) {
final var extension = getAttachmentExtension(filename, contentType);
return new File(attachmentsPath, attachmentId.toString() + extension + ".preview");
}
private File getAttachmentFile(
SignalServiceAttachmentRemoteId attachmentId, Optional<String> filename, Optional<String> contentType
SignalServiceAttachmentRemoteId attachmentId,
Optional<String> filename,
Optional<String> contentType
) {
final var extension = getAttachmentExtension(filename, contentType);
return new File(attachmentsPath, attachmentId.toString() + extension);
}
private static String getAttachmentExtension(
final Optional<String> filename, final Optional<String> contentType
) {
private static String getAttachmentExtension(final Optional<String> filename, final Optional<String> contentType) {
return filename.filter(f -> f.contains("."))
.map(f -> f.substring(f.lastIndexOf(".") + 1))
.or(() -> contentType.flatMap(MimeUtils::guessExtensionFromMimeType))

View file

@ -24,7 +24,8 @@ public abstract class Database implements AutoCloseable {
}
public static <T extends Database> T initDatabase(
File databaseFile, Function<HikariDataSource, T> newDatabase
File databaseFile,
Function<HikariDataSource, T> newDatabase
) throws SQLException {
HikariDataSource dataSource = null;
@ -94,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

@ -65,10 +65,12 @@ import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.AccountEntropyPool;
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore;
import org.whispersystems.signalservice.api.SignalServiceDataStore;
import org.whispersystems.signalservice.api.account.AccountAttributes;
import org.whispersystems.signalservice.api.account.PreKeyCollection;
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.ServiceId;
@ -114,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();
@ -138,6 +140,8 @@ public class SignalAccount implements Closeable {
private String registrationLockPin;
private MasterKey pinMasterKey;
private StorageKey storageKey;
private AccountEntropyPool accountEntropyPool;
private MediaRootBackupKey mediaRootBackupKey;
private ProfileKey profileKey;
private Settings settings;
@ -189,7 +193,10 @@ public class SignalAccount implements Closeable {
}
public static SignalAccount load(
File dataPath, String accountPath, boolean waitForLock, final Settings settings
File dataPath,
String accountPath,
boolean waitForLock,
final Settings settings
) throws IOException {
logger.trace("Opening account file");
final var fileName = getFileName(dataPath, accountPath);
@ -285,7 +292,9 @@ public class SignalAccount implements Closeable {
final IdentityKeyPair aciIdentity,
final IdentityKeyPair pniIdentity,
final ProfileKey profileKey,
final MasterKey masterKey
final MasterKey masterKey,
final AccountEntropyPool accountEntropyPool,
final MediaRootBackupKey mediaRootBackupKey
) {
this.deviceId = 0;
this.number = number;
@ -301,7 +310,14 @@ public class SignalAccount implements Closeable {
this.registered = false;
this.isMultiDevice = true;
setLastReceiveTimestamp(0L);
this.pinMasterKey = masterKey;
if (accountEntropyPool != null) {
this.pinMasterKey = null;
this.accountEntropyPool = accountEntropyPool;
} else {
this.pinMasterKey = masterKey;
this.accountEntropyPool = null;
}
this.mediaRootBackupKey = mediaRootBackupKey;
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
this.setStorageManifest(null);
this.storageKey = null;
@ -316,7 +332,9 @@ public class SignalAccount implements Closeable {
}
public void finishLinking(
final int deviceId, final PreKeyCollection aciPreKeys, final PreKeyCollection pniPreKeys
final int deviceId,
final PreKeyCollection aciPreKeys,
final PreKeyCollection pniPreKeys
) {
this.registered = true;
this.deviceId = deviceId;
@ -334,6 +352,7 @@ public class SignalAccount implements Closeable {
final PreKeyCollection pniPreKeys
) {
this.pinMasterKey = masterKey;
this.accountEntropyPool = null;
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
this.setStorageManifest(null);
this.storageKey = null;
@ -375,7 +394,9 @@ public class SignalAccount implements Closeable {
}
private void mergeRecipients(
final Connection connection, RecipientId recipientId, RecipientId toBeMergedRecipientId
final Connection connection,
RecipientId recipientId,
RecipientId toBeMergedRecipientId
) throws SQLException {
getMessageCache().mergeRecipients(recipientId, toBeMergedRecipientId);
getGroupStore().mergeRecipients(connection, recipientId, toBeMergedRecipientId);
@ -438,9 +459,7 @@ public class SignalAccount implements Closeable {
return f.exists() && !f.isDirectory() && f.length() > 0L;
}
private void load(
File dataPath, String accountPath, final Settings settings
) throws IOException {
private void load(File dataPath, String accountPath, final Settings settings) throws IOException {
logger.trace("Loading account file {}", accountPath);
this.dataPath = dataPath;
this.accountPath = accountPath;
@ -494,6 +513,12 @@ public class SignalAccount implements Closeable {
if (storage.storageKey != null) {
storageKey = new StorageKey(base64.decode(storage.storageKey));
}
if (storage.accountEntropyPool != null) {
accountEntropyPool = new AccountEntropyPool(storage.accountEntropyPool);
}
if (storage.mediaRootBackupKey != null) {
mediaRootBackupKey = new MediaRootBackupKey(base64.decode(storage.mediaRootBackupKey));
}
if (storage.profileKey != null) {
try {
profileKey = new ProfileKey(base64.decode(storage.profileKey));
@ -786,7 +811,8 @@ public class SignalAccount implements Closeable {
}
private void loadLegacyStores(
final JsonNode rootNode, final LegacyJsonSignalProtocolStore legacySignalProtocolStore
final JsonNode rootNode,
final LegacyJsonSignalProtocolStore legacySignalProtocolStore
) {
var legacyRecipientStoreNode = rootNode.get("recipientStore");
if (legacyRecipientStoreNode != null) {
@ -801,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()));
@ -812,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()
@ -975,6 +1003,8 @@ public class SignalAccount implements Closeable {
registrationLockPin,
pinMasterKey == null ? null : base64.encodeToString(pinMasterKey.serialize()),
storageKey == null ? null : base64.encodeToString(storageKey.serialize()),
accountEntropyPool == null ? null : accountEntropyPool.getValue(),
mediaRootBackupKey == null ? null : base64.encodeToString(mediaRootBackupKey.getValue()),
profileKey == null ? null : base64.encodeToString(profileKey.serialize()),
usernameLink == null ? null : base64.encodeToString(usernameLink.getEntropy()),
usernameLink == null ? null : usernameLink.getServerId().toString());
@ -1436,6 +1466,10 @@ public class SignalAccount implements Closeable {
return selfRecipientId;
}
public Profile getSelfRecipientProfile() {
return recipientStore.getProfile(selfRecipientId);
}
public String getSessionId(final String forNumber) {
final var keyValueStore = getKeyValueStore();
final var sessionNumber = keyValueStore.getEntry(verificationSessionNumber);
@ -1506,16 +1540,28 @@ public class SignalAccount implements Closeable {
public MasterKey getPinBackedMasterKey() {
if (registrationLockPin == null) {
return null;
} else if (!isPrimaryDevice()) {
return getMasterKey();
}
return pinMasterKey;
return getOrCreatePinMasterKey();
}
public MasterKey getOrCreatePinMasterKey() {
if (pinMasterKey == null) {
pinMasterKey = KeyUtils.createMasterKey();
save();
final var key = getMasterKey();
if (key != null) {
return key;
}
return pinMasterKey;
return getOrCreateAccountEntropyPool().deriveMasterKey();
}
private MasterKey getMasterKey() {
if (pinMasterKey != null) {
return pinMasterKey;
} else if (accountEntropyPool != null) {
return accountEntropyPool.deriveMasterKey();
}
return null;
}
public void setMasterKey(MasterKey masterKey) {
@ -1523,14 +1569,19 @@ public class SignalAccount implements Closeable {
return;
}
this.pinMasterKey = masterKey;
if (masterKey != null) {
this.storageKey = null;
}
save();
}
public StorageKey getOrCreateStorageKey() {
if (pinMasterKey != null) {
return pinMasterKey.deriveStorageServiceKey();
} else if (storageKey != null) {
if (storageKey != null) {
return storageKey;
} else if (pinMasterKey != null) {
return pinMasterKey.deriveStorageServiceKey();
} else if (accountEntropyPool != null) {
return accountEntropyPool.deriveMasterKey().deriveStorageServiceKey();
} else if (!isPrimaryDevice() || !isMultiDevice()) {
// Only upload storage, if a pin master key already exists or linked devices exist
return null;
@ -1547,6 +1598,40 @@ public class SignalAccount implements Closeable {
save();
}
public AccountEntropyPool getOrCreateAccountEntropyPool() {
if (accountEntropyPool == null) {
accountEntropyPool = AccountEntropyPool.Companion.generate();
save();
}
return accountEntropyPool;
}
public void setAccountEntropyPool(final AccountEntropyPool accountEntropyPool) {
this.accountEntropyPool = accountEntropyPool;
if (accountEntropyPool != null) {
this.storageKey = null;
this.pinMasterKey = null;
}
save();
}
public boolean needsStorageKeyMigration() {
return isPrimaryDevice() && (storageKey != null || pinMasterKey != null);
}
public MediaRootBackupKey getOrCreateMediaRootBackupKey() {
if (mediaRootBackupKey == null) {
mediaRootBackupKey = KeyUtils.createMediaRootBackupKey();
save();
}
return mediaRootBackupKey;
}
public void setMediaRootBackupKey(final MediaRootBackupKey mediaRootBackupKey) {
this.mediaRootBackupKey = mediaRootBackupKey;
save();
}
public String getRecoveryPassword() {
final var masterKey = getPinBackedMasterKey();
if (masterKey == null) {
@ -1569,7 +1654,7 @@ public class SignalAccount implements Closeable {
return Optional.empty();
}
try (var inputStream = new FileInputStream(storageManifestFile)) {
return Optional.of(SignalStorageManifest.deserialize(inputStream.readAllBytes()));
return Optional.of(SignalStorageManifest.Companion.deserialize(inputStream.readAllBytes()));
} catch (IOException e) {
logger.warn("Failed to read local storage manifest.", e);
return Optional.empty();
@ -1876,6 +1961,8 @@ public class SignalAccount implements Closeable {
String registrationLockPin,
String pinMasterKey,
String storageKey,
String accountEntropyPool,
String mediaRootBackupKey,
String profileKey,
String usernameLinkEntropy,
String usernameLinkServerId

View file

@ -41,9 +41,7 @@ public class UnknownStorageIdStore {
}
}
public List<StorageId> getUnknownStorageIds(
Connection connection, Collection<Integer> types
) throws SQLException {
public List<StorageId> getUnknownStorageIds(Connection connection, Collection<Integer> types) throws SQLException {
final var typesCommaSeparated = types.stream().map(String::valueOf).collect(Collectors.joining(","));
final var sql = (
"""

View file

@ -72,7 +72,8 @@ public class Utils {
}
public static <T> T executeQuerySingleRow(
PreparedStatement statement, ResultSetMapper<T> mapper
PreparedStatement statement,
ResultSetMapper<T> mapper
) throws SQLException {
final var resultSet = statement.executeQuery();
if (!resultSet.next()) {
@ -82,7 +83,8 @@ public class Utils {
}
public static <T> Optional<T> executeQueryForOptional(
PreparedStatement statement, ResultSetMapper<T> mapper
PreparedStatement statement,
ResultSetMapper<T> mapper
) throws SQLException {
final var resultSet = statement.executeQuery();
if (!resultSet.next()) {
@ -92,7 +94,8 @@ public class Utils {
}
public static <T> Stream<T> executeQueryForStream(
PreparedStatement statement, ResultSetMapper<T> mapper
PreparedStatement statement,
ResultSetMapper<T> mapper
) throws SQLException {
final var resultSet = statement.executeQuery();

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;
@ -41,7 +41,9 @@ public class AccountsStore {
private final AccountLoader accountLoader;
public AccountsStore(
final File dataPath, final ServiceEnvironment serviceEnvironment, final AccountLoader accountLoader
final File dataPath,
final ServiceEnvironment serviceEnvironment,
final AccountLoader accountLoader
) throws IOException {
this.dataPath = dataPath;
this.serviceEnvironment = getServiceEnvironmentString(serviceEnvironment);
@ -179,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());
}
@ -202,7 +204,9 @@ public class AccountsStore {
}
private AccountsStorage upgradeAccountsFile(
final FileChannel fileChannel, final AccountsStorage storage, final int accountsVersion
final FileChannel fileChannel,
final AccountsStorage storage,
final int accountsVersion
) {
try {
List<AccountsStorage.Account> newAccounts = storage.accounts();

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();
@ -59,7 +67,8 @@ public class ConfigurationStore {
}
public void setUnidentifiedDeliveryIndicators(
final Connection connection, final boolean value
final Connection connection,
final boolean value
) throws SQLException {
if (keyValueStore.storeEntry(connection, unidentifiedDeliveryIndicators, value)) {
recipientStore.rotateSelfStorageId(connection);
@ -70,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();
@ -86,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();
@ -102,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();
@ -118,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();
@ -125,7 +150,8 @@ public class ConfigurationStore {
}
public void setPhoneNumberSharingMode(
final Connection connection, final PhoneNumberSharingMode value
final Connection connection,
final PhoneNumberSharingMode value
) throws SQLException {
if (keyValueStore.storeEntry(connection, phoneNumberSharingMode, value)) {
recipientStore.rotateSelfStorageId(connection);
@ -136,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

@ -31,7 +31,9 @@ public final class GroupInfoV2 extends GroupInfo {
private final RecipientResolver recipientResolver;
public GroupInfoV2(
final GroupIdV2 groupId, final GroupMasterKey masterKey, final RecipientResolver recipientResolver
final GroupIdV2 groupId,
final GroupMasterKey masterKey,
final RecipientResolver recipientResolver
) {
this.groupId = groupId;
this.masterKey = masterKey;

View file

@ -121,7 +121,10 @@ public class GroupStore {
}
public void storeStorageRecord(
final Connection connection, final GroupId groupId, final StorageId storageId, final byte[] storageRecord
final Connection connection,
final GroupId groupId,
final StorageId storageId,
final byte[] storageRecord
) throws SQLException {
final var groupTable = groupId instanceof GroupIdV1 ? TABLE_GROUP_V1 : TABLE_GROUP_V2;
final var deleteSql = (
@ -250,7 +253,8 @@ public class GroupStore {
}
public GroupInfoV2 getGroupOrPartialMigrate(
Connection connection, final GroupMasterKey groupMasterKey
Connection connection,
final GroupMasterKey groupMasterKey
) throws SQLException {
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
final var groupId = GroupUtils.getGroupIdV2(groupSecretParams);
@ -258,9 +262,7 @@ public class GroupStore {
return getGroupOrPartialMigrate(connection, groupMasterKey, groupId);
}
public GroupInfoV2 getGroupOrPartialMigrate(
final GroupMasterKey groupMasterKey, final GroupIdV2 groupId
) {
public GroupInfoV2 getGroupOrPartialMigrate(final GroupMasterKey groupMasterKey, final GroupIdV2 groupId) {
try (final var connection = database.getConnection()) {
return getGroupOrPartialMigrate(connection, groupMasterKey, groupId);
} catch (SQLException e) {
@ -269,7 +271,9 @@ public class GroupStore {
}
private GroupInfoV2 getGroupOrPartialMigrate(
Connection connection, final GroupMasterKey groupMasterKey, final GroupIdV2 groupId
Connection connection,
final GroupMasterKey groupMasterKey,
final GroupIdV2 groupId
) throws SQLException {
switch (getGroup(connection, (GroupId) groupId)) {
case GroupInfoV1 groupInfoV1 -> {
@ -325,7 +329,9 @@ public class GroupStore {
}
public void mergeRecipients(
final Connection connection, final RecipientId recipientId, final RecipientId toBeMergedRecipientId
final Connection connection,
final RecipientId recipientId,
final RecipientId toBeMergedRecipientId
) throws SQLException {
final var sql = (
"""
@ -360,7 +366,9 @@ public class GroupStore {
}
public void updateStorageIds(
Connection connection, Map<GroupIdV1, StorageId> storageIdV1Map, Map<GroupIdV2, StorageId> storageIdV2Map
Connection connection,
Map<GroupIdV1, StorageId> storageIdV1Map,
Map<GroupIdV2, StorageId> storageIdV2Map
) throws SQLException {
final var sql = (
"""
@ -385,9 +393,7 @@ public class GroupStore {
}
}
public void updateStorageId(
Connection connection, GroupId groupId, StorageId storageId
) throws SQLException {
public void updateStorageId(Connection connection, GroupId groupId, StorageId storageId) throws SQLException {
final var sqlV1 = (
"""
UPDATE %s
@ -460,7 +466,9 @@ public class GroupStore {
}
private void insertOrReplaceGroup(
final Connection connection, Long internalId, final GroupInfo group
final Connection connection,
Long internalId,
final GroupInfo group
) throws SQLException {
if (group instanceof GroupInfoV1 groupV1) {
if (internalId != null) {

View file

@ -151,7 +151,8 @@ public class LegacyGroupStore {
@Override
public List<Member> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
JsonParser jsonParser,
DeserializationContext deserializationContext
) throws IOException {
var addresses = new ArrayList<Member>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
@ -184,7 +185,8 @@ public class LegacyGroupStore {
@Override
public List<Object> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
JsonParser jsonParser,
DeserializationContext deserializationContext
) throws IOException {
var groups = new ArrayList<>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);

View file

@ -11,9 +11,7 @@ public class IdentityInfo {
private final TrustLevel trustLevel;
private final long addedTimestamp;
IdentityInfo(
final String address, IdentityKey identityKey, TrustLevel trustLevel, long addedTimestamp
) {
IdentityInfo(final String address, IdentityKey identityKey, TrustLevel trustLevel, long addedTimestamp) {
this.address = address;
this.identityKey = identityKey;
this.trustLevel = trustLevel;

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;
@ -49,7 +50,9 @@ public class IdentityKeyStore {
}
public IdentityKeyStore(
final Database database, final TrustNewIdentity trustNewIdentity, RecipientStore recipientStore
final Database database,
final TrustNewIdentity trustNewIdentity,
RecipientStore recipientStore
) {
this.database = database;
this.trustNewIdentity = trustNewIdentity;
@ -60,19 +63,21 @@ 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(
final Connection connection, final ServiceId serviceId, final IdentityKey identityKey
public IdentityChange saveIdentity(
final Connection connection,
final ServiceId serviceId,
final IdentityKey identityKey
) throws SQLException {
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);
@ -81,18 +86,24 @@ public class IdentityKeyStore {
}
}
private boolean saveIdentity(
final Connection connection, final String address, final IdentityKey identityKey
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) {
@ -230,9 +241,7 @@ public class IdentityKeyStore {
logger.debug("Complete identities migration took {}ms", (System.nanoTime() - start) / 1000000);
}
private IdentityInfo loadIdentity(
final Connection connection, final String address
) throws SQLException {
private IdentityInfo loadIdentity(final Connection connection, final String address) throws SQLException {
final var sql = (
"""
SELECT i.address, i.identity_key, i.added_timestamp, i.trust_level

View file

@ -41,7 +41,9 @@ public class LegacyIdentityKeyStore {
static final Pattern identityFileNamePattern = Pattern.compile("(\\d+)");
private static List<IdentityInfo> getIdentities(
final File identitiesPath, final RecipientResolver resolver, final RecipientAddressResolver addressResolver
final File identitiesPath,
final RecipientResolver resolver,
final RecipientAddressResolver addressResolver
) {
final var files = identitiesPath.listFiles();
if (files == null) {
@ -66,7 +68,9 @@ public class LegacyIdentityKeyStore {
}
private static IdentityInfo loadIdentityLocked(
final RecipientId recipientId, RecipientAddressResolver addressResolver, final File identitiesPath
final RecipientId recipientId,
RecipientAddressResolver addressResolver,
final File identitiesPath
) {
final var file = getIdentityFile(recipientId, identitiesPath);
if (!file.exists()) {

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,20 +72,28 @@ 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;
}
}
public <T> boolean storeEntry(
final Connection connection, final KeyValueEntry<T> key, final T value
final Connection connection,
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;
}
@ -93,12 +110,16 @@ public class KeyValueStore {
setParameterValue(statement, 2, key.clazz(), value);
statement.executeUpdate();
}
synchronized (cache) {
cache.put(key, value);
}
return true;
}
@SuppressWarnings("unchecked")
private static <T> T readValueFromResultSet(
final KeyValueEntry<T> key, final ResultSet resultSet
final KeyValueEntry<T> key,
final ResultSet resultSet
) throws SQLException {
Object value;
final var clazz = key.clazz();
@ -134,7 +155,10 @@ public class KeyValueStore {
}
private static <T> void setParameterValue(
final PreparedStatement statement, final int parameterIndex, final Class<T> clazz, final T value
final PreparedStatement statement,
final int parameterIndex,
final Class<T> clazz,
final T value
) throws SQLException {
if (clazz == int.class || clazz == Integer.class) {
if (value == null) {

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

@ -34,7 +34,8 @@ public class LegacyProfileStore {
@Override
public List<LegacySignalProfileEntry> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
JsonParser jsonParser,
DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);

View file

@ -18,6 +18,7 @@ public interface ProfileStore {
void storeProfileKey(RecipientId recipientId, ProfileKey profileKey);
void storeExpiringProfileKeyCredential(
RecipientId recipientId, ExpiringProfileKeyCredential expiringProfileKeyCredential
RecipientId recipientId,
ExpiringProfileKeyCredential expiringProfileKeyCredential
);
}

View file

@ -32,7 +32,9 @@ public class LegacyJsonIdentityKeyStore {
private final int localRegistrationId;
private LegacyJsonIdentityKeyStore(
final List<LegacyIdentityInfo> identities, IdentityKeyPair identityKeyPair, int localRegistrationId
final List<LegacyIdentityInfo> identities,
IdentityKeyPair identityKeyPair,
int localRegistrationId
) {
this.identities = identities;
this.identityKeyPair = identityKeyPair;
@ -77,7 +79,8 @@ public class LegacyJsonIdentityKeyStore {
@Override
public LegacyJsonIdentityKeyStore deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
JsonParser jsonParser,
DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);

View file

@ -26,7 +26,8 @@ public class LegacyJsonPreKeyStore {
@Override
public LegacyJsonPreKeyStore deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
JsonParser jsonParser,
DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);

View file

@ -31,7 +31,8 @@ public class LegacyJsonSessionStore {
@Override
public LegacyJsonSessionStore deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
JsonParser jsonParser,
DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);

View file

@ -26,7 +26,8 @@ public class LegacyJsonSignedPreKeyStore {
@Override
public LegacyJsonSignedPreKeyStore deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
JsonParser jsonParser,
DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);

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);
}
@ -172,7 +172,9 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
@Override
public void storeSenderKey(
final SignalProtocolAddress sender, final UUID distributionId, final SenderKeyRecord record
final SignalProtocolAddress sender,
final UUID distributionId,
final SenderKeyRecord record
) {
senderKeyStore.storeSenderKey(sender, distributionId, record);
}
@ -189,7 +191,8 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
@Override
public void markSenderKeySharedWith(
final DistributionId distributionId, final Collection<SignalProtocolAddress> addresses
final DistributionId distributionId,
final Collection<SignalProtocolAddress> addresses
) {
senderKeyStore.markSenderKeySharedWith(distributionId, addresses);
}

View file

@ -98,9 +98,7 @@ public class CdsiStore {
}
}
private static void removeNumbers(
final Connection connection, final Set<String> numbers
) throws SQLException {
private static void removeNumbers(final Connection connection, final Set<String> numbers) throws SQLException {
final var sql = (
"""
DELETE FROM %s
@ -116,7 +114,9 @@ public class CdsiStore {
}
private static void addNumbers(
final Connection connection, final Set<String> numbers, final long lastSeen
final Connection connection,
final Set<String> numbers,
final long lastSeen
) throws SQLException {
final var sql = (
"""
@ -135,7 +135,9 @@ public class CdsiStore {
}
private static void updateLastSeen(
final Connection connection, final Set<String> numbers, final long lastSeen
final Connection connection,
final Set<String> numbers,
final long lastSeen
) throws SQLException {
final var sql = (
"""

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager.storage.recipients;
public class InvalidAddress extends AssertionError {
InvalidAddress(String message) {
super(message);
}
}

View file

@ -27,7 +27,8 @@ public class LegacyRecipientStore {
@Override
public List<RecipientAddress> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
JsonParser jsonParser,
DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);

View file

@ -15,7 +15,8 @@ public class MergeRecipientHelper {
private static final Logger logger = LoggerFactory.getLogger(MergeRecipientHelper.class);
static Pair<RecipientId, List<RecipientId>> resolveRecipientTrustedLocked(
Store store, RecipientAddress address
Store store,
RecipientAddress address
) throws SQLException {
// address has at least one of serviceId/pni and optionally number/username
@ -38,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());
}
@ -82,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

@ -27,7 +27,7 @@ public record RecipientAddress(
pni = Optional.empty();
}
if (aci.isEmpty() && pni.isEmpty() && number.isEmpty() && username.isEmpty()) {
throw new AssertionError("Must have either a ServiceId, username or E164 number!");
throw new InvalidAddress("Must have either a ServiceId, username or E164 number!");
}
}
@ -69,8 +69,8 @@ public record RecipientAddress(
}
public RecipientAddress(org.asamk.signal.manager.api.RecipientAddress address) {
this(address.aci().map(ACI::parseOrNull),
address.pni().map(PNI::parseOrNull),
this(address.aci().map(ACI::parseOrThrow),
address.pni().map(PNI::parseOrThrow),
address.number(),
address.username());
}
@ -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

@ -208,7 +208,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
public RecipientId resolveRecipientByNumber(
final String number, Supplier<ServiceId> serviceIdSupplier
final String number,
Supplier<ServiceId> serviceIdSupplier
) throws UnregisteredRecipientException {
final Optional<RecipientWithAddress> byNumber;
try (final var connection = database.getConnection()) {
@ -238,7 +239,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
public RecipientId resolveRecipientByUsername(
final String username, Supplier<ACI> aciSupplier
final String username,
Supplier<ACI> aciSupplier
) throws UnregisteredRecipientException {
final Optional<RecipientWithAddress> byUsername;
try (final var connection = database.getConnection()) {
@ -301,7 +303,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
@Override
public RecipientId resolveRecipientTrusted(
final Optional<ACI> aci, final Optional<PNI> pni, final Optional<String> number
final Optional<ACI> aci,
final Optional<PNI> pni,
final Optional<String> number
) {
return resolveRecipientTrusted(new RecipientAddress(aci, pni, number, Optional.empty()));
}
@ -335,7 +339,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
"""
SELECT r._id, r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp
FROM %s r
WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE
WHERE (r.number IS NOT NULL OR r.pni IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE
"""
).formatted(TABLE_RECIPIENT, SQL_IS_CONTACT);
try (final var connection = database.getConnection()) {
@ -388,11 +392,23 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
try (final var statement = connection.prepareStatement(sql)) {
statement.setBytes(1, storageId.getRaw());
return Utils.executeQuerySingleRow(statement, this::getRecipientFromResultSet);
} catch (InvalidAddress e) {
try (final var statement = connection.prepareStatement("""
UPDATE %s SET aci=NULL, pni=NULL, username=NULL, number=NULL, storage_id=NULL WHERE storage_id = ?
""".formatted(TABLE_RECIPIENT))) {
statement.setBytes(1, storageId.getRaw());
statement.executeUpdate();
}
connection.commit();
throw e;
}
}
public List<Recipient> getRecipients(
boolean onlyContacts, Optional<Boolean> blocked, Set<RecipientId> recipientIds, Optional<String> name
boolean onlyContacts,
Optional<Boolean> blocked,
Set<RecipientId> recipientIds,
Optional<String> name
) {
final var sqlWhere = new ArrayList<String>();
if (onlyContacts) {
@ -419,7 +435,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
r.discoverable,
r.storage_record
FROM %s r
WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s
WHERE (r.number IS NOT NULL OR r.pni IS NOT NULL OR r.aci IS NOT NULL) AND %s
"""
).formatted(TABLE_RECIPIENT, sqlWhere.isEmpty() ? "TRUE" : String.join(" AND ", sqlWhere));
final var selfAddress = selfAddressProvider.getSelfAddress();
@ -505,7 +521,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
"""
SELECT r._id
FROM %s r
WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL)
WHERE (r.aci IS NOT NULL OR r.pni IS NOT NULL)
"""
).formatted(TABLE_RECIPIENT);
try (final var statement = connection.prepareStatement(sql)) {
@ -518,7 +534,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
"""
SELECT r._id
FROM %s r
WHERE r.storage_id IS NULL AND r.unregistered_timestamp IS NULL
WHERE r.storage_id IS NULL AND r.unregistered_timestamp IS NULL AND (r.aci IS NOT NULL OR r.pni IS NOT NULL)
"""
).formatted(TABLE_RECIPIENT);
final var updateSql = (
@ -614,14 +630,17 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
public void storeProfileKey(
Connection connection, RecipientId recipientId, final ProfileKey profileKey
Connection connection,
RecipientId recipientId,
final ProfileKey profileKey
) throws SQLException {
storeProfileKey(connection, recipientId, profileKey, true);
}
@Override
public void storeExpiringProfileKeyCredential(
RecipientId recipientId, final ExpiringProfileKeyCredential profileKeyCredential
RecipientId recipientId,
final ExpiringProfileKeyCredential profileKeyCredential
) {
try (final var connection = database.getConnection()) {
storeExpiringProfileKeyCredential(connection, recipientId, profileKeyCredential);
@ -661,7 +680,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
public void updateStorageId(
Connection connection, RecipientId recipientId, StorageId storageId
Connection connection,
RecipientId recipientId,
StorageId storageId
) throws SQLException {
final var sql = (
"""
@ -813,7 +834,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
public void storeContact(
final Connection connection, final RecipientId recipientId, final Contact contact
final Connection connection,
final RecipientId recipientId,
final Contact contact
) throws SQLException {
final var sql = (
"""
@ -852,7 +875,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
final Connection connection, final List<StorageId> storageIds
final Connection connection,
final List<StorageId> storageIds
) throws SQLException {
final var sql = (
"""
@ -910,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) {
@ -965,11 +989,17 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
private void markUnregisteredAndSplitIfNecessary(
final Connection connection, final RecipientId recipientId
final Connection connection,
final RecipientId recipientId
) 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);
@ -977,7 +1007,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
private void markDiscoverable(
final Connection connection, final RecipientId recipientId, final boolean discoverable
final Connection connection,
final RecipientId recipientId,
final boolean discoverable
) throws SQLException {
final var sql = (
"""
@ -993,9 +1025,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
}
private void markRegistered(
final Connection connection, final RecipientId recipientId
) throws SQLException {
private void markRegistered(final Connection connection, final RecipientId recipientId) throws SQLException {
final var sql = (
"""
UPDATE %s
@ -1009,9 +1039,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
}
private void markUnregistered(
final Connection connection, final RecipientId recipientId
) throws SQLException {
private void markUnregistered(final Connection connection, final RecipientId recipientId) throws SQLException {
final var sql = (
"""
UPDATE %s
@ -1046,7 +1074,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
public void storeProfile(
final Connection connection, final RecipientId recipientId, final Profile profile
final Connection connection,
final RecipientId recipientId,
final Profile profile
) throws SQLException {
final var sql = (
"""
@ -1079,7 +1109,10 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
private void storeProfileKey(
Connection connection, RecipientId recipientId, final ProfileKey profileKey, boolean resetProfile
Connection connection,
RecipientId recipientId,
final ProfileKey profileKey,
boolean resetProfile
) throws SQLException {
if (profileKey != null) {
final var recipientProfileKey = getProfileKey(connection, recipientId);
@ -1111,7 +1144,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
private RecipientAddress resolveRecipientAddress(
final Connection connection, final RecipientId recipientId
final Connection connection,
final RecipientId recipientId
) throws SQLException {
final var sql = (
"""
@ -1150,7 +1184,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
private Pair<RecipientId, List<RecipientId>> resolveRecipientTrustedLocked(
final Connection connection, final RecipientAddress address, final boolean isSelf
final Connection connection,
final RecipientAddress address,
final boolean isSelf
) throws SQLException {
if (address.hasSingleIdentifier() || (
!isSelf && selfAddressProvider.getSelfAddress().matches(address)
@ -1168,7 +1204,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
private void mergeRecipients(
final Connection connection, final RecipientId recipientId, final List<RecipientId> toBeMergedRecipientIds
final Connection connection,
final RecipientId recipientId,
final List<RecipientId> toBeMergedRecipientIds
) throws SQLException {
for (final var toBeMergedRecipientId : toBeMergedRecipientIds) {
recipientMergeHandler.mergeRecipients(connection, recipientId, toBeMergedRecipientId);
@ -1177,9 +1215,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
}
private RecipientId resolveRecipientLocked(
Connection connection, RecipientAddress address
) throws SQLException {
private RecipientId resolveRecipientLocked(Connection connection, RecipientAddress address) throws SQLException {
final var byAci = address.aci().isEmpty()
? Optional.<RecipientWithAddress>empty()
: findByServiceId(connection, address.aci().get());
@ -1236,7 +1272,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
private RecipientId addNewRecipient(
final Connection connection, final RecipientAddress address
final Connection connection,
final RecipientAddress address
) throws SQLException {
final var sql = (
"""
@ -1277,7 +1314,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
private void updateRecipientAddress(
Connection connection, RecipientId recipientId, final RecipientAddress address
Connection connection,
RecipientId recipientId,
final RecipientAddress address
) throws SQLException {
recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId));
final var sql = (
@ -1312,7 +1351,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
private void mergeRecipientsLocked(
Connection connection, RecipientId recipientId, RecipientId toBeMergedRecipientId
Connection connection,
RecipientId recipientId,
RecipientId toBeMergedRecipientId
) throws SQLException {
final var contact = getContact(connection, recipientId);
if (contact == null) {
@ -1343,7 +1384,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
private Optional<RecipientWithAddress> findByNumber(
final Connection connection, final String number
final Connection connection,
final String number
) throws SQLException {
final var sql = """
SELECT r._id, r.number, r.aci, r.pni, r.username
@ -1358,7 +1400,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
private Optional<RecipientWithAddress> findByUsername(
final Connection connection, final String username
final Connection connection,
final String username
) throws SQLException {
final var sql = """
SELECT r._id, r.number, r.aci, r.pni, r.username
@ -1373,7 +1416,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
private Optional<RecipientWithAddress> findByServiceId(
final Connection connection, final ServiceId serviceId
final Connection connection,
final ServiceId serviceId
) throws SQLException {
var recipientWithAddress = Optional.ofNullable(recipientAddressCache.get(serviceId));
if (recipientWithAddress.isPresent()) {
@ -1394,7 +1438,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
private Set<RecipientWithAddress> findAllByAddress(
final Connection connection, final RecipientAddress address
final Connection connection,
final RecipientAddress address
) throws SQLException {
final var sql = """
SELECT r._id, r.number, r.aci, r.pni, r.username
@ -1447,7 +1492,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
private ExpiringProfileKeyCredential getExpiringProfileKeyCredential(
final Connection connection, final RecipientId recipientId
final Connection connection,
final RecipientId recipientId
) throws SQLException {
final var sql = (
"""
@ -1593,7 +1639,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
public interface RecipientMergeHandler {
void mergeRecipients(
final Connection connection, RecipientId recipientId, RecipientId toBeMergedRecipientId
final Connection connection,
RecipientId recipientId,
RecipientId toBeMergedRecipientId
) throws SQLException;
}
@ -1617,7 +1665,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
@Override
public void updateRecipientAddress(
final RecipientId recipientId, final RecipientAddress address
final RecipientId recipientId,
final RecipientAddress address
) throws SQLException {
RecipientStore.this.updateRecipientAddress(connection, recipientId, address);
}

View file

@ -44,7 +44,9 @@ public interface RecipientTrustedResolver {
@Override
public RecipientId resolveRecipientTrusted(
final Optional<ACI> aci, final Optional<PNI> pni, final Optional<String> number
final Optional<ACI> aci,
final Optional<PNI> pni,
final Optional<String> number
) {
return recipientTrustedResolverSupplier.get().resolveRecipientTrusted(aci, pni, number);
}

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