Compare commits

...

279 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
AsamK
277652e3f2 Bump version to 0.13.9 2024-10-28 23:37:26 +01:00
AsamK
181086aefe Update graalvm config
Fixes #1612
2024-10-28 10:58:14 +01:00
AsamK
a1d552698a Fix verifyAccount
Fixes #1614
2024-10-28 10:42:26 +01:00
AsamK
1b959608c3 Simplify verification code request 2024-10-28 10:35:18 +01:00
AsamK
acddef6e35 Prepare next release 2024-10-26 15:02:28 +02:00
AsamK
0e77870b59 Bump version to 0.13.7 2024-10-26 15:01:05 +02:00
AsamK
0a287b0b3e Reformat files 2024-10-26 13:10:33 +02:00
AsamK
5a4f4ba6db Implement message expiration timer version
Fixes #1605
2024-10-26 13:08:21 +02:00
AsamK
5171107a29 Run CI with Java 23 2024-10-25 17:22:47 +02:00
AsamK
47e6fc1769 Update dependencies 2024-10-25 17:20:07 +02:00
AsamK
9afd4e4328 Update libsignal-service 2024-10-25 17:20:07 +02:00
Jailson Dias
eac2a47163 add group info on json message 2024-10-25 17:19:17 +02:00
Jailson Dias
5646f65195 add received and delivered timestamps on json message 2024-10-25 17:19:17 +02:00
AsamK
fab1b96c21 Upload text attachment also if there no other attachments
Fixes #1598
2024-09-29 09:43:51 +02:00
AsamK
91eacc18c2 Refactor attachment upload 2024-09-29 09:43:26 +02:00
AsamK
7f1fc932ad Add constant for MAX_MESSAGE_BODY_SIZE 2024-09-29 09:43:05 +02:00
AsamK
304c44064d Prepare next release 2024-09-28 22:14:26 +02:00
AsamK
946c483f35 Bump version to 0.13.7 2024-09-28 22:13:24 +02:00
AsamK
69d691f416 Update graalvm reflect-config
Fixes #1590
2024-09-28 20:41:58 +02:00
AsamK
a6ab8f7e80 Remove v2 suffix from secure value recovery 2024-09-28 20:41:58 +02:00
AsamK
19dc2d446b Update libsignal-service 2024-09-28 20:41:58 +02:00
AsamK
8524037900 Update gradle 2024-09-28 12:13:49 +02:00
AsamK
dbbc3fbd71 Remove unnecessary proto conversion 2024-09-24 17:28:24 +02:00
AsamK
c6e93126fa Fix truncating cdsi table
Fixes #1587
2024-09-12 23:09:20 +02:00
AsamK
352699c4b6 Update artifact CI actions 2024-09-11 17:50:01 +02:00
AsamK
bea2772491 Update graalvm buildtools 2024-09-10 19:16:42 +02:00
AsamK
bab8ddf35a Update slf4j 2024-09-09 18:31:47 +02:00
AsamK
1d5d16f57e Adapt signal_jni file names in graalvm config 2024-09-09 18:31:34 +02:00
AsamK
6f9e9e9302 Prepare next release 2024-09-08 19:24:18 +02:00
AsamK
20add0f27b Downgrade dbus-java 2024-09-08 19:24:18 +02:00
AsamK
eca3c6fa30 Replace deprecated DBusMap 2024-09-08 19:24:18 +02:00
AsamK
a0d1b081ff Bump version to 0.13.6 2024-09-08 18:59:22 +02:00
AsamK
d852c60c37 Update dependencies 2024-09-08 18:55:23 +02:00
AsamK
2b5451f74a Update client dependencies 2024-09-08 18:51:56 +02:00
AsamK
b322716215 Update CHANGELOG.md 2024-09-08 10:18:26 +02:00
AsamK
41726d339f Update libsignal-service-java 2024-09-08 10:18:17 +02:00
AsamK
52bb92dce4 Call getter instead of using cached instance variable 2024-09-08 10:15:02 +02:00
AsamK
65adfdd6f5 Add signal CDN 3 2024-09-08 10:15:02 +02:00
AsamK
d8b1a2fffe Fix stripping the correct identifiers when merging recipients 2024-09-08 09:24:51 +02:00
AsamK
ea436ecb64 Fix possible db dead lock
Fixes #1483
2024-09-08 09:22:52 +02:00
AsamK
82fbde4f19 Adapt code style 2024-09-08 09:00:39 +02:00
AsamK
19b15e68e4 Improve addDevice error message
Fixes #1573
2024-09-08 09:00:34 +02:00
AsamK
b51f849fe6 Send sync message for read/viewed receipt messages
Fixes #1570
2024-09-08 08:51:33 +02:00
AsamK
7cc0ef1c70 Improve error message and log output for failed jsonrpc commands 2024-09-08 08:30:31 +02:00
AsamK
485c4fd467 Improve daemon deprecation message 2024-09-08 08:30:08 +02:00
AsamK
bda395191b Don't set previousE164s param if cdsi token is empty
Fixes #1576
2024-09-08 08:29:54 +02:00
AsamK
cb129db95e Update libsignal-service 2024-08-20 17:39:39 +02:00
AsamK
130cee9906 Fix sending to groups with non sender key capable members
Regression in 0.13.5

Fixes #1568
2024-08-20 17:13:57 +02:00
AsamK
6bdc9a4b16 Update gradle 2024-08-20 17:10:03 +02:00
AsamK
a69a9b7b0e Add method to update group endorsements 2024-08-20 17:10:03 +02:00
AsamK
6ea373fbd5 Prepare next release 2024-08-20 17:10:03 +02:00
Ingmar Lippert
62da514850
Add more specificity to addDevice (#1561) 2024-08-20 16:59:52 +02:00
AsamK
fe3934171d Bump version to 0.13.5 2024-07-25 22:47:16 +02:00
AsamK
3d11221732 Update jackson 2024-07-25 22:43:55 +02:00
AsamK
e961488b1a Update man page 2024-07-25 22:40:46 +02:00
AsamK
2db3d3259e Use UploadSpec for attachment uploads 2024-07-25 22:31:35 +02:00
AsamK
e51b1ee23a Update libsignal-service 2024-07-25 16:27:24 +02:00
AsamK
5ff66728e3 Update libsignal-service 2024-06-26 15:38:55 +02:00
AsamK
baf7b74a61 Prepare next release 2024-06-06 11:15:02 +02:00
AsamK
c716f94649 Bump version to 0.13.4 2024-06-06 10:47:42 +02:00
AsamK
8b355918e8 Use jvm running gradle if it's compatible with targetCompatibility 2024-06-06 10:40:21 +02:00
AsamK
67012b40b1 Update user agent 2024-06-06 10:16:53 +02:00
AsamK
fd402b52c8 Update doc 2024-06-06 10:16:40 +02:00
AsamK
5a97b9e134 Update groups when using listGroups command
Fixes #1517
2024-06-06 10:07:20 +02:00
AsamK
17596795c2 Only store profile keys for group history if none is known yet 2024-06-06 10:07:20 +02:00
AsamK
10b9c264fd Update libsignal-service
Fixes #1530
2024-06-06 10:07:20 +02:00
AsamK
a2b002ac5e Add java 22 to CI 2024-06-06 09:12:40 +02:00
AsamK
6d764db114 Update dependencies 2024-06-02 13:26:17 +02:00
AsamK
777cfbb69f Update gradle wrapper 2024-06-02 13:25:09 +02:00
AsamK
9781c56571 Improve username update error message
Fixes #1535
2024-05-24 16:09:07 +02:00
AsamK
04cf54263e Fix getUserStatus command with only username parameter
Related #1535
2024-05-23 12:46:15 +02:00
AsamK
6baf0eac13 Fix type parsing in JSON RPC mode
Fixes #1533
2024-05-21 20:28:16 +02:00
AsamK
fb21a42cce Update graalvm build tools 2024-05-18 22:13:29 +02:00
AsamK
53d7e0f08b Handle all possible identifiers of a RecipientAddress
Fixes #1516
2024-05-17 18:02:05 +02:00
AsamK
8f756cd90c Update libsignal-service 2024-05-09 21:32:34 +02:00
Sebastian Scheibner
fb81bf1d05
Update README.md 2024-05-09 21:20:38 +02:00
AsamK
04726f005c Save account file after setting username link
Fixes #1515
2024-05-01 09:08:00 +02:00
AsamK
09e3e7f335 Rotate storageId after setting username 2024-05-01 09:07:39 +02:00
AsamK
90b1e4bc02 Delete username link when deleting username 2024-05-01 09:04:03 +02:00
AsamK
3f31f1a8a6 Update metainfo 2024-04-21 09:34:06 +02:00
dependabot[bot]
3cd8e323c9
Bump rustls from 0.21.10 to 0.21.11 in /client (#1511)
Bumps [rustls](https://github.com/rustls/rustls) from 0.21.10 to 0.21.11.
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.21.10...v/0.21.11)

---
updated-dependencies:
- dependency-name: rustls
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-20 13:53:36 +02:00
AsamK
7060faf5d3 Prepare next release 2024-04-19 19:25:27 +02:00
AsamK
c9f2cca024 Bump version to 0.13.3 2024-04-19 19:21:07 +02:00
AsamK
f0054372b8 Add timestamp to account file
Closes #1498
2024-04-19 17:25:43 +02:00
AsamK
0a82c51b79 Update dependencies 2024-04-19 17:17:32 +02:00
AsamK
cef83d962c Fix missing null check 2024-04-19 17:07:29 +02:00
AsamK
c1775913b9 Implement dbus support for listIdentities
Fixes #195
2024-04-19 17:07:18 +02:00
AsamK
e4c5144fbf Add more details to listContacts command
Fixes #1502
2024-04-17 21:26:16 +02:00
AsamK
8aeaf927e6 Add missing parts for new nick name and note columns 2024-04-17 21:21:28 +02:00
AsamK
7e0d4c9b89 Store profile phone number sharing mode and discoverable state 2024-04-17 21:08:09 +02:00
AsamK
71de8e63cc Cache newly created session record
Fixes #1481
2024-04-15 19:23:50 +02:00
AsamK
e456d06cb0 Add aci,pni to API RecipientAddress 2024-04-15 19:23:50 +02:00
AsamK
e0cd5b987e Add handling for new nickname and note fields 2024-04-15 19:23:50 +02:00
Stephen Brennan
e5ebb732cb
Document the unit of "start" and "length" for mentions and text styles (#1505)
The unit of UTF-16 code units is not necessarily obvious for users of
languages that index strings by Unicode code points. Provide a pointer
to an FAQ entry as well:

https://github.com/AsamK/signal-cli/wiki/FAQ#string-indexing-units

Closes #1504

Signed-off-by: Stephen Brennan <stephen@brennan.io>
2024-04-13 20:26:15 +02:00
AsamK
419beee29a Update libsignal-service-java 2024-04-06 14:04:56 +02:00
AsamK
d4e1f9b7f1 Remove unnecessary config field 2024-04-06 14:04:56 +02:00
AsamK
edce33ae15 Disable java 22 until gradle supports it 2024-04-06 14:04:56 +02:00
dependabot[bot]
95f9e18de2
Bump h2 from 0.3.24 to 0.3.26 in /client (#1501)
Bumps [h2](https://github.com/hyperium/h2) from 0.3.24 to 0.3.26.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.26/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.24...v0.3.26)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-06 14:04:48 +02:00
AsamK
95d4b7d012 Update gradle wrapper 2024-04-06 12:34:55 +02:00
AsamK
17c24b3ff2 Update libsignal-service-java 2024-03-27 22:58:13 +01:00
AsamK
be0e8ddd8a Add reregister to tests 2024-03-27 22:58:13 +01:00
AsamK
49cc9cd9f8 Update CHANGELOG.md 2024-03-23 15:00:02 +01:00
AsamK
c85c995fef Prepare next release 2024-03-23 10:34:45 +01:00
AsamK
dda23e76ac Bump version to 0.13.2 2024-03-23 09:57:15 +01:00
AsamK
95e70b9d15 Add Java 22 to CI 2024-03-23 09:50:59 +01:00
AsamK
abddf24752 Update user agent 2024-03-23 09:50:59 +01:00
AsamK
d356d92b5e Extend getUserStatus command for usernames 2024-03-22 10:54:42 +01:00
AsamK
8b4f377cf1 Update dependencies 2024-03-22 10:04:57 +01:00
dependabot[bot]
323a801600
Bump mio from 0.8.10 to 0.8.11 in /client (#1488)
Bumps [mio](https://github.com/tokio-rs/mio) from 0.8.10 to 0.8.11.
- [Release notes](https://github.com/tokio-rs/mio/releases)
- [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/mio/compare/v0.8.10...v0.8.11)

---
updated-dependencies:
- dependency-name: mio
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-09 13:08:46 +01:00
AsamK
3372992dc2 Add appstream metainfo file 2024-03-09 11:35:36 +01:00
AsamK
ee39733978 Update libsignal-service 2024-03-07 17:44:19 +01:00
AsamK
5e17fe8414 Fix typo 2024-02-27 19:35:38 +01:00
AsamK
aebe64571d Prepare next release 2024-02-27 18:20:21 +01:00
AsamK
0cc2da690a Bump version to 0.13.1 2024-02-27 18:18:30 +01:00
AsamK
2424fc1f53 Add register parameter to force reregistration 2024-02-27 18:12:43 +01:00
AsamK
2e4cd0eddc Only retry messages after identity was trusted
Fixes #1477
2024-02-27 17:52:21 +01:00
AsamK
189b21dbde Improve error message if captcha is rejected by server
Fixes #1328
2024-02-26 22:13:57 +01:00
AsamK
e77d9e3d60 Update libsignal-service 2024-02-26 22:07:36 +01:00
AsamK
df76aa9919 Default number sharing to NOBODY
Matches the official apps behavior.

Closes #1472
2024-02-26 18:27:09 +01:00
AsamK
378ac23c6c Update account attributes after setting a pin
Ensures that the recovery password gets set immediately.

Related to #1447
2024-02-26 18:23:37 +01:00
AsamK
fc2ae856d2 Adapt account record processor for linked devices 2024-02-25 19:41:34 +01:00
AsamK
57164ad7fb Prevent crash when receiving already migrated group v1 from storage
Fixes #1471
2024-02-25 19:41:10 +01:00
AsamK
6c44662496 Allow overriding user agent string
Not recommended, as it could lead to issues with newer Signal protocol changes.

Fixes #1476
2024-02-25 18:27:20 +01:00
AsamK
22ac3cb50f Removing linked devices only works on the primary device 2024-02-25 18:12:36 +01:00
AsamK
b76964f219 Improve warning message 2024-02-25 17:54:18 +01:00
AsamK
2f3c064462 Update documentation 2024-02-25 17:46:27 +01:00
AsamK
c9002d9481 Ignore failure when uploading PNI prekeys
Can happen if PNI identity key hasn't been sent to the server yet.
2024-02-25 17:46:09 +01:00
AsamK
83d471818d Allow setting a username with explicit descriminator
Fixes #1469
2024-02-22 20:00:00 +01:00
AsamK
0bb2a64781 Add missing field handling in account record processor 2024-02-22 20:00:00 +01:00
AsamK
08ba774b71 Update graalvm buildtools 2024-02-20 17:37:59 +01:00
AsamK
59c1f4eed2 Show information when requesting voice verification without SMS verification
Fixes #1373
2024-02-20 17:37:31 +01:00
AsamK
f1e3b5c9cc Add uniqueness check to db migration 2024-02-20 17:05:56 +01:00
AsamK
7bc7242f08 Add uniqueness check to db migration 2024-02-20 11:03:32 +01:00
Viktor Szépe
6f407ab509
Fix typos (#1459) 2024-02-19 08:20:24 +01:00
AsamK
db9acaf4ff Set snapshot version 2024-02-18 22:14:09 +01:00
233 changed files with 6530 additions and 3019 deletions

View file

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
java: [ '21' ] java: [ '21', '24' ]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -26,15 +26,26 @@ jobs:
distribution: 'zulu' distribution: 'zulu'
java-version: ${{ matrix.java }} java-version: ${{ matrix.java }}
- name: Setup Gradle - name: Setup Gradle
uses: gradle/gradle-build-action@v2 uses: gradle/actions/setup-gradle@v4
with: with:
dependency-graph: generate-and-submit dependency-graph: generate-and-submit
- name: Install asciidoc
run: sudo apt update && sudo apt --no-install-recommends install -y asciidoc-base
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew --no-daemon build run: ./gradlew --no-daemon build
- name: Build man page
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 - name: Compress archive
run: gzip -n -9 build/distributions/signal-cli-*.tar run: gzip -n -9 build/distributions/signal-cli-*.tar
- name: Archive production artifacts - name: Archive production artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: signal-cli-archive-${{ matrix.java }} name: signal-cli-archive-${{ matrix.java }}
path: build/distributions/signal-cli-*.tar.gz path: build/distributions/signal-cli-*.tar.gz
@ -54,7 +65,32 @@ jobs:
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew --no-daemon nativeCompile run: ./gradlew --no-daemon nativeCompile
- name: Archive production artifacts - name: Archive production artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: signal-cli-native name: signal-cli-native
path: build/native/nativeCompile/signal-cli path: build/native/nativeCompile/signal-cli
build-client:
strategy:
matrix:
os:
- ubuntu
- macos
- windows
runs-on: ${{ matrix.os }}-latest
defaults:
run:
working-directory: ./client
steps:
- uses: actions/checkout@v4
- name: Install rust
run: rustup default stable
- name: Build client
run: cargo build --release --verbose
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: signal-cli-client-${{ matrix.os }}
path: |
client/target/release/signal-cli-client
client/target/release/signal-cli-client.exe

View file

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

View file

@ -35,7 +35,7 @@ jobs:
steps: steps:
- name: Download signal-cli build from CI workflow - name: Download signal-cli build from CI workflow
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
- name: Get signal-cli version - name: Get signal-cli version
id: cli_ver id: cli_ver
@ -168,7 +168,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Download signal-cli build from CI workflow - name: Download signal-cli build from CI workflow
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
- name: Get signal-cli version - name: Get signal-cli version
id: cli_ver id: cli_ver
@ -182,7 +182,7 @@ jobs:
tar xf ./"${ARCHIVE_DIR}"/*.tar.gz tar xf ./"${ARCHIVE_DIR}"/*.tar.gz
rm -r signal-cli-archive-* signal-cli-native rm -r signal-cli-archive-* signal-cli-native
mkdir -p build/install/ mkdir -p build/install/
mv ./signal-cli-*/ build/install/signal-cli mv ./signal-cli-"${GITHUB_REF_NAME#v}"/ build/install/signal-cli
- name: Build Image - name: Build Image
id: build_image id: build_image
@ -218,7 +218,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Download signal-cli build from CI workflow - name: Download signal-cli build from CI workflow
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
- name: Get signal-cli version - name: Get signal-cli version
id: cli_ver id: cli_ver

View file

@ -5,8 +5,8 @@
<option name="GENERATE_FINAL_LOCALS" value="true" /> <option name="GENERATE_FINAL_LOCALS" value="true" />
<option name="GENERATE_FINAL_PARAMETERS" value="true" /> <option name="GENERATE_FINAL_PARAMETERS" value="true" />
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" /> <option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="50" /> <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="50" /> <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="IMPORT_LAYOUT_TABLE"> <option name="IMPORT_LAYOUT_TABLE">
<value> <value>
<package name="com" withSubpackages="true" static="false" /> <package name="com" withSubpackages="true" static="false" />

View file

@ -2,6 +2,232 @@
## [Unreleased] ## [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
- Fix verify command
## [0.13.8] - 2024-10-26
Requires libsignal-client version 0.58.2
### Fixed
- Fix sending large text messages
- Fix setting message expiration timer with recent Signal apps
### Improved
- Add group name and timestamps on json message (Thanks @jailson-dias)
## [0.13.7] - 2024-09-28
Requires libsignal-client version 0.58.0
### Fixed
- Fix unnecessary log output
- Fix issue with CDSI sync with invalid token
## [0.13.6] - 2024-09-08
Requires libsignal-client version 0.56.0
### Improved
- Send sync message to linked devices when sending read/viewed receipts
### Fixed
- Fix issue with sending to some groups
- Fix CDSI sync if no token is stored
- Fix possible db dead lock during storage sync
## [0.13.5] - 2024-07-25
Requires libsignal-client version 0.52.2
### Fixed
- Fixed device linking, due to new feature flag
## [0.13.4] - 2024-06-06
**Attention**: Now requires libsignal-client version 0.47.0
### Improved
- Improve username update error message
- Update groups when using listGroups command
### Fixed
- Update libsignal to fix graalvm native startup
- Fix issue with saving username link
- Fix sendMessageRequestResponse type parameter parsing in JSON RPC mode
- Fix getUserStatus command with only username parameter
## [0.13.3] - 2024-04-19
**Attention**: Now requires libsignal-client version 0.44.0
### Added
- Support for reading contact nickname and notes
- Add `--internal` and `--detailed` parameters to `listContacts` command
### Fixed
- Fix issue with sending messages when a new session is created
## [0.13.2] - 2024-03-23
**Attention**: Now requires libsignal-client version 0.40.1
### Added
- Add `--username` parameter to `getUserStatus` command
### Fixed
- Fixed setting and retrieving PIN after server changes
## [0.13.1] - 2024-02-27
### Added
- Add `--reregister` parameter to force registration of an already registered account
### Fixed
- Fixed rare issue with duplicate PNIs during migration
### Improved
- Show information when requesting voice verification without prior SMS verification
- Username can now be set with an explicit discriminator (e.g. testname.000)
- Improve behavior when PNI prekeys upload fails
- Improve `submitRateLimitChallenge` error message if captcha is rejected by server
- Only retry messages after an identity was trusted
### Changed
- Default number sharing to NOBODY, to match the official apps behavior.
## [0.13.0] - 2024-02-18 ## [0.13.0] - 2024-02-18
**Attention**: Now requires Java 21 and libsignal-client version 0.39.2 **Attention**: Now requires Java 21 and libsignal-client version 0.39.2
@ -471,7 +697,7 @@
- Improve exit code for message sending. - Improve exit code for message sending.
Exit with 0 status code if the message was sent successfully to at least Exit with 0 status code if the message was sent successfully to at least
one recipient, otherwise exit with status code 2 or 4 (for untrusted). one recipient, otherwise exit with status code 2 or 4 (for untrusted).
- Download profiles in parallel for improved perfomance - Download profiles in parallel for improved performance
- `--verbose` flag can be specified multiple times for additional log output - `--verbose` flag can be specified multiple times for additional log output
- Enable more security options for systemd service file - Enable more security options for systemd service file
- Rename sandbox to staging environment, to match the upstream name. - Rename sandbox to staging environment, to match the upstream name.
@ -612,7 +838,8 @@
### Added ### Added
- New parameters for `updateGroup` command for group v2 features: - New parameters for `updateGroup` command for group v2 features:
`--description`, `--remove-member`, `--admin`, `--remove-admin`, `--reset-link`, `--link`, `--set-permission-add-member`, `--set-permission-edit-details`, `--expiration` `--description`, `--remove-member`, `--admin`, `--remove-admin`, `--reset-link`, `--link`,
`--set-permission-add-member`, `--set-permission-edit-details`, `--expiration`
- New `--admin` parameter for `quitGroup` to set an admin before leaving the group - New `--admin` parameter for `quitGroup` to set an admin before leaving the group
- New `--delete` parameter for `quitGroup`, to delete the local group data - New `--delete` parameter for `quitGroup`, to delete the local group data
- New 'sendTyping' command to send typing indicators - New 'sendTyping' command to send typing indicators

View file

@ -3,7 +3,7 @@
signal-cli is a commandline interface for the [Signal messenger](https://signal.org/). signal-cli is a commandline interface for the [Signal messenger](https://signal.org/).
It supports registering, verifying, sending and receiving messages. It supports registering, verifying, sending and receiving messages.
signal-cli uses a [patched libsignal-service-java](https://github.com/Turasa/libsignal-service-java), signal-cli uses a [patched libsignal-service-java](https://github.com/Turasa/libsignal-service-java),
extracted from the [Signal-Android source code](https://github.com/signalapp/Signal-Android/tree/main/libsignal/service). extracted from the [Signal-Android source code](https://github.com/signalapp/Signal-Android/tree/main/libsignal-service).
For registering you need a phone number where you can receive SMS or incoming calls. For registering you need a phone number where you can receive SMS or incoming calls.
signal-cli is primarily intended to be used on servers to notify admins of important events. signal-cli is primarily intended to be used on servers to notify admins of important events.
@ -11,6 +11,10 @@ For this use-case, it has a daemon mode with JSON-RPC interface ([man page](http
and D-BUS interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)) . and D-BUS interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)) .
For the JSON-RPC interface there's also a simple [example client](https://github.com/AsamK/signal-cli/tree/master/client), written in Rust. For the JSON-RPC interface there's also a simple [example client](https://github.com/AsamK/signal-cli/tree/master/client), written in Rust.
signal-cli needs to be kept up-to-date to keep up with Signal-Server changes.
The official Signal clients expire after three months and then the Signal-Server can make incompatible changes.
So signal-cli releases older than three months may not work correctly.
## Installation ## Installation
You can [build signal-cli](#building) yourself or use You can [build signal-cli](#building) yourself or use
@ -55,8 +59,15 @@ of all country codes.)
signal-cli -a ACCOUNT register signal-cli -a ACCOUNT register
You can register Signal using a landline number. In this case you can skip SMS verification process and jump directly You can register Signal using a landline number. In this case, you need to follow the procedure below:
to the voice call verification by adding the `--voice` switch at the end of above register command. * Attempt a SMS verification process first (`signal-cli -a ACCOUNT register`)
* You will get an error `400 (InvalidTransportModeException)`, this is normal
* Wait 60 seconds
* Attempt a voice call verification by adding the `--voice` switch and wait for the call:
```sh
signal-cli -a ACCOUNT register --voice
```
Registering may require solving a CAPTCHA Registering may require solving a CAPTCHA
challenge: [Registration with captcha](https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha) challenge: [Registration with captcha](https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha)
@ -72,6 +83,12 @@ of all country codes.)
signal-cli -a ACCOUNT send -m "This is a message" RECIPIENT signal-cli -a ACCOUNT send -m "This is a message" RECIPIENT
``` ```
* Send a message to a username, usernames need to be prefixed with `u:`
```sh
signal-cli -a ACCOUNT send -m "This is a message" u:USERNAME.000
```
* Pipe the message content from another process. * Pipe the message content from another process.
uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT
@ -127,7 +144,7 @@ version installed, you can replace `./gradlew` with `gradle` in the following st
It is possible to build a native binary with [GraalVM](https://www.graalvm.org). This is still experimental and will not It is possible to build a native binary with [GraalVM](https://www.graalvm.org). This is still experimental and will not
work in all situations. work in all situations.
1. [Install GraalVM and setup the enviroment](https://www.graalvm.org/docs/getting-started/#install-graalvm) 1. [Install GraalVM and setup the environment](https://www.graalvm.org/docs/getting-started/#install-graalvm)
2. Execute Gradle: 2. Execute Gradle:
./gradlew nativeCompile ./gradlew nativeCompile

View file

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

View file

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

View file

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

1327
client/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ use std::{path::PathBuf, time::Duration};
use clap::Parser; use clap::Parser;
use jsonrpsee::core::client::{Error as RpcError, Subscription, SubscriptionClientT}; use jsonrpsee::core::client::{Error as RpcError, Subscription, SubscriptionClientT};
use serde_json::Value; use serde_json::{Error, Value};
use tokio::{select, time::sleep}; use tokio::{select, time::sleep};
use cli::Cli; use cli::Cli;
@ -60,8 +60,13 @@ async fn handle_command(
.delete_local_account_data(cli.account, ignore_registered) .delete_local_account_data(cli.account, ignore_registered)
.await .await
} }
CliCommands::GetUserStatus { recipient } => { CliCommands::GetUserStatus {
client.get_user_status(cli.account, recipient).await recipient,
username,
} => {
client
.get_user_status(cli.account, recipient, username)
.await
} }
CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await, CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await,
CliCommands::Link { name } => { CliCommands::Link { name } => {
@ -70,7 +75,7 @@ async fn handle_command(
.await .await
.map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))? .map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))?
.device_link_uri; .device_link_uri;
println!("{}", url); println!("{url}");
client.finish_link(url, name).await client.finish_link(url, name).await
} }
CliCommands::ListAccounts => client.list_accounts().await, CliCommands::ListAccounts => client.list_accounts().await,
@ -139,6 +144,7 @@ async fn handle_command(
end_session, end_session,
message, message,
attachment, attachment,
view_once,
mention, mention,
text_style, text_style,
quote_timestamp, quote_timestamp,
@ -165,6 +171,7 @@ async fn handle_command(
end_session, end_session,
message.unwrap_or_default(), message.unwrap_or_default(),
attachment, attachment,
view_once,
mention, mention,
text_style, text_style,
quote_timestamp, quote_timestamp,
@ -477,30 +484,37 @@ async fn connect(cli: Cli) -> Result<Value, RpcError> {
handle_command(cli, client).await handle_command(cli, client).await
} else { } else {
let socket_path = cli #[cfg(windows)]
.json_rpc_socket {
.clone() Err(RpcError::Custom("Invalid socket".into()))
.unwrap_or(None) }
.or_else(|| { #[cfg(unix)]
std::env::var_os("XDG_RUNTIME_DIR").map(|runtime_dir| { {
PathBuf::from(runtime_dir) let socket_path = cli
.join(DEFAULT_SOCKET_SUFFIX) .json_rpc_socket
.into() .clone()
.unwrap_or(None)
.or_else(|| {
std::env::var_os("XDG_RUNTIME_DIR").map(|runtime_dir| {
PathBuf::from(runtime_dir)
.join(DEFAULT_SOCKET_SUFFIX)
.into()
})
}) })
}) .unwrap_or_else(|| ("/run".to_owned() + DEFAULT_SOCKET_SUFFIX).into());
.unwrap_or_else(|| ("/run".to_owned() + DEFAULT_SOCKET_SUFFIX).into()); let client = jsonrpc::connect_unix(socket_path)
let client = jsonrpc::connect_unix(socket_path) .await
.await .map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
handle_command(cli, client).await handle_command(cli, client).await
}
} }
} }
async fn stream_next( async fn stream_next(
timeout: f64, timeout: f64,
stream: &mut Subscription<Value>, stream: &mut Subscription<Value>,
) -> Option<Result<Value, RpcError>> { ) -> Option<Result<Value, Error>> {
if timeout < 0.0 { if timeout < 0.0 {
stream.next().await stream.next().await
} else { } else {

View file

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

View file

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

View file

@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="console-application">
<id>org.asamk.SignalCli</id>
<name>signal-cli</name>
<summary>Use Signal messenger in terminal</summary>
<developer id="org.asamk">
<name>AsamK</name>
</developer>
<icon type="stock">org.asamk.SignalCli</icon>
<keywords>
<keyword>signal</keyword>
<keyword>signal-cli</keyword>
<keyword>messenger</keyword>
<keyword>messaging</keyword>
</keywords>
<url type="bugtracker">https://github.com/AsamK/signal-cli/issues</url>
<url type="homepage">https://github.com/AsamK/signal-cli</url>
<url type="donation">https://github.com/sponsors/AsamK</url>
<url type="faq">https://github.com/AsamK/signal-cli/discussions</url>
<url type="vcs-browser">https://github.com/AsamK/signal-cli</url>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-only</project_license>
<description>
<p>
signal-cli is an unofficial commandline interface for the Signal Messenger.
It supports many Signal functions, including registering, verifying, sending and receiving messages.
For registering you need a phone number where you can receive SMS or incoming calls.
Alternatively signal-cli can be linked to an existing App account.
</p>
</description>
<categories>
<category>Utility</category>
<category>Java</category>
</categories>
<provides>
<binary>signal-cli</binary>
</provides>
<content_rating type="oars-1.1">
<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>
<release version="0.13.8" date="2024-10-26">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.8</url>
</release>
<release version="0.13.7" date="2024-09-28">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.7</url>
</release>
<release version="0.13.6" date="2024-09-08">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.6</url>
</release>
<release version="0.13.5" date="2024-07-25">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.5</url>
</release>
<release version="0.13.4" date="2024-06-06">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.4</url>
</release>
<release version="0.13.3" date="2024-04-19">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.3</url>
</release>
<release version="0.13.2" date="2024-03-23">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.2</url>
</release>
<release version="0.13.1" date="2024-02-27">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.1</url>
</release>
<release version="0.13.0" date="2024-02-18">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.0</url>
</release>
<release version="0.12.8" date="2024-02-08">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.12.8</url>
</release>
<release version="0.12.7" date="2023-12-15">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.12.7</url>
</release>
</releases>
</component>

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

View file

@ -1,7 +1,13 @@
[ [
{
"name":"[B"
},
{ {
"name":"[Z" "name":"[Z"
}, },
{
"name":"[[B"
},
{ {
"name":"com.sun.security.auth.module.UnixSystem", "name":"com.sun.security.auth.module.UnixSystem",
"fields":[{"name":"gid"}, {"name":"groups"}, {"name":"uid"}, {"name":"username"}] "fields":[{"name":"gid"}, {"name":"groups"}, {"name":"uid"}, {"name":"username"}]
@ -12,12 +18,19 @@
}, },
{ {
"name":"java.lang.Class", "name":"java.lang.Class",
"methods":[{"name":"getCanonicalName","parameterTypes":[] }] "methods":[{"name":"getCanonicalName","parameterTypes":[] }, {"name":"getClassLoader","parameterTypes":[] }]
}, },
{ {
"name":"java.lang.ClassLoader", "name":"java.lang.ClassLoader",
"methods":[{"name":"getPlatformClassLoader","parameterTypes":[] }, {"name":"loadClass","parameterTypes":["java.lang.String"] }] "methods":[{"name":"getPlatformClassLoader","parameterTypes":[] }, {"name":"loadClass","parameterTypes":["java.lang.String"] }]
}, },
{
"name":"java.lang.ClassNotFoundException"
},
{
"name":"java.lang.Enum",
"methods":[{"name":"ordinal","parameterTypes":[] }]
},
{ {
"name":"java.lang.IllegalArgumentException", "name":"java.lang.IllegalArgumentException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }] "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
@ -26,24 +39,46 @@
"name":"java.lang.IllegalStateException", "name":"java.lang.IllegalStateException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }] "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
}, },
{
"name":"java.lang.Long",
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{
"name":"java.lang.NoClassDefFoundError"
},
{ {
"name":"java.lang.NoSuchMethodError" "name":"java.lang.NoSuchMethodError"
}, },
{ {
"name":"java.lang.String" "name":"java.lang.String"
}, },
{
"name":"java.lang.Thread",
"methods":[{"name":"currentThread","parameterTypes":[] }, {"name":"getStackTrace","parameterTypes":[] }]
},
{ {
"name":"java.lang.Throwable", "name":"java.lang.Throwable",
"methods":[{"name":"getMessage","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }] "methods":[{"name":"getMessage","parameterTypes":[] }, {"name":"setStackTrace","parameterTypes":["java.lang.StackTraceElement[]"] }, {"name":"toString","parameterTypes":[] }]
}, },
{ {
"name":"java.lang.UnsatisfiedLinkError", "name":"java.lang.UnsatisfiedLinkError",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }] "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
}, },
{
"name":"java.util.HashMap",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"java.util.Map",
"methods":[{"name":"get","parameterTypes":["java.lang.Object"] }, {"name":"put","parameterTypes":["java.lang.Object","java.lang.Object"] }, {"name":"remove","parameterTypes":["java.lang.Object"] }]
},
{ {
"name":"java.util.UUID", "name":"java.util.UUID",
"methods":[{"name":"<init>","parameterTypes":["long","long"] }, {"name":"getLeastSignificantBits","parameterTypes":[] }, {"name":"getMostSignificantBits","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":["long","long"] }, {"name":"getLeastSignificantBits","parameterTypes":[] }, {"name":"getMostSignificantBits","parameterTypes":[] }]
}, },
{
"name":"jdk.internal.loader.ClassLoaders$AppClassLoader"
},
{ {
"name":"jdk.internal.loader.ClassLoaders$PlatformClassLoader" "name":"jdk.internal.loader.ClassLoaders$PlatformClassLoader"
}, },
@ -59,6 +94,42 @@
"name":"org.graalvm.jniutils.JNIExceptionWrapperEntryPoints", "name":"org.graalvm.jniutils.JNIExceptionWrapperEntryPoints",
"methods":[{"name":"getClassName","parameterTypes":["java.lang.Class"] }] "methods":[{"name":"getClassName","parameterTypes":["java.lang.Class"] }]
}, },
{
"name":"org.signal.libsignal.internal.CompletableFuture",
"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",
"methods":[{"name":"<init>","parameterTypes":["java.util.Map","int"] }]
},
{
"name":"org.signal.libsignal.net.CdsiLookupResponse$Entry",
"methods":[{"name":"<init>","parameterTypes":["byte[]","byte[]"] }]
},
{
"name":"org.signal.libsignal.net.ChatService"
},
{
"name":"org.signal.libsignal.net.ChatService$DebugInfo"
},
{
"name":"org.signal.libsignal.net.ChatService$Response"
},
{
"name":"org.signal.libsignal.net.ChatService$ResponseAndDebugInfo"
},
{
"name":"org.signal.libsignal.net.NetworkException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.net.RetryLaterException",
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{ {
"name":"org.signal.libsignal.protocol.DuplicateMessageException", "name":"org.signal.libsignal.protocol.DuplicateMessageException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }] "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
@ -136,6 +207,9 @@
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore$Direction", "name":"org.signal.libsignal.protocol.state.IdentityKeyStore$Direction",
"fields":[{"name":"RECEIVING"}, {"name":"SENDING"}] "fields":[{"name":"RECEIVING"}, {"name":"SENDING"}]
}, },
{
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore$IdentityChange"
},
{ {
"name":"org.signal.libsignal.protocol.state.KyberPreKeyRecord", "name":"org.signal.libsignal.protocol.state.KyberPreKeyRecord",
"fields":[{"name":"unsafeHandle"}] "fields":[{"name":"unsafeHandle"}]
@ -165,6 +239,10 @@
{ {
"name":"org.signal.libsignal.protocol.state.SignedPreKeyStore" "name":"org.signal.libsignal.protocol.state.SignedPreKeyStore"
}, },
{
"name":"org.signal.libsignal.usernames.BadDiscriminatorCharacterException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{ {
"name":"org.signal.libsignal.usernames.BadNicknameCharacterException", "name":"org.signal.libsignal.usernames.BadNicknameCharacterException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }] "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
@ -173,6 +251,10 @@
"name":"org.signal.libsignal.usernames.CannotBeEmptyException", "name":"org.signal.libsignal.usernames.CannotBeEmptyException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }] "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", "name":"org.signal.libsignal.usernames.MissingSeparatorException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }] "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]

View file

@ -14,6 +14,9 @@
{ {
"interfaces":["org.asamk.Signal$Group"] "interfaces":["org.asamk.Signal$Group"]
}, },
{
"interfaces":["org.asamk.Signal$Identity"]
},
{ {
"interfaces":["org.asamk.SignalControl"] "interfaces":["org.asamk.SignalControl"]
}, },

View file

@ -39,6 +39,48 @@
{ {
"name":"[Ljava.sql.Statement;" "name":"[Ljava.sql.Statement;"
}, },
{
"name":"[Lorg.asamk.signal.commands.ListStickerPacksCommand$JsonStickerPack$JsonSticker;"
},
{
"name":"[Lorg.asamk.signal.json.JsonAttachment;"
},
{
"name":"[Lorg.asamk.signal.json.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;"
},
{
"name":"[Lorg.asamk.signal.json.JsonPreview;"
},
{
"name":"[Lorg.asamk.signal.json.JsonQuotedAttachment;"
},
{
"name":"[Lorg.asamk.signal.json.JsonSharedContact;"
},
{
"name":"[Lorg.asamk.signal.json.JsonSyncReadMessage;"
},
{
"name":"[Lorg.asamk.signal.json.JsonTextStyle;"
},
{
"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;" "name":"[Lorg.whispersystems.signalservice.api.groupsv2.TemporalCredential;"
}, },
@ -103,6 +145,13 @@
"name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl", "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl",
"methods":[{"name":"<init>","parameterTypes":[] }] "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", "name":"com.squareup.wire.internal.ImmutableList",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -161,7 +210,7 @@
"name":"com.zaxxer.hikari.HikariConfig", "name":"com.zaxxer.hikari.HikariConfig",
"allDeclaredFields":true, "allDeclaredFields":true,
"queryAllPublicMethods":true, "queryAllPublicMethods":true,
"methods":[{"name":"getAllowPoolSuspension","parameterTypes":[] }, {"name":"getAutoCommit","parameterTypes":[] }, {"name":"getCatalog","parameterTypes":[] }, {"name":"getConnectionInitSql","parameterTypes":[] }, {"name":"getConnectionTestQuery","parameterTypes":[] }, {"name":"getConnectionTimeout","parameterTypes":[] }, {"name":"getDataSource","parameterTypes":[] }, {"name":"getDataSourceClassName","parameterTypes":[] }, {"name":"getDataSourceJNDI","parameterTypes":[] }, {"name":"getDataSourceProperties","parameterTypes":[] }, {"name":"getDriverClassName","parameterTypes":[] }, {"name":"getExceptionOverrideClassName","parameterTypes":[] }, {"name":"getHealthCheckProperties","parameterTypes":[] }, {"name":"getHealthCheckRegistry","parameterTypes":[] }, {"name":"getIdleTimeout","parameterTypes":[] }, {"name":"getInitializationFailTimeout","parameterTypes":[] }, {"name":"getIsolateInternalQueries","parameterTypes":[] }, {"name":"getJdbcUrl","parameterTypes":[] }, {"name":"getKeepaliveTime","parameterTypes":[] }, {"name":"getLeakDetectionThreshold","parameterTypes":[] }, {"name":"getMaxLifetime","parameterTypes":[] }, {"name":"getMaximumPoolSize","parameterTypes":[] }, {"name":"getMetricRegistry","parameterTypes":[] }, {"name":"getMetricsTrackerFactory","parameterTypes":[] }, {"name":"getMinimumIdle","parameterTypes":[] }, {"name":"getPassword","parameterTypes":[] }, {"name":"getPoolName","parameterTypes":[] }, {"name":"getReadOnly","parameterTypes":[] }, {"name":"getRegisterMbeans","parameterTypes":[] }, {"name":"getScheduledExecutor","parameterTypes":[] }, {"name":"getSchema","parameterTypes":[] }, {"name":"getThreadFactory","parameterTypes":[] }, {"name":"getTransactionIsolation","parameterTypes":[] }, {"name":"getUsername","parameterTypes":[] }, {"name":"getValidationTimeout","parameterTypes":[] }, {"name":"isAllowPoolSuspension","parameterTypes":[] }, {"name":"isAutoCommit","parameterTypes":[] }, {"name":"isIsolateInternalQueries","parameterTypes":[] }, {"name":"isReadOnly","parameterTypes":[] }, {"name":"isRegisterMbeans","parameterTypes":[] }, {"name":"setAllowPoolSuspension","parameterTypes":["boolean"] }, {"name":"setAutoCommit","parameterTypes":["boolean"] }, {"name":"setCatalog","parameterTypes":["java.lang.String"] }, {"name":"setClass","parameterTypes":["java.lang.Class"] }, {"name":"setConnectionInitSql","parameterTypes":["java.lang.String"] }, {"name":"setConnectionTestQuery","parameterTypes":["java.lang.String"] }, {"name":"setConnectionTimeout","parameterTypes":["long"] }, {"name":"setDataSource","parameterTypes":["javax.sql.DataSource"] }, {"name":"setDataSourceClassName","parameterTypes":["java.lang.String"] }, {"name":"setDataSourceJNDI","parameterTypes":["java.lang.String"] }, {"name":"setDataSourceProperties","parameterTypes":["java.util.Properties"] }, {"name":"setDriverClassName","parameterTypes":["java.lang.String"] }, {"name":"setExceptionOverrideClassName","parameterTypes":["java.lang.String"] }, {"name":"setHealthCheckProperties","parameterTypes":["java.util.Properties"] }, {"name":"setHealthCheckRegistry","parameterTypes":["java.lang.Object"] }, {"name":"setIdleTimeout","parameterTypes":["long"] }, {"name":"setInitializationFailTimeout","parameterTypes":["long"] }, {"name":"setIsolateInternalQueries","parameterTypes":["boolean"] }, {"name":"setJdbcUrl","parameterTypes":["java.lang.String"] }, {"name":"setKeepaliveTime","parameterTypes":["long"] }, {"name":"setLeakDetectionThreshold","parameterTypes":["long"] }, {"name":"setMaxLifetime","parameterTypes":["long"] }, {"name":"setMaximumPoolSize","parameterTypes":["int"] }, {"name":"setMetricRegistry","parameterTypes":["java.lang.Object"] }, {"name":"setMetricsTrackerFactory","parameterTypes":["com.zaxxer.hikari.metrics.MetricsTrackerFactory"] }, {"name":"setMinimumIdle","parameterTypes":["int"] }, {"name":"setPassword","parameterTypes":["java.lang.String"] }, {"name":"setPoolName","parameterTypes":["java.lang.String"] }, {"name":"setReadOnly","parameterTypes":["boolean"] }, {"name":"setRegisterMbeans","parameterTypes":["boolean"] }, {"name":"setScheduledExecutor","parameterTypes":["java.util.concurrent.ScheduledExecutorService"] }, {"name":"setSchema","parameterTypes":["java.lang.String"] }, {"name":"setThreadFactory","parameterTypes":["java.util.concurrent.ThreadFactory"] }, {"name":"setTransactionIsolation","parameterTypes":["java.lang.String"] }, {"name":"setUsername","parameterTypes":["java.lang.String"] }, {"name":"setValidationTimeout","parameterTypes":["long"] }] "methods":[{"name":"getAllowPoolSuspension","parameterTypes":[] }, {"name":"getAutoCommit","parameterTypes":[] }, {"name":"getCatalog","parameterTypes":[] }, {"name":"getConnectionInitSql","parameterTypes":[] }, {"name":"getConnectionTestQuery","parameterTypes":[] }, {"name":"getConnectionTimeout","parameterTypes":[] }, {"name":"getCredentials","parameterTypes":[] }, {"name":"getDataSource","parameterTypes":[] }, {"name":"getDataSourceClassName","parameterTypes":[] }, {"name":"getDataSourceJNDI","parameterTypes":[] }, {"name":"getDataSourceProperties","parameterTypes":[] }, {"name":"getDriverClassName","parameterTypes":[] }, {"name":"getExceptionOverride","parameterTypes":[] }, {"name":"getExceptionOverrideClassName","parameterTypes":[] }, {"name":"getHealthCheckProperties","parameterTypes":[] }, {"name":"getHealthCheckRegistry","parameterTypes":[] }, {"name":"getIdleTimeout","parameterTypes":[] }, {"name":"getInitializationFailTimeout","parameterTypes":[] }, {"name":"getIsolateInternalQueries","parameterTypes":[] }, {"name":"getJdbcUrl","parameterTypes":[] }, {"name":"getKeepaliveTime","parameterTypes":[] }, {"name":"getLeakDetectionThreshold","parameterTypes":[] }, {"name":"getMaxLifetime","parameterTypes":[] }, {"name":"getMaximumPoolSize","parameterTypes":[] }, {"name":"getMetricRegistry","parameterTypes":[] }, {"name":"getMetricsTrackerFactory","parameterTypes":[] }, {"name":"getMinimumIdle","parameterTypes":[] }, {"name":"getPassword","parameterTypes":[] }, {"name":"getPoolName","parameterTypes":[] }, {"name":"getReadOnly","parameterTypes":[] }, {"name":"getRegisterMbeans","parameterTypes":[] }, {"name":"getScheduledExecutor","parameterTypes":[] }, {"name":"getSchema","parameterTypes":[] }, {"name":"getThreadFactory","parameterTypes":[] }, {"name":"getTransactionIsolation","parameterTypes":[] }, {"name":"getUsername","parameterTypes":[] }, {"name":"getValidationTimeout","parameterTypes":[] }, {"name":"isAllowPoolSuspension","parameterTypes":[] }, {"name":"isAutoCommit","parameterTypes":[] }, {"name":"isIsolateInternalQueries","parameterTypes":[] }, {"name":"isReadOnly","parameterTypes":[] }, {"name":"isRegisterMbeans","parameterTypes":[] }, {"name":"setAllowPoolSuspension","parameterTypes":["boolean"] }, {"name":"setAutoCommit","parameterTypes":["boolean"] }, {"name":"setCatalog","parameterTypes":["java.lang.String"] }, {"name":"setClass","parameterTypes":["java.lang.Class"] }, {"name":"setConnectionInitSql","parameterTypes":["java.lang.String"] }, {"name":"setConnectionTestQuery","parameterTypes":["java.lang.String"] }, {"name":"setConnectionTimeout","parameterTypes":["long"] }, {"name":"setCredentials","parameterTypes":["com.zaxxer.hikari.util.Credentials"] }, {"name":"setDataSource","parameterTypes":["javax.sql.DataSource"] }, {"name":"setDataSourceClassName","parameterTypes":["java.lang.String"] }, {"name":"setDataSourceJNDI","parameterTypes":["java.lang.String"] }, {"name":"setDataSourceProperties","parameterTypes":["java.util.Properties"] }, {"name":"setDriverClassName","parameterTypes":["java.lang.String"] }, {"name":"setExceptionOverride","parameterTypes":["com.zaxxer.hikari.SQLExceptionOverride"] }, {"name":"setExceptionOverrideClassName","parameterTypes":["java.lang.String"] }, {"name":"setHealthCheckProperties","parameterTypes":["java.util.Properties"] }, {"name":"setHealthCheckRegistry","parameterTypes":["java.lang.Object"] }, {"name":"setIdleTimeout","parameterTypes":["long"] }, {"name":"setInitializationFailTimeout","parameterTypes":["long"] }, {"name":"setIsolateInternalQueries","parameterTypes":["boolean"] }, {"name":"setJdbcUrl","parameterTypes":["java.lang.String"] }, {"name":"setKeepaliveTime","parameterTypes":["long"] }, {"name":"setLeakDetectionThreshold","parameterTypes":["long"] }, {"name":"setMaxLifetime","parameterTypes":["long"] }, {"name":"setMaximumPoolSize","parameterTypes":["int"] }, {"name":"setMetricRegistry","parameterTypes":["java.lang.Object"] }, {"name":"setMetricsTrackerFactory","parameterTypes":["com.zaxxer.hikari.metrics.MetricsTrackerFactory"] }, {"name":"setMinimumIdle","parameterTypes":["int"] }, {"name":"setPassword","parameterTypes":["java.lang.String"] }, {"name":"setPoolName","parameterTypes":["java.lang.String"] }, {"name":"setReadOnly","parameterTypes":["boolean"] }, {"name":"setRegisterMbeans","parameterTypes":["boolean"] }, {"name":"setScheduledExecutor","parameterTypes":["java.util.concurrent.ScheduledExecutorService"] }, {"name":"setSchema","parameterTypes":["java.lang.String"] }, {"name":"setThreadFactory","parameterTypes":["java.util.concurrent.ThreadFactory"] }, {"name":"setTransactionIsolation","parameterTypes":["java.lang.String"] }, {"name":"setUsername","parameterTypes":["java.lang.String"] }, {"name":"setValidationTimeout","parameterTypes":["long"] }]
}, },
{ {
"name":"com.zaxxer.hikari.pool.PoolBase", "name":"com.zaxxer.hikari.pool.PoolBase",
@ -188,9 +237,14 @@
{ {
"name":"java.io.FilePermission" "name":"java.io.FilePermission"
}, },
{
"name":"java.io.OutputStream"
},
{ {
"name":"java.io.Serializable", "name":"java.io.Serializable",
"allDeclaredMethods":true "allDeclaredFields":true,
"allDeclaredMethods":true,
"allDeclaredClasses":true
}, },
{ {
"name":"java.lang.Boolean", "name":"java.lang.Boolean",
@ -203,6 +257,9 @@
"name":"java.lang.Class", "name":"java.lang.Class",
"methods":[{"name":"getPermittedSubclasses","parameterTypes":[] }, {"name":"getRecordComponents","parameterTypes":[] }, {"name":"isRecord","parameterTypes":[] }, {"name":"isSealed","parameterTypes":[] }] "methods":[{"name":"getPermittedSubclasses","parameterTypes":[] }, {"name":"getRecordComponents","parameterTypes":[] }, {"name":"isRecord","parameterTypes":[] }, {"name":"isSealed","parameterTypes":[] }]
}, },
{
"name":"java.lang.ClassValue"
},
{ {
"name":"java.lang.Comparable", "name":"java.lang.Comparable",
"allDeclaredMethods":true "allDeclaredMethods":true
@ -245,7 +302,8 @@
"allDeclaredMethods":true "allDeclaredMethods":true
}, },
{ {
"name":"java.lang.Object" "name":"java.lang.Object",
"methods":[{"name":"equals","parameterTypes":["java.lang.Object"] }, {"name":"hashCode","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }]
}, },
{ {
"name":"java.lang.Record", "name":"java.lang.Record",
@ -367,6 +425,11 @@
"allDeclaredFields":true, "allDeclaredFields":true,
"allDeclaredMethods":true "allDeclaredMethods":true
}, },
{
"name":"java.util.AbstractMap",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{ {
"name":"java.util.ArrayList", "name":"java.util.ArrayList",
"allDeclaredMethods":true, "allDeclaredMethods":true,
@ -381,6 +444,45 @@
"allDeclaredMethods":true, "allDeclaredMethods":true,
"allDeclaredConstructors":true "allDeclaredConstructors":true
}, },
{
"name":"java.util.ImmutableCollections$AbstractImmutableCollection",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"java.util.ImmutableCollections$AbstractImmutableList",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"java.util.ImmutableCollections$AbstractImmutableMap",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"java.util.ImmutableCollections$List12",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{
"name":"java.util.ImmutableCollections$ListN",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{
"name":"java.util.ImmutableCollections$Map1",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{
"name":"java.util.ImmutableCollections$MapN",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{ {
"name":"java.util.LinkedHashMap", "name":"java.util.LinkedHashMap",
"allDeclaredMethods":true, "allDeclaredMethods":true,
@ -395,7 +497,8 @@
"methods":[{"name":"getUnicodeLocaleType","parameterTypes":["java.lang.String"] }] "methods":[{"name":"getUnicodeLocaleType","parameterTypes":["java.lang.String"] }]
}, },
{ {
"name":"java.util.Map" "name":"java.util.Map",
"queryAllDeclaredMethods":true
}, },
{ {
"name":"java.util.Optional", "name":"java.util.Optional",
@ -436,6 +539,10 @@
"name":"java.util.concurrent.atomic.Striped64", "name":"java.util.concurrent.atomic.Striped64",
"fields":[{"name":"base"}, {"name":"cellsBusy"}] "fields":[{"name":"base"}, {"name":"cellsBusy"}]
}, },
{
"name":"java.util.concurrent.atomic.Striped64$Cell",
"fields":[{"name":"value"}]
},
{ {
"name":"javax.security.auth.x500.X500Principal", "name":"javax.security.auth.x500.X500Principal",
"methods":[{"name":"<init>","parameterTypes":["sun.security.x509.X500Name"] }] "methods":[{"name":"<init>","parameterTypes":["sun.security.x509.X500Name"] }]
@ -509,6 +616,9 @@
{ {
"name":"kotlin.String" "name":"kotlin.String"
}, },
{
"name":"kotlin.Unit"
},
{ {
"name":"kotlin.collections.AbstractCollection", "name":"kotlin.collections.AbstractCollection",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -561,6 +671,13 @@
{ {
"name":"long[]" "name":"long[]"
}, },
{
"name":"okhttp3.internal.connection.RealConnectionPool",
"fields":[{"name":"addressStates"}]
},
{
"name":"okio.BufferedSink"
},
{ {
"name":"okio.ByteString" "name":"okio.ByteString"
}, },
@ -568,7 +685,7 @@
"name":"org.asamk.Signal", "name":"org.asamk.Signal",
"allDeclaredMethods":true, "allDeclaredMethods":true,
"allDeclaredClasses":true, "allDeclaredClasses":true,
"methods":[{"name":"getContactName","parameterTypes":["java.lang.String"] }, {"name":"getDevice","parameterTypes":["long"] }, {"name":"getGroup","parameterTypes":["byte[]"] }, {"name":"getSelfNumber","parameterTypes":[] }, {"name":"getThisDevice","parameterTypes":[] }, {"name":"listDevices","parameterTypes":[] }, {"name":"sendGroupMessageReaction","parameterTypes":["java.lang.String","boolean","java.lang.String","long","byte[]"] }, {"name":"sendMessage","parameterTypes":["java.lang.String","java.util.List","java.lang.String"] }, {"name":"sendMessage","parameterTypes":["java.lang.String","java.util.List","java.util.List"] }, {"name":"sendMessageReaction","parameterTypes":["java.lang.String","boolean","java.lang.String","long","java.util.List"] }, {"name":"subscribeReceive","parameterTypes":[] }, {"name":"unsubscribeReceive","parameterTypes":[] }, {"name":"version","parameterTypes":[] }] "methods":[{"name":"getContactName","parameterTypes":["java.lang.String"] }, {"name":"getDevice","parameterTypes":["long"] }, {"name":"getGroup","parameterTypes":["byte[]"] }, {"name":"getSelfNumber","parameterTypes":[] }, {"name":"getThisDevice","parameterTypes":[] }, {"name":"listDevices","parameterTypes":[] }, {"name":"listIdentities","parameterTypes":[] }, {"name":"sendGroupMessageReaction","parameterTypes":["java.lang.String","boolean","java.lang.String","long","byte[]"] }, {"name":"sendMessage","parameterTypes":["java.lang.String","java.util.List","java.lang.String"] }, {"name":"sendMessage","parameterTypes":["java.lang.String","java.util.List","java.util.List"] }, {"name":"sendMessageReaction","parameterTypes":["java.lang.String","boolean","java.lang.String","long","java.util.List"] }, {"name":"subscribeReceive","parameterTypes":[] }, {"name":"unsubscribeReceive","parameterTypes":[] }, {"name":"version","parameterTypes":[] }]
}, },
{ {
"name":"org.asamk.Signal$Configuration", "name":"org.asamk.Signal$Configuration",
@ -647,7 +764,9 @@
}, },
{ {
"name":"org.asamk.Signal$StructIdentity", "name":"org.asamk.Signal$StructIdentity",
"allDeclaredFields":true "allDeclaredFields":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["org.freedesktop.dbus.DBusPath","java.lang.String","java.lang.String"] }]
}, },
{ {
"name":"org.asamk.Signal$SyncMessageReceived", "name":"org.asamk.Signal$SyncMessageReceived",
@ -689,7 +808,7 @@
"allDeclaredFields":true, "allDeclaredFields":true,
"allDeclaredMethods":true, "allDeclaredMethods":true,
"allDeclaredConstructors":true, "allDeclaredConstructors":true,
"methods":[{"name":"isRegistered","parameterTypes":[] }, {"name":"number","parameterTypes":[] }, {"name":"recipient","parameterTypes":[] }, {"name":"uuid","parameterTypes":[] }] "methods":[{"name":"isRegistered","parameterTypes":[] }, {"name":"number","parameterTypes":[] }, {"name":"recipient","parameterTypes":[] }, {"name":"username","parameterTypes":[] }, {"name":"uuid","parameterTypes":[] }]
}, },
{ {
"name":"org.asamk.signal.commands.ListAccountsCommand$JsonAccount", "name":"org.asamk.signal.commands.ListAccountsCommand$JsonAccount",
@ -789,6 +908,34 @@
"queryAllDeclaredConstructors":true, "queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"pin","parameterTypes":[] }, {"name":"verificationCode","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"pin","parameterTypes":[] }, {"name":"verificationCode","parameterTypes":[] }]
}, },
{
"name":"org.asamk.signal.dbus.DbusProperties",
"queryAllDeclaredMethods":true
},
{
"name":"org.asamk.signal.dbus.DbusSignalControlImpl",
"queryAllDeclaredMethods":true
},
{
"name":"org.asamk.signal.dbus.DbusSignalImpl",
"queryAllDeclaredMethods":true
},
{
"name":"org.asamk.signal.dbus.DbusSignalImpl$DbusSignalConfigurationImpl",
"queryAllDeclaredMethods":true
},
{
"name":"org.asamk.signal.dbus.DbusSignalImpl$DbusSignalDeviceImpl",
"queryAllDeclaredMethods":true
},
{
"name":"org.asamk.signal.dbus.DbusSignalImpl$DbusSignalGroupImpl",
"queryAllDeclaredMethods":true
},
{
"name":"org.asamk.signal.dbus.DbusSignalImpl$DbusSignalIdentityImpl",
"queryAllDeclaredMethods":true
},
{ {
"name":"org.asamk.signal.json.JsonAttachment", "name":"org.asamk.signal.json.JsonAttachment",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -845,6 +992,27 @@
"queryAllDeclaredConstructors":true, "queryAllDeclaredConstructors":true,
"methods":[{"name":"id","parameterTypes":[] }, {"name":"opaque","parameterTypes":[] }, {"name":"sdp","parameterTypes":[] }, {"name":"type","parameterTypes":[] }] "methods":[{"name":"id","parameterTypes":[] }, {"name":"opaque","parameterTypes":[] }, {"name":"sdp","parameterTypes":[] }, {"name":"type","parameterTypes":[] }]
}, },
{
"name":"org.asamk.signal.json.JsonContact",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"color","parameterTypes":[] }, {"name":"familyName","parameterTypes":[] }, {"name":"givenName","parameterTypes":[] }, {"name":"internal","parameterTypes":[] }, {"name":"isBlocked","parameterTypes":[] }, {"name":"isHidden","parameterTypes":[] }, {"name":"messageExpirationTime","parameterTypes":[] }, {"name":"name","parameterTypes":[] }, {"name":"nickFamilyName","parameterTypes":[] }, {"name":"nickGivenName","parameterTypes":[] }, {"name":"nickName","parameterTypes":[] }, {"name":"note","parameterTypes":[] }, {"name":"number","parameterTypes":[] }, {"name":"profile","parameterTypes":[] }, {"name":"profileSharing","parameterTypes":[] }, {"name":"unregistered","parameterTypes":[] }, {"name":"username","parameterTypes":[] }, {"name":"uuid","parameterTypes":[] }]
},
{
"name":"org.asamk.signal.json.JsonContact$JsonInternal",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"capabilities","parameterTypes":[] }, {"name":"discoverableByPhonenumber","parameterTypes":[] }, {"name":"sharesPhoneNumber","parameterTypes":[] }, {"name":"unidentifiedAccessMode","parameterTypes":[] }]
},
{
"name":"org.asamk.signal.json.JsonContact$JsonProfile",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"about","parameterTypes":[] }, {"name":"aboutEmoji","parameterTypes":[] }, {"name":"familyName","parameterTypes":[] }, {"name":"givenName","parameterTypes":[] }, {"name":"hasAvatar","parameterTypes":[] }, {"name":"lastUpdateTimestamp","parameterTypes":[] }, {"name":"mobileCoinAddress","parameterTypes":[] }]
},
{ {
"name":"org.asamk.signal.json.JsonContactAddress", "name":"org.asamk.signal.json.JsonContactAddress",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -871,7 +1039,7 @@
"allDeclaredFields":true, "allDeclaredFields":true,
"allDeclaredMethods":true, "allDeclaredMethods":true,
"allDeclaredConstructors":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", "name":"org.asamk.signal.json.JsonContactPhone",
@ -906,7 +1074,7 @@
"allDeclaredFields":true, "allDeclaredFields":true,
"allDeclaredMethods":true, "allDeclaredMethods":true,
"allDeclaredConstructors":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", "name":"org.asamk.signal.json.JsonMention",
@ -920,7 +1088,7 @@
"allDeclaredFields":true, "allDeclaredFields":true,
"allDeclaredMethods":true, "allDeclaredMethods":true,
"allDeclaredConstructors":true, "allDeclaredConstructors":true,
"methods":[{"name":"callMessage","parameterTypes":[] }, {"name":"dataMessage","parameterTypes":[] }, {"name":"editMessage","parameterTypes":[] }, {"name":"receiptMessage","parameterTypes":[] }, {"name":"source","parameterTypes":[] }, {"name":"sourceDevice","parameterTypes":[] }, {"name":"sourceName","parameterTypes":[] }, {"name":"sourceNumber","parameterTypes":[] }, {"name":"sourceUuid","parameterTypes":[] }, {"name":"storyMessage","parameterTypes":[] }, {"name":"syncMessage","parameterTypes":[] }, {"name":"timestamp","parameterTypes":[] }, {"name":"typingMessage","parameterTypes":[] }] "methods":[{"name":"callMessage","parameterTypes":[] }, {"name":"dataMessage","parameterTypes":[] }, {"name":"editMessage","parameterTypes":[] }, {"name":"receiptMessage","parameterTypes":[] }, {"name":"serverDeliveredTimestamp","parameterTypes":[] }, {"name":"serverReceivedTimestamp","parameterTypes":[] }, {"name":"source","parameterTypes":[] }, {"name":"sourceDevice","parameterTypes":[] }, {"name":"sourceName","parameterTypes":[] }, {"name":"sourceNumber","parameterTypes":[] }, {"name":"sourceUuid","parameterTypes":[] }, {"name":"storyMessage","parameterTypes":[] }, {"name":"syncMessage","parameterTypes":[] }, {"name":"timestamp","parameterTypes":[] }, {"name":"typingMessage","parameterTypes":[] }]
}, },
{ {
"name":"org.asamk.signal.json.JsonPayment", "name":"org.asamk.signal.json.JsonPayment",
@ -1128,7 +1296,7 @@
"allDeclaredFields":true, "allDeclaredFields":true,
"queryAllDeclaredMethods":true, "queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true, "queryAllDeclaredConstructors":true,
"methods":[{"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":"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", "name":"org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData",
@ -1245,6 +1413,12 @@
"name":"org.asamk.signal.manager.storage.profiles.LegacyProfileStore$ProfileStoreDeserializer", "name":"org.asamk.signal.manager.storage.profiles.LegacyProfileStore$ProfileStoreDeserializer",
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }]
}, },
{
"name":"org.asamk.signal.manager.storage.profiles.LegacySignalProfile",
"allDeclaredFields":true,
"allDeclaredMethods":true,
"allDeclaredConstructors":true
},
{ {
"name":"org.asamk.signal.manager.storage.profiles.LegacySignalProfileEntry", "name":"org.asamk.signal.manager.storage.profiles.LegacySignalProfileEntry",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -1380,6 +1554,14 @@
"name":"org.bouncycastle.jcajce.provider.asymmetric.COMPOSITE$Mappings", "name":"org.bouncycastle.jcajce.provider.asymmetric.COMPOSITE$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }] "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":[] }]
},
{ {
"name":"org.bouncycastle.jcajce.provider.asymmetric.DH$Mappings", "name":"org.bouncycastle.jcajce.provider.asymmetric.DH$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }]
@ -1436,14 +1618,30 @@
"name":"org.bouncycastle.jcajce.provider.asymmetric.LMS$Mappings", "name":"org.bouncycastle.jcajce.provider.asymmetric.LMS$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }] "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", "name":"org.bouncycastle.jcajce.provider.asymmetric.NTRU$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }]
}, },
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.NoSig$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{ {
"name":"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings", "name":"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }]
}, },
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.SLHDSA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{ {
"name":"org.bouncycastle.jcajce.provider.asymmetric.SPHINCSPlus$Mappings", "name":"org.bouncycastle.jcajce.provider.asymmetric.SPHINCSPlus$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }]
@ -1799,6 +1997,10 @@
"name":"org.bouncycastle.pqc.jcajce.provider.XMSS$Mappings", "name":"org.bouncycastle.pqc.jcajce.provider.XMSS$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }]
}, },
{
"name":"org.freedesktop.dbus.connections.base.GlobalHandler",
"queryAllDeclaredMethods":true
},
{ {
"name":"org.freedesktop.dbus.errors.ServiceUnknown", "name":"org.freedesktop.dbus.errors.ServiceUnknown",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }] "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
@ -1831,7 +2033,7 @@
"name":"org.freedesktop.dbus.interfaces.Properties", "name":"org.freedesktop.dbus.interfaces.Properties",
"allDeclaredMethods":true, "allDeclaredMethods":true,
"allDeclaredClasses":true, "allDeclaredClasses":true,
"methods":[{"name":"Get","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"GetAll","parameterTypes":["java.lang.String"] }] "methods":[{"name":"Get","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"GetAll","parameterTypes":["java.lang.String"] }, {"name":"Set","parameterTypes":["java.lang.String","java.lang.String","java.lang.Object"] }]
}, },
{ {
"name":"org.freedesktop.dbus.interfaces.Properties$PropertiesChanged", "name":"org.freedesktop.dbus.interfaces.Properties$PropertiesChanged",
@ -1852,7 +2054,10 @@
"name":"org.signal.libsignal.protocol.IdentityKey" "name":"org.signal.libsignal.protocol.IdentityKey"
}, },
{ {
"name":"org.signal.libsignal.protocol.ServiceId" "name":"org.signal.libsignal.protocol.ServiceId",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
}, },
{ {
"name":"org.signal.libsignal.protocol.SignalProtocolAddress" "name":"org.signal.libsignal.protocol.SignalProtocolAddress"
@ -2096,6 +2301,9 @@
"name":"org.signal.storageservice.protos.groups.local.DecryptedTimer", "name":"org.signal.storageservice.protos.groups.local.DecryptedTimer",
"fields":[{"name":"duration_"}] "fields":[{"name":"duration_"}]
}, },
{
"name":"org.slf4j.Logger"
},
{ {
"name":"org.sqlite.JDBC" "name":"org.sqlite.JDBC"
}, },
@ -2111,7 +2319,7 @@
"allDeclaredFields":true, "allDeclaredFields":true,
"allDeclaredMethods":true, "allDeclaredMethods":true,
"allDeclaredConstructors":true, "allDeclaredConstructors":true,
"methods":[{"name":"getAnnouncementGroup","parameterTypes":[] }, {"name":"getChangeNumber","parameterTypes":[] }, {"name":"getGiftBadges","parameterTypes":[] }, {"name":"getPaymentActivation","parameterTypes":[] }, {"name":"getPni","parameterTypes":[] }, {"name":"getSenderKey","parameterTypes":[] }, {"name":"getStorage","parameterTypes":[] }, {"name":"getStories","parameterTypes":[] }] "methods":[{"name":"getAnnouncementGroup","parameterTypes":[] }, {"name":"getAttachmentBackfill","parameterTypes":[] }, {"name":"getChangeNumber","parameterTypes":[] }, {"name":"getDeleteSync","parameterTypes":[] }, {"name":"getGiftBadges","parameterTypes":[] }, {"name":"getPaymentActivation","parameterTypes":[] }, {"name":"getPni","parameterTypes":[] }, {"name":"getSenderKey","parameterTypes":[] }, {"name":"getStorage","parameterTypes":[] }, {"name":"getStorageServiceEncryptionV2","parameterTypes":[] }, {"name":"getStories","parameterTypes":[] }, {"name":"getVersionedExpirationTimer","parameterTypes":[] }]
}, },
{ {
"name":"org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest", "name":"org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest",
@ -2138,6 +2346,20 @@
{ {
"name":"org.whispersystems.signalservice.api.groupsv2.TemporalCredential[]" "name":"org.whispersystems.signalservice.api.groupsv2.TemporalCredential[]"
}, },
{
"name":"org.whispersystems.signalservice.api.keys.OneTimePreKeyCounts",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse",
"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", "name":"org.whispersystems.signalservice.api.messages.calls.HangupMessage",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -2198,14 +2420,21 @@
"name":"org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite", "name":"org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite",
"allDeclaredFields":true, "allDeclaredFields":true,
"allDeclaredMethods":true, "allDeclaredMethods":true,
"allDeclaredConstructors":true "allDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","byte[]","byte[]","byte[]","byte[]","byte[]","boolean","boolean","byte[]","java.util.List"] }, {"name":"getAbout","parameterTypes":[] }, {"name":"getAboutEmoji","parameterTypes":[] }, {"name":"getAvatar","parameterTypes":[] }, {"name":"getBadgeIds","parameterTypes":[] }, {"name":"getCommitment","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPaymentAddress","parameterTypes":[] }, {"name":"getPhoneNumberSharing","parameterTypes":[] }, {"name":"getSameAvatar","parameterTypes":[] }, {"name":"getVersion","parameterTypes":[] }]
},
{
"name":"org.whispersystems.signalservice.api.provisioning.ProvisioningMessage",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
}, },
{ {
"name":"org.whispersystems.signalservice.api.push.ServiceId", "name":"org.whispersystems.signalservice.api.push.ServiceId",
"allDeclaredFields":true, "allDeclaredFields":true,
"queryAllDeclaredMethods":true, "queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true, "queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["org.signal.libsignal.protocol.ServiceId"] }] "methods":[{"name":"<init>","parameterTypes":["org.signal.libsignal.protocol.ServiceId"] }, {"name":"equals","parameterTypes":["java.lang.Object"] }, {"name":"hashCode","parameterTypes":[] }, {"name":"logString","parameterTypes":[] }, {"name":"toByteArray","parameterTypes":[] }, {"name":"toByteString","parameterTypes":[] }, {"name":"toProtocolAddress","parameterTypes":["int"] }, {"name":"toString","parameterTypes":[] }]
}, },
{ {
"name":"org.whispersystems.signalservice.api.push.ServiceId$ACI" "name":"org.whispersystems.signalservice.api.push.ServiceId$ACI"
@ -2215,7 +2444,7 @@
"allDeclaredFields":true, "allDeclaredFields":true,
"queryAllDeclaredMethods":true, "queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true, "queryAllDeclaredConstructors":true,
"methods":[{"name":"fromLibSignal","parameterTypes":["org.signal.libsignal.protocol.ServiceId"] }, {"name":"parseOrNull","parameterTypes":["java.lang.String"] }, {"name":"parseOrNull","parameterTypes":["okio.ByteString"] }, {"name":"parseOrNull","parameterTypes":["byte[]"] }, {"name":"parseOrThrow","parameterTypes":["java.lang.String"] }, {"name":"parseOrThrow","parameterTypes":["okio.ByteString"] }, {"name":"parseOrThrow","parameterTypes":["byte[]"] }] "methods":[{"name":"equals","parameterTypes":["java.lang.Object"] }, {"name":"fromLibSignal","parameterTypes":["org.signal.libsignal.protocol.ServiceId"] }, {"name":"hashCode","parameterTypes":[] }, {"name":"parseOrNull","parameterTypes":["java.lang.String"] }, {"name":"parseOrNull","parameterTypes":["java.lang.String","boolean"] }, {"name":"parseOrNull","parameterTypes":["okio.ByteString"] }, {"name":"parseOrNull","parameterTypes":["byte[]"] }, {"name":"parseOrThrow","parameterTypes":["java.lang.String"] }, {"name":"parseOrThrow","parameterTypes":["okio.ByteString"] }, {"name":"parseOrThrow","parameterTypes":["byte[]"] }, {"name":"toString","parameterTypes":[] }]
}, },
{ {
"name":"org.whispersystems.signalservice.api.push.ServiceId$PNI" "name":"org.whispersystems.signalservice.api.push.ServiceId$PNI"
@ -2243,6 +2472,12 @@
"queryAllDeclaredConstructors":true, "queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String"] }] "methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String"] }]
}, },
{
"name":"org.whispersystems.signalservice.api.ratelimit.SubmitRecaptchaChallengePayload",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{ {
"name":"org.whispersystems.signalservice.api.storage.StorageAuthResponse", "name":"org.whispersystems.signalservice.api.storage.StorageAuthResponse",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -2250,6 +2485,13 @@
"allDeclaredConstructors":true, "allDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }]
}, },
{
"name":"org.whispersystems.signalservice.api.svr.Svr3Credentials",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","byte[]"] }, {"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","byte[]","int","kotlin.jvm.internal.DefaultConstructorMarker"] }, {"name":"getAuthCredentials","parameterTypes":[] }]
},
{ {
"name":"org.whispersystems.signalservice.internal.contacts.crypto.SignatureBodyEntity", "name":"org.whispersystems.signalservice.internal.contacts.crypto.SignatureBodyEntity",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -2350,6 +2592,13 @@
"name":"org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse", "name":"org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse",
"fields":[{"name":"bitField0_"}, {"name":"data_"}, {"name":"status_"}, {"name":"token_"}, {"name":"tries_"}] "fields":[{"name":"bitField0_"}, {"name":"data_"}, {"name":"status_"}, {"name":"token_"}, {"name":"tries_"}]
}, },
{
"name":"org.whispersystems.signalservice.internal.push.AttachmentUploadForm",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["int","java.lang.String","java.util.Map","java.lang.String"] }, {"name":"<init>","parameterTypes":["int","java.lang.String","java.util.Map","java.lang.String","int","kotlin.jvm.internal.DefaultConstructorMarker"] }]
},
{ {
"name":"org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes", "name":"org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -2364,6 +2613,10 @@
"allDeclaredConstructors":true, "allDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }]
}, },
{
"name":"org.whispersystems.signalservice.internal.push.ByteArrayDeserializerBase64",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{ {
"name":"org.whispersystems.signalservice.internal.push.CdsiAuthResponse", "name":"org.whispersystems.signalservice.internal.push.CdsiAuthResponse",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -2475,6 +2728,7 @@
"allDeclaredFields":true, "allDeclaredFields":true,
"allDeclaredClasses":true, "allDeclaredClasses":true,
"queryAllDeclaredMethods":true, "queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"accountAttributes","parameterTypes":[] }, {"name":"aciPqLastResortPreKey","parameterTypes":[] }, {"name":"aciSignedPreKey","parameterTypes":[] }, {"name":"pniPqLastResortPreKey","parameterTypes":[] }, {"name":"pniSignedPreKey","parameterTypes":[] }, {"name":"verificationCode","parameterTypes":[] }] "methods":[{"name":"accountAttributes","parameterTypes":[] }, {"name":"aciPqLastResortPreKey","parameterTypes":[] }, {"name":"aciSignedPreKey","parameterTypes":[] }, {"name":"pniPqLastResortPreKey","parameterTypes":[] }, {"name":"pniSignedPreKey","parameterTypes":[] }, {"name":"verificationCode","parameterTypes":[] }]
}, },
{ {
@ -2702,7 +2956,28 @@
"allDeclaredFields":true, "allDeclaredFields":true,
"queryAllDeclaredMethods":true, "queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true, "queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }, {"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","java.lang.String","java.lang.String","org.whispersystems.signalservice.internal.push.WhoAmIResponse$Entitlements"] }, {"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","java.lang.String","java.lang.String","org.whispersystems.signalservice.internal.push.WhoAmIResponse$Entitlements","int","kotlin.jvm.internal.DefaultConstructorMarker"] }]
},
{
"name":"org.whispersystems.signalservice.internal.push.WhoAmIResponse$BackupEntitlement",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.lang.Long","java.lang.Long"] }]
},
{
"name":"org.whispersystems.signalservice.internal.push.WhoAmIResponse$BadgeEntitlement",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.Boolean","java.lang.Long"] }, {"name":"<init>","parameterTypes":["java.lang.String","java.lang.Boolean","java.lang.Long","int","kotlin.jvm.internal.DefaultConstructorMarker"] }]
},
{
"name":"org.whispersystems.signalservice.internal.push.WhoAmIResponse$Entitlements",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.util.List","org.whispersystems.signalservice.internal.push.WhoAmIResponse$BackupEntitlement"] }, {"name":"<init>","parameterTypes":["java.util.List","org.whispersystems.signalservice.internal.push.WhoAmIResponse$BackupEntitlement","int","kotlin.jvm.internal.DefaultConstructorMarker"] }]
}, },
{ {
"name":"org.whispersystems.signalservice.internal.serialize.protos.AddressProto", "name":"org.whispersystems.signalservice.internal.serialize.protos.AddressProto",
@ -2726,7 +3001,26 @@
}, },
{ {
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord", "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", "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$PinnedConversation",
@ -2740,17 +3034,53 @@
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$UsernameLink", "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$UsernameLink",
"allDeclaredFields":true "allDeclaredFields":true
}, },
{
"name":"org.whispersystems.signalservice.internal.storage.protos.AvatarColor"
},
{ {
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord", "name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord",
"allDeclaredFields":true,
"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",
"allDeclaredFields":true "allDeclaredFields":true
}, },
{ {
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record", "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", "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", "name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord",
@ -2760,6 +3090,9 @@
"name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord$Identifier", "name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord$Identifier",
"fields":[{"name":"raw_"}, {"name":"type_"}] "fields":[{"name":"raw_"}, {"name":"type_"}]
}, },
{
"name":"org.whispersystems.signalservice.internal.storage.protos.OptionalBool"
},
{ {
"name":"org.whispersystems.signalservice.internal.storage.protos.Payments", "name":"org.whispersystems.signalservice.internal.storage.protos.Payments",
"allDeclaredFields":true "allDeclaredFields":true

View file

@ -180,10 +180,14 @@
"pattern":"\\Qkotlin/ranges/ranges.kotlin_builtins\\E" "pattern":"\\Qkotlin/ranges/ranges.kotlin_builtins\\E"
}, { }, {
"pattern":"\\Qkotlin/reflect/reflect.kotlin_builtins\\E" "pattern":"\\Qkotlin/reflect/reflect.kotlin_builtins\\E"
}, {
"pattern":"\\Qlibsignal_jni.dylib\\E"
}, { }, {
"pattern":"\\Qlibsignal_jni.so\\E" "pattern":"\\Qlibsignal_jni.so\\E"
}, {
"pattern":"\\Qlibsignal_jni_aarch64.dylib\\E"
}, {
"pattern":"\\Qlibsignal_jni_amd64.dylib\\E"
}, {
"pattern":"\\Qlibsignal_jni_amd64.so\\E"
}, { }, {
"pattern":"\\Qorg/asamk/signal/manager/config/ias.store\\E" "pattern":"\\Qorg/asamk/signal/manager/config/ias.store\\E"
}, { }, {
@ -194,6 +198,8 @@
"pattern":"\\Qorg/sqlite/native/Linux/x86_64/libsqlitejdbc.so\\E" "pattern":"\\Qorg/sqlite/native/Linux/x86_64/libsqlitejdbc.so\\E"
}, { }, {
"pattern":"\\Qsignal_jni.dll\\E" "pattern":"\\Qsignal_jni.dll\\E"
}, {
"pattern":"\\Qsignal_jni_amd64.dll\\E"
}, { }, {
"pattern":"\\Qsqlite-jdbc.properties\\E" "pattern":"\\Qsqlite-jdbc.properties\\E"
}, { }, {

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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

14
gradlew vendored
View file

@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015-2021 the original authors. # Copyright © 2015 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -84,7 +86,7 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || 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. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@ -112,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@ -203,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command: # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped. # and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line. # treated as '${Hostname}' itself on the command line.
@ -211,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \ -classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.

6
gradlew.bat vendored
View file

@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@ -68,11 +70,11 @@ goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell

View file

@ -1,10 +1,14 @@
package org.asamk.signal.manager; package org.asamk.signal.manager;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import org.asamk.signal.manager.api.AlreadyReceivingException; import org.asamk.signal.manager.api.AlreadyReceivingException;
import org.asamk.signal.manager.api.AttachmentInvalidException; import org.asamk.signal.manager.api.AttachmentInvalidException;
import org.asamk.signal.manager.api.CaptchaRejectedException;
import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.Configuration; import org.asamk.signal.manager.api.Configuration;
import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Device;
import org.asamk.signal.manager.api.DeviceLimitExceededException;
import org.asamk.signal.manager.api.DeviceLinkUrl; import org.asamk.signal.manager.api.DeviceLinkUrl;
import org.asamk.signal.manager.api.Group; import org.asamk.signal.manager.api.Group;
import org.asamk.signal.manager.api.GroupId; import org.asamk.signal.manager.api.GroupId;
@ -26,6 +30,7 @@ import org.asamk.signal.manager.api.NotAGroupMemberException;
import org.asamk.signal.manager.api.NotPrimaryDeviceException; import org.asamk.signal.manager.api.NotPrimaryDeviceException;
import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.PendingAdminApprovalException; import org.asamk.signal.manager.api.PendingAdminApprovalException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.ReceiveConfig; import org.asamk.signal.manager.api.ReceiveConfig;
@ -43,9 +48,10 @@ import org.asamk.signal.manager.api.UpdateGroup;
import org.asamk.signal.manager.api.UpdateProfile; import org.asamk.signal.manager.api.UpdateProfile;
import org.asamk.signal.manager.api.UserStatus; import org.asamk.signal.manager.api.UserStatus;
import org.asamk.signal.manager.api.UsernameLinkUrl; import org.asamk.signal.manager.api.UsernameLinkUrl;
import org.asamk.signal.manager.api.UsernameStatus;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import java.io.Closeable; import java.io.Closeable;
import java.io.File; import java.io.File;
@ -61,7 +67,7 @@ import java.util.Set;
public interface Manager extends Closeable { public interface Manager extends Closeable {
static boolean isValidNumber(final String e164Number, final String countryCode) { static boolean isValidNumber(final String e164Number, final String countryCode) {
return PhoneNumberFormatter.isValidNumber(e164Number, countryCode); return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode);
} }
static boolean isSignalClientAvailable() { static boolean isSignalClientAvailable() {
@ -90,6 +96,8 @@ public interface Manager extends Closeable {
*/ */
Map<String, UserStatus> getUserStatus(Set<String> numbers) throws IOException, RateLimitException; Map<String, UserStatus> getUserStatus(Set<String> numbers) throws IOException, RateLimitException;
Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) throws IOException;
void updateAccountAttributes( void updateAccountAttributes(
String deviceName, String deviceName,
Boolean unrestrictedUnidentifiedSender, Boolean unrestrictedUnidentifiedSender,
@ -124,41 +132,52 @@ public interface Manager extends Closeable {
void deleteUsername() throws IOException; void deleteUsername() throws IOException;
void startChangeNumber( void startChangeNumber(
String newNumber, boolean voiceVerification, String captcha String newNumber,
) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, NotPrimaryDeviceException; boolean voiceVerification,
String captcha
) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, NotPrimaryDeviceException, VerificationMethodNotAvailableException;
void finishChangeNumber( void finishChangeNumber(
String newNumber, String verificationCode, String pin String newNumber,
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException; String verificationCode,
String pin
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException;
void unregister() throws IOException; void unregister() throws IOException;
void deleteAccount() throws IOException; void deleteAccount() throws IOException;
void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException; void submitRateLimitRecaptchaChallenge(
String challenge,
String captcha
) throws IOException, CaptchaRejectedException;
List<Device> getLinkedDevices() throws IOException; List<Device> getLinkedDevices() throws IOException;
void removeLinkedDevices(int deviceId) throws IOException; void removeLinkedDevices(int deviceId) throws IOException, NotPrimaryDeviceException;
void addDeviceLink(DeviceLinkUrl linkUri) throws IOException, InvalidDeviceLinkException, NotPrimaryDeviceException; void addDeviceLink(DeviceLinkUrl linkUri) throws IOException, InvalidDeviceLinkException, NotPrimaryDeviceException, DeviceLimitExceededException;
void setRegistrationLockPin(Optional<String> pin) throws IOException, NotPrimaryDeviceException; void setRegistrationLockPin(Optional<String> pin) throws IOException, NotPrimaryDeviceException;
List<Group> getGroups(); List<Group> getGroups();
SendGroupMessageResults quitGroup( SendGroupMessageResults quitGroup(
GroupId groupId, Set<RecipientIdentifier.Single> groupAdmins GroupId groupId,
Set<RecipientIdentifier.Single> groupAdmins
) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException; ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException;
void deleteGroup(GroupId groupId) throws IOException; void deleteGroup(GroupId groupId) throws IOException;
Pair<GroupId, SendGroupMessageResults> createGroup( Pair<GroupId, SendGroupMessageResults> createGroup(
String name, Set<RecipientIdentifier.Single> members, String avatarFile String name,
Set<RecipientIdentifier.Single> members,
String avatarFile
) throws IOException, AttachmentInvalidException, UnregisteredRecipientException; ) throws IOException, AttachmentInvalidException, UnregisteredRecipientException;
SendGroupMessageResults updateGroup( SendGroupMessageResults updateGroup(
final GroupId groupId, final UpdateGroup updateGroup final GroupId groupId,
final UpdateGroup updateGroup
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException; ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException;
Pair<GroupId, SendGroupMessageResults> joinGroup( Pair<GroupId, SendGroupMessageResults> joinGroup(
@ -166,27 +185,29 @@ public interface Manager extends Closeable {
) throws IOException, InactiveGroupLinkException, PendingAdminApprovalException; ) throws IOException, InactiveGroupLinkException, PendingAdminApprovalException;
SendMessageResults sendTypingMessage( SendMessageResults sendTypingMessage(
TypingAction action, Set<RecipientIdentifier> recipients TypingAction action,
Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
SendMessageResults sendReadReceipt( SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List<Long> messageIds);
RecipientIdentifier.Single sender, List<Long> messageIds
);
SendMessageResults sendViewedReceipt( SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List<Long> messageIds);
RecipientIdentifier.Single sender, List<Long> messageIds
);
SendMessageResults sendMessage( SendMessageResults sendMessage(
Message message, Set<RecipientIdentifier> recipients, boolean notifySelf Message message,
Set<RecipientIdentifier> recipients,
boolean notifySelf
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException; ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
SendMessageResults sendEditMessage( SendMessageResults sendEditMessage(
Message message, Set<RecipientIdentifier> recipients, long editTargetTimestamp Message message,
Set<RecipientIdentifier> recipients,
long editTargetTimestamp
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException; ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
SendMessageResults sendRemoteDeleteMessage( SendMessageResults sendRemoteDeleteMessage(
long targetSentTimestamp, Set<RecipientIdentifier> recipients long targetSentTimestamp,
Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
SendMessageResults sendMessageReaction( SendMessageResults sendMessageReaction(
@ -199,13 +220,16 @@ public interface Manager extends Closeable {
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException; ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
SendMessageResults sendPaymentNotificationMessage( SendMessageResults sendPaymentNotificationMessage(
byte[] receipt, String note, RecipientIdentifier.Single recipient byte[] receipt,
String note,
RecipientIdentifier.Single recipient
) throws IOException; ) throws IOException;
SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException; SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException;
SendMessageResults sendMessageRequestResponse( SendMessageResults sendMessageRequestResponse(
MessageEnvelope.Sync.MessageRequestResponse.Type type, Set<RecipientIdentifier> recipientIdentifiers MessageEnvelope.Sync.MessageRequestResponse.Type type,
Set<RecipientIdentifier> recipientIdentifiers
); );
void hideRecipient(RecipientIdentifier.Single recipient); void hideRecipient(RecipientIdentifier.Single recipient);
@ -215,22 +239,30 @@ public interface Manager extends Closeable {
void deleteContact(RecipientIdentifier.Single recipient); void deleteContact(RecipientIdentifier.Single recipient);
void setContactName( 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; ) throws NotPrimaryDeviceException, UnregisteredRecipientException;
void setContactsBlocked( void setContactsBlocked(
Collection<RecipientIdentifier.Single> recipient, boolean blocked Collection<RecipientIdentifier.Single> recipient,
boolean blocked
) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException; ) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException;
void setGroupsBlocked( void setGroupsBlocked(
Collection<GroupId> groupId, boolean blocked Collection<GroupId> groupId,
boolean blocked
) throws GroupNotFoundException, IOException, NotPrimaryDeviceException; ) throws GroupNotFoundException, IOException, NotPrimaryDeviceException;
/** /**
* Change the expiration timer for a contact * Change the expiration timer for a contact
*/ */
void setExpirationTimer( void setExpirationTimer(
RecipientIdentifier.Single recipient, int messageExpirationTimer RecipientIdentifier.Single recipient,
int messageExpirationTimer
) throws IOException, UnregisteredRecipientException; ) throws IOException, UnregisteredRecipientException;
/** /**
@ -269,7 +301,9 @@ public interface Manager extends Closeable {
* Receive new messages from server, returns if no new message arrive in a timespan of timeout. * Receive new messages from server, returns if no new message arrive in a timespan of timeout.
*/ */
void receiveMessages( void receiveMessages(
Optional<Duration> timeout, Optional<Integer> maxMessages, ReceiveMessageHandler handler Optional<Duration> timeout,
Optional<Integer> maxMessages,
ReceiveMessageHandler handler
) throws IOException, AlreadyReceivingException; ) throws IOException, AlreadyReceivingException;
void stopReceiveMessages(); void stopReceiveMessages();
@ -301,7 +335,8 @@ public interface Manager extends Closeable {
* @param recipient account of the identity * @param recipient account of the identity
*/ */
boolean trustIdentityVerified( boolean trustIdentityVerified(
RecipientIdentifier.Single recipient, IdentityVerificationCode verificationCode RecipientIdentifier.Single recipient,
IdentityVerificationCode verificationCode
) throws UnregisteredRecipientException; ) throws UnregisteredRecipientException;
/** /**

View file

@ -3,8 +3,10 @@ package org.asamk.signal.manager;
import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
@ -12,12 +14,15 @@ import java.io.IOException;
public interface RegistrationManager extends Closeable { public interface RegistrationManager extends Closeable {
void register( void register(
boolean voiceVerification, String captcha boolean voiceVerification,
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException; String captcha,
final boolean forceRegister
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException;
void verifyAccount( void verifyAccount(
String verificationCode, String pin String verificationCode,
) throws IOException, PinLockedException, IncorrectPinException; String pin
) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException;
void deleteLocalAccountData() throws IOException; void deleteLocalAccountData() throws IOException;

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,16 @@
package org.asamk.signal.manager.api;
public class CaptchaRejectedException extends Exception {
public CaptchaRejectedException() {
super("Captcha rejected");
}
public CaptchaRejectedException(final String message) {
super(message);
}
public CaptchaRejectedException(final String message, final Throwable cause) {
super(message, cause);
}
}

View file

@ -6,8 +6,12 @@ public record Contact(
String givenName, String givenName,
String familyName, String familyName,
String nickName, String nickName,
String nickNameGivenName,
String nickNameFamilyName,
String note,
String color, String color,
int messageExpirationTime, int messageExpirationTime,
int messageExpirationTimeVersion,
long muteUntil, long muteUntil,
boolean hideStory, boolean hideStory,
boolean isBlocked, boolean isBlocked,
@ -21,8 +25,12 @@ public record Contact(
this(builder.givenName, this(builder.givenName,
builder.familyName, builder.familyName,
builder.nickName, builder.nickName,
builder.nickNameGivenName,
builder.nickNameFamilyName,
builder.note,
builder.color, builder.color,
builder.messageExpirationTime, builder.messageExpirationTime,
builder.messageExpirationTimeVersion,
builder.muteUntil, builder.muteUntil,
builder.hideStory, builder.hideStory,
builder.isBlocked, builder.isBlocked,
@ -41,8 +49,12 @@ public record Contact(
builder.givenName = copy.givenName(); builder.givenName = copy.givenName();
builder.familyName = copy.familyName(); builder.familyName = copy.familyName();
builder.nickName = copy.nickName(); builder.nickName = copy.nickName();
builder.nickNameGivenName = copy.nickNameGivenName();
builder.nickNameFamilyName = copy.nickNameFamilyName();
builder.note = copy.note();
builder.color = copy.color(); builder.color = copy.color();
builder.messageExpirationTime = copy.messageExpirationTime(); builder.messageExpirationTime = copy.messageExpirationTime();
builder.messageExpirationTimeVersion = copy.messageExpirationTimeVersion();
builder.muteUntil = copy.muteUntil(); builder.muteUntil = copy.muteUntil();
builder.hideStory = copy.hideStory(); builder.hideStory = copy.hideStory();
builder.isBlocked = copy.isBlocked(); builder.isBlocked = copy.isBlocked();
@ -73,8 +85,12 @@ public record Contact(
private String givenName; private String givenName;
private String familyName; private String familyName;
private String nickName; private String nickName;
private String nickNameGivenName;
private String nickNameFamilyName;
private String note;
private String color; private String color;
private int messageExpirationTime; private int messageExpirationTime;
private int messageExpirationTimeVersion = 1;
private long muteUntil; private long muteUntil;
private boolean hideStory; private boolean hideStory;
private boolean isBlocked; private boolean isBlocked;
@ -105,6 +121,21 @@ public record Contact(
return this; return this;
} }
public Builder withNickNameGivenName(final String val) {
nickNameGivenName = val;
return this;
}
public Builder withNickNameFamilyName(final String val) {
nickNameFamilyName = val;
return this;
}
public Builder withNote(final String val) {
note = val;
return this;
}
public Builder withColor(final String val) { public Builder withColor(final String val) {
color = val; color = val;
return this; return this;
@ -115,6 +146,11 @@ public record Contact(
return this; return this;
} }
public Builder withMessageExpirationTimeVersion(final int val) {
messageExpirationTimeVersion = val;
return this;
}
public Builder withMuteUntil(final long val) { public Builder withMuteUntil(final long val) {
muteUntil = val; muteUntil = val;
return this; return this;

View file

@ -0,0 +1,12 @@
package org.asamk.signal.manager.api;
public class DeviceLimitExceededException extends Exception {
public DeviceLimitExceededException(final String message) {
super(message);
}
public DeviceLimitExceededException(final String message, final Throwable cause) {
super(message, cause);
}
}

View file

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

View file

@ -27,7 +27,9 @@ public record Group(
) { ) {
public static Group from( 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(), return new Group(groupInfo.getGroupId(),
groupInfo.getTitle(), groupInfo.getTitle(),

View file

@ -1,17 +1,10 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
import org.signal.libsignal.protocol.IdentityKey;
public record Identity( public record Identity(
RecipientAddress recipient, RecipientAddress recipient,
IdentityKey identityKey, byte[] fingerprint,
String safetyNumber, String safetyNumber,
byte[] scannableSafetyNumber, byte[] scannableSafetyNumber,
TrustLevel trustLevel, TrustLevel trustLevel,
long dateAddedTimestamp long dateAddedTimestamp
) { ) {}
public byte[] getFingerprint() {
return identityKey.getPublicKey().serialize();
}
}

View file

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

View file

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

View file

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

View file

@ -3,5 +3,16 @@ package org.asamk.signal.manager.api;
public enum PhoneNumberSharingMode { public enum PhoneNumberSharingMode {
EVERYBODY, EVERYBODY,
CONTACTS, CONTACTS,
NOBODY, NOBODY;
public static PhoneNumberSharingMode valueOfOrNull(String value) {
if (value == null) {
return null;
}
try {
return valueOf(value);
} catch (IllegalArgumentException ignored) {
return null;
}
}
} }

View file

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

View file

@ -26,6 +26,8 @@ public class Profile {
private final Set<Capability> capabilities; private final Set<Capability> capabilities;
private final PhoneNumberSharingMode phoneNumberSharingMode;
public Profile( public Profile(
final long lastUpdateTimestamp, final long lastUpdateTimestamp,
final String givenName, final String givenName,
@ -35,7 +37,8 @@ public class Profile {
final String avatarUrlPath, final String avatarUrlPath,
final byte[] mobileCoinAddress, final byte[] mobileCoinAddress,
final UnidentifiedAccessMode unidentifiedAccessMode, final UnidentifiedAccessMode unidentifiedAccessMode,
final Set<Capability> capabilities final Set<Capability> capabilities,
final PhoneNumberSharingMode phoneNumberSharingMode
) { ) {
this.lastUpdateTimestamp = lastUpdateTimestamp; this.lastUpdateTimestamp = lastUpdateTimestamp;
this.givenName = givenName; this.givenName = givenName;
@ -46,6 +49,7 @@ public class Profile {
this.mobileCoinAddress = mobileCoinAddress; this.mobileCoinAddress = mobileCoinAddress;
this.unidentifiedAccessMode = unidentifiedAccessMode; this.unidentifiedAccessMode = unidentifiedAccessMode;
this.capabilities = capabilities; this.capabilities = capabilities;
this.phoneNumberSharingMode = phoneNumberSharingMode;
} }
private Profile(final Builder builder) { private Profile(final Builder builder) {
@ -58,6 +62,7 @@ public class Profile {
mobileCoinAddress = builder.mobileCoinAddress; mobileCoinAddress = builder.mobileCoinAddress;
unidentifiedAccessMode = builder.unidentifiedAccessMode; unidentifiedAccessMode = builder.unidentifiedAccessMode;
capabilities = builder.capabilities; capabilities = builder.capabilities;
phoneNumberSharingMode = builder.phoneNumberSharingMode;
} }
public static Builder newBuilder() { public static Builder newBuilder() {
@ -136,6 +141,10 @@ public class Profile {
return capabilities; return capabilities;
} }
public PhoneNumberSharingMode getPhoneNumberSharingMode() {
return phoneNumberSharingMode;
}
public enum UnidentifiedAccessMode { public enum UnidentifiedAccessMode {
UNKNOWN, UNKNOWN,
DISABLED, DISABLED,
@ -153,9 +162,7 @@ public class Profile {
public enum Capability { public enum Capability {
storage, storage,
gv1Migration, storageServiceEncryptionV2Capability;
senderKey,
announcementGroup;
public static Capability valueOfOrNull(String value) { public static Capability valueOfOrNull(String value) {
try { try {
@ -203,6 +210,7 @@ public class Profile {
private byte[] mobileCoinAddress; private byte[] mobileCoinAddress;
private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN; private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN;
private Set<Capability> capabilities = Collections.emptySet(); private Set<Capability> capabilities = Collections.emptySet();
private PhoneNumberSharingMode phoneNumberSharingMode;
private long lastUpdateTimestamp = 0; private long lastUpdateTimestamp = 0;
private Builder() { private Builder() {
@ -243,6 +251,11 @@ public class Profile {
return this; return this;
} }
public Builder withPhoneNumberSharingMode(final PhoneNumberSharingMode val) {
phoneNumberSharingMode = val;
return this;
}
public Profile build() { public Profile build() {
return new Profile(this); return new Profile(this);
} }

View file

@ -31,12 +31,12 @@ public class ProofRequiredException extends Exception {
} }
public enum Option { public enum Option {
RECAPTCHA, CAPTCHA,
PUSH_CHALLENGE; PUSH_CHALLENGE;
static Option from(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException.Option option) { static Option from(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException.Option option) {
return switch (option) { return switch (option) {
case RECAPTCHA -> RECAPTCHA; case CAPTCHA -> CAPTCHA;
case PUSH_CHALLENGE -> PUSH_CHALLENGE; case PUSH_CHALLENGE -> PUSH_CHALLENGE;
}; };
} }

View file

@ -20,13 +20,16 @@ public class Recipient {
private final Profile profile; private final Profile profile;
private final Boolean discoverable;
public Recipient( public Recipient(
final RecipientId recipientId, final RecipientId recipientId,
final RecipientAddress address, final RecipientAddress address,
final Contact contact, final Contact contact,
final ProfileKey profileKey, final ProfileKey profileKey,
final ExpiringProfileKeyCredential expiringProfileKeyCredential, final ExpiringProfileKeyCredential expiringProfileKeyCredential,
final Profile profile final Profile profile,
final Boolean discoverable
) { ) {
this.recipientId = recipientId; this.recipientId = recipientId;
this.address = address; this.address = address;
@ -34,6 +37,7 @@ public class Recipient {
this.profileKey = profileKey; this.profileKey = profileKey;
this.expiringProfileKeyCredential = expiringProfileKeyCredential; this.expiringProfileKeyCredential = expiringProfileKeyCredential;
this.profile = profile; this.profile = profile;
this.discoverable = discoverable;
} }
private Recipient(final Builder builder) { private Recipient(final Builder builder) {
@ -41,8 +45,9 @@ public class Recipient {
address = builder.address; address = builder.address;
contact = builder.contact; contact = builder.contact;
profileKey = builder.profileKey; profileKey = builder.profileKey;
expiringProfileKeyCredential = builder.expiringProfileKeyCredential1; expiringProfileKeyCredential = builder.expiringProfileKeyCredential;
profile = builder.profile; profile = builder.profile;
discoverable = builder.discoverable;
} }
public static Builder newBuilder() { public static Builder newBuilder() {
@ -55,7 +60,7 @@ public class Recipient {
builder.address = copy.getAddress(); builder.address = copy.getAddress();
builder.contact = copy.getContact(); builder.contact = copy.getContact();
builder.profileKey = copy.getProfileKey(); builder.profileKey = copy.getProfileKey();
builder.expiringProfileKeyCredential1 = copy.getExpiringProfileKeyCredential(); builder.expiringProfileKeyCredential = copy.getExpiringProfileKeyCredential();
builder.profile = copy.getProfile(); builder.profile = copy.getProfile();
return builder; return builder;
} }
@ -84,6 +89,10 @@ public class Recipient {
return profile; return profile;
} }
public Boolean getDiscoverable() {
return discoverable;
}
@Override @Override
public boolean equals(final Object o) { public boolean equals(final Object o) {
if (this == o) return true; if (this == o) return true;
@ -108,8 +117,9 @@ public class Recipient {
private RecipientAddress address; private RecipientAddress address;
private Contact contact; private Contact contact;
private ProfileKey profileKey; private ProfileKey profileKey;
private ExpiringProfileKeyCredential expiringProfileKeyCredential1; private ExpiringProfileKeyCredential expiringProfileKeyCredential;
private Profile profile; private Profile profile;
private Boolean discoverable;
private Builder() { private Builder() {
} }
@ -135,7 +145,7 @@ public class Recipient {
} }
public Builder withExpiringProfileKeyCredential(final ExpiringProfileKeyCredential val) { public Builder withExpiringProfileKeyCredential(final ExpiringProfileKeyCredential val) {
expiringProfileKeyCredential1 = val; expiringProfileKeyCredential = val;
return this; return this;
} }
@ -144,6 +154,11 @@ public class Recipient {
return this; return this;
} }
public Builder withDiscoverable(final Boolean val) {
discoverable = val;
return this;
}
public Recipient build() { public Recipient build() {
return new Recipient(this); return new Recipient(this);
} }

View file

@ -1,49 +1,55 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
public record RecipientAddress(Optional<UUID> uuid, Optional<String> number, Optional<String> username) { public record RecipientAddress(
Optional<String> aci, Optional<String> pni, Optional<String> number, Optional<String> username
) {
public static final UUID UNKNOWN_UUID = UuidUtil.UNKNOWN_UUID; public static final UUID UNKNOWN_UUID = UuidUtil.UNKNOWN_UUID;
/** /**
* Construct a RecipientAddress. * Construct a RecipientAddress.
* *
* @param uuid The UUID of the user, if available. * @param aci The ACI of the user, if available.
* @param pni The PNI of the user, if available.
* @param number The phone number of the user, if available. * @param number The phone number of the user, if available.
*/ */
public RecipientAddress { public RecipientAddress {
uuid = uuid.isPresent() && uuid.get().equals(UNKNOWN_UUID) ? Optional.empty() : uuid; if (aci.isEmpty() && pni.isEmpty() && number.isEmpty() && username.isEmpty()) {
if (uuid.isEmpty() && number.isEmpty() && username.isEmpty()) { throw new AssertionError("Must have either a ACI, PNI, username or E164 number!");
throw new AssertionError("Must have either a UUID, username or E164 number!");
} }
} }
public RecipientAddress(UUID uuid, String e164) { public RecipientAddress(String e164) {
this(Optional.ofNullable(uuid), Optional.ofNullable(e164), Optional.empty()); this(null, null, e164, null);
}
public RecipientAddress(UUID uuid, String e164, String username) {
this(Optional.ofNullable(uuid), Optional.ofNullable(e164), Optional.ofNullable(username));
}
public RecipientAddress(SignalServiceAddress address) {
this(Optional.of(address.getServiceId().getRawUuid()), address.getNumber(), Optional.empty());
} }
public RecipientAddress(UUID uuid) { public RecipientAddress(UUID uuid) {
this(Optional.of(uuid), Optional.empty(), Optional.empty()); this(uuid.toString(), null, null, null);
}
public RecipientAddress(String aci, String pni, String e164, String username) {
this(Optional.ofNullable(aci),
Optional.ofNullable(pni),
Optional.ofNullable(e164),
Optional.ofNullable(username));
}
public Optional<UUID> uuid() {
return aci.map(UUID::fromString);
} }
public String getIdentifier() { public String getIdentifier() {
if (uuid.isPresent()) { if (aci.isPresent()) {
return uuid.get().toString(); return aci.get();
} else if (number.isPresent()) { } else if (number.isPresent()) {
return number.get(); return number.get();
} else if (pni.isPresent()) {
return pni.get();
} else if (username.isPresent()) { } else if (username.isPresent()) {
return username.get(); return username.get();
} else { } else {
@ -54,17 +60,16 @@ public record RecipientAddress(Optional<UUID> uuid, Optional<String> number, Opt
public String getLegacyIdentifier() { public String getLegacyIdentifier() {
if (number.isPresent()) { if (number.isPresent()) {
return number.get(); return number.get();
} else if (uuid.isPresent()) {
return uuid.get().toString();
} else if (username.isPresent()) {
return username.get();
} else { } else {
throw new AssertionError("Given the checks in the constructor, this should not be possible."); return getIdentifier();
} }
} }
public boolean matches(RecipientAddress other) { public boolean matches(RecipientAddress other) {
return (uuid.isPresent() && other.uuid.isPresent() && uuid.get().equals(other.uuid.get())) return (aci.isPresent() && other.aci.isPresent() && aci.get().equals(other.aci.get()))
|| (
pni.isPresent() && other.pni.isPresent() && pni.get().equals(other.pni.get())
)
|| (number.isPresent() && other.number.isPresent() && number.get().equals(other.number.get())) || (number.isPresent() && other.number.isPresent() && number.get().equals(other.number.get()))
|| (username.isPresent() && other.username.isPresent() && username.get().equals(other.username.get())); || (username.isPresent() && other.username.isPresent() && username.get().equals(other.username.get()));
} }

View file

@ -1,8 +1,8 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
import org.asamk.signal.manager.util.PhoneNumberFormatter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.UUID; import java.util.UUID;
@ -24,31 +24,37 @@ public sealed interface RecipientIdentifier {
sealed interface Single extends RecipientIdentifier { sealed interface Single extends RecipientIdentifier {
static Single fromString(String identifier, String localNumber) throws InvalidNumberException { static Single fromString(String identifier, String localNumber) throws InvalidNumberException {
try { if (UuidUtil.isUuid(identifier)) {
if (UuidUtil.isUuid(identifier)) { return new Uuid(UUID.fromString(identifier));
return new Uuid(UUID.fromString(identifier));
}
if (identifier.startsWith("u:")) {
return new Username(identifier.substring(2));
}
final var normalizedNumber = PhoneNumberFormatter.formatNumber(identifier, localNumber);
if (!normalizedNumber.equals(identifier)) {
final Logger logger = LoggerFactory.getLogger(RecipientIdentifier.class);
logger.debug("Normalized number {} to {}.", identifier, normalizedNumber);
}
return new Number(normalizedNumber);
} catch (org.whispersystems.signalservice.api.util.InvalidNumberException e) {
throw new InvalidNumberException(e.getMessage(), e);
} }
if (identifier.startsWith("PNI:")) {
final var pni = identifier.substring(4);
if (!UuidUtil.isUuid(pni)) {
throw new InvalidNumberException("Invalid PNI");
}
return new Pni(UUID.fromString(pni));
}
if (identifier.startsWith("u:")) {
return new Username(identifier.substring(2));
}
final var normalizedNumber = PhoneNumberFormatter.formatNumber(identifier, localNumber);
if (!normalizedNumber.equals(identifier)) {
final Logger logger = LoggerFactory.getLogger(RecipientIdentifier.class);
logger.debug("Normalized number {} to {}.", identifier, normalizedNumber);
}
return new Number(normalizedNumber);
} }
static Single fromAddress(RecipientAddress address) { static Single fromAddress(RecipientAddress address) {
if (address.number().isPresent()) { if (address.number().isPresent()) {
return new Number(address.number().get()); return new Number(address.number().get());
} else if (address.uuid().isPresent()) { } else if (address.aci().isPresent()) {
return new Uuid(address.uuid().get()); return new Uuid(UUID.fromString(address.aci().get()));
} else if (address.pni().isPresent()) {
return new Pni(UUID.fromString(address.pni().get().substring(4)));
} else if (address.username().isPresent()) { } else if (address.username().isPresent()) {
return new Username(address.username().get()); return new Username(address.username().get());
} }
@ -71,6 +77,19 @@ public sealed interface RecipientIdentifier {
} }
} }
record Pni(UUID pni) implements Single {
@Override
public String getIdentifier() {
return "PNI:" + pni.toString();
}
@Override
public RecipientAddress toPartialRecipientAddress() {
return new RecipientAddress(null, getIdentifier(), null, null);
}
}
record Number(String number) implements Single { record Number(String number) implements Single {
@Override @Override
@ -80,7 +99,7 @@ public sealed interface RecipientIdentifier {
@Override @Override
public RecipientAddress toPartialRecipientAddress() { public RecipientAddress toPartialRecipientAddress() {
return new RecipientAddress(null, number); return new RecipientAddress(number);
} }
} }
@ -93,7 +112,7 @@ public sealed interface RecipientIdentifier {
@Override @Override
public RecipientAddress toPartialRecipientAddress() { public RecipientAddress toPartialRecipientAddress() {
return new RecipientAddress(null, null, username); return new RecipientAddress(null, null, null, username);
} }
} }

View file

@ -0,0 +1,5 @@
package org.asamk.signal.manager.api;
import java.util.UUID;
public record UsernameStatus(String username, UUID uuid, boolean unrestrictedUnidentifiedAccess) {}

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager.api;
public class VerificationMethodNotAvailableException extends Exception {
public VerificationMethodNotAvailableException() {
super("Invalid verification method");
}
}

View file

@ -1,9 +1,10 @@
package org.asamk.signal.manager.config; package org.asamk.signal.manager.config;
import org.signal.libsignal.net.Network.Environment;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.HttpProxy;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl; import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
import org.whispersystems.signalservice.internal.configuration.SignalProxy; import org.whispersystems.signalservice.internal.configuration.SignalProxy;
@ -27,12 +28,14 @@ class LiveConfig {
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder() private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
.decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"); .decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF");
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57"; private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
private static final String SVR2_MRENCLAVE = "a6622ad4656e1abcd0bc0ff17c229477747d2ded0495c4ebee7ed35c1789fa97"; private static final String SVR2_MRENCLAVE_LEGACY_LEGACY = "9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570";
private static final String SVR2_MRENCLAVE_DEPRECATED = "6ee1042f9e20f880326686dd4ba50c25359f01e9f733eeba4382bca001d45094"; private static final String SVR2_MRENCLAVE_LEGACY = "093be9ea32405e85ae28dbb48eb668aebeb7dbe29517b9b86ad4bec4dfe0e6a6";
private static final String SVR2_MRENCLAVE = "29cd63c87bea751e3bfd0fbd401279192e2e5c99948b4ee9437eafc4968355fb";
private static final String URL = "https://chat.signal.org"; private static final String URL = "https://chat.signal.org";
private static final String CDN_URL = "https://cdn.signal.org"; private static final String CDN_URL = "https://cdn.signal.org";
private static final String CDN2_URL = "https://cdn2.signal.org"; private static final String CDN2_URL = "https://cdn2.signal.org";
private static final String CDN3_URL = "https://cdn3.signal.org";
private static final String STORAGE_URL = "https://storage.signal.org"; private static final String STORAGE_URL = "https://storage.signal.org";
private static final String SIGNAL_CDSI_URL = "https://cdsi.signal.org"; private static final String SIGNAL_CDSI_URL = "https://cdsi.signal.org";
private static final String SIGNAL_SVR2_URL = "https://svr2.signal.org"; private static final String SIGNAL_SVR2_URL = "https://svr2.signal.org";
@ -40,15 +43,18 @@ class LiveConfig {
private static final Optional<Dns> dns = Optional.empty(); private static final Optional<Dns> dns = Optional.empty();
private static final Optional<SignalProxy> proxy = Optional.empty(); private static final Optional<SignalProxy> proxy = Optional.empty();
private static final Optional<HttpProxy> systemProxy = Optional.empty();
private static final byte[] zkGroupServerPublicParams = Base64.getDecoder() private static final byte[] zkGroupServerPublicParams = Base64.getDecoder()
.decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0I="); .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==");
private static final byte[] genericServerPublicParams = Base64.getDecoder() private static final byte[] genericServerPublicParams = Base64.getDecoder()
.decode("AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN"); .decode("AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN");
private static final byte[] backupServerPublicParams = Base64.getDecoder() private static final byte[] backupServerPublicParams = Base64.getDecoder()
.decode("AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O"); .decode("AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O");
private static final Environment LIBSIGNAL_NET_ENV = Environment.PRODUCTION;
static SignalServiceConfiguration createDefaultServiceConfiguration( static SignalServiceConfiguration createDefaultServiceConfiguration(
final List<Interceptor> interceptors final List<Interceptor> interceptors
) { ) {
@ -56,21 +62,25 @@ class LiveConfig {
Map.of(0, Map.of(0,
new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)},
2, 2,
new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}), new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)},
3,
new SignalCdnUrl[]{new SignalCdnUrl(CDN3_URL, TRUST_STORE)}),
new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)}, new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)},
new SignalCdsiUrl[]{new SignalCdsiUrl(SIGNAL_CDSI_URL, TRUST_STORE)}, new SignalCdsiUrl[]{new SignalCdsiUrl(SIGNAL_CDSI_URL, TRUST_STORE)},
new SignalSvr2Url[]{new SignalSvr2Url(SIGNAL_SVR2_URL, TRUST_STORE, null, null)}, new SignalSvr2Url[]{new SignalSvr2Url(SIGNAL_SVR2_URL, TRUST_STORE, null, null)},
interceptors, interceptors,
dns, dns,
proxy, proxy,
systemProxy,
zkGroupServerPublicParams, zkGroupServerPublicParams,
genericServerPublicParams, genericServerPublicParams,
backupServerPublicParams); backupServerPublicParams,
false);
} }
static ECPublicKey getUnidentifiedSenderTrustRoot() { static ECPublicKey getUnidentifiedSenderTrustRoot() {
try { try {
return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0); return new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT);
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
@ -78,10 +88,11 @@ class LiveConfig {
static ServiceEnvironmentConfig getServiceEnvironmentConfig(List<Interceptor> interceptors) { static ServiceEnvironmentConfig getServiceEnvironmentConfig(List<Interceptor> interceptors) {
return new ServiceEnvironmentConfig(LIVE, return new ServiceEnvironmentConfig(LIVE,
LIBSIGNAL_NET_ENV,
createDefaultServiceConfiguration(interceptors), createDefaultServiceConfiguration(interceptors),
getUnidentifiedSenderTrustRoot(), getUnidentifiedSenderTrustRoot(),
CDSI_MRENCLAVE, CDSI_MRENCLAVE,
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_DEPRECATED)); List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_LEGACY_LEGACY));
} }
private LiveConfig() { private LiveConfig() {

View file

@ -20,6 +20,7 @@ public class ServiceConfig {
public static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; public static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
public static final long MAX_ENVELOPE_SIZE = 0; public static final long MAX_ENVELOPE_SIZE = 0;
public static final int MAX_MESSAGE_SIZE_BYTES = 2000;
public static final long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024; public static final long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024;
public static final boolean AUTOMATIC_NETWORK_RETRY = true; public static final boolean AUTOMATIC_NETWORK_RETRY = true;
public static final int GROUP_MAX_SIZE = 1001; public static final int GROUP_MAX_SIZE = 1001;
@ -27,14 +28,15 @@ public class ServiceConfig {
public static final long UNREGISTERED_LIFESPAN = TimeUnit.DAYS.toMillis(30); public static final long UNREGISTERED_LIFESPAN = TimeUnit.DAYS.toMillis(30);
public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) { public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) {
final var giftBadges = !isPrimaryDevice; final var deleteSync = !isPrimaryDevice;
final var pni = !isPrimaryDevice; final var storageEncryptionV2 = !isPrimaryDevice;
final var paymentActivation = !isPrimaryDevice; final var attachmentBackfill = !isPrimaryDevice;
return new AccountAttributes.Capabilities(true, true, true, true, true, giftBadges, pni, paymentActivation); return new AccountAttributes.Capabilities(true, deleteSync, true, storageEncryptionV2, attachmentBackfill);
} }
public static ServiceEnvironmentConfig getServiceEnvironmentConfig( public static ServiceEnvironmentConfig getServiceEnvironmentConfig(
ServiceEnvironment serviceEnvironment, String userAgent ServiceEnvironment serviceEnvironment,
String userAgent
) { ) {
final Interceptor userAgentInterceptor = chain -> chain.proceed(chain.request() final Interceptor userAgentInterceptor = chain -> chain.proceed(chain.request()
.newBuilder() .newBuilder()

View file

@ -1,6 +1,7 @@
package org.asamk.signal.manager.config; package org.asamk.signal.manager.config;
import org.asamk.signal.manager.api.ServiceEnvironment; import org.asamk.signal.manager.api.ServiceEnvironment;
import org.signal.libsignal.net.Network;
import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
@ -8,6 +9,7 @@ import java.util.List;
public record ServiceEnvironmentConfig( public record ServiceEnvironmentConfig(
ServiceEnvironment type, ServiceEnvironment type,
Network.Environment netEnvironment,
SignalServiceConfiguration signalServiceConfiguration, SignalServiceConfiguration signalServiceConfiguration,
ECPublicKey unidentifiedSenderTrustRoot, ECPublicKey unidentifiedSenderTrustRoot,
String cdsiMrenclave, String cdsiMrenclave,

View file

@ -1,9 +1,10 @@
package org.asamk.signal.manager.config; package org.asamk.signal.manager.config;
import org.signal.libsignal.net.Network;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.HttpProxy;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl; import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
import org.whispersystems.signalservice.internal.configuration.SignalProxy; import org.whispersystems.signalservice.internal.configuration.SignalProxy;
@ -27,12 +28,14 @@ class StagingConfig {
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder() private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
.decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx"); .decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx");
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57"; private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
private static final String SVR2_MRENCLAVE = "acb1973aa0bbbd14b3b4e06f145497d948fd4a98efc500fcce363b3b743ec482"; private static final String SVR2_MRENCLAVE_LEGACY_LEGACY = "38e01eff4fe357dc0b0e8ef7a44b4abc5489fbccba3a78780f3872c277f62bf3";
private static final String SVR2_MRENCLAVE_DEPRECATED = "a8a261420a6bb9b61aa25bf8a79e8bd20d7652531feb3381cbffd446d270be95"; private static final String SVR2_MRENCLAVE_LEGACY = "2e8cefe6e3f389d8426adb24e9b7fb7adf10902c96f06f7bbcee36277711ed91";
private static final String SVR2_MRENCLAVE = "a75542d82da9f6914a1e31f8a7407053b99cc99a0e7291d8fbd394253e19b036";
private static final String URL = "https://chat.staging.signal.org"; private static final String URL = "https://chat.staging.signal.org";
private static final String CDN_URL = "https://cdn-staging.signal.org"; private static final String CDN_URL = "https://cdn-staging.signal.org";
private static final String CDN2_URL = "https://cdn2-staging.signal.org"; private static final String CDN2_URL = "https://cdn2-staging.signal.org";
private static final String CDN3_URL = "https://cdn3-staging.signal.org";
private static final String STORAGE_URL = "https://storage-staging.signal.org"; private static final String STORAGE_URL = "https://storage-staging.signal.org";
private static final String SIGNAL_CDSI_URL = "https://cdsi.staging.signal.org"; private static final String SIGNAL_CDSI_URL = "https://cdsi.staging.signal.org";
private static final String SIGNAL_SVR2_URL = "https://svr2.staging.signal.org"; private static final String SIGNAL_SVR2_URL = "https://svr2.staging.signal.org";
@ -40,15 +43,18 @@ class StagingConfig {
private static final Optional<Dns> dns = Optional.empty(); private static final Optional<Dns> dns = Optional.empty();
private static final Optional<SignalProxy> proxy = Optional.empty(); private static final Optional<SignalProxy> proxy = Optional.empty();
private static final Optional<HttpProxy> systemProxy = Optional.empty();
private static final byte[] zkGroupServerPublicParams = Base64.getDecoder() private static final byte[] zkGroupServerPublicParams = Base64.getDecoder()
.decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCM="); .decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ==");
private static final byte[] genericServerPublicParams = Base64.getDecoder() private static final byte[] genericServerPublicParams = Base64.getDecoder()
.decode("AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N"); .decode("AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N");
private static final byte[] backupServerPublicParams = Base64.getDecoder() private static final byte[] backupServerPublicParams = Base64.getDecoder()
.decode("AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8"); .decode("AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8");
private static final Network.Environment LIBSIGNAL_NET_ENV = Network.Environment.STAGING;
static SignalServiceConfiguration createDefaultServiceConfiguration( static SignalServiceConfiguration createDefaultServiceConfiguration(
final List<Interceptor> interceptors final List<Interceptor> interceptors
) { ) {
@ -56,21 +62,25 @@ class StagingConfig {
Map.of(0, Map.of(0,
new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)},
2, 2,
new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}), new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)},
3,
new SignalCdnUrl[]{new SignalCdnUrl(CDN3_URL, TRUST_STORE)}),
new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)}, new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)},
new SignalCdsiUrl[]{new SignalCdsiUrl(SIGNAL_CDSI_URL, TRUST_STORE)}, new SignalCdsiUrl[]{new SignalCdsiUrl(SIGNAL_CDSI_URL, TRUST_STORE)},
new SignalSvr2Url[]{new SignalSvr2Url(SIGNAL_SVR2_URL, TRUST_STORE, null, null)}, new SignalSvr2Url[]{new SignalSvr2Url(SIGNAL_SVR2_URL, TRUST_STORE, null, null)},
interceptors, interceptors,
dns, dns,
proxy, proxy,
systemProxy,
zkGroupServerPublicParams, zkGroupServerPublicParams,
genericServerPublicParams, genericServerPublicParams,
backupServerPublicParams); backupServerPublicParams,
false);
} }
static ECPublicKey getUnidentifiedSenderTrustRoot() { static ECPublicKey getUnidentifiedSenderTrustRoot() {
try { try {
return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0); return new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT);
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
@ -78,10 +88,11 @@ class StagingConfig {
static ServiceEnvironmentConfig getServiceEnvironmentConfig(List<Interceptor> interceptors) { static ServiceEnvironmentConfig getServiceEnvironmentConfig(List<Interceptor> interceptors) {
return new ServiceEnvironmentConfig(STAGING, return new ServiceEnvironmentConfig(STAGING,
LIBSIGNAL_NET_ENV,
createDefaultServiceConfiguration(interceptors), createDefaultServiceConfiguration(interceptors),
getUnidentifiedSenderTrustRoot(), getUnidentifiedSenderTrustRoot(),
CDSI_MRENCLAVE, CDSI_MRENCLAVE,
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_DEPRECATED)); List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_LEGACY_LEGACY));
} }
private StagingConfig() { private StagingConfig() {

View file

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

View file

@ -3,10 +3,11 @@ package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.DeviceLinkUrl; import org.asamk.signal.manager.api.DeviceLinkUrl;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
import org.asamk.signal.manager.internal.SignalDependencies; import org.asamk.signal.manager.internal.SignalDependencies;
import org.asamk.signal.manager.jobs.SyncStorageJob; import org.asamk.signal.manager.jobs.SyncStorageJob;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
@ -26,11 +27,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest; import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; 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.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.PNI; import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity; import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException; import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
@ -38,6 +41,7 @@ import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotReserve
import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException; import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException; import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.DeviceNameUtil;
import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException;
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity; import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage; import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
import org.whispersystems.signalservice.internal.push.SyncMessage; import org.whispersystems.signalservice.internal.push.SyncMessage;
@ -48,12 +52,13 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import okio.ByteString; import okio.ByteString;
import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID; 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; import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
public class AccountHelper { public class AccountHelper {
@ -99,9 +104,9 @@ public class AccountHelper {
checkWhoAmiI(); checkWhoAmiI();
} }
if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) { if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
context.getSyncHelper().requestSyncPniIdentity(); throw new IOException("Missing PNI identity key, relinking required");
} }
if (account.getPreviousStorageVersion() < 4 if (account.getPreviousStorageVersion() < 10
&& account.isPrimaryDevice() && account.isPrimaryDevice()
&& account.getRegistrationLockPin() != null) { && account.getRegistrationLockPin() != null) {
migrateRegistrationPin(); migrateRegistrationPin();
@ -163,20 +168,25 @@ public class AccountHelper {
} }
public void startChangeNumber( public void startChangeNumber(
String newNumber, boolean voiceVerification, String captcha String newNumber,
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException { boolean voiceVerification,
String captcha
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException {
final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword()); final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword());
String sessionId = NumberVerificationUtils.handleVerificationSession(accountManager, final var registrationApi = accountManager.getRegistrationApi();
String sessionId = NumberVerificationUtils.handleVerificationSession(registrationApi,
account.getSessionId(newNumber), account.getSessionId(newNumber),
id -> account.setSessionId(newNumber, id), id -> account.setSessionId(newNumber, id),
voiceVerification, voiceVerification,
captcha); captcha);
NumberVerificationUtils.requestVerificationCode(accountManager, sessionId, voiceVerification); NumberVerificationUtils.requestVerificationCode(registrationApi, sessionId, voiceVerification);
} }
public void finishChangeNumber( public void finishChangeNumber(
String newNumber, String verificationCode, String pin String newNumber,
) throws IncorrectPinException, PinLockedException, IOException { String verificationCode,
String pin
) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
for (var attempts = 0; attempts < 5; attempts++) { for (var attempts = 0; attempts < 5; attempts++) {
try { try {
finishChangeNumberInternal(newNumber, verificationCode, pin); finishChangeNumberInternal(newNumber, verificationCode, pin);
@ -193,8 +203,10 @@ public class AccountHelper {
} }
private void finishChangeNumberInternal( private void finishChangeNumberInternal(
String newNumber, String verificationCode, String pin String newNumber,
) throws IncorrectPinException, PinLockedException, IOException { String verificationCode,
String pin
) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
final var pniIdentity = KeyUtils.generateIdentityKeyPair(); final var pniIdentity = KeyUtils.generateIdentityKeyPair();
final var encryptedDeviceMessages = new ArrayList<OutgoingPushMessage>(); final var encryptedDeviceMessages = new ArrayList<OutgoingPushMessage>();
final var devicePniSignedPreKeys = new HashMap<Integer, SignedPreKeyEntity>(); final var devicePniSignedPreKeys = new HashMap<Integer, SignedPreKeyEntity>();
@ -218,20 +230,30 @@ public class AccountHelper {
final var messageSender = dependencies.getMessageSender(); final var messageSender = dependencies.getMessageSender();
for (final var deviceId : deviceIds) { for (final var deviceId : deviceIds) {
// Signed Prekey // Signed Prekey
final var signedPreKeyRecord = KeyUtils.generateSignedPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID), final SignedPreKeyRecord signedPreKeyRecord;
pniIdentity.getPrivateKey()); try {
final var signedPreKeyEntity = new SignedPreKeyEntity(signedPreKeyRecord.getId(), signedPreKeyRecord = KeyUtils.generateSignedPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
signedPreKeyRecord.getKeyPair().getPublicKey(), pniIdentity.getPrivateKey());
signedPreKeyRecord.getSignature()); final var signedPreKeyEntity = new SignedPreKeyEntity(signedPreKeyRecord.getId(),
devicePniSignedPreKeys.put(deviceId, signedPreKeyEntity); signedPreKeyRecord.getKeyPair().getPublicKey(),
signedPreKeyRecord.getSignature());
devicePniSignedPreKeys.put(deviceId, signedPreKeyEntity);
} catch (InvalidKeyException e) {
throw new AssertionError("unexpected invalid key", e);
}
// Last-resort kyber prekey // Last-resort kyber prekey
final var lastResortKyberPreKeyRecord = KeyUtils.generateKyberPreKeyRecord(KeyUtils.getRandomInt( final KyberPreKeyRecord lastResortKyberPreKeyRecord;
PREKEY_MAXIMUM_ID), pniIdentity.getPrivateKey()); try {
final var kyberPreKeyEntity = new KyberPreKeyEntity(lastResortKyberPreKeyRecord.getId(), lastResortKyberPreKeyRecord = KeyUtils.generateKyberPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
lastResortKyberPreKeyRecord.getKeyPair().getPublicKey(), pniIdentity.getPrivateKey());
lastResortKyberPreKeyRecord.getSignature()); final var kyberPreKeyEntity = new KyberPreKeyEntity(lastResortKyberPreKeyRecord.getId(),
devicePniLastResortKyberPreKeys.put(deviceId, kyberPreKeyEntity); lastResortKyberPreKeyRecord.getKeyPair().getPublicKey(),
lastResortKyberPreKeyRecord.getSignature());
devicePniLastResortKyberPreKeys.put(deviceId, kyberPreKeyEntity);
} catch (InvalidKeyException e) {
throw new AssertionError("unexpected invalid key", e);
}
// Registration Id // Registration Id
var pniRegistrationId = -1; var pniRegistrationId = -1;
@ -268,14 +290,14 @@ public class AccountHelper {
pin, pin,
context.getPinHelper(), context.getPinHelper(),
(sessionId1, verificationCode1, registrationLock) -> { (sessionId1, verificationCode1, registrationLock) -> {
final var accountManager = dependencies.getAccountManager(); final var registrationApi = dependencies.getRegistrationApi();
final var accountApi = dependencies.getAccountApi();
try { try {
Utils.handleResponseException(accountManager.verifyAccount(verificationCode1, sessionId1)); handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1));
} catch (AlreadyVerifiedException e) { } catch (AlreadyVerifiedException e) {
// Already verified so can continue changing number // Already verified so can continue changing number
} }
return Utils.handleResponseException(accountManager.changeNumber(new ChangePhoneNumberRequest( return handleResponseException(accountApi.changeNumber(new ChangePhoneNumberRequest(sessionId1,
sessionId1,
null, null,
newNumber, newNumber,
registrationLock, registrationLock,
@ -295,9 +317,7 @@ public class AccountHelper {
handlePniChangeNumberMessage(selfChangeNumber, updatePni); handlePniChangeNumberMessage(selfChangeNumber, updatePni);
} }
public void handlePniChangeNumberMessage( public void handlePniChangeNumberMessage(final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni) {
final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni
) {
if (pniChangeNumber.identityKeyPair != null if (pniChangeNumber.identityKeyPair != null
&& pniChangeNumber.registrationId != null && pniChangeNumber.registrationId != null
&& pniChangeNumber.signedPreKey != null) { && pniChangeNumber.signedPreKey != null) {
@ -320,27 +340,48 @@ public class AccountHelper {
public static final int USERNAME_MIN_LENGTH = 3; public static final int USERNAME_MIN_LENGTH = 3;
public static final int USERNAME_MAX_LENGTH = 32; public static final int USERNAME_MAX_LENGTH = 32;
public void reserveUsername(String nickname) throws IOException, BaseUsernameException { public void reserveUsernameFromNickname(String nickname) throws IOException, BaseUsernameException {
final var currentUsername = account.getUsername(); final var currentUsername = account.getUsername();
if (currentUsername != null) { if (currentUsername != null) {
final var currentNickname = currentUsername.substring(0, currentUsername.indexOf('.')); final var currentNickname = currentUsername.substring(0, currentUsername.indexOf('.'));
if (currentNickname.equals(nickname)) { if (currentNickname.equals(nickname)) {
try { try {
refreshCurrentUsername(); refreshCurrentUsername();
return;
} catch (IOException | BaseUsernameException e) { } catch (IOException | BaseUsernameException e) {
logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username"); logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
} }
return;
} }
} }
final var candidates = Username.candidatesFrom(nickname, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH); final var candidates = Username.candidatesFrom(nickname, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH);
reserveUsername(candidates);
}
public void reserveExactUsername(String username) throws IOException, BaseUsernameException {
final var currentUsername = account.getUsername();
if (currentUsername != null) {
if (currentUsername.equals(username)) {
try {
refreshCurrentUsername();
return;
} catch (IOException | BaseUsernameException e) {
logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
}
}
}
final var candidates = List.of(new Username(username));
reserveUsername(candidates);
}
private void reserveUsername(final List<Username> candidates) throws IOException {
final var candidateHashes = new ArrayList<String>(); final var candidateHashes = new ArrayList<String>();
for (final var candidate : candidates) { for (final var candidate : candidates) {
candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash())); candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash()));
} }
final var response = dependencies.getAccountManager().reserveUsername(candidateHashes); final var response = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes));
final var hashIndex = candidateHashes.indexOf(response.getUsernameHash()); final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
if (hashIndex == -1) { if (hashIndex == -1) {
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes."); logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
@ -350,13 +391,48 @@ public class AccountHelper {
logger.debug("[reserveUsername] Successfully reserved username."); logger.debug("[reserveUsername] Successfully reserved username.");
final var username = candidates.get(hashIndex); final var username = candidates.get(hashIndex);
final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username); final var linkComponents = confirmUsernameAndCreateNewLink(username);
account.setUsername(username.getUsername()); account.setUsername(username.getUsername());
account.setUsernameLink(linkComponents); account.setUsernameLink(linkComponents);
account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress()); account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
account.getRecipientStore().rotateSelfStorageId();
logger.debug("[confirmUsername] Successfully confirmed username."); logger.debug("[confirmUsername] Successfully confirmed username.");
} }
public UsernameLinkComponents createUsernameLink(Username username) throws IOException {
try {
Username.UsernameLink link = username.generateLink();
return handleResponseException(dependencies.getAccountApi().createUsernameLink(link));
} catch (BaseUsernameException e) {
throw new AssertionError(e);
}
}
private UsernameLinkComponents confirmUsernameAndCreateNewLink(Username username) throws IOException {
try {
Username.UsernameLink link = username.generateLink();
UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link));
return new UsernameLinkComponents(link.getEntropy(), serverId);
} catch (BaseUsernameException e) {
throw new AssertionError(e);
}
}
private UsernameLinkComponents reclaimUsernameAndLink(
Username username,
UsernameLinkComponents linkComponents
) throws IOException {
try {
Username.UsernameLink link = username.generateLink(linkComponents.getEntropy());
UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link));
return new UsernameLinkComponents(link.getEntropy(), serverId);
} catch (BaseUsernameException e) {
throw new AssertionError(e);
}
}
public void refreshCurrentUsername() throws IOException, BaseUsernameException { public void refreshCurrentUsername() throws IOException, BaseUsernameException {
final var localUsername = account.getUsername(); final var localUsername = account.getUsername();
if (localUsername == null) { if (localUsername == null) {
@ -387,6 +463,7 @@ public class AccountHelper {
e.getClass().getSimpleName()); e.getClass().getSimpleName());
account.setUsername(null); account.setUsername(null);
account.setUsernameLink(null); account.setUsernameLink(null);
account.getRecipientStore().rotateSelfStorageId();
throw e; throw e;
} }
} else { } else {
@ -398,23 +475,24 @@ public class AccountHelper {
final var usernameLink = account.getUsernameLink(); final var usernameLink = account.getUsernameLink();
if (usernameLink == null) { if (usernameLink == null) {
dependencies.getAccountManager() handleResponseException(dependencies.getAccountApi()
.reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash()))); .reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash()))));
logger.debug("[reserveUsername] Successfully reserved existing username."); logger.debug("[reserveUsername] Successfully reserved existing username.");
final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username); final var linkComponents = confirmUsernameAndCreateNewLink(username);
account.setUsernameLink(linkComponents); account.setUsernameLink(linkComponents);
logger.debug("[confirmUsername] Successfully confirmed existing username."); logger.debug("[confirmUsername] Successfully confirmed existing username.");
} else { } else {
final var linkComponents = dependencies.getAccountManager().reclaimUsernameAndLink(username, usernameLink); final var linkComponents = reclaimUsernameAndLink(username, usernameLink);
account.setUsernameLink(linkComponents); account.setUsernameLink(linkComponents);
logger.debug("[confirmUsername] Successfully reclaimed existing username and link."); logger.debug("[confirmUsername] Successfully reclaimed existing username and link.");
} }
account.getRecipientStore().rotateSelfStorageId();
} }
private void tryToSetUsernameLink(Username username) { private void tryToSetUsernameLink(Username username) {
for (var i = 1; i < 4; i++) { for (var i = 1; i < 4; i++) {
try { try {
final var linkComponents = dependencies.getAccountManager().createUsernameLink(username); final var linkComponents = createUsernameLink(username);
account.setUsernameLink(linkComponents); account.setUsernameLink(linkComponents);
break; break;
} catch (IOException e) { } catch (IOException e) {
@ -424,7 +502,8 @@ public class AccountHelper {
} }
public void deleteUsername() throws IOException { public void deleteUsername() throws IOException {
dependencies.getAccountManager().deleteUsername(); handleResponseException(dependencies.getAccountApi().deleteUsername());
account.setUsernameLink(null);
account.setUsername(null); account.setUsername(null);
logger.debug("[deleteUsername] Successfully deleted the username."); logger.debug("[deleteUsername] Successfully deleted the username.");
} }
@ -436,31 +515,39 @@ public class AccountHelper {
} }
public void updateAccountAttributes() throws IOException { public void updateAccountAttributes() throws IOException {
dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null)); handleResponseException(dependencies.getAccountApi().setAccountAttributes(account.getAccountAttributes(null)));
} }
public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, InvalidDeviceLinkException { public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, org.asamk.signal.manager.api.DeviceLimitExceededException {
var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); final var linkDeviceApi = dependencies.getLinkDeviceApi();
final LinkedDeviceVerificationCodeResponse verificationCode;
try { try {
dependencies.getAccountManager() verificationCode = handleResponseException(linkDeviceApi.getDeviceVerificationCode());
.addDevice(deviceLinkInfo.deviceIdentifier(), } catch (DeviceLimitExceededException e) {
deviceLinkInfo.deviceKey(), throw new org.asamk.signal.manager.api.DeviceLimitExceededException("Too many linked devices", e);
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); account.setMultiDevice(true);
context.getJobExecutor().enqueueJob(new SyncStorageJob()); context.getJobExecutor().enqueueJob(new SyncStorageJob());
} }
public void removeLinkedDevices(int deviceId) throws IOException { public void removeLinkedDevices(int deviceId) throws IOException {
dependencies.getAccountManager().removeDevice(deviceId); handleResponseException(dependencies.getLinkDeviceApi().removeDevice(deviceId));
var devices = dependencies.getAccountManager().getDevices(); var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
account.setMultiDevice(devices.size() > 1); account.setMultiDevice(devices.size() > 1);
} }
@ -468,22 +555,25 @@ public class AccountHelper {
var masterKey = account.getOrCreatePinMasterKey(); var masterKey = account.getOrCreatePinMasterKey();
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey); context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
dependencies.getAccountManager().enableRegistrationLock(masterKey); handleResponseException(dependencies.getAccountApi()
.enableRegistrationLock(masterKey.deriveRegistrationLock()));
} }
public void setRegistrationPin(String pin) throws IOException { public void setRegistrationPin(String pin) throws IOException {
var masterKey = account.getOrCreatePinMasterKey(); var masterKey = account.getOrCreatePinMasterKey();
context.getPinHelper().setRegistrationLockPin(pin, masterKey); context.getPinHelper().setRegistrationLockPin(pin, masterKey);
dependencies.getAccountManager().enableRegistrationLock(masterKey); handleResponseException(dependencies.getAccountApi()
.enableRegistrationLock(masterKey.deriveRegistrationLock()));
account.setRegistrationLockPin(pin); account.setRegistrationLockPin(pin);
updateAccountAttributes();
} }
public void removeRegistrationPin() throws IOException { public void removeRegistrationPin() throws IOException {
// Remove KBS Pin // Remove KBS Pin
context.getPinHelper().removeRegistrationLockPin(); context.getPinHelper().removeRegistrationLockPin();
dependencies.getAccountManager().disableRegistrationLock(); handleResponseException(dependencies.getAccountApi().disableRegistrationLock());
account.setRegistrationLockPin(null); account.setRegistrationLockPin(null);
} }
@ -492,7 +582,7 @@ public class AccountHelper {
// When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
// If this is the primary device, other users can't send messages to this number anymore. // If this is the primary device, other users can't send messages to this number anymore.
// If this is a linked device, other users can still send messages, but this device doesn't receive them anymore. // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
dependencies.getAccountManager().setGcmId(Optional.empty()); handleResponseException(dependencies.getAccountApi().clearFcmToken());
account.setRegistered(false); account.setRegistered(false);
unregisteredListener.call(); unregisteredListener.call();
@ -506,7 +596,7 @@ public class AccountHelper {
} }
account.setRegistrationLockPin(null); account.setRegistrationLockPin(null);
dependencies.getAccountManager().deleteAccount(); handleResponseException(dependencies.getAccountApi().deleteAccount());
account.setRegistered(false); account.setRegistered(false);
unregisteredListener.call(); unregisteredListener.call();

View file

@ -9,6 +9,7 @@ import org.asamk.signal.manager.util.IOUtils;
import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.protocol.InvalidMessageException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
@ -44,18 +45,37 @@ public class AttachmentHelper {
} }
public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException { public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
var attachmentStreams = AttachmentUtils.createAttachmentStreams(attachments); final var attachmentStreams = createAttachmentStreams(attachments);
// Upload attachments here, so we only upload once even for multiple recipients try {
var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size()); // Upload attachments here, so we only upload once even for multiple recipients
for (var attachmentStream : attachmentStreams) { final var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size());
attachmentPointers.add(uploadAttachment(attachmentStream)); for (final var attachmentStream : attachmentStreams) {
attachmentPointers.add(uploadAttachment(attachmentStream));
}
return attachmentPointers;
} finally {
for (final var attachmentStream : attachmentStreams) {
attachmentStream.close();
}
} }
return attachmentPointers; }
private List<SignalServiceAttachmentStream> createAttachmentStreams(List<String> attachments) throws AttachmentInvalidException, IOException {
if (attachments == null) {
return null;
}
final var signalServiceAttachments = new ArrayList<SignalServiceAttachmentStream>(attachments.size());
for (var attachment : attachments) {
final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
signalServiceAttachments.add(AttachmentUtils.createAttachmentStream(attachment, uploadSpec));
}
return signalServiceAttachments;
} }
public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException { public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException {
var attachmentStream = AttachmentUtils.createAttachmentStream(attachment); final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
var attachmentStream = AttachmentUtils.createAttachmentStream(attachment, uploadSpec);
return uploadAttachment(attachmentStream); return uploadAttachment(attachmentStream);
} }
@ -91,9 +111,7 @@ public class AttachmentHelper {
retrieveAttachment(attachment, input -> IOUtils.copyStream(input, outputStream)); retrieveAttachment(attachment, input -> IOUtils.copyStream(input, outputStream));
} }
public void retrieveAttachment( public void retrieveAttachment(SignalServiceAttachment attachment, AttachmentHandler consumer) throws IOException {
SignalServiceAttachment attachment, AttachmentHandler consumer
) throws IOException {
if (attachment.isStream()) { if (attachment.isStream()) {
var input = attachment.asStream().getInputStream(); var input = attachment.asStream().getInputStream();
// don't close input stream here, it might be reused later (e.g. with contact sync messages ...) // don't close input stream here, it might be reused later (e.g. with contact sync messages ...)
@ -118,11 +136,18 @@ public class AttachmentHelper {
} }
private InputStream retrieveAttachmentAsStream( private InputStream retrieveAttachmentAsStream(
SignalServiceAttachmentPointer pointer, File tmpFile SignalServiceAttachmentPointer pointer,
File tmpFile
) throws IOException { ) throws IOException {
if (pointer.getDigest().isEmpty()) {
throw new IOException("Attachment pointer has no digest.");
}
try { try {
return dependencies.getMessageReceiver() return dependencies.getMessageReceiver()
.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); .retrieveAttachment(pointer,
tmpFile,
ServiceConfig.MAX_ATTACHMENT_SIZE,
AttachmentCipherInputStream.IntegrityCheck.forEncryptedDigest(pointer.getDigest().get()));
} catch (MissingConfigurationException | InvalidMessageException e) { } catch (MissingConfigurationException | InvalidMessageException e) {
throw new IOException(e); throw new IOException(e);
} }

View file

@ -17,7 +17,14 @@ public class ContactHelper {
return sourceContact != null && sourceContact.isBlocked(); return sourceContact != null && sourceContact.isBlocked();
} }
public void setContactName(final RecipientId recipientId, final String givenName, final String familyName) { public void setContactName(
final RecipientId recipientId,
final String givenName,
final String familyName,
final String nickGivenName,
final String nickFamilyName,
final String note
) {
var contact = account.getContactStore().getContact(recipientId); var contact = account.getContactStore().getContact(recipientId);
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
builder.withIsHidden(false); builder.withIsHidden(false);
@ -27,6 +34,15 @@ public class ContactHelper {
if (familyName != null) { if (familyName != null) {
builder.withFamilyName(familyName); builder.withFamilyName(familyName);
} }
if (nickGivenName != null) {
builder.withNickNameGivenName(nickGivenName);
}
if (nickFamilyName != null) {
builder.withNickNameFamilyName(nickFamilyName);
}
if (note != null) {
builder.withNote(note);
}
account.getContactStore().storeContact(recipientId, builder.build()); account.getContactStore().storeContact(recipientId, builder.build());
} }
@ -36,8 +52,36 @@ public class ContactHelper {
return; return;
} }
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
final var version = contact == null
? 1
: contact.messageExpirationTimeVersion() == Integer.MAX_VALUE
? Integer.MAX_VALUE
: contact.messageExpirationTimeVersion() + 1;
account.getContactStore() account.getContactStore()
.storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build()); .storeContact(recipientId,
builder.withMessageExpirationTime(messageExpirationTimer)
.withMessageExpirationTimeVersion(version)
.build());
}
public void setExpirationTimer(
RecipientId recipientId,
int messageExpirationTimer,
int messageExpirationTimerVersion
) {
var contact = account.getContactStore().getContact(recipientId);
if (contact != null && (
contact.messageExpirationTime() == messageExpirationTimer
|| contact.messageExpirationTimeVersion() >= messageExpirationTimerVersion
)) {
return;
}
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
account.getContactStore()
.storeContact(recipientId,
builder.withMessageExpirationTime(messageExpirationTimer)
.withMessageExpirationTimeVersion(messageExpirationTimerVersion)
.build());
} }
public void setContactBlocked(RecipientId recipientId, boolean blocked) { public void setContactBlocked(RecipientId recipientId, boolean blocked) {

View file

@ -114,7 +114,7 @@ public class Context implements AutoCloseable {
} }
PinHelper getPinHelper() { PinHelper getPinHelper() {
return getOrCreate(() -> pinHelper, () -> pinHelper = new PinHelper(dependencies.getSecureValueRecoveryV2())); return getOrCreate(() -> pinHelper, () -> pinHelper = new PinHelper(dependencies.getSecureValueRecovery()));
} }
public PreKeyHelper getPreKeyHelper() { public PreKeyHelper getPreKeyHelper() {

View file

@ -33,13 +33,16 @@ import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams; import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.GroupChangeResponse;
import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo; import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupChangeLog;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupResponse;
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
import org.whispersystems.signalservice.api.groupsv2.ReceivedGroupSendEndorsements;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
@ -79,6 +82,16 @@ public class GroupHelper {
return getGroup(groupId, false); return getGroup(groupId, false);
} }
public void updateGroupSendEndorsements(GroupId groupId) {
getGroup(groupId, true);
}
public List<GroupInfo> getGroups() {
final var groups = account.getGroupStore().getGroups();
groups.forEach(group -> fillOrUpdateGroup(group, false));
return groups;
}
public boolean isGroupBlocked(final GroupId groupId) { public boolean isGroupBlocked(final GroupId groupId) {
var group = getGroup(groupId); var group = getGroup(groupId);
return group != null && group.isBlocked(); return group != null && group.isBlocked();
@ -100,11 +113,14 @@ public class GroupHelper {
return Optional.empty(); return Optional.empty();
} }
return Optional.of(AttachmentUtils.createAttachmentStream(streamDetails, Optional.empty())); final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
return Optional.of(AttachmentUtils.createAttachmentStream(streamDetails, Optional.empty(), uploadSpec));
} }
public GroupInfoV2 getOrMigrateGroup( 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); final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
@ -127,9 +143,10 @@ public class GroupHelper {
} }
if (group == null) { if (group == null) {
try { try {
group = context.getGroupV2Helper().getDecryptedGroup(groupSecretParams); final var response = context.getGroupV2Helper().getDecryptedGroup(groupSecretParams);
if (group != null) { if (response != null) {
group = handleDecryptedGroupResponse(groupInfoV2, response);
storeProfileKeysFromHistory(groupSecretParams, groupInfoV2, group); storeProfileKeysFromHistory(groupSecretParams, groupInfoV2, group);
} }
} catch (NotAGroupMemberException ignored) { } catch (NotAGroupMemberException ignored) {
@ -150,8 +167,41 @@ public class GroupHelper {
return groupInfoV2; return groupInfoV2;
} }
private DecryptedGroup handleDecryptedGroupResponse(
GroupInfoV2 groupInfoV2,
final DecryptedGroupResponse decryptedGroupResponse
) {
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
ReceivedGroupSendEndorsements groupSendEndorsements = dependencies.getGroupsV2Operations()
.forGroup(groupSecretParams)
.receiveGroupSendEndorsements(account.getAci(),
decryptedGroupResponse.getGroup(),
decryptedGroupResponse.getGroupSendEndorsementsResponse());
// TODO save group endorsements
return decryptedGroupResponse.getGroup();
}
private GroupChange handleGroupChangeResponse(
final GroupInfoV2 groupInfoV2,
final GroupChangeResponse groupChangeResponse
) {
ReceivedGroupSendEndorsements groupSendEndorsements = dependencies.getGroupsV2Operations()
.forGroup(GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()))
.receiveGroupSendEndorsements(account.getAci(),
groupInfoV2.getGroup(),
groupChangeResponse.groupSendEndorsementsResponse);
// TODO save group endorsements
return groupChangeResponse.groupChange;
}
public Pair<GroupId, SendGroupMessageResults> createGroup( public Pair<GroupId, SendGroupMessageResults> createGroup(
String name, Set<RecipientId> members, String avatarFile String name,
Set<RecipientId> members,
String avatarFile
) throws IOException, AttachmentInvalidException { ) throws IOException, AttachmentInvalidException {
final var selfRecipientId = account.getSelfRecipientId(); final var selfRecipientId = account.getSelfRecipientId();
if (members != null && members.contains(selfRecipientId)) { if (members != null && members.contains(selfRecipientId)) {
@ -175,7 +225,7 @@ public class GroupHelper {
final var gv2 = gv2Pair.first(); final var gv2 = gv2Pair.first();
final var decryptedGroup = gv2Pair.second(); final var decryptedGroup = gv2Pair.second();
gv2.setGroup(decryptedGroup); gv2.setGroup(handleDecryptedGroupResponse(gv2, decryptedGroup));
gv2.setProfileSharingEnabled(true); gv2.setProfileSharingEnabled(true);
if (avatarBytes != null) { if (avatarBytes != null) {
context.getAvatarStore() context.getAvatarStore()
@ -271,7 +321,7 @@ public class GroupHelper {
var group = getGroupForUpdating(groupId); var group = getGroupForUpdating(groupId);
if (group instanceof GroupInfoV2 groupInfoV2) { if (group instanceof GroupInfoV2 groupInfoV2) {
Pair<DecryptedGroup, GroupChange> groupChangePair; Pair<DecryptedGroup, GroupChangeResponse> groupChangePair;
try { try {
groupChangePair = context.getGroupV2Helper().updateSelfProfileKey(groupInfoV2); groupChangePair = context.getGroupV2Helper().updateSelfProfileKey(groupInfoV2);
} catch (ConflictException e) { } catch (ConflictException e) {
@ -280,7 +330,9 @@ public class GroupHelper {
groupChangePair = context.getGroupV2Helper().updateSelfProfileKey(groupInfoV2); groupChangePair = context.getGroupV2Helper().updateSelfProfileKey(groupInfoV2);
} }
if (groupChangePair != null) { if (groupChangePair != null) {
sendUpdateGroupV2Message(groupInfoV2, groupChangePair.first(), groupChangePair.second()); sendUpdateGroupV2Message(groupInfoV2,
groupChangePair.first(),
handleGroupChangeResponse(groupInfoV2, groupChangePair.second()));
} }
} }
} }
@ -298,11 +350,12 @@ public class GroupHelper {
if (groupJoinInfo.pendingAdminApproval) { if (groupJoinInfo.pendingAdminApproval) {
throw new PendingAdminApprovalException("You have already requested to join the group."); throw new PendingAdminApprovalException("You have already requested to join the group.");
} }
final var groupChange = context.getGroupV2Helper() final var changeResponse = context.getGroupV2Helper()
.joinGroup(inviteLinkUrl.getGroupMasterKey(), inviteLinkUrl.getPassword(), groupJoinInfo); .joinGroup(inviteLinkUrl.getGroupMasterKey(), inviteLinkUrl.getPassword(), groupJoinInfo);
final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(), final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(),
groupJoinInfo.revision + 1, groupJoinInfo.revision + 1,
groupChange.encode()); changeResponse.groupChange == null ? null : changeResponse.groupChange.encode());
final var groupChange = handleGroupChangeResponse(group, changeResponse);
if (group.getGroup() == null) { if (group.getGroup() == null) {
// Only requested member, can't send update to group members // Only requested member, can't send update to group members
@ -316,7 +369,8 @@ public class GroupHelper {
} }
public SendGroupMessageResults quitGroup( public SendGroupMessageResults quitGroup(
final GroupId groupId, final Set<RecipientId> newAdmins final GroupId groupId,
final Set<RecipientId> newAdmins
) throws IOException, LastGroupAdminException, NotAGroupMemberException, GroupNotFoundException { ) throws IOException, LastGroupAdminException, NotAGroupMemberException, GroupNotFoundException {
var group = getGroupForUpdating(groupId); var group = getGroupForUpdating(groupId);
if (group instanceof GroupInfoV1) { if (group instanceof GroupInfoV1) {
@ -349,9 +403,7 @@ public class GroupHelper {
context.getJobExecutor().enqueueJob(new SyncStorageJob()); context.getJobExecutor().enqueueJob(new SyncStorageJob());
} }
public SendGroupMessageResults sendGroupInfoRequest( public SendGroupMessageResults sendGroupInfoRequest(GroupIdV1 groupId, RecipientId recipientId) throws IOException {
GroupIdV1 groupId, RecipientId recipientId
) throws IOException {
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize()); var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize());
var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build()); var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build());
@ -361,7 +413,8 @@ public class GroupHelper {
} }
public SendGroupMessageResults sendGroupInfoMessage( public SendGroupMessageResults sendGroupInfoMessage(
GroupIdV1 groupId, RecipientId recipientId GroupIdV1 groupId,
RecipientId recipientId
) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
GroupInfoV1 g; GroupInfoV1 g;
var group = getGroupForUpdating(groupId); var group = getGroupForUpdating(groupId);
@ -382,34 +435,46 @@ public class GroupHelper {
private GroupInfo getGroup(GroupId groupId, boolean forceUpdate) { private GroupInfo getGroup(GroupId groupId, boolean forceUpdate) {
final var group = account.getGroupStore().getGroup(groupId); final var group = account.getGroupStore().getGroup(groupId);
if (group instanceof GroupInfoV2 groupInfoV2) { fillOrUpdateGroup(group, forceUpdate);
if (forceUpdate || (!groupInfoV2.isPermissionDenied() && groupInfoV2.getGroup() == null)) {
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
DecryptedGroup decryptedGroup;
try {
decryptedGroup = context.getGroupV2Helper().getDecryptedGroup(groupSecretParams);
} catch (NotAGroupMemberException e) {
groupInfoV2.setPermissionDenied(true);
decryptedGroup = null;
}
if (decryptedGroup != null) {
try {
storeProfileKeysFromHistory(groupSecretParams, groupInfoV2, decryptedGroup);
} catch (NotAGroupMemberException ignored) {
}
storeProfileKeysFromMembers(decryptedGroup);
final var avatar = decryptedGroup.avatar;
if (!avatar.isEmpty()) {
downloadGroupAvatar(groupInfoV2.getGroupId(), groupSecretParams, avatar);
}
}
groupInfoV2.setGroup(decryptedGroup);
account.getGroupStore().updateGroup(group);
}
}
return group; return group;
} }
private void fillOrUpdateGroup(final GroupInfo group, final boolean forceUpdate) {
if (!(group instanceof GroupInfoV2 groupInfoV2)) {
return;
}
if (!forceUpdate && (groupInfoV2.isPermissionDenied() || groupInfoV2.getGroup() != null)) {
return;
}
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
DecryptedGroup decryptedGroup;
try {
final var response = context.getGroupV2Helper().getDecryptedGroup(groupSecretParams);
if (response == null) {
return;
}
decryptedGroup = handleDecryptedGroupResponse(groupInfoV2, response);
} catch (NotAGroupMemberException e) {
groupInfoV2.setPermissionDenied(true);
account.getGroupStore().updateGroup(group);
return;
}
try {
storeProfileKeysFromHistory(groupSecretParams, groupInfoV2, decryptedGroup);
} catch (NotAGroupMemberException ignored) {
}
storeProfileKeysFromMembers(decryptedGroup);
final var avatar = decryptedGroup.avatar;
if (!avatar.isEmpty()) {
downloadGroupAvatar(groupInfoV2.getGroupId(), groupSecretParams, avatar);
}
groupInfoV2.setGroup(decryptedGroup);
account.getGroupStore().updateGroup(group);
}
private void downloadGroupAvatar(GroupIdV2 groupId, GroupSecretParams groupSecretParams, String cdnKey) { private void downloadGroupAvatar(GroupIdV2 groupId, GroupSecretParams groupSecretParams, String cdnKey) {
try { try {
context.getAvatarStore() context.getAvatarStore()
@ -421,7 +486,9 @@ public class GroupHelper {
} }
private void retrieveGroupV2Avatar( private void retrieveGroupV2Avatar(
GroupSecretParams groupSecretParams, String cdnKey, OutputStream outputStream GroupSecretParams groupSecretParams,
String cdnKey,
OutputStream outputStream
) throws IOException { ) throws IOException {
var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams); var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
@ -478,15 +545,19 @@ public class GroupHelper {
) throws NotAGroupMemberException { ) throws NotAGroupMemberException {
final var revisionWeWereAdded = context.getGroupV2Helper().findRevisionWeWereAdded(newDecryptedGroup); final var revisionWeWereAdded = context.getGroupV2Helper().findRevisionWeWereAdded(newDecryptedGroup);
final var localRevision = localGroup.getGroup() == null ? 0 : localGroup.getGroup().revision; final var localRevision = localGroup.getGroup() == null ? 0 : localGroup.getGroup().revision;
final var sendEndorsementsExpirationMs = 0L;// TODO store expiration localGroup.getGroup() == null ? 0 : localGroup.getGroup().revision;
var fromRevision = Math.max(revisionWeWereAdded, localRevision); var fromRevision = Math.max(revisionWeWereAdded, localRevision);
final var newProfileKeys = new HashMap<RecipientId, ProfileKey>(); final var newProfileKeys = new HashMap<RecipientId, ProfileKey>();
while (true) { while (true) {
final var page = context.getGroupV2Helper().getDecryptedGroupHistoryPage(groupSecretParams, fromRevision); final var page = context.getGroupV2Helper()
page.getResults() .getDecryptedGroupHistoryPage(groupSecretParams, fromRevision, sendEndorsementsExpirationMs);
if (page == null) {
break;
}
page.getChangeLogs()
.stream() .stream()
.map(DecryptedGroupHistoryEntry::getChange) .map(DecryptedGroupChangeLog::getChange)
.filter(Optional::isPresent) .filter(Objects::nonNull)
.map(Optional::get)
.map(context.getGroupV2Helper()::getAuthoritativeProfileKeyFromChange) .map(context.getGroupV2Helper()::getAuthoritativeProfileKeyFromChange)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.forEach(p -> { .forEach(p -> {
@ -495,13 +566,16 @@ public class GroupHelper {
final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId); final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId);
newProfileKeys.put(recipientId, profileKey); newProfileKeys.put(recipientId, profileKey);
}); });
if (!page.getPagingData().hasMorePages()) { if (!page.getPagingData().getHasMorePages()) {
break; break;
} }
fromRevision = page.getPagingData().getNextPageRevision(); fromRevision = page.getPagingData().getNextPageRevision();
} }
newProfileKeys.forEach(account.getProfileStore()::storeProfileKey); newProfileKeys.entrySet()
.stream()
.filter(entry -> account.getProfileStore().getProfileKey(entry.getKey()) == null)
.forEach(entry -> account.getProfileStore().storeProfileKey(entry.getKey(), entry.getValue()));
} }
private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
@ -520,7 +594,10 @@ public class GroupHelper {
} }
private SendGroupMessageResults updateGroupV1( 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 { ) throws IOException, AttachmentInvalidException {
updateGroupV1Details(gv1, name, members, avatarFile); updateGroupV1Details(gv1, name, members, avatarFile);
@ -533,7 +610,10 @@ public class GroupHelper {
} }
private void updateGroupV1Details( 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 { ) throws IOException {
if (name != null) { if (name != null) {
g.name = name; g.name = name;
@ -552,7 +632,8 @@ public class GroupHelper {
* Change the expiration timer for a group * Change the expiration timer for a group
*/ */
private void setExpirationTimer( private void setExpirationTimer(
GroupInfoV1 groupInfoV1, int messageExpirationTimer GroupInfoV1 groupInfoV1,
int messageExpirationTimer
) throws NotAGroupMemberException, GroupNotFoundException, IOException, GroupSendingNotAllowedException { ) throws NotAGroupMemberException, GroupNotFoundException, IOException, GroupSendingNotAllowedException {
groupInfoV1.messageExpirationTime = messageExpirationTimer; groupInfoV1.messageExpirationTime = messageExpirationTimer;
account.getGroupStore().updateGroup(groupInfoV1); account.getGroupStore().updateGroup(groupInfoV1);
@ -586,7 +667,9 @@ public class GroupHelper {
final var groupV2Helper = context.getGroupV2Helper(); final var groupV2Helper = context.getGroupV2Helper();
if (group.isPendingMember(account.getSelfRecipientId())) { if (group.isPendingMember(account.getSelfRecipientId())) {
var groupGroupChangePair = groupV2Helper.acceptInvite(group); var groupGroupChangePair = groupV2Helper.acceptInvite(group);
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
handleGroupChangeResponse(group, groupGroupChangePair.second()));
} }
if (members != null) { if (members != null) {
@ -594,14 +677,18 @@ public class GroupHelper {
requestingMembers.retainAll(group.getRequestingMembers()); requestingMembers.retainAll(group.getRequestingMembers());
if (!requestingMembers.isEmpty()) { if (!requestingMembers.isEmpty()) {
var groupGroupChangePair = groupV2Helper.approveJoinRequestMembers(group, requestingMembers); var groupGroupChangePair = groupV2Helper.approveJoinRequestMembers(group, requestingMembers);
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
handleGroupChangeResponse(group, groupGroupChangePair.second()));
} }
final var newMembers = new HashSet<>(members); final var newMembers = new HashSet<>(members);
newMembers.removeAll(group.getMembers()); newMembers.removeAll(group.getMembers());
newMembers.removeAll(group.getRequestingMembers()); newMembers.removeAll(group.getRequestingMembers());
if (!newMembers.isEmpty()) { if (!newMembers.isEmpty()) {
var groupGroupChangePair = groupV2Helper.addMembers(group, newMembers); var groupGroupChangePair = groupV2Helper.addMembers(group, newMembers);
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
handleGroupChangeResponse(group, groupGroupChangePair.second()));
} }
} }
@ -617,20 +704,26 @@ public class GroupHelper {
existingRemoveMembers.remove(account.getSelfRecipientId());// self can be removed with sendQuitGroupMessage existingRemoveMembers.remove(account.getSelfRecipientId());// self can be removed with sendQuitGroupMessage
if (!existingRemoveMembers.isEmpty()) { if (!existingRemoveMembers.isEmpty()) {
var groupGroupChangePair = groupV2Helper.removeMembers(group, existingRemoveMembers); var groupGroupChangePair = groupV2Helper.removeMembers(group, existingRemoveMembers);
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
handleGroupChangeResponse(group, groupGroupChangePair.second()));
} }
var pendingRemoveMembers = new HashSet<>(removeMembers); var pendingRemoveMembers = new HashSet<>(removeMembers);
pendingRemoveMembers.retainAll(group.getPendingMembers()); pendingRemoveMembers.retainAll(group.getPendingMembers());
if (!pendingRemoveMembers.isEmpty()) { if (!pendingRemoveMembers.isEmpty()) {
var groupGroupChangePair = groupV2Helper.revokeInvitedMembers(group, pendingRemoveMembers); var groupGroupChangePair = groupV2Helper.revokeInvitedMembers(group, pendingRemoveMembers);
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
handleGroupChangeResponse(group, groupGroupChangePair.second()));
} }
var requestingRemoveMembers = new HashSet<>(removeMembers); var requestingRemoveMembers = new HashSet<>(removeMembers);
requestingRemoveMembers.retainAll(group.getRequestingMembers()); requestingRemoveMembers.retainAll(group.getRequestingMembers());
if (!requestingRemoveMembers.isEmpty()) { if (!requestingRemoveMembers.isEmpty()) {
var groupGroupChangePair = groupV2Helper.refuseJoinRequestMembers(group, requestingRemoveMembers); var groupGroupChangePair = groupV2Helper.refuseJoinRequestMembers(group, requestingRemoveMembers);
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
handleGroupChangeResponse(group, groupGroupChangePair.second()));
} }
} }
@ -643,7 +736,7 @@ public class GroupHelper {
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, true); var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, true);
result = sendUpdateGroupV2Message(group, result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(), groupGroupChangePair.first(),
groupGroupChangePair.second()); handleGroupChangeResponse(group, groupGroupChangePair.second()));
} }
} }
} }
@ -656,7 +749,7 @@ public class GroupHelper {
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false); var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false);
result = sendUpdateGroupV2Message(group, result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(), groupGroupChangePair.first(),
groupGroupChangePair.second()); handleGroupChangeResponse(group, groupGroupChangePair.second()));
} }
} }
} }
@ -666,7 +759,9 @@ public class GroupHelper {
newlyBannedMembers.removeAll(group.getBannedMembers()); newlyBannedMembers.removeAll(group.getBannedMembers());
if (!newlyBannedMembers.isEmpty()) { if (!newlyBannedMembers.isEmpty()) {
var groupGroupChangePair = groupV2Helper.banMembers(group, newlyBannedMembers); var groupGroupChangePair = groupV2Helper.banMembers(group, newlyBannedMembers);
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
handleGroupChangeResponse(group, groupGroupChangePair.second()));
} }
} }
@ -675,38 +770,52 @@ public class GroupHelper {
existingUnbanMembers.retainAll(group.getBannedMembers()); existingUnbanMembers.retainAll(group.getBannedMembers());
if (!existingUnbanMembers.isEmpty()) { if (!existingUnbanMembers.isEmpty()) {
var groupGroupChangePair = groupV2Helper.unbanMembers(group, existingUnbanMembers); var groupGroupChangePair = groupV2Helper.unbanMembers(group, existingUnbanMembers);
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
handleGroupChangeResponse(group, groupGroupChangePair.second()));
} }
} }
if (resetGroupLink) { if (resetGroupLink) {
var groupGroupChangePair = groupV2Helper.resetGroupLinkPassword(group); var groupGroupChangePair = groupV2Helper.resetGroupLinkPassword(group);
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
handleGroupChangeResponse(group, groupGroupChangePair.second()));
} }
if (groupLinkState != null) { if (groupLinkState != null) {
var groupGroupChangePair = groupV2Helper.setGroupLinkState(group, groupLinkState); var groupGroupChangePair = groupV2Helper.setGroupLinkState(group, groupLinkState);
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
handleGroupChangeResponse(group, groupGroupChangePair.second()));
} }
if (addMemberPermission != null) { if (addMemberPermission != null) {
var groupGroupChangePair = groupV2Helper.setAddMemberPermission(group, addMemberPermission); var groupGroupChangePair = groupV2Helper.setAddMemberPermission(group, addMemberPermission);
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
handleGroupChangeResponse(group, groupGroupChangePair.second()));
} }
if (editDetailsPermission != null) { if (editDetailsPermission != null) {
var groupGroupChangePair = groupV2Helper.setEditDetailsPermission(group, editDetailsPermission); var groupGroupChangePair = groupV2Helper.setEditDetailsPermission(group, editDetailsPermission);
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
handleGroupChangeResponse(group, groupGroupChangePair.second()));
} }
if (expirationTimer != null) { if (expirationTimer != null) {
var groupGroupChangePair = groupV2Helper.setMessageExpirationTimer(group, expirationTimer); var groupGroupChangePair = groupV2Helper.setMessageExpirationTimer(group, expirationTimer);
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
handleGroupChangeResponse(group, groupGroupChangePair.second()));
} }
if (isAnnouncementGroup != null) { if (isAnnouncementGroup != null) {
var groupGroupChangePair = groupV2Helper.setIsAnnouncementGroup(group, isAnnouncementGroup); var groupGroupChangePair = groupV2Helper.setIsAnnouncementGroup(group, isAnnouncementGroup);
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
handleGroupChangeResponse(group, groupGroupChangePair.second()));
} }
if (name != null || description != null || avatarFile != null) { if (name != null || description != null || avatarFile != null) {
@ -715,7 +824,9 @@ public class GroupHelper {
context.getAvatarStore() context.getAvatarStore()
.storeGroupAvatar(group.getGroupId(), outputStream -> outputStream.write(avatarFile)); .storeGroupAvatar(group.getGroupId(), outputStream -> outputStream.write(avatarFile));
} }
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
handleGroupChangeResponse(group, groupGroupChangePair.second()));
} }
return result; return result;
@ -735,7 +846,8 @@ public class GroupHelper {
} }
private SendGroupMessageResults quitGroupV2( private SendGroupMessageResults quitGroupV2(
final GroupInfoV2 groupInfoV2, final Set<RecipientId> newAdmins final GroupInfoV2 groupInfoV2,
final Set<RecipientId> newAdmins
) throws LastGroupAdminException, IOException { ) throws LastGroupAdminException, IOException {
final var currentAdmins = groupInfoV2.getAdminMembers(); final var currentAdmins = groupInfoV2.getAdminMembers();
newAdmins.removeAll(currentAdmins); newAdmins.removeAll(currentAdmins);
@ -751,7 +863,8 @@ public class GroupHelper {
groupInfoV2.setGroup(groupGroupChangePair.first()); groupInfoV2.setGroup(groupGroupChangePair.first());
account.getGroupStore().updateGroup(groupInfoV2); account.getGroupStore().updateGroup(groupInfoV2);
var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().encode()); var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2,
handleGroupChangeResponse(groupInfoV2, groupGroupChangePair.second()).encode());
return sendGroupMessage(messageBuilder, return sendGroupMessage(messageBuilder,
groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId()), groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId()),
groupInfoV2.getDistributionId()); groupInfoV2.getDistributionId());
@ -788,7 +901,9 @@ public class GroupHelper {
} }
private SendGroupMessageResults sendUpdateGroupV2Message( private SendGroupMessageResults sendUpdateGroupV2Message(
GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange GroupInfoV2 group,
DecryptedGroup newDecryptedGroup,
GroupChange groupChange
) throws IOException { ) throws IOException {
final var selfRecipientId = account.getSelfRecipientId(); final var selfRecipientId = account.getSelfRecipientId();
final var members = group.getMembersIncludingPendingWithout(selfRecipientId); final var members = group.getMembersIncludingPendingWithout(selfRecipientId);

View file

@ -19,6 +19,7 @@ import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.GroupChangeResponse;
import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
@ -27,6 +28,8 @@ import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember; import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate; import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
import org.whispersystems.signalservice.api.groupsv2.GroupHistoryPage; import org.whispersystems.signalservice.api.groupsv2.GroupHistoryPage;
@ -41,6 +44,7 @@ import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -75,24 +79,25 @@ class GroupV2Helper {
groupApiCredentials = null; groupApiCredentials = null;
} }
DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) throws NotAGroupMemberException { DecryptedGroupResponse getDecryptedGroup(final GroupSecretParams groupSecretParams) throws NotAGroupMemberException {
try { try {
final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams); final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
return dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupsV2AuthorizationString); return dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupsV2AuthorizationString);
} catch (NonSuccessfulResponseCodeException e) { } catch (NonSuccessfulResponseCodeException e) {
if (e.getCode() == 403) { if (e.code == 403) {
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null); throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
} }
logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage()); logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
return null; return null;
} catch (IOException | VerificationFailedException | InvalidGroupStateException e) { } catch (IOException | VerificationFailedException | InvalidGroupStateException | InvalidInputException e) {
logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage()); logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
return null; return null;
} }
} }
DecryptedGroupJoinInfo getDecryptedGroupJoinInfo( DecryptedGroupJoinInfo getDecryptedGroupJoinInfo(
GroupMasterKey groupMasterKey, GroupLinkPassword password GroupMasterKey groupMasterKey,
GroupLinkPassword password
) throws IOException, GroupLinkNotActiveException { ) throws IOException, GroupLinkNotActiveException {
var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
@ -103,19 +108,27 @@ class GroupV2Helper {
} }
GroupHistoryPage getDecryptedGroupHistoryPage( GroupHistoryPage getDecryptedGroupHistoryPage(
final GroupSecretParams groupSecretParams, int fromRevision final GroupSecretParams groupSecretParams,
int fromRevision,
long sendEndorsementsExpirationMs
) throws NotAGroupMemberException { ) throws NotAGroupMemberException {
try { try {
final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams); final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
return dependencies.getGroupsV2Api() return dependencies.getGroupsV2Api()
.getGroupHistoryPage(groupSecretParams, fromRevision, groupsV2AuthorizationString, false); .getGroupHistoryPage(groupSecretParams,
fromRevision,
groupsV2AuthorizationString,
false,
sendEndorsementsExpirationMs);
} catch (NotInGroupException e) {
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
} catch (NonSuccessfulResponseCodeException e) { } catch (NonSuccessfulResponseCodeException e) {
if (e.getCode() == 403) { if (e.code == 403) {
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null); throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
} }
logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage()); logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage());
return null; return null;
} catch (IOException | VerificationFailedException | InvalidGroupStateException e) { } catch (IOException | VerificationFailedException | InvalidGroupStateException | InvalidInputException e) {
logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage()); logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage());
return null; return null;
} }
@ -132,9 +145,7 @@ class GroupV2Helper {
return partialDecryptedGroup.revision; return partialDecryptedGroup.revision;
} }
Pair<GroupInfoV2, DecryptedGroup> createGroup( Pair<GroupInfoV2, DecryptedGroupResponse> createGroup(String name, Set<RecipientId> members, byte[] avatarFile) {
String name, Set<RecipientId> members, byte[] avatarFile
) {
final var newGroup = buildNewGroup(name, members, avatarFile); final var newGroup = buildNewGroup(name, members, avatarFile);
if (newGroup == null) { if (newGroup == null) {
return null; return null;
@ -143,16 +154,16 @@ class GroupV2Helper {
final var groupSecretParams = newGroup.getGroupSecretParams(); final var groupSecretParams = newGroup.getGroupSecretParams();
final GroupsV2AuthorizationString groupAuthForToday; final GroupsV2AuthorizationString groupAuthForToday;
final DecryptedGroup decryptedGroup; final DecryptedGroupResponse response;
try { try {
groupAuthForToday = getGroupAuthForToday(groupSecretParams); groupAuthForToday = getGroupAuthForToday(groupSecretParams);
dependencies.getGroupsV2Api().putNewGroup(newGroup, groupAuthForToday); dependencies.getGroupsV2Api().putNewGroup(newGroup, groupAuthForToday);
decryptedGroup = dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupAuthForToday); response = dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupAuthForToday);
} catch (IOException | VerificationFailedException | InvalidGroupStateException e) { } catch (IOException | VerificationFailedException | InvalidGroupStateException | InvalidInputException e) {
logger.warn("Failed to create V2 group: {}", e.getMessage()); logger.warn("Failed to create V2 group: {}", e.getMessage());
return null; return null;
} }
if (decryptedGroup == null) { if (response == null) {
logger.warn("Failed to create V2 group, unknown error!"); logger.warn("Failed to create V2 group, unknown error!");
return null; return null;
} }
@ -161,12 +172,10 @@ class GroupV2Helper {
final var masterKey = groupSecretParams.getMasterKey(); final var masterKey = groupSecretParams.getMasterKey();
var g = new GroupInfoV2(groupId, masterKey, context.getAccount().getRecipientResolver()); var g = new GroupInfoV2(groupId, masterKey, context.getAccount().getRecipientResolver());
return new Pair<>(g, decryptedGroup); return new Pair<>(g, response);
} }
private GroupsV2Operations.NewGroup buildNewGroup( private GroupsV2Operations.NewGroup buildNewGroup(String name, Set<RecipientId> members, byte[] avatar) {
String name, Set<RecipientId> members, byte[] avatar
) {
final var profileKeyCredential = context.getProfileHelper() final var profileKeyCredential = context.getProfileHelper()
.getExpiringProfileKeyCredential(context.getAccount().getSelfRecipientId()); .getExpiringProfileKeyCredential(context.getAccount().getSelfRecipientId());
if (profileKeyCredential == null) { if (profileKeyCredential == null) {
@ -195,8 +204,11 @@ class GroupV2Helper {
0); 0);
} }
Pair<DecryptedGroup, GroupChange> updateGroup( Pair<DecryptedGroup, GroupChangeResponse> updateGroup(
GroupInfoV2 groupInfoV2, String name, String description, byte[] avatarFile GroupInfoV2 groupInfoV2,
String name,
String description,
byte[] avatarFile
) throws IOException { ) throws IOException {
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams); var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
@ -218,8 +230,9 @@ class GroupV2Helper {
return commitChange(groupInfoV2, change); return commitChange(groupInfoV2, change);
} }
Pair<DecryptedGroup, GroupChange> addMembers( Pair<DecryptedGroup, GroupChangeResponse> addMembers(
GroupInfoV2 groupInfoV2, Set<RecipientId> newMembers GroupInfoV2 groupInfoV2,
Set<RecipientId> newMembers
) throws IOException { ) throws IOException {
GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
@ -244,8 +257,9 @@ class GroupV2Helper {
return commitChange(groupInfoV2, change); return commitChange(groupInfoV2, change);
} }
Pair<DecryptedGroup, GroupChange> leaveGroup( Pair<DecryptedGroup, GroupChangeResponse> leaveGroup(
GroupInfoV2 groupInfoV2, Set<RecipientId> membersToMakeAdmin GroupInfoV2 groupInfoV2,
Set<RecipientId> membersToMakeAdmin
) throws IOException { ) throws IOException {
var pendingMembersList = groupInfoV2.getGroup().pendingMembers; var pendingMembersList = groupInfoV2.getGroup().pendingMembers;
final var selfAci = getSelfAci(); final var selfAci = getSelfAci();
@ -264,8 +278,9 @@ class GroupV2Helper {
return commitChange(groupInfoV2, groupOperations.createLeaveAndPromoteMembersToAdmin(selfAci, adminUuids)); return commitChange(groupInfoV2, groupOperations.createLeaveAndPromoteMembersToAdmin(selfAci, adminUuids));
} }
Pair<DecryptedGroup, GroupChange> removeMembers( Pair<DecryptedGroup, GroupChangeResponse> removeMembers(
GroupInfoV2 groupInfoV2, Set<RecipientId> members GroupInfoV2 groupInfoV2,
Set<RecipientId> members
) throws IOException { ) throws IOException {
final var memberUuids = members.stream() final var memberUuids = members.stream()
.map(context.getRecipientHelper()::resolveSignalServiceAddress) .map(context.getRecipientHelper()::resolveSignalServiceAddress)
@ -276,8 +291,9 @@ class GroupV2Helper {
return ejectMembers(groupInfoV2, memberUuids); return ejectMembers(groupInfoV2, memberUuids);
} }
Pair<DecryptedGroup, GroupChange> approveJoinRequestMembers( Pair<DecryptedGroup, GroupChangeResponse> approveJoinRequestMembers(
GroupInfoV2 groupInfoV2, Set<RecipientId> members GroupInfoV2 groupInfoV2,
Set<RecipientId> members
) throws IOException { ) throws IOException {
final var memberUuids = members.stream() final var memberUuids = members.stream()
.map(context.getRecipientHelper()::resolveSignalServiceAddress) .map(context.getRecipientHelper()::resolveSignalServiceAddress)
@ -287,8 +303,9 @@ class GroupV2Helper {
return approveJoinRequest(groupInfoV2, memberUuids); return approveJoinRequest(groupInfoV2, memberUuids);
} }
Pair<DecryptedGroup, GroupChange> refuseJoinRequestMembers( Pair<DecryptedGroup, GroupChangeResponse> refuseJoinRequestMembers(
GroupInfoV2 groupInfoV2, Set<RecipientId> members GroupInfoV2 groupInfoV2,
Set<RecipientId> members
) throws IOException { ) throws IOException {
final var memberUuids = members.stream() final var memberUuids = members.stream()
.map(context.getRecipientHelper()::resolveSignalServiceAddress) .map(context.getRecipientHelper()::resolveSignalServiceAddress)
@ -297,8 +314,9 @@ class GroupV2Helper {
return refuseJoinRequest(groupInfoV2, memberUuids); return refuseJoinRequest(groupInfoV2, memberUuids);
} }
Pair<DecryptedGroup, GroupChange> revokeInvitedMembers( Pair<DecryptedGroup, GroupChangeResponse> revokeInvitedMembers(
GroupInfoV2 groupInfoV2, Set<RecipientId> members GroupInfoV2 groupInfoV2,
Set<RecipientId> members
) throws IOException { ) throws IOException {
var pendingMembersList = groupInfoV2.getGroup().pendingMembers; var pendingMembersList = groupInfoV2.getGroup().pendingMembers;
final var memberUuids = members.stream() final var memberUuids = members.stream()
@ -311,8 +329,9 @@ class GroupV2Helper {
return revokeInvites(groupInfoV2, memberUuids); return revokeInvites(groupInfoV2, memberUuids);
} }
Pair<DecryptedGroup, GroupChange> banMembers( Pair<DecryptedGroup, GroupChangeResponse> banMembers(
GroupInfoV2 groupInfoV2, Set<RecipientId> block GroupInfoV2 groupInfoV2,
Set<RecipientId> block
) throws IOException { ) throws IOException {
GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
@ -329,8 +348,9 @@ class GroupV2Helper {
return commitChange(groupInfoV2, change); return commitChange(groupInfoV2, change);
} }
Pair<DecryptedGroup, GroupChange> unbanMembers( Pair<DecryptedGroup, GroupChangeResponse> unbanMembers(
GroupInfoV2 groupInfoV2, Set<RecipientId> block GroupInfoV2 groupInfoV2,
Set<RecipientId> block
) throws IOException { ) throws IOException {
GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
@ -345,15 +365,16 @@ class GroupV2Helper {
return commitChange(groupInfoV2, change); return commitChange(groupInfoV2, change);
} }
Pair<DecryptedGroup, GroupChange> resetGroupLinkPassword(GroupInfoV2 groupInfoV2) throws IOException { Pair<DecryptedGroup, GroupChangeResponse> resetGroupLinkPassword(GroupInfoV2 groupInfoV2) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
final var newGroupLinkPassword = GroupLinkPassword.createNew().serialize(); final var newGroupLinkPassword = GroupLinkPassword.createNew().serialize();
final var change = groupOperations.createModifyGroupLinkPasswordChange(newGroupLinkPassword); final var change = groupOperations.createModifyGroupLinkPasswordChange(newGroupLinkPassword);
return commitChange(groupInfoV2, change); return commitChange(groupInfoV2, change);
} }
Pair<DecryptedGroup, GroupChange> setGroupLinkState( Pair<DecryptedGroup, GroupChangeResponse> setGroupLinkState(
GroupInfoV2 groupInfoV2, GroupLinkState state GroupInfoV2 groupInfoV2,
GroupLinkState state
) throws IOException { ) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
@ -367,8 +388,9 @@ class GroupV2Helper {
return commitChange(groupInfoV2, change); return commitChange(groupInfoV2, change);
} }
Pair<DecryptedGroup, GroupChange> setEditDetailsPermission( Pair<DecryptedGroup, GroupChangeResponse> setEditDetailsPermission(
GroupInfoV2 groupInfoV2, GroupPermission permission GroupInfoV2 groupInfoV2,
GroupPermission permission
) throws IOException { ) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
@ -377,8 +399,9 @@ class GroupV2Helper {
return commitChange(groupInfoV2, change); return commitChange(groupInfoV2, change);
} }
Pair<DecryptedGroup, GroupChange> setAddMemberPermission( Pair<DecryptedGroup, GroupChangeResponse> setAddMemberPermission(
GroupInfoV2 groupInfoV2, GroupPermission permission GroupInfoV2 groupInfoV2,
GroupPermission permission
) throws IOException { ) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
@ -387,7 +410,7 @@ class GroupV2Helper {
return commitChange(groupInfoV2, change); return commitChange(groupInfoV2, change);
} }
Pair<DecryptedGroup, GroupChange> updateSelfProfileKey(GroupInfoV2 groupInfoV2) throws IOException { Pair<DecryptedGroup, GroupChangeResponse> updateSelfProfileKey(GroupInfoV2 groupInfoV2) throws IOException {
Optional<DecryptedMember> selfInGroup = groupInfoV2.getGroup() == null Optional<DecryptedMember> selfInGroup = groupInfoV2.getGroup() == null
? Optional.empty() ? Optional.empty()
: DecryptedGroupUtil.findMemberByAci(groupInfoV2.getGroup().members, getSelfAci()); : DecryptedGroupUtil.findMemberByAci(groupInfoV2.getGroup().members, getSelfAci());
@ -417,7 +440,7 @@ class GroupV2Helper {
return commitChange(groupInfoV2, change); return commitChange(groupInfoV2, change);
} }
GroupChange joinGroup( GroupChangeResponse joinGroup(
GroupMasterKey groupMasterKey, GroupMasterKey groupMasterKey,
GroupLinkPassword groupLinkPassword, GroupLinkPassword groupLinkPassword,
DecryptedGroupJoinInfo decryptedGroupJoinInfo DecryptedGroupJoinInfo decryptedGroupJoinInfo
@ -444,7 +467,7 @@ class GroupV2Helper {
return commitChange(groupSecretParams, decryptedGroupJoinInfo.revision, change, groupLinkPassword); return commitChange(groupSecretParams, decryptedGroupJoinInfo.revision, change, groupLinkPassword);
} }
Pair<DecryptedGroup, GroupChange> acceptInvite(GroupInfoV2 groupInfoV2) throws IOException { Pair<DecryptedGroup, GroupChangeResponse> acceptInvite(GroupInfoV2 groupInfoV2) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
final var selfRecipientId = context.getAccount().getSelfRecipientId(); final var selfRecipientId = context.getAccount().getSelfRecipientId();
@ -461,8 +484,10 @@ class GroupV2Helper {
return commitChange(groupInfoV2, change); return commitChange(groupInfoV2, change);
} }
Pair<DecryptedGroup, GroupChange> setMemberAdmin( Pair<DecryptedGroup, GroupChangeResponse> setMemberAdmin(
GroupInfoV2 groupInfoV2, RecipientId recipientId, boolean admin GroupInfoV2 groupInfoV2,
RecipientId recipientId,
boolean admin
) throws IOException { ) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId); final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
@ -475,16 +500,18 @@ class GroupV2Helper {
} }
} }
Pair<DecryptedGroup, GroupChange> setMessageExpirationTimer( Pair<DecryptedGroup, GroupChangeResponse> setMessageExpirationTimer(
GroupInfoV2 groupInfoV2, int messageExpirationTimer GroupInfoV2 groupInfoV2,
int messageExpirationTimer
) throws IOException { ) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
final var change = groupOperations.createModifyGroupTimerChange(messageExpirationTimer); final var change = groupOperations.createModifyGroupTimerChange(messageExpirationTimer);
return commitChange(groupInfoV2, change); return commitChange(groupInfoV2, change);
} }
Pair<DecryptedGroup, GroupChange> setIsAnnouncementGroup( Pair<DecryptedGroup, GroupChangeResponse> setIsAnnouncementGroup(
GroupInfoV2 groupInfoV2, boolean isAnnouncementGroup GroupInfoV2 groupInfoV2,
boolean isAnnouncementGroup
) throws IOException { ) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
final var change = groupOperations.createAnnouncementGroupChange(isAnnouncementGroup); final var change = groupOperations.createAnnouncementGroupChange(isAnnouncementGroup);
@ -511,8 +538,9 @@ class GroupV2Helper {
return dependencies.getGroupsV2Operations().forGroup(groupSecretParams); return dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
} }
private Pair<DecryptedGroup, GroupChange> revokeInvites( private Pair<DecryptedGroup, GroupChangeResponse> revokeInvites(
GroupInfoV2 groupInfoV2, Set<DecryptedPendingMember> pendingMembers GroupInfoV2 groupInfoV2,
Set<DecryptedPendingMember> pendingMembers
) throws IOException { ) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
final var uuidCipherTexts = pendingMembers.stream().map(member -> { final var uuidCipherTexts = pendingMembers.stream().map(member -> {
@ -525,29 +553,33 @@ class GroupV2Helper {
return commitChange(groupInfoV2, groupOperations.createRemoveInvitationChange(uuidCipherTexts)); return commitChange(groupInfoV2, groupOperations.createRemoveInvitationChange(uuidCipherTexts));
} }
private Pair<DecryptedGroup, GroupChange> approveJoinRequest( private Pair<DecryptedGroup, GroupChangeResponse> approveJoinRequest(
GroupInfoV2 groupInfoV2, Set<UUID> uuids GroupInfoV2 groupInfoV2,
Set<UUID> uuids
) throws IOException { ) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
return commitChange(groupInfoV2, groupOperations.createApproveGroupJoinRequest(uuids)); return commitChange(groupInfoV2, groupOperations.createApproveGroupJoinRequest(uuids));
} }
private Pair<DecryptedGroup, GroupChange> refuseJoinRequest( private Pair<DecryptedGroup, GroupChangeResponse> refuseJoinRequest(
GroupInfoV2 groupInfoV2, Set<ServiceId> serviceIds GroupInfoV2 groupInfoV2,
Set<ServiceId> serviceIds
) throws IOException { ) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
return commitChange(groupInfoV2, groupOperations.createRefuseGroupJoinRequest(serviceIds, false, List.of())); return commitChange(groupInfoV2, groupOperations.createRefuseGroupJoinRequest(serviceIds, false, List.of()));
} }
private Pair<DecryptedGroup, GroupChange> ejectMembers( private Pair<DecryptedGroup, GroupChangeResponse> ejectMembers(
GroupInfoV2 groupInfoV2, Set<ACI> members GroupInfoV2 groupInfoV2,
Set<ACI> members
) throws IOException { ) throws IOException {
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(members, false, List.of())); return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(members, false, List.of()));
} }
private Pair<DecryptedGroup, GroupChange> commitChange( private Pair<DecryptedGroup, GroupChangeResponse> commitChange(
GroupInfoV2 groupInfoV2, GroupChange.Actions.Builder change GroupInfoV2 groupInfoV2,
GroupChange.Actions.Builder change
) throws IOException { ) throws IOException {
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
final var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams); final var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
@ -567,10 +599,12 @@ class GroupV2Helper {
var signedGroupChange = dependencies.getGroupsV2Api() var signedGroupChange = dependencies.getGroupsV2Api()
.patchGroup(changeActions, getGroupAuthForToday(groupSecretParams), Optional.empty()); .patchGroup(changeActions, getGroupAuthForToday(groupSecretParams), Optional.empty());
groupInfoV2.setGroup(decryptedGroupState);
return new Pair<>(decryptedGroupState, signedGroupChange); return new Pair<>(decryptedGroupState, signedGroupChange);
} }
private GroupChange commitChange( private GroupChangeResponse commitChange(
GroupSecretParams groupSecretParams, GroupSecretParams groupSecretParams,
int currentRevision, int currentRevision,
GroupChange.Actions.Builder change, GroupChange.Actions.Builder change,
@ -622,11 +656,13 @@ class GroupV2Helper {
DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) { DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) {
if (signedGroupChange != null) { if (signedGroupChange != null) {
var groupOperations = dependencies.getGroupsV2Operations() final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
.forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey)); final var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
final var groupId = groupSecretParams.getPublicParams().getGroupIdentifier();
try { 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) { } catch (VerificationFailedException | InvalidGroupStateException | IOException e) {
return null; return null;
} }
@ -668,7 +704,8 @@ class GroupV2Helper {
} }
private GroupsV2AuthorizationString getAuthorizationString( private GroupsV2AuthorizationString getAuthorizationString(
final GroupSecretParams groupSecretParams, final long todaySeconds final GroupSecretParams groupSecretParams,
final long todaySeconds
) throws VerificationFailedException { ) throws VerificationFailedException {
var authCredentialResponse = groupApiCredentials.get(todaySeconds); var authCredentialResponse = groupApiCredentials.get(todaySeconds);
final var aci = getSelfAci(); final var aci = getSelfAci();

View file

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

View file

@ -22,7 +22,6 @@ import org.asamk.signal.manager.api.GroupId;
import org.asamk.signal.manager.api.GroupNotFoundException; import org.asamk.signal.manager.api.GroupNotFoundException;
import org.asamk.signal.manager.api.MessageEnvelope; import org.asamk.signal.manager.api.MessageEnvelope;
import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.Profile;
import org.asamk.signal.manager.api.ReceiveConfig; import org.asamk.signal.manager.api.ReceiveConfig;
import org.asamk.signal.manager.api.StickerPackId; import org.asamk.signal.manager.api.StickerPackId;
import org.asamk.signal.manager.api.TrustLevel; import org.asamk.signal.manager.api.TrustLevel;
@ -32,6 +31,7 @@ import org.asamk.signal.manager.internal.SignalDependencies;
import org.asamk.signal.manager.jobs.RetrieveStickerPackJob; import org.asamk.signal.manager.jobs.RetrieveStickerPackJob;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.groups.GroupInfoV1; 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.recipients.RecipientId;
import org.asamk.signal.manager.storage.stickers.StickerPack; import org.asamk.signal.manager.storage.stickers.StickerPack;
import org.signal.libsignal.metadata.ProtocolInvalidKeyException; import org.signal.libsignal.metadata.ProtocolInvalidKeyException;
@ -41,6 +41,7 @@ import org.signal.libsignal.metadata.ProtocolNoSessionException;
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
import org.signal.libsignal.metadata.SelfSendException; import org.signal.libsignal.metadata.SelfSendException;
import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.protocol.UsePqRatchet;
import org.signal.libsignal.protocol.groups.GroupSessionBuilder; import org.signal.libsignal.protocol.groups.GroupSessionBuilder;
import org.signal.libsignal.protocol.message.DecryptionErrorMessage; import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.InvalidInputException;
@ -105,7 +106,7 @@ public final class IncomingMessageHandler {
try { try {
final var cipherResult = dependencies.getCipher(destination == null final var cipherResult = dependencies.getCipher(destination == null
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp(), UsePqRatchet.NO);
content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp()); content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
if (content == null) { if (content == null) {
return new Pair<>(List.of(), null); return new Pair<>(List.of(), null);
@ -143,7 +144,7 @@ public final class IncomingMessageHandler {
try { try {
final var cipherResult = dependencies.getCipher(destination == null final var cipherResult = dependencies.getCipher(destination == null
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp(), UsePqRatchet.NO);
content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp()); content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
if (content == null) { if (content == null) {
return new Pair<>(List.of(), null); return new Pair<>(List.of(), null);
@ -157,33 +158,22 @@ public final class IncomingMessageHandler {
} catch (ProtocolInvalidKeyIdException | ProtocolInvalidKeyException | ProtocolNoSessionException | } catch (ProtocolInvalidKeyIdException | ProtocolInvalidKeyException | ProtocolNoSessionException |
ProtocolInvalidMessageException e) { ProtocolInvalidMessageException e) {
logger.debug("Failed to decrypt incoming message", e); logger.debug("Failed to decrypt incoming message", e);
if (e instanceof ProtocolInvalidKeyIdException) {
actions.add(RefreshPreKeysAction.create());
}
final var sender = account.getRecipientResolver().resolveRecipient(e.getSender()); final var sender = account.getRecipientResolver().resolveRecipient(e.getSender());
if (context.getContactHelper().isContactBlocked(sender)) { if (context.getContactHelper().isContactBlocked(sender)) {
logger.debug("Received invalid message from blocked contact, ignoring."); logger.debug("Received invalid message from blocked contact, ignoring.");
} else { } else {
final var senderProfile = context.getProfileHelper().getRecipientProfile(sender);
final var selfProfile = context.getProfileHelper().getSelfProfile();
var serviceId = ServiceId.parseOrNull(e.getSender()); var serviceId = ServiceId.parseOrNull(e.getSender());
if (serviceId == null) {
// Workaround for libsignal-client issue #492
serviceId = account.getRecipientAddressResolver()
.resolveRecipientAddress(sender)
.serviceId()
.orElse(null);
}
if (serviceId != null) { if (serviceId != null) {
final var isSelf = sender.equals(account.getSelfRecipientId()) final var isSelf = sender.equals(account.getSelfRecipientId())
&& e.getSenderDevice() == account.getDeviceId(); && e.getSenderDevice() == account.getDeviceId();
final var isSenderSenderKeyCapable = senderProfile != null && senderProfile.getCapabilities() logger.debug("Received invalid message, queuing renew session action.");
.contains(Profile.Capability.senderKey); actions.add(new RenewSessionAction(sender, serviceId, destination));
final var isSelfSenderKeyCapable = selfProfile != null && selfProfile.getCapabilities() if (!isSelf) {
.contains(Profile.Capability.senderKey);
if (!isSelf && isSenderSenderKeyCapable && isSelfSenderKeyCapable) {
logger.debug("Received invalid message, requesting message resend."); logger.debug("Received invalid message, requesting message resend.");
actions.add(new SendRetryMessageRequestAction(sender, serviceId, e, envelope, destination)); actions.add(new SendRetryMessageRequestAction(sender, e, envelope));
} else {
logger.debug("Received invalid message, queuing renew session action.");
actions.add(new RenewSessionAction(sender, serviceId, destination));
} }
} else { } else {
logger.debug("Received invalid message from invalid sender: {}", e.getSender()); logger.debug("Received invalid message from invalid sender: {}", e.getSender());
@ -204,11 +194,13 @@ public final class IncomingMessageHandler {
} }
private SignalServiceContent validate( private SignalServiceContent validate(
Envelope envelope, SignalServiceCipherResult cipherResult, long serverDeliveredTimestamp Envelope envelope,
SignalServiceCipherResult cipherResult,
long serverDeliveredTimestamp
) throws ProtocolInvalidKeyException, ProtocolInvalidMessageException, UnsupportedDataMessageException, InvalidMessageStructureException { ) throws ProtocolInvalidKeyException, ProtocolInvalidMessageException, UnsupportedDataMessageException, InvalidMessageStructureException {
final var content = cipherResult.getContent(); final var content = cipherResult.getContent();
final var envelopeMetadata = cipherResult.getMetadata(); final var envelopeMetadata = cipherResult.getMetadata();
final var validationResult = EnvelopeContentValidator.INSTANCE.validate(envelope, content); final var validationResult = EnvelopeContentValidator.INSTANCE.validate(envelope, content, account.getAci());
if (validationResult instanceof EnvelopeContentValidator.Result.Invalid v) { if (validationResult instanceof EnvelopeContentValidator.Result.Invalid v) {
logger.warn("Invalid content! {}", v.getReason(), v.getThrowable()); logger.warn("Invalid content! {}", v.getReason(), v.getThrowable());
@ -294,7 +286,9 @@ public final class IncomingMessageHandler {
} }
public List<HandleAction> handleMessage( public List<HandleAction> handleMessage(
SignalServiceEnvelope envelope, SignalServiceContent content, ReceiveConfig receiveConfig SignalServiceEnvelope envelope,
SignalServiceContent content,
ReceiveConfig receiveConfig
) { ) {
var actions = new ArrayList<HandleAction>(); var actions = new ArrayList<HandleAction>();
final var senderDeviceAddress = getSender(envelope, content); final var senderDeviceAddress = getSender(envelope, content);
@ -395,7 +389,8 @@ public final class IncomingMessageHandler {
} }
private boolean handlePniSignatureMessage( private boolean handlePniSignatureMessage(
final SignalServicePniSignatureMessage message, final SignalServiceAddress senderAddress final SignalServicePniSignatureMessage message,
final SignalServiceAddress senderAddress
) { ) {
final var aci = senderAddress.getServiceId(); final var aci = senderAddress.getServiceId();
final var aciIdentity = account.getIdentityKeyStore().getIdentityInfo(aci); final var aciIdentity = account.getIdentityKeyStore().getIdentityInfo(aci);
@ -534,12 +529,12 @@ public final class IncomingMessageHandler {
} }
if (syncMessage.getBlockedList().isPresent()) { if (syncMessage.getBlockedList().isPresent()) {
final var blockedListMessage = syncMessage.getBlockedList().get(); final var blockedListMessage = syncMessage.getBlockedList().get();
for (var address : blockedListMessage.getAddresses()) { for (var individual : blockedListMessage.individuals) {
context.getContactHelper() final var address = new RecipientAddress(individual.getAci(), individual.getE164());
.setContactBlocked(account.getRecipientResolver().resolveRecipient(address), true); final var recipientId = account.getRecipientResolver().resolveRecipient(address);
context.getContactHelper().setContactBlocked(recipientId, true);
} }
for (var groupId : blockedListMessage.getGroupIds() for (var groupId : blockedListMessage.groupIds.stream()
.stream()
.map(GroupId::unknownVersion) .map(GroupId::unknownVersion)
.collect(Collectors.toSet())) { .collect(Collectors.toSet())) {
try { try {
@ -594,14 +589,22 @@ public final class IncomingMessageHandler {
} }
if (syncMessage.getKeys().isPresent()) { if (syncMessage.getKeys().isPresent()) {
final var keysMessage = syncMessage.getKeys().get(); final var keysMessage = syncMessage.getKeys().get();
if (keysMessage.getStorageService().isPresent()) { if (keysMessage.getAccountEntropyPool() != null) {
final var storageKey = keysMessage.getStorageService().get(); 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); account.setStorageKey(storageKey);
actions.add(SyncStorageDataAction.create()); actions.add(SyncStorageDataAction.create());
} }
if (keysMessage.getMaster().isPresent()) { if (keysMessage.getMediaRootBackupKey() != null) {
final var masterKey = keysMessage.getMaster().get(); final var mrb = keysMessage.getMediaRootBackupKey();
account.setMasterKey(masterKey); account.setMediaRootBackupKey(mrb);
actions.add(SyncStorageDataAction.create()); actions.add(SyncStorageDataAction.create());
} }
} }
@ -816,7 +819,9 @@ public final class IncomingMessageHandler {
} }
} else if (conversationPartnerAddress != null) { } else if (conversationPartnerAddress != null) {
context.getContactHelper() context.getContactHelper()
.setExpirationTimer(conversationPartnerAddress.recipientId(), message.getExpiresInSeconds()); .setExpirationTimer(conversationPartnerAddress.recipientId(),
message.getExpiresInSeconds(),
message.getExpireTimerVersion());
} }
} }
if (!ignoreAttachments) { if (!ignoreAttachments) {
@ -877,7 +882,9 @@ public final class IncomingMessageHandler {
} }
private List<HandleAction> handleSignalServiceStoryMessage( private List<HandleAction> handleSignalServiceStoryMessage(
SignalServiceStoryMessage message, RecipientId source, boolean ignoreAttachments SignalServiceStoryMessage message,
RecipientId source,
boolean ignoreAttachments
) { ) {
var actions = new ArrayList<HandleAction>(); var actions = new ArrayList<HandleAction>();
if (message.getGroupContext().isPresent()) { if (message.getGroupContext().isPresent()) {
@ -958,7 +965,7 @@ public final class IncomingMessageHandler {
private DeviceAddress getDestination(SignalServiceEnvelope envelope) { private DeviceAddress getDestination(SignalServiceEnvelope envelope) {
final var destination = envelope.getDestinationServiceId(); 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.getSelfRecipientId(), account.getAci(), account.getDeviceId());
} }
return new DeviceAddress(account.getRecipientResolver().resolveRecipient(destination), return new DeviceAddress(account.getRecipientResolver().resolveRecipient(destination),

View file

@ -21,9 +21,7 @@ public class PinHelper {
this.secureValueRecoveries = secureValueRecoveries; this.secureValueRecoveries = secureValueRecoveries;
} }
public void setRegistrationLockPin( public void setRegistrationLockPin(String pin, MasterKey masterKey) throws IOException {
String pin, MasterKey masterKey
) throws IOException {
IOException exception = null; IOException exception = null;
for (final var secureValueRecovery : secureValueRecoveries) { for (final var secureValueRecovery : secureValueRecoveries) {
try { try {
@ -32,11 +30,11 @@ public class PinHelper {
case SecureValueRecovery.BackupResponse.Success success -> { case SecureValueRecovery.BackupResponse.Success success -> {
} }
case SecureValueRecovery.BackupResponse.ServerRejected serverRejected -> case SecureValueRecovery.BackupResponse.ServerRejected serverRejected ->
logger.warn("Backup svr2 failed: ServerRejected"); logger.warn("Backup svr failed: ServerRejected");
case SecureValueRecovery.BackupResponse.EnclaveNotFound enclaveNotFound -> case SecureValueRecovery.BackupResponse.EnclaveNotFound enclaveNotFound ->
logger.warn("Backup svr2 failed: EnclaveNotFound"); logger.warn("Backup svr failed: EnclaveNotFound");
case SecureValueRecovery.BackupResponse.ExposeFailure exposeFailure -> case SecureValueRecovery.BackupResponse.ExposeFailure exposeFailure ->
logger.warn("Backup svr2 failed: ExposeFailure"); logger.warn("Backup svr failed: ExposeFailure");
case SecureValueRecovery.BackupResponse.ApplicationError error -> case SecureValueRecovery.BackupResponse.ApplicationError error ->
throw new IOException(error.getException()); throw new IOException(error.getException());
case SecureValueRecovery.BackupResponse.NetworkError error -> throw error.getException(); case SecureValueRecovery.BackupResponse.NetworkError error -> throw error.getException();
@ -82,14 +80,19 @@ public class PinHelper {
} }
public SecureValueRecovery.RestoreResponse.Success getRegistrationLockData( public SecureValueRecovery.RestoreResponse.Success getRegistrationLockData(
String pin, LockedException lockedException String pin,
LockedException lockedException
) throws IOException, IncorrectPinException { ) throws IOException, IncorrectPinException {
var svr2Credentials = lockedException.getSvr2Credentials(); var svr2Credentials = lockedException.getSvr2Credentials();
if (svr2Credentials != null) { if (svr2Credentials != null) {
IOException exception = null; IOException exception = null;
for (final var secureValueRecovery : secureValueRecoveries) { for (final var secureValueRecovery : secureValueRecoveries) {
try { try {
return getRegistrationLockData(secureValueRecovery, svr2Credentials, pin); final var lockData = getRegistrationLockData(secureValueRecovery, svr2Credentials, pin);
if (lockData == null) {
continue;
}
return lockData;
} catch (IOException e) { } catch (IOException e) {
exception = e; exception = e;
} }
@ -103,9 +106,11 @@ public class PinHelper {
} }
public SecureValueRecovery.RestoreResponse.Success getRegistrationLockData( public SecureValueRecovery.RestoreResponse.Success getRegistrationLockData(
SecureValueRecovery secureValueRecovery, AuthCredentials authCredentials, String pin SecureValueRecovery secureValueRecovery,
AuthCredentials authCredentials,
String pin
) throws IOException, IncorrectPinException { ) throws IOException, IncorrectPinException {
final var restoreResponse = secureValueRecovery.restoreDataPreRegistration(authCredentials, pin); final var restoreResponse = secureValueRecovery.restoreDataPreRegistration(authCredentials, null, pin);
switch (restoreResponse) { switch (restoreResponse) {
case SecureValueRecovery.RestoreResponse.Success s -> { case SecureValueRecovery.RestoreResponse.Success s -> {

View file

@ -11,16 +11,19 @@ import org.signal.libsignal.protocol.state.PreKeyRecord;
import org.signal.libsignal.protocol.state.SignedPreKeyRecord; import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.NetworkResultUtil;
import org.whispersystems.signalservice.api.account.PreKeyUpload; import org.whispersystems.signalservice.api.account.PreKeyUpload;
import org.whispersystems.signalservice.api.keys.OneTimePreKeyCounts;
import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.internal.push.OneTimePreKeyCounts; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_STALE_AGE; import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_STALE_AGE;
import static org.asamk.signal.manager.config.ServiceConfig.SIGNED_PREKEY_ROTATE_AGE; import static org.asamk.signal.manager.config.ServiceConfig.SIGNED_PREKEY_ROTATE_AGE;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class PreKeyHelper { public class PreKeyHelper {
@ -29,9 +32,7 @@ public class PreKeyHelper {
private final SignalAccount account; private final SignalAccount account;
private final SignalDependencies dependencies; private final SignalDependencies dependencies;
public PreKeyHelper( public PreKeyHelper(final SignalAccount account, final SignalDependencies dependencies) {
final SignalAccount account, final SignalDependencies dependencies
) {
this.account = account; this.account = account;
this.dependencies = dependencies; this.dependencies = dependencies;
} }
@ -78,11 +79,12 @@ public class PreKeyHelper {
} }
private boolean refreshPreKeysIfNecessary( private boolean refreshPreKeysIfNecessary(
final ServiceIdType serviceIdType, final IdentityKeyPair identityKeyPair final ServiceIdType serviceIdType,
final IdentityKeyPair identityKeyPair
) throws IOException { ) throws IOException {
OneTimePreKeyCounts preKeyCounts; OneTimePreKeyCounts preKeyCounts;
try { try {
preKeyCounts = dependencies.getAccountManager().getPreKeyCounts(serviceIdType); preKeyCounts = handleResponseException(dependencies.getKeysApi().getAvailablePreKeyCounts(serviceIdType));
} catch (AuthorizationFailedException e) { } catch (AuthorizationFailedException e) {
logger.debug("Failed to get pre key count, ignoring: " + e.getClass().getSimpleName()); logger.debug("Failed to get pre key count, ignoring: " + e.getClass().getSimpleName());
preKeyCounts = new OneTimePreKeyCounts(0, 0); preKeyCounts = new OneTimePreKeyCounts(0, 0);
@ -143,7 +145,7 @@ public class PreKeyHelper {
kyberPreKeyRecords); kyberPreKeyRecords);
var needsReset = false; var needsReset = false;
try { try {
dependencies.getAccountManager().setPreKeys(preKeyUpload); NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeys(preKeyUpload));
try { try {
if (preKeyRecords != null) { if (preKeyRecords != null) {
account.addPreKeys(serviceIdType, preKeyRecords); account.addPreKeys(serviceIdType, preKeyRecords);
@ -171,6 +173,11 @@ public class PreKeyHelper {
} catch (AuthorizationFailedException e) { } catch (AuthorizationFailedException e) {
// This can happen when the primary device has changed phone number // This can happen when the primary device has changed phone number
logger.warn("Failed to updated pre keys: {}", e.getMessage()); logger.warn("Failed to updated pre keys: {}", e.getMessage());
} catch (NonSuccessfulResponseCodeException e) {
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.");
} }
return needsReset; return needsReset;
} }
@ -215,7 +222,8 @@ public class PreKeyHelper {
} }
private List<KyberPreKeyRecord> generateKyberPreKeys( private List<KyberPreKeyRecord> generateKyberPreKeys(
ServiceIdType serviceIdType, final IdentityKeyPair identityKeyPair ServiceIdType serviceIdType,
final IdentityKeyPair identityKeyPair
) { ) {
final var accountData = account.getAccountData(serviceIdType); final var accountData = account.getAccountData(serviceIdType);
final var offset = accountData.getPreKeyMetadata().getNextKyberPreKeyId(); final var offset = accountData.getPreKeyMetadata().getNextKyberPreKeyId();
@ -240,7 +248,9 @@ public class PreKeyHelper {
} }
private KyberPreKeyRecord generateLastResortKyberPreKey( 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 accountData = account.getAccountData(serviceIdType);
final var signedPreKeyId = accountData.getPreKeyMetadata().getNextKyberPreKeyId() + offset; final var signedPreKeyId = accountData.getPreKeyMetadata().getNextKyberPreKeyId() + offset;

View file

@ -16,13 +16,15 @@ import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.PaymentUtils; import org.asamk.signal.manager.util.PaymentUtils;
import org.asamk.signal.manager.util.ProfileUtils; import org.asamk.signal.manager.util.ProfileUtils;
import org.asamk.signal.manager.util.Utils; import org.asamk.signal.manager.util.Utils;
import org.jetbrains.annotations.Nullable;
import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; 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.AvatarUploadParams;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
@ -195,9 +197,10 @@ public final class ProfileHelper {
: avatar == null ? AvatarUploadParams.unchanged(true) : AvatarUploadParams.unchanged(false); : avatar == null ? AvatarUploadParams.unchanged(true) : AvatarUploadParams.unchanged(false);
final var paymentsAddress = Optional.ofNullable(newProfile.getMobileCoinAddress()) final var paymentsAddress = Optional.ofNullable(newProfile.getMobileCoinAddress())
.map(address -> PaymentUtils.signPaymentsAddress(address, .map(address -> PaymentUtils.signPaymentsAddress(address,
account.getAciIdentityKeyPair().getPrivateKey())); account.getAciIdentityKeyPair().getPrivateKey()))
.orElse(null);
logger.debug("Uploading new profile"); logger.debug("Uploading new profile");
final var avatarPath = dependencies.getAccountManager() final var avatarPath = NetworkResultUtil.toSetProfileLegacy(dependencies.getProfileApi()
.setVersionedProfile(account.getAci(), .setVersionedProfile(account.getAci(),
account.getProfileKey(), account.getProfileKey(),
newProfile.getInternalServiceName(), newProfile.getInternalServiceName(),
@ -207,9 +210,9 @@ public final class ProfileHelper {
avatarUploadParams, avatarUploadParams,
List.of(/* TODO implement support for badges */), List.of(/* TODO implement support for badges */),
account.getConfigurationStore().getPhoneNumberSharingMode() account.getConfigurationStore().getPhoneNumberSharingMode()
== PhoneNumberSharingMode.EVERYBODY); == PhoneNumberSharingMode.EVERYBODY));
if (!avatarUploadParams.keepTheSame) { if (!avatarUploadParams.keepTheSame) {
builder.withAvatarUrlPath(avatarPath.orElse(null)); builder.withAvatarUrlPath(avatarPath);
} }
newProfile = builder.build(); newProfile = builder.build();
} }
@ -270,7 +273,9 @@ public final class ProfileHelper {
} }
private Profile decryptProfileAndDownloadAvatar( 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(); final var avatarPath = encryptedProfile.getAvatar();
downloadProfileAvatar(recipientId, avatarPath, profileKey); downloadProfileAvatar(recipientId, avatarPath, profileKey);
@ -279,7 +284,9 @@ public final class ProfileHelper {
} }
public void downloadProfileAvatar( 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); var profile = account.getProfileStore().getProfile(recipientId);
if (profile == null || !Objects.equals(avatarPath, profile.getAvatarUrlPath())) { if (profile == null || !Objects.equals(avatarPath, profile.getAvatarUrlPath())) {
@ -307,7 +314,8 @@ public final class ProfileHelper {
} }
private Single<ProfileAndCredential> retrieveProfile( private Single<ProfileAndCredential> retrieveProfile(
RecipientId recipientId, SignalServiceProfile.RequestType requestType RecipientId recipientId,
SignalServiceProfile.RequestType requestType
) { ) {
var unidentifiedAccess = getUnidentifiedAccess(recipientId); var unidentifiedAccess = getUnidentifiedAccess(recipientId);
var profileKey = Optional.ofNullable(account.getProfileStore().getProfileKey(recipientId)); var profileKey = Optional.ofNullable(account.getProfileStore().getProfileKey(recipientId));
@ -330,13 +338,6 @@ public final class ProfileHelper {
final var profile = account.getProfileStore().getProfile(recipientId); 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; Profile newProfile = null;
if (profileKey.isPresent()) { if (profileKey.isPresent()) {
logger.trace("Decrypting profile"); logger.trace("Decrypting profile");
@ -352,6 +353,18 @@ public final class ProfileHelper {
.build(); .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 { try {
logger.trace("Storing identity"); logger.trace("Storing identity");
final var identityKey = new IdentityKey(Base64.getDecoder().decode(encryptedProfile.getIdentityKey())); final var identityKey = new IdentityKey(Base64.getDecoder().decode(encryptedProfile.getIdentityKey()));
@ -363,6 +376,7 @@ public final class ProfileHelper {
logger.trace("Storing profile"); logger.trace("Storing profile");
account.getProfileStore().storeProfile(recipientId, newProfile); account.getProfileStore().storeProfile(recipientId, newProfile);
account.getRecipientStore().markRegistered(recipientId, true);
logger.trace("Done handling retrieved profile"); logger.trace("Done handling retrieved profile");
}).doOnError(e -> { }).doOnError(e -> {
@ -374,6 +388,10 @@ public final class ProfileHelper {
.withUnidentifiedAccessMode(Profile.UnidentifiedAccessMode.UNKNOWN) .withUnidentifiedAccessMode(Profile.UnidentifiedAccessMode.UNKNOWN)
.withCapabilities(Set.of()) .withCapabilities(Set.of())
.build(); .build();
if (e instanceof NotFoundException) {
logger.debug("Marking recipient {} as unregistered after 404 profile fetch.", recipientId);
account.getRecipientStore().markRegistered(recipientId, false);
}
account.getProfileStore().storeProfile(recipientId, newProfile); account.getProfileStore().storeProfile(recipientId, newProfile);
}); });
@ -382,7 +400,7 @@ public final class ProfileHelper {
private Single<ProfileAndCredential> retrieveProfile( private Single<ProfileAndCredential> retrieveProfile(
SignalServiceAddress address, SignalServiceAddress address,
Optional<ProfileKey> profileKey, Optional<ProfileKey> profileKey,
Optional<UnidentifiedAccess> unidentifiedAccess, @Nullable SealedSenderAccess unidentifiedAccess,
SignalServiceProfile.RequestType requestType SignalServiceProfile.RequestType requestType
) { ) {
final var profileService = dependencies.getProfileService(); final var profileService = dependencies.getProfileService();
@ -402,9 +420,7 @@ public final class ProfileHelper {
}); });
} }
private void downloadProfileAvatar( private void downloadProfileAvatar(RecipientAddress address, String avatarPath, ProfileKey profileKey) {
RecipientAddress address, String avatarPath, ProfileKey profileKey
) {
if (avatarPath == null) { if (avatarPath == null) {
try { try {
context.getAvatarStore().deleteProfileAvatar(address); context.getAvatarStore().deleteProfileAvatar(address);
@ -424,7 +440,9 @@ public final class ProfileHelper {
} }
private void retrieveProfileAvatar( private void retrieveProfileAvatar(
String avatarPath, ProfileKey profileKey, OutputStream outputStream String avatarPath,
ProfileKey profileKey,
OutputStream outputStream
) throws IOException { ) throws IOException {
var tmpFile = IOUtils.createTempFile(); var tmpFile = IOUtils.createTempFile();
try (var input = dependencies.getMessageReceiver() try (var input = dependencies.getMessageReceiver()
@ -445,13 +463,7 @@ public final class ProfileHelper {
} }
} }
private Optional<UnidentifiedAccess> getUnidentifiedAccess(RecipientId recipientId) { private @Nullable SealedSenderAccess getUnidentifiedAccess(RecipientId recipientId) {
var unidentifiedAccess = context.getUnidentifiedAccessHelper().getAccessFor(recipientId, true); return context.getUnidentifiedAccessHelper().getSealedSenderAccessFor(recipientId, true);
if (unidentifiedAccess.isPresent()) {
return unidentifiedAccess.get().getTargetUnidentifiedAccess();
}
return Optional.empty();
} }
} }

View file

@ -11,10 +11,10 @@ import org.asamk.signal.manager.storage.messageCache.CachedMessage;
import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalWebSocket;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState; import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException; import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException;
@ -28,7 +28,6 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
public class ReceiveHelper { public class ReceiveHelper {
@ -41,7 +40,6 @@ public class ReceiveHelper {
private final Context context; private final Context context;
private ReceiveConfig receiveConfig = new ReceiveConfig(false, false, false); private ReceiveConfig receiveConfig = new ReceiveConfig(false, false, false);
private boolean needsToRetryFailedMessages = false;
private boolean hasCaughtUpWithOldMessages = false; private boolean hasCaughtUpWithOldMessages = false;
private boolean isWaitingForMessage = false; private boolean isWaitingForMessage = false;
private boolean shouldStop = false; private boolean shouldStop = false;
@ -59,10 +57,6 @@ public class ReceiveHelper {
dependencies.setAllowStories(!receiveConfig.ignoreStories()); dependencies.setAllowStories(!receiveConfig.ignoreStories());
} }
public void setNeedsToRetryFailedMessages(final boolean needsToRetryFailedMessages) {
this.needsToRetryFailedMessages = needsToRetryFailedMessages;
}
public void setAuthenticationFailureListener(final Callable authenticationFailureListener) { public void setAuthenticationFailureListener(final Callable authenticationFailureListener) {
this.authenticationFailureListener = authenticationFailureListener; this.authenticationFailureListener = authenticationFailureListener;
} }
@ -88,22 +82,25 @@ public class ReceiveHelper {
} }
public void receiveMessages( public void receiveMessages(
Duration timeout, boolean returnOnTimeout, Integer maxMessages, Manager.ReceiveMessageHandler handler Duration timeout,
boolean returnOnTimeout,
Integer maxMessages,
Manager.ReceiveMessageHandler handler
) throws IOException { ) throws IOException {
needsToRetryFailedMessages = true; account.setNeedsToRetryFailedMessages(true);
hasCaughtUpWithOldMessages = false; hasCaughtUpWithOldMessages = false;
// Use a Map here because java Set doesn't have a get method ... // Use a Map here because java Set doesn't have a get method ...
Map<HandleAction, HandleAction> queuedActions = new HashMap<>(); Map<HandleAction, HandleAction> queuedActions = new HashMap<>();
final var signalWebSocket = dependencies.getSignalWebSocket(); final var signalWebSocket = dependencies.getAuthenticatedSignalWebSocket();
final var webSocketStateDisposable = Observable.merge(signalWebSocket.getUnidentifiedWebSocketState(), final var webSocketStateDisposable = signalWebSocket.getState()
signalWebSocket.getWebSocketState())
.subscribeOn(Schedulers.computation()) .subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation()) .observeOn(Schedulers.computation())
.distinctUntilChanged() .distinctUntilChanged()
.subscribe(this::onWebSocketStateChange); .subscribe(this::onWebSocketStateChange);
signalWebSocket.connect(); signalWebSocket.connect();
signalWebSocket.registerKeepAliveToken("receive");
try { try {
receiveMessagesInternal(signalWebSocket, timeout, returnOnTimeout, maxMessages, handler, queuedActions); receiveMessagesInternal(signalWebSocket, timeout, returnOnTimeout, maxMessages, handler, queuedActions);
@ -111,6 +108,7 @@ public class ReceiveHelper {
hasCaughtUpWithOldMessages = false; hasCaughtUpWithOldMessages = false;
handleQueuedActions(queuedActions.keySet()); handleQueuedActions(queuedActions.keySet());
queuedActions.clear(); queuedActions.clear();
signalWebSocket.removeKeepAliveToken("receive");
signalWebSocket.disconnect(); signalWebSocket.disconnect();
webSocketStateDisposable.dispose(); webSocketStateDisposable.dispose();
shouldStop = false; shouldStop = false;
@ -118,7 +116,7 @@ public class ReceiveHelper {
} }
private void receiveMessagesInternal( private void receiveMessagesInternal(
final SignalWebSocket signalWebSocket, final SignalWebSocket.AuthenticatedWebSocket signalWebSocket,
Duration timeout, Duration timeout,
boolean returnOnTimeout, boolean returnOnTimeout,
Integer maxMessages, Integer maxMessages,
@ -130,9 +128,8 @@ public class ReceiveHelper {
isWaitingForMessage = false; isWaitingForMessage = false;
while (!shouldStop && remainingMessages != 0) { while (!shouldStop && remainingMessages != 0) {
if (needsToRetryFailedMessages) { if (account.getNeedsToRetryFailedMessages()) {
retryFailedReceivedMessages(handler); retryFailedReceivedMessages(handler);
needsToRetryFailedMessages = false;
} }
SignalServiceEnvelope envelope; SignalServiceEnvelope envelope;
final CachedMessage[] cachedMessage = {null}; final CachedMessage[] cachedMessage = {null};
@ -235,9 +232,9 @@ public class ReceiveHelper {
if (exception instanceof UntrustedIdentityException) { if (exception instanceof UntrustedIdentityException) {
logger.debug("Keeping message with untrusted identity in message cache"); logger.debug("Keeping message with untrusted identity in message cache");
final var address = ((UntrustedIdentityException) exception).getSender(); final var address = ((UntrustedIdentityException) exception).getSender();
if (envelope.getSourceServiceId().isEmpty() && address.uuid().isPresent()) { if (envelope.getSourceServiceId().isEmpty() && address.aci().isPresent()) {
final var recipientId = account.getRecipientResolver() final var recipientId = account.getRecipientResolver()
.resolveRecipient(ACI.from(address.uuid().get())); .resolveRecipient(ACI.parseOrThrow(address.aci().get()));
try { try {
cachedMessage[0] = account.getMessageCache() cachedMessage[0] = account.getMessageCache()
.replaceSender(cachedMessage[0], recipientId); .replaceSender(cachedMessage[0], recipientId);
@ -266,10 +263,12 @@ public class ReceiveHelper {
} }
} }
handleQueuedActions(queuedActions); handleQueuedActions(queuedActions);
account.setNeedsToRetryFailedMessages(false);
} }
private List<HandleAction> retryFailedReceivedMessage( private List<HandleAction> retryFailedReceivedMessage(
final Manager.ReceiveMessageHandler handler, final CachedMessage cachedMessage final Manager.ReceiveMessageHandler handler,
final CachedMessage cachedMessage
) { ) {
var envelope = cachedMessage.loadEnvelope(); var envelope = cachedMessage.loadEnvelope();
if (envelope == null) { if (envelope == null) {
@ -282,8 +281,8 @@ public class ReceiveHelper {
final var exception = result.second(); final var exception = result.second();
if (exception instanceof UntrustedIdentityException) { if (exception instanceof UntrustedIdentityException) {
if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 30) { if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 14) {
// Envelope is more than a month old, cleaning up. // Envelope is more than two weeks old, cleaning up.
cachedMessage.delete(); cachedMessage.delete();
return null; return null;
} }

View file

@ -3,7 +3,6 @@ package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.RecipientIdentifier;
import org.asamk.signal.manager.api.UnregisteredRecipientException; import org.asamk.signal.manager.api.UnregisteredRecipientException;
import org.asamk.signal.manager.api.UsernameLinkUrl; import org.asamk.signal.manager.api.UsernameLinkUrl;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.internal.SignalDependencies; import org.asamk.signal.manager.internal.SignalDependencies;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientId;
@ -11,12 +10,14 @@ import org.signal.libsignal.usernames.BaseUsernameException;
import org.signal.libsignal.usernames.Username; import org.signal.libsignal.usernames.Username;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.cds.CdsiV2Service;
import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.PNI; import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException;
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException; import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
import org.whispersystems.signalservice.api.services.CdsiV2Service; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import java.io.IOException; import java.io.IOException;
import java.util.Collection; import java.util.Collection;
@ -25,8 +26,10 @@ import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import static org.asamk.signal.manager.config.ServiceConfig.MAXIMUM_ONE_OFF_REQUEST_SIZE; import static org.asamk.signal.manager.config.ServiceConfig.MAXIMUM_ONE_OFF_REQUEST_SIZE;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class RecipientHelper { public class RecipientHelper {
@ -34,12 +37,10 @@ public class RecipientHelper {
private final SignalAccount account; private final SignalAccount account;
private final SignalDependencies dependencies; private final SignalDependencies dependencies;
private final ServiceEnvironmentConfig serviceEnvironmentConfig;
public RecipientHelper(final Context context) { public RecipientHelper(final Context context) {
this.account = context.getAccount(); this.account = context.getAccount();
this.dependencies = context.getDependencies(); this.dependencies = context.getDependencies();
this.serviceEnvironmentConfig = dependencies.getServiceEnvironmentConfig();
} }
public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) {
@ -68,7 +69,7 @@ public class RecipientHelper {
.toSignalServiceAddress(); .toSignalServiceAddress();
} }
public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException { public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException, IOException {
final var recipientIds = new HashSet<RecipientId>(recipients.size()); final var recipientIds = new HashSet<RecipientId>(recipients.size());
for (var number : recipients) { for (var number : recipients) {
final var recipientId = resolveRecipient(number); final var recipientId = resolveRecipient(number);
@ -78,10 +79,11 @@ public class RecipientHelper {
} }
public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredRecipientException { public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) { if (recipient instanceof RecipientIdentifier.Uuid(UUID uuid)) {
return account.getRecipientResolver().resolveRecipient(ACI.from(uuidRecipient.uuid())); return account.getRecipientResolver().resolveRecipient(ACI.from(uuid));
} else if (recipient instanceof RecipientIdentifier.Number numberRecipient) { } else if (recipient instanceof RecipientIdentifier.Pni(UUID pni)) {
final var number = numberRecipient.number(); return account.getRecipientResolver().resolveRecipient(PNI.from(pni));
} else if (recipient instanceof RecipientIdentifier.Number(String number)) {
return account.getRecipientStore().resolveRecipientByNumber(number, () -> { return account.getRecipientStore().resolveRecipientByNumber(number, () -> {
try { try {
return getRegisteredUserByNumber(number); return getRegisteredUserByNumber(number);
@ -89,38 +91,70 @@ public class RecipientHelper {
return null; return null;
} }
}); });
} else if (recipient instanceof RecipientIdentifier.Username usernameRecipient) { } else if (recipient instanceof RecipientIdentifier.Username(String username)) {
var username = usernameRecipient.username();
try { try {
UsernameLinkUrl usernameLinkUrl = UsernameLinkUrl.fromUri(username); return resolveRecipientByUsernameOrLink(username, false);
final var components = usernameLinkUrl.getComponents(); } catch (Exception e) {
final var encryptedUsername = dependencies.getAccountManager() return null;
.getEncryptedUsernameFromLinkServerId(components.getServerId());
final var link = new Username.UsernameLink(components.getEntropy(), encryptedUsername);
username = Username.fromLink(link).getUsername();
} catch (UsernameLinkUrl.InvalidUsernameLinkException e) {
} catch (IOException | BaseUsernameException e) {
throw new RuntimeException(e);
} }
final String finalUsername = username;
return account.getRecipientStore().resolveRecipientByUsername(finalUsername, () -> {
try {
return getRegisteredUserByUsername(finalUsername);
} catch (Exception e) {
return null;
}
});
} }
throw new AssertionError("Unexpected RecipientIdentifier: " + recipient); throw new AssertionError("Unexpected RecipientIdentifier: " + recipient);
} }
public RecipientId resolveRecipientByUsernameOrLink(
String username,
boolean forceRefresh
) throws UnregisteredRecipientException, IOException {
final Username finalUsername;
try {
finalUsername = getUsernameFromUsernameOrLink(username);
} catch (IOException | BaseUsernameException e) {
throw new RuntimeException(e);
}
if (forceRefresh) {
try {
final var aci = handleResponseException(dependencies.getUsernameApi().getAciByUsername(finalUsername));
return account.getRecipientStore().resolveRecipientTrusted(aci, finalUsername.getUsername());
} 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 handleResponseException(dependencies.getUsernameApi().getAciByUsername(finalUsername));
} catch (Exception e) {
return null;
}
});
}
private Username getUsernameFromUsernameOrLink(String username) throws BaseUsernameException, IOException {
try {
final var usernameLinkUrl = UsernameLinkUrl.fromUri(username);
final var components = usernameLinkUrl.getComponents();
final var encryptedUsername = handleResponseException(dependencies.getUsernameApi()
.getEncryptedUsernameFromLinkServerId(components.getServerId()));
final var link = new Username.UsernameLink(components.getEntropy(), encryptedUsername);
return Username.fromLink(link);
} catch (UsernameLinkUrl.InvalidUsernameLinkException e) {
return new Username(username);
}
}
public Optional<RecipientId> resolveRecipientOptional(final RecipientIdentifier.Single recipient) { public Optional<RecipientId> resolveRecipientOptional(final RecipientIdentifier.Single recipient) {
try { try {
return Optional.of(resolveRecipient(recipient)); return Optional.of(resolveRecipient(recipient));
} catch (UnregisteredRecipientException e) { } catch (UnregisteredRecipientException e) {
if (recipient instanceof RecipientIdentifier.Number r) { if (recipient instanceof RecipientIdentifier.Number(String number)) {
return account.getRecipientStore().resolveRecipientByNumberOptional(r.number()); return account.getRecipientStore().resolveRecipientByNumberOptional(number);
} else { } else {
return Optional.empty(); return Optional.empty();
} }
@ -156,7 +190,8 @@ public class RecipientHelper {
} }
private Map<String, RegisteredUser> getRegisteredUsers( private Map<String, RegisteredUser> getRegisteredUsers(
final Set<String> numbers, final boolean isPartialRefresh final Set<String> numbers,
final boolean isPartialRefresh
) throws IOException { ) throws IOException {
Map<String, RegisteredUser> registeredUsers = getRegisteredUsersV2(numbers, isPartialRefresh); Map<String, RegisteredUser> registeredUsers = getRegisteredUsersV2(numbers, isPartialRefresh);
@ -166,7 +201,8 @@ public class RecipientHelper {
final var unregisteredUsers = new HashSet<>(numbers); final var unregisteredUsers = new HashSet<>(numbers);
unregisteredUsers.removeAll(registeredUsers.keySet()); unregisteredUsers.removeAll(registeredUsers.keySet());
account.getRecipientStore().markUnregistered(unregisteredUsers); account.getRecipientStore().markUndiscoverablePossiblyUnregistered(unregisteredUsers);
account.getRecipientStore().markDiscoverable(registeredUsers.keySet());
return registeredUsers; return registeredUsers;
} }
@ -176,17 +212,18 @@ public class RecipientHelper {
try { try {
aciMap = getRegisteredUsers(Set.of(number), true); aciMap = getRegisteredUsers(Set.of(number), true);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null, number)); throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(number));
} }
final var user = aciMap.get(number); final var user = aciMap.get(number);
if (user == null) { if (user == null) {
throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null, number)); throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(number));
} }
return user.getServiceId(); return user.getServiceId();
} }
private Map<String, RegisteredUser> getRegisteredUsersV2( private Map<String, RegisteredUser> getRegisteredUsersV2(
final Set<String> numbers, boolean isPartialRefresh final Set<String> numbers,
boolean isPartialRefresh
) throws IOException { ) throws IOException {
final var previousNumbers = isPartialRefresh ? Set.<String>of() : account.getCdsiStore().getAllNumbers(); final var previousNumbers = isPartialRefresh ? Set.<String>of() : account.getCdsiStore().getAllNumbers();
final var newNumbers = new HashSet<>(numbers) {{ final var newNumbers = new HashSet<>(numbers) {{
@ -206,13 +243,13 @@ public class RecipientHelper {
final CdsiV2Service.Response response; final CdsiV2Service.Response response;
try { try {
response = dependencies.getAccountManager() response = handleResponseException(dependencies.getCdsApi()
.getRegisteredUsersWithCdsi(previousNumbers, .getRegisteredUsers(token.isEmpty() ? Set.of() : previousNumbers,
newNumbers, newNumbers,
account.getRecipientStore().getServiceIdToProfileKeyMap(), account.getRecipientStore().getServiceIdToProfileKeyMap(),
token, token,
serviceEnvironmentConfig.cdsiMrenclave(),
null, null,
dependencies.getLibSignalNetwork(),
newToken -> { newToken -> {
if (isPartialRefresh) { if (isPartialRefresh) {
account.getCdsiStore().updateAfterPartialCdsQuery(newNumbers); account.getCdsiStore().updateAfterPartialCdsQuery(newNumbers);
@ -228,8 +265,8 @@ public class RecipientHelper {
account.setCdsiToken(newToken); account.setCdsiToken(newToken);
account.setLastRecipientsRefresh(System.currentTimeMillis()); account.setLastRecipientsRefresh(System.currentTimeMillis());
} }
}); }));
} catch (CdsiInvalidTokenException e) { } catch (CdsiInvalidTokenException | CdsiInvalidArgumentException e) {
account.setCdsiToken(null); account.setCdsiToken(null);
account.getCdsiStore().clearAll(); account.getCdsiStore().clearAll();
throw e; throw e;
@ -245,10 +282,6 @@ public class RecipientHelper {
return registeredUsers; return registeredUsers;
} }
private ACI getRegisteredUserByUsername(String username) throws IOException, BaseUsernameException {
return dependencies.getAccountManager().getAciByUsername(new Username(username));
}
public record RegisteredUser(Optional<ACI> aci, Optional<PNI> pni) { public record RegisteredUser(Optional<ACI> aci, Optional<PNI> pni) {
public RegisteredUser { public RegisteredUser {

View file

@ -13,6 +13,7 @@ import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.groups.GroupInfo;
import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.sendLog.MessageSendLogEntry; import org.asamk.signal.manager.storage.sendLog.MessageSendLogEntry;
import org.jetbrains.annotations.Nullable;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.InvalidRegistrationIdException; import org.signal.libsignal.protocol.InvalidRegistrationIdException;
import org.signal.libsignal.protocol.NoSessionException; import org.signal.libsignal.protocol.NoSessionException;
@ -22,9 +23,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.ContentHint;
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.groupsv2.GroupSendEndorsements;
import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage;
@ -84,8 +86,8 @@ public class SendHelper {
account.getContactStore().storeContact(recipientId, contact); account.getContactStore().storeContact(recipientId, contact);
} }
final var expirationTime = contact.messageExpirationTime(); messageBuilder.withExpiration(contact.messageExpirationTime());
messageBuilder.withExpiration(expirationTime); messageBuilder.withExpireTimerVersion(contact.messageExpirationTimeVersion());
if (!contact.isBlocked()) { if (!contact.isBlocked()) {
final var profileKey = account.getProfileKey().serialize(); final var profileKey = account.getProfileKey().serialize();
@ -123,7 +125,8 @@ public class SendHelper {
} }
public SendMessageResult sendReceiptMessage( public SendMessageResult sendReceiptMessage(
final SignalServiceReceiptMessage receiptMessage, final RecipientId recipientId final SignalServiceReceiptMessage receiptMessage,
final RecipientId recipientId
) { ) {
final var messageSendLogStore = account.getMessageSendLogStore(); final var messageSendLogStore = account.getMessageSendLogStore();
final var result = handleSendMessage(recipientId, final var result = handleSendMessage(recipientId,
@ -155,7 +158,9 @@ public class SendHelper {
} }
public SendMessageResult sendRetryReceipt( 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: {}", logger.debug("Sending retry receipt for {} to {}, device: {}",
errorMessage.getTimestamp(), errorMessage.getTimestamp(),
@ -181,12 +186,13 @@ public class SendHelper {
} }
public SendMessageResult sendSelfMessage( public SendMessageResult sendSelfMessage(
SignalServiceDataMessage.Builder messageBuilder, Optional<Long> editTargetTimestamp SignalServiceDataMessage.Builder messageBuilder,
Optional<Long> editTargetTimestamp
) { ) {
final var recipientId = account.getSelfRecipientId(); final var recipientId = account.getSelfRecipientId();
final var contact = account.getContactStore().getContact(recipientId); final var contact = account.getContactStore().getContact(recipientId);
final var expirationTime = contact != null ? contact.messageExpirationTime() : 0; messageBuilder.withExpiration(contact != null ? contact.messageExpirationTime() : 0);
messageBuilder.withExpiration(expirationTime); messageBuilder.withExpireTimerVersion(contact != null ? contact.messageExpirationTimeVersion() : 1);
var message = messageBuilder.build(); var message = messageBuilder.build();
return sendSelfMessage(message, editTargetTimestamp); return sendSelfMessage(message, editTargetTimestamp);
@ -199,31 +205,20 @@ public class SendHelper {
return SendMessageResult.success(account.getSelfAddress(), List.of(), false, false, 0, Optional.empty()); return SendMessageResult.success(account.getSelfAddress(), List.of(), false, false, 0, Optional.empty());
} }
try { try {
return messageSender.sendSyncMessage(message, context.getUnidentifiedAccessHelper().getAccessForSync()); return messageSender.sendSyncMessage(message);
} catch (UnregisteredUserException e) { } catch (Throwable e) {
var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId()); var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
return SendMessageResult.unregisteredFailure(address); try {
} catch (ProofRequiredException e) { return SignalServiceMessageSender.mapSendErrorToSendResult(e, System.currentTimeMillis(), address);
var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId()); } catch (IOException ex) {
return SendMessageResult.proofRequiredFailure(address, e); logger.warn("Failed to send message due to IO exception: {}", e.getMessage());
} catch (RateLimitException e) { logger.debug("Exception", e);
var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId()); return SendMessageResult.networkFailure(address);
logger.warn("Sending failed due to rate limiting from the signal server: {}", e.getMessage()); }
return SendMessageResult.rateLimitFailure(address, e);
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
return SendMessageResult.identityFailure(address, e.getIdentityKey());
} catch (IOException e) {
var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
logger.warn("Failed to send message due to IO exception: {}", e.getMessage());
logger.debug("Exception", e);
return SendMessageResult.networkFailure(address);
} }
} }
public SendMessageResult sendTypingMessage( public SendMessageResult sendTypingMessage(SignalServiceTypingMessage message, RecipientId recipientId) {
SignalServiceTypingMessage message, RecipientId recipientId
) {
final var result = handleSendMessage(recipientId, final var result = handleSendMessage(recipientId,
(messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendTyping(List.of( (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendTyping(List.of(
address), List.of(unidentifiedAccess), message, null).getFirst()); address), List.of(unidentifiedAccess), message, null).getFirst());
@ -232,7 +227,8 @@ public class SendHelper {
} }
public List<SendMessageResult> sendGroupTypingMessage( public List<SendMessageResult> sendGroupTypingMessage(
SignalServiceTypingMessage message, GroupId groupId SignalServiceTypingMessage message,
GroupId groupId
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
final var g = getGroupForSending(groupId); final var g = getGroupForSending(groupId);
if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) { if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
@ -245,7 +241,9 @@ public class SendHelper {
} }
public SendMessageResult resendMessage( 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); logger.trace("Resending message {} to {}", timestamp, recipientId);
if (messageSendLogEntry.groupId().isEmpty()) { if (messageSendLogEntry.groupId().isEmpty()) {
@ -261,7 +259,7 @@ public class SendHelper {
} }
final var groupId = messageSendLogEntry.groupId().get(); final var groupId = messageSendLogEntry.groupId().get();
final var group = account.getGroupStore().getGroup(groupId); final var group = context.getGroupHelper().getGroup(groupId);
if (group == null) { if (group == null) {
logger.debug("Could not find a matching group for the groupId {}! Skipping message send.", logger.debug("Could not find a matching group for the groupId {}! Skipping message send.",
@ -380,10 +378,11 @@ public class SendHelper {
() -> false, () -> false,
urgent, urgent,
editTargetTimestamp.get()); editTargetTimestamp.get());
final SenderKeySenderHandler senderKeySender = (distId, recipients, unidentifiedAccess, isRecipientUpdate) -> messageSender.sendGroupDataMessage( final SenderKeySenderHandler senderKeySender = (distId, recipients, unidentifiedAccess, groupSendEndorsements, isRecipientUpdate) -> messageSender.sendGroupDataMessage(
distId, distId,
recipients, recipients,
unidentifiedAccess, unidentifiedAccess,
groupSendEndorsements,
isRecipientUpdate, isRecipientUpdate,
contentHint, contentHint,
message, message,
@ -436,9 +435,11 @@ public class SendHelper {
unidentifiedAccess, unidentifiedAccess,
message, message,
() -> false), () -> false),
(distId, recipients, unidentifiedAccess, isRecipientUpdate) -> messageSender.sendGroupTyping(distId, (distId, recipients, unidentifiedAccess, groupSendEndorsements, isRecipientUpdate) -> messageSender.sendGroupTyping(
distId,
recipients, recipients,
unidentifiedAccess, unidentifiedAccess,
groupSendEndorsements,
message), message),
recipientIds, recipientIds,
distributionId); distributionId);
@ -523,23 +524,11 @@ public class SendHelper {
} }
private Set<RecipientId> getSenderKeyCapableRecipientIds(final Set<RecipientId> recipientIds) { private Set<RecipientId> getSenderKeyCapableRecipientIds(final Set<RecipientId> recipientIds) {
final var selfProfile = context.getProfileHelper().getSelfProfile();
if (selfProfile == null || !selfProfile.getCapabilities().contains(Profile.Capability.senderKey)) {
logger.debug("Not all of our devices support sender key. Using legacy.");
return Set.of();
}
final var senderKeyTargets = new HashSet<RecipientId>(); final var senderKeyTargets = new HashSet<RecipientId>();
final var recipientList = new ArrayList<>(recipientIds); final var recipientList = new ArrayList<>(recipientIds);
final var profiles = context.getProfileHelper().getRecipientProfiles(recipientList).iterator();
for (final var recipientId : recipientList) { for (final var recipientId : recipientList) {
final var profile = profiles.next(); final var access = context.getUnidentifiedAccessHelper().getSealedSenderAccessFor(recipientId);
if (profile == null || !profile.getCapabilities().contains(Profile.Capability.senderKey)) { if (access == null) {
continue;
}
final var access = context.getUnidentifiedAccessHelper().getAccessFor(recipientId);
if (access.isEmpty() || access.get().getTargetUnidentifiedAccess().isEmpty()) {
continue; continue;
} }
@ -568,13 +557,16 @@ public class SendHelper {
} }
private List<SendMessageResult> sendGroupMessageInternalWithLegacy( 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 { ) throws IOException {
final var recipientIdList = new ArrayList<>(recipientIds); final var recipientIdList = new ArrayList<>(recipientIds);
final var addresses = recipientIdList.stream() final var addresses = recipientIdList.stream()
.map(context.getRecipientHelper()::resolveSignalServiceAddress) .map(context.getRecipientHelper()::resolveSignalServiceAddress)
.toList(); .toList();
final var unidentifiedAccesses = context.getUnidentifiedAccessHelper().getAccessFor(recipientIdList); final var unidentifiedAccesses = context.getUnidentifiedAccessHelper()
.getSealedSenderAccessFor(recipientIdList);
try { try {
final var results = sender.send(addresses, unidentifiedAccesses, isRecipientUpdate); final var results = sender.send(addresses, unidentifiedAccesses, isRecipientUpdate);
@ -613,15 +605,14 @@ public class SendHelper {
List<UnidentifiedAccess> unidentifiedAccesses = context.getUnidentifiedAccessHelper() List<UnidentifiedAccess> unidentifiedAccesses = context.getUnidentifiedAccessHelper()
.getAccessFor(recipientIdList) .getAccessFor(recipientIdList)
.stream() .stream()
.map(Optional::get)
.map(UnidentifiedAccessPair::getTargetUnidentifiedAccess)
.map(Optional::get)
.toList(); .toList();
final GroupSendEndorsements groupSendEndorsements = null;//TODO
try { try {
List<SendMessageResult> results = sender.send(distributionId, List<SendMessageResult> results = sender.send(distributionId,
addresses, addresses,
unidentifiedAccesses, unidentifiedAccesses,
groupSendEndorsements,
isRecipientUpdate); isRecipientUpdate);
final var successCount = results.stream().filter(SendMessageResult::isSuccess).count(); final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
@ -660,7 +651,9 @@ public class SendHelper {
} }
private SendMessageResult sendMessage( private SendMessageResult sendMessage(
SignalServiceDataMessage message, RecipientId recipientId, Optional<Long> editTargetTimestamp SignalServiceDataMessage message,
RecipientId recipientId,
Optional<Long> editTargetTimestamp
) { ) {
final var messageSendLogStore = account.getMessageSendLogStore(); final var messageSendLogStore = account.getMessageSendLogStore();
final var urgent = true; final var urgent = true;
@ -696,7 +689,7 @@ public class SendHelper {
try { try {
return s.send(messageSender, return s.send(messageSender,
address, address,
context.getUnidentifiedAccessHelper().getAccessFor(recipientId), context.getUnidentifiedAccessHelper().getSealedSenderAccessFor(recipientId),
includePniSignature); includePniSignature);
} catch (UnregisteredUserException e) { } catch (UnregisteredUserException e) {
final RecipientId newRecipientId; final RecipientId newRecipientId;
@ -708,22 +701,17 @@ public class SendHelper {
address = context.getRecipientHelper().resolveSignalServiceAddress(newRecipientId); address = context.getRecipientHelper().resolveSignalServiceAddress(newRecipientId);
return s.send(messageSender, return s.send(messageSender,
address, address,
context.getUnidentifiedAccessHelper().getAccessFor(newRecipientId), context.getUnidentifiedAccessHelper().getSealedSenderAccessFor(newRecipientId),
includePniSignature); includePniSignature);
} }
} catch (UnregisteredUserException e) { } catch (Throwable e) {
return SendMessageResult.unregisteredFailure(address); try {
} catch (ProofRequiredException e) { return SignalServiceMessageSender.mapSendErrorToSendResult(e, System.currentTimeMillis(), address);
return SendMessageResult.proofRequiredFailure(address, e); } catch (IOException ex) {
} catch (RateLimitException e) { logger.warn("Failed to send message due to IO exception: {}", e.getMessage());
logger.warn("Sending failed due to rate limiting from the signal server: {}", e.getMessage()); logger.debug("Exception", e);
return SendMessageResult.rateLimitFailure(address, e); return SendMessageResult.networkFailure(address);
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { }
return SendMessageResult.identityFailure(address, e.getIdentityKey());
} catch (IOException e) {
logger.warn("Failed to send message due to IO exception: {}", e.getMessage());
logger.debug("Exception", e);
return SendMessageResult.networkFailure(address);
} }
} }
@ -784,7 +772,7 @@ public class SendHelper {
SendMessageResult send( SendMessageResult send(
SignalServiceMessageSender messageSender, SignalServiceMessageSender messageSender,
SignalServiceAddress address, SignalServiceAddress address,
Optional<UnidentifiedAccessPair> unidentifiedAccess, @Nullable SealedSenderAccess unidentifiedAccess,
boolean includePniSignature boolean includePniSignature
) throws IOException, UnregisteredUserException, ProofRequiredException, RateLimitException, org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; ) throws IOException, UnregisteredUserException, ProofRequiredException, RateLimitException, org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
} }
@ -795,6 +783,7 @@ public class SendHelper {
DistributionId distributionId, DistributionId distributionId,
List<SignalServiceAddress> recipients, List<SignalServiceAddress> recipients,
List<UnidentifiedAccess> unidentifiedAccess, List<UnidentifiedAccess> unidentifiedAccess,
GroupSendEndorsements groupSendEndorsements,
boolean isRecipientUpdate boolean isRecipientUpdate
) throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException, InvalidRegistrationIdException; ) throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException, InvalidRegistrationIdException;
} }
@ -803,7 +792,7 @@ public class SendHelper {
List<SendMessageResult> send( List<SendMessageResult> send(
List<SignalServiceAddress> recipients, List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess, List<SealedSenderAccess> unidentifiedAccess,
boolean isRecipientUpdate boolean isRecipientUpdate
) throws IOException, UntrustedIdentityException; ) throws IOException, UntrustedIdentityException;
} }

View file

@ -30,7 +30,9 @@ public class StickerHelper {
} }
public StickerPack addOrUpdateStickerPack( 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); final var sticker = account.getStickerStore().getStickerPack(stickerPackId);
if (sticker != null) { if (sticker != null) {
@ -50,7 +52,8 @@ public class StickerHelper {
} }
public JsonStickerPack getOrRetrieveStickerPack( public JsonStickerPack getOrRetrieveStickerPack(
StickerPackId packId, byte[] packKey StickerPackId packId,
byte[] packKey
) throws InvalidStickerException { ) throws InvalidStickerException {
try { try {
retrieveStickerPack(packId, packKey); 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.GroupIdV1;
import org.asamk.signal.manager.api.GroupIdV2; 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.internal.SignalDependencies;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.recipients.RecipientId; 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.signal.libsignal.protocol.InvalidKeyException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.storage.RecordIkm;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest; import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord; import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.api.storage.StorageKey; 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.ManifestRecord;
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
import java.io.IOException; import java.io.IOException;
import java.sql.Connection; import java.sql.Connection;
@ -32,9 +39,10 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class StorageHelper { public class StorageHelper {
private static final Logger logger = LoggerFactory.getLogger(StorageHelper.class); private static final Logger logger = LoggerFactory.getLogger(StorageHelper.class);
@ -54,7 +62,7 @@ public class StorageHelper {
} }
public void syncDataWithStorage() throws IOException { public void syncDataWithStorage() throws IOException {
final var storageKey = account.getOrCreateStorageKey(); var storageKey = account.getOrCreateStorageKey();
if (storageKey == null) { if (storageKey == null) {
if (!account.isPrimaryDevice()) { if (!account.isPrimaryDevice()) {
logger.debug("Storage key unknown, requesting from primary device."); logger.debug("Storage key unknown, requesting from primary device.");
@ -65,52 +73,76 @@ public class StorageHelper {
logger.trace("Reading manifest from remote storage"); logger.trace("Reading manifest from remote storage");
final var localManifestVersion = account.getStorageManifestVersion(); final var localManifestVersion = account.getStorageManifestVersion();
final var localManifest = account.getStorageManifest().orElse(SignalStorageManifest.EMPTY); final var localManifest = account.getStorageManifest().orElse(SignalStorageManifest.Companion.getEMPTY());
SignalStorageManifest remoteManifest; final var storageServiceRepository = dependencies.getStorageServiceRepository();
try { final var result = storageServiceRepository.getStorageManifestIfDifferentVersion(storageKey,
remoteManifest = dependencies.getAccountManager() localManifestVersion);
.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());
var needsForcePush = false; var needsForcePush = false;
if (remoteManifest.getVersion() > localManifestVersion) { final var remoteManifest = switch (result) {
logger.trace("Remote version was newer, reading records."); case ManifestIfDifferentVersionResult.DifferentVersion diff -> {
needsForcePush = readDataFromStorage(storageKey, localManifest, remoteManifest); final var manifest = diff.getManifest();
} else if (remoteManifest.getVersion() < localManifest.getVersion()) { storeManifestLocally(manifest);
logger.debug("Remote storage manifest version was older. User might have switched accounts."); yield manifest;
} }
logger.trace("Done reading data from remote storage"); 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) { if (remoteManifest != null) {
storeManifestLocally(remoteManifest); 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"); logger.trace("Adding missing storageIds to local data");
account.getRecipientStore().setMissingStorageIds(); account.getRecipientStore().setMissingStorageIds();
account.getGroupStore().setMissingStorageIds(); account.getGroupStore().setMissingStorageIds();
var needsMultiDeviceSync = false; var needsMultiDeviceSync = false;
try {
needsMultiDeviceSync = writeToStorage(storageKey, remoteManifest, needsForcePush); if (account.needsStorageKeyMigration()) {
} catch (RetryLaterException e) { logger.debug("Storage needs force push due to new account entropy pool");
// TODO retry later // Set new aep and reset previous master key and storage key
return; 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) { if (needsForcePush) {
@ -131,6 +163,23 @@ public class StorageHelper {
logger.debug("Done syncing data with remote storage"); 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( private boolean readDataFromStorage(
final StorageKey storageKey, final StorageKey storageKey,
final SignalStorageManifest localManifest, final SignalStorageManifest localManifest,
@ -140,36 +189,37 @@ public class StorageHelper {
try (final var connection = account.getAccountDatabase().getConnection()) { try (final var connection = account.getAccountDatabase().getConnection()) {
connection.setAutoCommit(false); connection.setAutoCommit(false);
var idDifference = findIdDifference(remoteManifest.getStorageIds(), localManifest.getStorageIds()); var idDifference = findIdDifference(remoteManifest.storageIds, localManifest.storageIds);
if (idDifference.hasTypeMismatches() && account.isPrimaryDevice()) { if (idDifference.hasTypeMismatches() && account.isPrimaryDevice()) {
logger.debug("Found type mismatches in the ID sets! Scheduling a force push after this sync completes."); logger.debug("Found type mismatches in the ID sets! Scheduling a force push after this sync completes.");
needsForcePush = true; needsForcePush = true;
} }
logger.debug("Pre-Merge ID Difference :: " + idDifference); logger.debug("Pre-Merge ID Difference :: {}", idDifference);
if (!idDifference.localOnlyIds().isEmpty()) {
final var updated = account.getRecipientStore()
.removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection, idDifference.localOnlyIds());
if (updated > 0) {
logger.warn(
"Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.",
updated);
}
}
if (!idDifference.isEmpty()) { if (!idDifference.isEmpty()) {
final var remoteOnlyRecords = getSignalStorageRecords(storageKey, idDifference.remoteOnlyIds()); final var remoteOnlyRecords = getSignalStorageRecords(storageKey,
remoteManifest,
idDifference.remoteOnlyIds());
if (remoteOnlyRecords.size() != idDifference.remoteOnlyIds().size()) { if (remoteOnlyRecords.size() != idDifference.remoteOnlyIds().size()) {
logger.debug("Could not find all remote-only records! Requested: " logger.debug(
+ idDifference.remoteOnlyIds() "Could not find all remote-only records! Requested: {}, Found: {}. These stragglers should naturally get deleted during the sync.",
.size() idDifference.remoteOnlyIds().size(),
+ ", Found: " remoteOnlyRecords.size());
+ remoteOnlyRecords.size() }
+ ". These stragglers should naturally get deleted during the sync.");
if (!idDifference.localOnlyIds().isEmpty()) {
final var updated = account.getRecipientStore()
.removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection,
idDifference.localOnlyIds());
if (updated > 0) {
logger.warn(
"Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.",
updated);
}
} }
final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords); final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords);
@ -194,18 +244,21 @@ public class StorageHelper {
return needsForcePush; 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()) { try (final var connection = account.getAccountDatabase().getConnection()) {
connection.setAutoCommit(false); connection.setAutoCommit(false);
final var knownUnknownIds = account.getUnknownStorageIdStore() final var knownUnknownIds = account.getUnknownStorageIdStore()
.getUnknownStorageIds(connection, KNOWN_TYPES); .getUnknownStorageIds(connection, KNOWN_TYPES);
if (!knownUnknownIds.isEmpty()) { 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); processKnownRecords(connection, remote);
account.getUnknownStorageIdStore() account.getUnknownStorageIdStore()
@ -218,22 +271,37 @@ public class StorageHelper {
} }
private boolean writeToStorage( private boolean writeToStorage(
final StorageKey storageKey, final SignalStorageManifest remoteManifest, final boolean needsForcePush final StorageKey storageKey,
final SignalStorageManifest remoteManifest,
final boolean needsForcePush
) throws IOException, RetryLaterException { ) throws IOException, RetryLaterException {
final WriteOperationResult remoteWriteOperation; final WriteOperationResult remoteWriteOperation;
try (final var connection = account.getAccountDatabase().getConnection()) { try (final var connection = account.getAccountDatabase().getConnection()) {
connection.setAutoCommit(false); connection.setAutoCommit(false);
final var localStorageIds = getAllLocalStorageIds(connection); var localStorageIds = getAllLocalStorageIds(connection);
final var idDifference = findIdDifference(remoteManifest.getStorageIds(), localStorageIds); var idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds);
logger.debug("ID Difference :: " + idDifference); logger.debug("ID Difference :: {}", idDifference);
final var unknownOnlyLocal = idDifference.localOnlyIds()
.stream()
.filter(id -> !KNOWN_TYPES.contains(id.getType()))
.toList();
if (!unknownOnlyLocal.isEmpty()) {
logger.debug("Storage ids with unknown type: {} to delete", unknownOnlyLocal.size());
account.getUnknownStorageIdStore().deleteUnknownStorageIds(connection, unknownOnlyLocal);
localStorageIds = getAllLocalStorageIds(connection);
idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds);
}
final var remoteDeletes = idDifference.remoteOnlyIds().stream().map(StorageId::getRaw).toList(); final var remoteDeletes = idDifference.remoteOnlyIds().stream().map(StorageId::getRaw).toList();
final var remoteInserts = buildLocalStorageRecords(connection, idDifference.localOnlyIds()); final var remoteInserts = buildLocalStorageRecords(connection, idDifference.localOnlyIds());
// TODO check if local storage record proto matches remote, then reset to remote storage_id // TODO check if local storage record proto matches remote, then reset to remote storage_id
remoteWriteOperation = new WriteOperationResult(new SignalStorageManifest(remoteManifest.getVersion() + 1, remoteWriteOperation = new WriteOperationResult(new SignalStorageManifest(remoteManifest.version + 1,
account.getDeviceId(), account.getDeviceId(),
remoteManifest.recordIkm,
localStorageIds), remoteInserts, remoteDeletes); localStorageIds), remoteInserts, remoteDeletes);
connection.commit(); connection.commit();
@ -242,39 +310,37 @@ public class StorageHelper {
} }
if (remoteWriteOperation.isEmpty()) { 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; return false;
} }
logger.debug("We have something to write remotely."); logger.debug("We have something to write remotely.");
logger.debug("WriteOperationResult :: " + remoteWriteOperation); logger.debug("WriteOperationResult :: {}", remoteWriteOperation);
StorageSyncValidations.validate(remoteWriteOperation, StorageSyncValidations.validate(remoteWriteOperation,
remoteManifest, remoteManifest,
needsForcePush, needsForcePush,
account.getSelfRecipientAddress()); account.getSelfRecipientAddress());
final Optional<SignalStorageManifest> conflict; final var result = dependencies.getStorageServiceRepository()
try { .writeStorageRecords(storageKey,
conflict = dependencies.getAccountManager() remoteWriteOperation.manifest(),
.writeStorageRecords(storageKey, remoteWriteOperation.inserts(),
remoteWriteOperation.manifest(), remoteWriteOperation.deletes());
remoteWriteOperation.inserts(), switch (result) {
remoteWriteOperation.deletes()); case WriteStorageRecordsResult.ConflictError ignored -> {
} catch (InvalidKeyException e) { logger.debug("Hit a conflict when trying to resolve the conflict! Retrying.");
logger.warn("Failed to decrypt conflicting storage manifest: {}", e.getMessage()); throw new RetryLaterException();
throw new IOException(e); }
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( private void forcePushToStorage(
@ -282,7 +348,8 @@ public class StorageHelper {
) throws IOException, RetryLaterException { ) throws IOException, RetryLaterException {
logger.debug("Force pushing local state to remote storage"); 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 newVersion = currentVersion + 1;
final var newStorageRecords = new ArrayList<SignalStorageRecord>(); final var newStorageRecords = new ArrayList<SignalStorageRecord>();
final Map<RecipientId, StorageId> newContactStorageIds; final Map<RecipientId, StorageId> newContactStorageIds;
@ -298,17 +365,19 @@ public class StorageHelper {
final var storageId = newContactStorageIds.get(recipientId); final var storageId = newContactStorageIds.get(recipientId);
if (storageId.getType() == ManifestRecord.Identifier.Type.ACCOUNT.getValue()) { if (storageId.getType() == ManifestRecord.Identifier.Type.ACCOUNT.getValue()) {
final var recipient = account.getRecipientStore().getRecipient(connection, recipientId); final var recipient = account.getRecipientStore().getRecipient(connection, recipientId);
final var accountRecord = StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(), final var accountRecord = StorageSyncModels.localToRemoteRecord(connection,
account.getConfigurationStore(),
recipient, recipient,
account.getUsernameLink(), account.getUsernameLink());
storageId.getRaw()); newStorageRecords.add(new SignalStorageRecord(storageId,
newStorageRecords.add(accountRecord); new StorageRecord.Builder().account(accountRecord).build()));
} else { } else {
final var recipient = account.getRecipientStore().getRecipient(connection, recipientId); final var recipient = account.getRecipientStore().getRecipient(connection, recipientId);
final var address = recipient.getAddress().getIdentifier(); final var address = recipient.getAddress().getIdentifier();
final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, address); final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, address);
final var record = StorageSyncModels.localToRemoteRecord(recipient, identity, storageId.getRaw()); final var record = StorageSyncModels.localToRemoteRecord(recipient, identity);
newStorageRecords.add(record); newStorageRecords.add(new SignalStorageRecord(storageId,
new StorageRecord.Builder().contact(record).build()));
} }
} }
@ -317,8 +386,9 @@ public class StorageHelper {
for (final var groupId : groupV1Ids) { for (final var groupId : groupV1Ids) {
final var storageId = newGroupV1StorageIds.get(groupId); final var storageId = newGroupV1StorageIds.get(groupId);
final var group = account.getGroupStore().getGroup(connection, groupId); final var group = account.getGroupStore().getGroup(connection, groupId);
final var record = StorageSyncModels.localToRemoteRecord(group, storageId.getRaw()); final var record = StorageSyncModels.localToRemoteRecord(group);
newStorageRecords.add(record); newStorageRecords.add(new SignalStorageRecord(storageId,
new StorageRecord.Builder().groupV1(record).build()));
} }
final var groupV2Ids = account.getGroupStore().getGroupV2Ids(connection); final var groupV2Ids = account.getGroupStore().getGroupV2Ids(connection);
@ -326,8 +396,9 @@ public class StorageHelper {
for (final var groupId : groupV2Ids) { for (final var groupId : groupV2Ids) {
final var storageId = newGroupV2StorageIds.get(groupId); final var storageId = newGroupV2StorageIds.get(groupId);
final var group = account.getGroupStore().getGroup(connection, groupId); final var group = account.getGroupStore().getGroup(connection, groupId);
final var record = StorageSyncModels.localToRemoteRecord(group, storageId.getRaw()); final var record = StorageSyncModels.localToRemoteRecord(group);
newStorageRecords.add(record); newStorageRecords.add(new SignalStorageRecord(storageId,
new StorageRecord.Builder().groupV2(record).build()));
} }
connection.commit(); connection.commit();
@ -336,34 +407,46 @@ public class StorageHelper {
} }
final var newStorageIds = newStorageRecords.stream().map(SignalStorageRecord::getId).toList(); 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()); StorageSyncValidations.validateForcePush(manifest, newStorageRecords, account.getSelfRecipientAddress());
final Optional<SignalStorageManifest> conflict; final WriteStorageRecordsResult result;
try { if (newVersion > 1) {
if (newVersion > 1) { logger.trace("Force-pushing data. Inserting {} IDs.", newStorageRecords.size());
logger.trace("Force-pushing data. Inserting {} IDs.", newStorageRecords.size()); result = dependencies.getStorageServiceRepository()
conflict = dependencies.getAccountManager() .resetAndWriteStorageRecords(storageServiceKey, manifest, newStorageRecords);
.resetStorageRecords(storageServiceKey, manifest, newStorageRecords); } else {
} else { logger.trace("First version, normal push. Inserting {} IDs.", newStorageRecords.size());
logger.trace("First version, normal push. Inserting {} IDs.", newStorageRecords.size()); result = dependencies.getStorageServiceRepository()
conflict = dependencies.getAccountManager() .writeStorageRecords(storageServiceKey, manifest, newStorageRecords, Collections.emptyList());
.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) { case WriteStorageRecordsResult.NetworkError networkError -> throw networkError.getException();
logger.debug("Hit an invalid key exception, which likely indicates a conflict.", e); case WriteStorageRecordsResult.StatusCodeError statusCodeError -> throw statusCodeError.getException();
throw new RetryLaterException(); 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()) { try (final var connection = account.getAccountDatabase().getConnection()) {
connection.setAutoCommit(false); connection.setAutoCommit(false);
account.getRecipientStore().updateStorageIds(connection, newContactStorageIds); account.getRecipientStore().updateStorageIds(connection, newContactStorageIds);
@ -403,21 +486,35 @@ public class StorageHelper {
private void storeManifestLocally( private void storeManifestLocally(
final SignalStorageManifest remoteManifest final SignalStorageManifest remoteManifest
) { ) {
account.setStorageManifestVersion(remoteManifest.getVersion()); account.setStorageManifestVersion(remoteManifest.version);
account.setStorageManifest(remoteManifest); account.setStorageManifest(remoteManifest);
} }
private List<SignalStorageRecord> getSignalStorageRecords( private List<SignalStorageRecord> getSignalStorageRecords(
final StorageKey storageKey, final List<StorageId> storageIds final StorageKey storageKey,
final SignalStorageManifest manifest,
final List<StorageId> storageIds
) throws IOException { ) throws IOException {
List<SignalStorageRecord> records; final var result = dependencies.getStorageServiceRepository()
try { .readStorageRecords(storageKey, manifest.recordIkm, storageIds);
records = dependencies.getAccountManager().readStorageRecords(storageKey, storageIds); return switch (result) {
} catch (InvalidKeyException e) { case StorageServiceRepository.StorageRecordResult.DecryptionError decryptionError -> {
logger.warn("Failed to read storage records, ignoring."); if (decryptionError.getException() instanceof InvalidKeyException) {
return List.of(); logger.warn("Failed to read storage records, ignoring.");
} yield List.of();
return records; } 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 { private List<StorageId> getAllLocalStorageIds(final Connection connection) throws SQLException {
@ -430,45 +527,52 @@ public class StorageHelper {
} }
private List<SignalStorageRecord> buildLocalStorageRecords( private List<SignalStorageRecord> buildLocalStorageRecords(
final Connection connection, final List<StorageId> storageIds final Connection connection,
final List<StorageId> storageIds
) throws SQLException { ) throws SQLException {
final var records = new ArrayList<SignalStorageRecord>(); final var records = new ArrayList<SignalStorageRecord>(storageIds.size());
for (final var storageId : storageIds) { for (final var storageId : storageIds) {
final var record = buildLocalStorageRecord(connection, storageId); final var record = buildLocalStorageRecord(connection, storageId);
if (record != null) { records.add(record);
records.add(record);
}
} }
return records; return records;
} }
private SignalStorageRecord buildLocalStorageRecord( private SignalStorageRecord buildLocalStorageRecord(
Connection connection, StorageId storageId Connection connection,
StorageId storageId
) throws SQLException { ) throws SQLException {
return switch (ManifestRecord.Identifier.Type.fromValue(storageId.getType())) { return switch (ManifestRecord.Identifier.Type.fromValue(storageId.getType())) {
case ManifestRecord.Identifier.Type.CONTACT -> { case ManifestRecord.Identifier.Type.CONTACT -> {
final var recipient = account.getRecipientStore().getRecipient(connection, storageId); final var recipient = account.getRecipientStore().getRecipient(connection, storageId);
final var address = recipient.getAddress().getIdentifier(); final var address = recipient.getAddress().getIdentifier();
final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, address); 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 -> { case ManifestRecord.Identifier.Type.GROUPV1 -> {
final var groupV1 = account.getGroupStore().getGroupV1(connection, storageId); 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 -> { case ManifestRecord.Identifier.Type.GROUPV2 -> {
final var groupV2 = account.getGroupStore().getGroupV2(connection, storageId); 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 -> { case ManifestRecord.Identifier.Type.ACCOUNT -> {
final var selfRecipient = account.getRecipientStore() final var selfRecipient = account.getRecipientStore()
.getRecipient(connection, account.getSelfRecipientId()); .getRecipient(connection, account.getSelfRecipientId());
yield StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
final var record = StorageSyncModels.localToRemoteRecord(connection,
account.getConfigurationStore(),
selfRecipient, selfRecipient,
account.getUsernameLink(), account.getUsernameLink());
storageId.getRaw()); 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. * exclusive to the local data set.
*/ */
private static IdDifferenceResult findIdDifference( private static IdDifferenceResult findIdDifference(
Collection<StorageId> remoteIds, Collection<StorageId> localIds Collection<StorageId> remoteIds,
Collection<StorageId> localIds
) { ) {
final var base64Encoder = Base64.getEncoder(); final var base64Encoder = Base64.getEncoder();
final var remoteByRawId = remoteIds.stream() final var remoteByRawId = remoteIds.stream()
@ -502,7 +607,7 @@ public class StorageHelper {
final var remote = remoteByRawId.get(rawId); final var remote = remoteByRawId.get(rawId);
final var local = localByRawId.get(rawId); final var local = localByRawId.get(rawId);
if (remote.getType() != local.getType() && local.getType() != 0) { if (remote.getType() != local.getType() && KNOWN_TYPES.contains(local.getType())) {
remoteOnlyRawIds.remove(rawId); remoteOnlyRawIds.remove(rawId);
localOnlyRawIds.remove(rawId); localOnlyRawIds.remove(rawId);
hasTypeMismatch = true; hasTypeMismatch = true;
@ -520,7 +625,8 @@ public class StorageHelper {
} }
private List<StorageId> processKnownRecords( private List<StorageId> processKnownRecords(
final Connection connection, List<SignalStorageRecord> records final Connection connection,
List<SignalStorageRecord> records
) throws SQLException { ) throws SQLException {
final var unknownRecords = new ArrayList<StorageId>(); final var unknownRecords = new ArrayList<StorageId>();
@ -530,13 +636,24 @@ public class StorageHelper {
final var groupV2RecordProcessor = new GroupV2RecordProcessor(account, connection); final var groupV2RecordProcessor = new GroupV2RecordProcessor(account, connection);
for (final var record : records) { for (final var record : records) {
logger.debug("Reading record of type {}", record.getType()); if (record.getProto().account != null) {
switch (ManifestRecord.Identifier.Type.fromValue(record.getType())) { logger.debug("Reading record {} of type account", record.getId());
case ACCOUNT -> accountRecordProcessor.process(record.getAccount().get()); accountRecordProcessor.process(StorageRecordConvertersKt.toSignalAccountRecord(record.getProto().account,
case GROUPV1 -> groupV1RecordProcessor.process(record.getGroupV1().get()); record.getId()));
case GROUPV2 -> groupV2RecordProcessor.process(record.getGroupV2().get()); } else if (record.getProto().groupV1 != null) {
case CONTACT -> contactRecordProcessor.process(record.getContact().get()); logger.debug("Reading record {} of type groupV1", record.getId());
case null, default -> unknownRecords.add(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

@ -9,7 +9,6 @@ import org.asamk.signal.manager.storage.groups.GroupInfoV1;
import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.stickers.StickerPack; import org.asamk.signal.manager.storage.stickers.StickerPack;
import org.asamk.signal.manager.util.AttachmentUtils;
import org.asamk.signal.manager.util.IOUtils; import org.asamk.signal.manager.util.IOUtils;
import org.asamk.signal.manager.util.MimeUtils; import org.asamk.signal.manager.util.MimeUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -18,11 +17,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage; import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact; import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactAvatar;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream; import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream; import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup; import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup;
@ -30,10 +30,13 @@ import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInp
import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream; import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream;
import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage; import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage;
import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage; import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.internal.push.SyncMessage; import org.whispersystems.signalservice.internal.push.SyncMessage;
@ -67,17 +70,12 @@ public class SyncHelper {
requestSyncData(SyncMessage.Request.Type.BLOCKED); requestSyncData(SyncMessage.Request.Type.BLOCKED);
requestSyncData(SyncMessage.Request.Type.CONFIGURATION); requestSyncData(SyncMessage.Request.Type.CONFIGURATION);
requestSyncKeys(); requestSyncKeys();
requestSyncPniIdentity();
} }
public void requestSyncKeys() { public void requestSyncKeys() {
requestSyncData(SyncMessage.Request.Type.KEYS); requestSyncData(SyncMessage.Request.Type.KEYS);
} }
public void requestSyncPniIdentity() {
requestSyncData(SyncMessage.Request.Type.PNI_IDENTITY);
}
public SendMessageResult sendSyncFetchProfileMessage() { public SendMessageResult sendSyncFetchProfileMessage() {
return context.getSendHelper() return context.getSendHelper()
.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)); .sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
@ -88,6 +86,22 @@ public class SyncHelper {
.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST)); .sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST));
} }
public void sendSyncReceiptMessage(ServiceId sender, SignalServiceReceiptMessage receiptMessage) {
if (receiptMessage.isReadReceipt()) {
final var readMessages = receiptMessage.getTimestamps()
.stream()
.map(t -> new ReadMessage(sender, t))
.toList();
context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forRead(readMessages));
} else if (receiptMessage.isViewedReceipt()) {
final var viewedMessages = receiptMessage.getTimestamps()
.stream()
.map(t -> new ViewedMessage(sender, t))
.toList();
context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forViewed(viewedMessages));
}
}
public void sendGroups() throws IOException { public void sendGroups() throws IOException {
var groupsFile = IOUtils.createTempFile(); var groupsFile = IOUtils.createTempFile();
@ -115,10 +129,12 @@ public class SyncHelper {
if (groupsFile.exists() && groupsFile.length() > 0) { if (groupsFile.exists() && groupsFile.length() > 0) {
try (var groupsFileStream = new FileInputStream(groupsFile)) { try (var groupsFileStream = new FileInputStream(groupsFile)) {
final var uploadSpec = context.getDependencies().getMessageSender().getResumableUploadSpec();
var attachmentStream = SignalServiceAttachment.newStreamBuilder() var attachmentStream = SignalServiceAttachment.newStreamBuilder()
.withStream(groupsFileStream) .withStream(groupsFileStream)
.withContentType(MimeUtils.OCTET_STREAM) .withContentType(MimeUtils.OCTET_STREAM)
.withLength(groupsFile.length()) .withLength(groupsFile.length())
.withResumableUploadSpec(uploadSpec)
.build(); .build();
context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
@ -144,7 +160,14 @@ public class SyncHelper {
final var contact = contactPair.second(); final var contact = contactPair.second();
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId); final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
out.write(getDeviceContact(address, recipientId, contact)); final var deviceContact = getDeviceContact(address, contact);
out.write(deviceContact);
deviceContact.getAvatar().ifPresent(a -> {
try {
a.getInputStream().close();
} catch (IOException ignored) {
}
});
} }
if (account.getProfileKey() != null) { if (account.getProfileKey() != null) {
@ -152,16 +175,25 @@ public class SyncHelper {
final var address = account.getSelfRecipientAddress(); final var address = account.getSelfRecipientAddress();
final var recipientId = account.getSelfRecipientId(); final var recipientId = account.getSelfRecipientId();
final var contact = account.getContactStore().getContact(recipientId); final var contact = account.getContactStore().getContact(recipientId);
out.write(getDeviceContact(address, recipientId, contact)); final var deviceContact = getDeviceContact(address, contact);
out.write(deviceContact);
deviceContact.getAvatar().ifPresent(a -> {
try {
a.getInputStream().close();
} catch (IOException ignored) {
}
});
} }
} }
if (contactsFile.exists() && contactsFile.length() > 0) { if (contactsFile.exists() && contactsFile.length() > 0) {
try (var contactsFileStream = new FileInputStream(contactsFile)) { try (var contactsFileStream = new FileInputStream(contactsFile)) {
final var uploadSpec = context.getDependencies().getMessageSender().getResumableUploadSpec();
var attachmentStream = SignalServiceAttachment.newStreamBuilder() var attachmentStream = SignalServiceAttachment.newStreamBuilder()
.withStream(contactsFileStream) .withStream(contactsFileStream)
.withContentType(MimeUtils.OCTET_STREAM) .withContentType(MimeUtils.OCTET_STREAM)
.withLength(contactsFile.length()) .withLength(contactsFile.length())
.withResumableUploadSpec(uploadSpec)
.build(); .build();
context.getSendHelper() context.getSendHelper()
@ -179,39 +211,25 @@ public class SyncHelper {
} }
@NotNull @NotNull
private DeviceContact getDeviceContact( private DeviceContact getDeviceContact(final RecipientAddress address, final Contact contact) throws IOException {
final RecipientAddress address, final RecipientId recipientId, final Contact contact
) throws IOException {
var currentIdentity = address.serviceId().isEmpty()
? null
: account.getIdentityKeyStore().getIdentityInfo(address.serviceId().get());
VerifiedMessage verifiedMessage = null;
if (currentIdentity != null) {
verifiedMessage = new VerifiedMessage(address.toSignalServiceAddress(),
currentIdentity.getIdentityKey(),
currentIdentity.getTrustLevel().toVerifiedState(),
currentIdentity.getDateAddedTimestamp());
}
var profileKey = account.getProfileStore().getProfileKey(recipientId);
return new DeviceContact(address.aci(), return new DeviceContact(address.aci(),
address.number(), address.number(),
Optional.ofNullable(contact == null ? null : contact.getName()), Optional.ofNullable(contact == null ? null : contact.getName()),
createContactAvatarAttachment(address), createContactAvatarAttachment(address),
Optional.ofNullable(contact == null ? null : contact.color()),
Optional.ofNullable(verifiedMessage),
Optional.ofNullable(profileKey),
contact != null && contact.isBlocked(),
Optional.ofNullable(contact == null ? null : contact.messageExpirationTime()), Optional.ofNullable(contact == null ? null : contact.messageExpirationTime()),
Optional.empty(), Optional.ofNullable(contact == null ? null : contact.messageExpirationTimeVersion()),
contact != null && contact.isArchived()); Optional.empty());
} }
public SendMessageResult sendBlockedList() { public SendMessageResult sendBlockedList() {
var addresses = new ArrayList<SignalServiceAddress>(); var addresses = new ArrayList<BlockedListMessage.Individual>();
for (var record : account.getContactStore().getContacts()) { for (var record : account.getContactStore().getContacts()) {
if (record.second().isBlocked()) { 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[]>(); var groupIds = new ArrayList<byte[]>();
@ -225,7 +243,9 @@ public class SyncHelper {
} }
public SendMessageResult sendVerifiedMessage( public SendMessageResult sendVerifiedMessage(
SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel SignalServiceAddress destination,
IdentityKey identityKey,
TrustLevel trustLevel
) { ) {
var verifiedMessage = new VerifiedMessage(destination, var verifiedMessage = new VerifiedMessage(destination,
identityKey, identityKey,
@ -235,13 +255,16 @@ public class SyncHelper {
} }
public SendMessageResult sendKeysMessage() { public SendMessageResult sendKeysMessage() {
var keysMessage = new KeysMessage(Optional.ofNullable(account.getOrCreateStorageKey()), var keysMessage = new KeysMessage(account.getOrCreateStorageKey(),
Optional.ofNullable(account.getOrCreatePinMasterKey())); account.getOrCreatePinMasterKey(),
account.getOrCreateAccountEntropyPool(),
account.getOrCreateMediaRootBackupKey());
return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage)); return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage));
} }
public SendMessageResult sendStickerOperationsMessage( 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 installStickerMessages = installStickers.stream().map(s -> getStickerPackOperationMessage(s, true));
var removeStickerMessages = removeStickers.stream().map(s -> getStickerPackOperationMessage(s, false)); var removeStickerMessages = removeStickers.stream().map(s -> getStickerPackOperationMessage(s, false));
@ -251,7 +274,8 @@ public class SyncHelper {
} }
private static StickerPackOperationMessage getStickerPackOperationMessage( private static StickerPackOperationMessage getStickerPackOperationMessage(
final StickerPack s, final boolean installed final StickerPack s,
final boolean installed
) { ) {
return new StickerPackOperationMessage(s.packId().serialize(), return new StickerPackOperationMessage(s.packId().serialize(),
s.packKey(), s.packKey(),
@ -317,7 +341,7 @@ public class SyncHelper {
c = s.read(); c = s.read();
} catch (IOException e) { } catch (IOException e) {
if (e.getMessage() != null && e.getMessage().contains("Missing contact address!")) { 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; continue;
} else { } else {
throw e; throw e;
@ -327,9 +351,6 @@ public class SyncHelper {
break; break;
} }
final var address = new RecipientAddress(c.getAci(), Optional.empty(), c.getE164(), Optional.empty()); final var address = new RecipientAddress(c.getAci(), Optional.empty(), c.getE164(), Optional.empty());
if (address.matches(account.getSelfRecipientAddress()) && c.getProfileKey().isPresent()) {
account.setProfileKey(c.getProfileKey().get());
}
final var recipientId = account.getRecipientTrustedResolver().resolveRecipientTrusted(address); final var recipientId = account.getRecipientTrustedResolver().resolveRecipientTrusted(address);
var contact = account.getContactStore().getContact(recipientId); var contact = account.getContactStore().getContact(recipientId);
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
@ -341,41 +362,36 @@ public class SyncHelper {
builder.withGivenName(c.getName().get()); builder.withGivenName(c.getName().get());
builder.withFamilyName(null); builder.withFamilyName(null);
} }
if (c.getColor().isPresent()) {
builder.withColor(c.getColor().get());
}
if (c.getProfileKey().isPresent()) {
account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get());
}
if (c.getVerified().isPresent()) {
final var verifiedMessage = c.getVerified().get();
account.getIdentityKeyStore()
.setIdentityTrustLevel(verifiedMessage.getDestination().getServiceId(),
verifiedMessage.getIdentityKey(),
TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
}
if (c.getExpirationTimer().isPresent()) { if (c.getExpirationTimer().isPresent()) {
builder.withMessageExpirationTime(c.getExpirationTimer().get()); if (c.getExpirationTimerVersion().isPresent() && (
contact == null || c.getExpirationTimerVersion().get() > contact.messageExpirationTimeVersion()
)) {
builder.withMessageExpirationTime(c.getExpirationTimer().get());
builder.withMessageExpirationTimeVersion(c.getExpirationTimerVersion().get());
} else {
logger.debug(
"[ContactSync] {} was synced with an old expiration timer. Ignoring. Received: {} Current: {}",
recipientId,
c.getExpirationTimerVersion(),
contact == null ? 1 : contact.messageExpirationTimeVersion());
}
} }
builder.withIsBlocked(c.isBlocked());
builder.withIsArchived(c.isArchived());
account.getContactStore().storeContact(recipientId, builder.build()); account.getContactStore().storeContact(recipientId, builder.build());
if (c.getAvatar().isPresent()) { if (c.getAvatar().isPresent()) {
downloadContactAvatar(c.getAvatar().get(), address); storeContactAvatar(c.getAvatar().get(), address);
} }
} }
} }
public SendMessageResult sendMessageRequestResponse( public SendMessageResult sendMessageRequestResponse(final MessageRequestResponse.Type type, final GroupId groupId) {
final MessageRequestResponse.Type type, final GroupId groupId
) {
final var response = MessageRequestResponseMessage.forGroup(groupId.serialize(), localToRemoteType(type)); final var response = MessageRequestResponseMessage.forGroup(groupId.serialize(), localToRemoteType(type));
return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response)); return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response));
} }
public SendMessageResult sendMessageRequestResponse( public SendMessageResult sendMessageRequestResponse(
final MessageRequestResponse.Type type, final RecipientId recipientId final MessageRequestResponse.Type type,
final RecipientId recipientId
) { ) {
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId); final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
if (address.serviceId().isEmpty()) { if (address.serviceId().isEmpty()) {
@ -396,20 +412,22 @@ public class SyncHelper {
return context.getSendHelper().sendSyncMessage(message); return context.getSendHelper().sendSyncMessage(message);
} }
private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(RecipientAddress address) throws IOException { private Optional<DeviceContactAvatar> createContactAvatarAttachment(RecipientAddress address) throws IOException {
final var streamDetails = context.getAvatarStore().retrieveContactAvatar(address); final var streamDetails = context.getAvatarStore().retrieveContactAvatar(address);
if (streamDetails == null) { if (streamDetails == null) {
return Optional.empty(); return Optional.empty();
} }
return Optional.of(AttachmentUtils.createAttachmentStream(streamDetails, Optional.empty())); return Optional.of(new DeviceContactAvatar(streamDetails.getStream(),
streamDetails.getLength(),
streamDetails.getContentType()));
} }
private void downloadContactAvatar(SignalServiceAttachment avatar, RecipientAddress address) { private void storeContactAvatar(DeviceContactAvatar avatar, RecipientAddress address) {
try { try {
context.getAvatarStore() context.getAvatarStore()
.storeContactAvatar(address, .storeContactAvatar(address,
outputStream -> context.getAttachmentHelper().retrieveAttachment(avatar, outputStream)); outputStream -> IOUtils.copyStream(avatar.getInputStream(), outputStream));
} catch (IOException e) { } catch (IOException e) {
logger.warn("Failed to download avatar for contact {}, ignoring: {}", address, e.getMessage()); logger.warn("Failed to download avatar for contact {}, ignoring: {}", address, e.getMessage());
} }

View file

@ -5,19 +5,21 @@ import org.asamk.signal.manager.api.Profile;
import org.asamk.signal.manager.internal.SignalDependencies; import org.asamk.signal.manager.internal.SignalDependencies;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.jetbrains.annotations.Nullable;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException; import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.signal.libsignal.metadata.certificate.SenderCertificate; import org.signal.libsignal.metadata.certificate.SenderCertificate;
import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class UnidentifiedAccessHelper { public class UnidentifiedAccessHelper {
private static final Logger logger = LoggerFactory.getLogger(UnidentifiedAccessHelper.class); private static final Logger logger = LoggerFactory.getLogger(UnidentifiedAccessHelper.class);
@ -42,63 +44,55 @@ public class UnidentifiedAccessHelper {
senderCertificate = null; senderCertificate = null;
} }
public List<Optional<UnidentifiedAccessPair>> getAccessFor(List<RecipientId> recipients) { public List<SealedSenderAccess> getSealedSenderAccessFor(List<RecipientId> recipients) {
return recipients.stream().map(this::getAccessFor).map(SealedSenderAccess::forIndividual).toList();
}
public @Nullable SealedSenderAccess getSealedSenderAccessFor(RecipientId recipient) {
return getSealedSenderAccessFor(recipient, false);
}
public @Nullable SealedSenderAccess getSealedSenderAccessFor(RecipientId recipient, boolean noRefresh) {
return SealedSenderAccess.forIndividual(getAccessFor(recipient, noRefresh));
}
public List<UnidentifiedAccess> getAccessFor(List<RecipientId> recipients) {
return recipients.stream().map(this::getAccessFor).toList(); return recipients.stream().map(this::getAccessFor).toList();
} }
public Optional<UnidentifiedAccessPair> getAccessFor(RecipientId recipient) { private @Nullable UnidentifiedAccess getAccessFor(RecipientId recipient) {
return getAccessFor(recipient, false); return getAccessFor(recipient, false);
} }
public Optional<UnidentifiedAccessPair> getAccessFor(RecipientId recipientId, boolean noRefresh) { private @Nullable UnidentifiedAccess getAccessFor(RecipientId recipientId, boolean noRefresh) {
var recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipientId, noRefresh); var recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipientId, noRefresh);
if (recipientUnidentifiedAccessKey == null) { if (recipientUnidentifiedAccessKey == null) {
logger.trace("Unidentified access not available for {}", recipientId); logger.trace("Unidentified access not available for {}", recipientId);
return Optional.empty(); return null;
} }
var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(noRefresh); var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(noRefresh);
if (selfUnidentifiedAccessKey == null) { if (selfUnidentifiedAccessKey == null) {
logger.trace("Unidentified access not available for self"); logger.trace("Unidentified access not available for self");
return Optional.empty(); return null;
} }
var senderCertificate = getSenderCertificateFor(recipientId); var senderCertificate = getSenderCertificateFor(recipientId);
if (senderCertificate == null) { if (senderCertificate == null) {
logger.trace("Unidentified access not available due to missing sender certificate"); logger.trace("Unidentified access not available due to missing sender certificate");
return Optional.empty(); return null;
} }
try { try {
return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(recipientUnidentifiedAccessKey, return new UnidentifiedAccess(recipientUnidentifiedAccessKey, senderCertificate, false);
senderCertificate,
false), new UnidentifiedAccess(selfUnidentifiedAccessKey, senderCertificate, false)));
} catch (InvalidCertificateException e) { } catch (InvalidCertificateException e) {
return Optional.empty(); return null;
}
}
public Optional<UnidentifiedAccessPair> getAccessForSync() {
var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(false);
var selfUnidentifiedAccessCertificate = getSenderCertificate();
if (selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
return Optional.empty();
}
try {
return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(selfUnidentifiedAccessKey,
selfUnidentifiedAccessCertificate,
false),
new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate, false)));
} catch (InvalidCertificateException e) {
return Optional.empty();
} }
} }
private byte[] getSenderCertificateFor(final RecipientId recipientId) { private byte[] getSenderCertificateFor(final RecipientId recipientId) {
final var sharingMode = account.getConfigurationStore().getPhoneNumberSharingMode(); final var sharingMode = account.getConfigurationStore().getPhoneNumberSharingMode();
if (sharingMode == null || sharingMode == PhoneNumberSharingMode.EVERYBODY || ( if (sharingMode == PhoneNumberSharingMode.EVERYBODY || (
sharingMode == PhoneNumberSharingMode.CONTACTS sharingMode == PhoneNumberSharingMode.CONTACTS
&& account.getContactStore().getContact(recipientId) != null && account.getContactStore().getContact(recipientId) != null
)) { )) {
@ -117,11 +111,12 @@ public class UnidentifiedAccessHelper {
return privacySenderCertificate.getSerialized(); return privacySenderCertificate.getSerialized();
} }
try { try {
final var certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy(); final var certificate = handleResponseException(dependencies.getCertificateApi()
.getSenderCertificateForPhoneNumberPrivacy());
privacySenderCertificate = new SenderCertificate(certificate); privacySenderCertificate = new SenderCertificate(certificate);
return certificate; return certificate;
} catch (IOException | InvalidCertificateException e) { } catch (IOException | InvalidCertificateException e) {
logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage()); logger.warn("Failed to get sender certificate (pnp), ignoring: {}", e.getMessage());
return null; return null;
} }
} }
@ -133,7 +128,7 @@ public class UnidentifiedAccessHelper {
return senderCertificate.getSerialized(); return senderCertificate.getSerialized();
} }
try { try {
final var certificate = dependencies.getAccountManager().getSenderCertificate(); final var certificate = handleResponseException(dependencies.getCertificateApi().getSenderCertificate());
this.senderCertificate = new SenderCertificate(certificate); this.senderCertificate = new SenderCertificate(certificate);
return certificate; return certificate;
} catch (IOException | InvalidCertificateException e) { } catch (IOException | InvalidCertificateException e) {
@ -166,7 +161,8 @@ public class UnidentifiedAccessHelper {
} }
private static byte[] getTargetUnidentifiedAccessKey( private static byte[] getTargetUnidentifiedAccessKey(
final Profile targetProfile, final ProfileKey theirProfileKey final Profile targetProfile,
final ProfileKey theirProfileKey
) { ) {
return switch (targetProfile.getUnidentifiedAccessMode()) { return switch (targetProfile.getUnidentifiedAccessMode()) {
case ENABLED -> theirProfileKey == null ? null : UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey); case ENABLED -> theirProfileKey == null ? null : UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);

View file

@ -19,9 +19,11 @@ package org.asamk.signal.manager.internal;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.api.AlreadyReceivingException; import org.asamk.signal.manager.api.AlreadyReceivingException;
import org.asamk.signal.manager.api.AttachmentInvalidException; import org.asamk.signal.manager.api.AttachmentInvalidException;
import org.asamk.signal.manager.api.CaptchaRejectedException;
import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.Configuration; import org.asamk.signal.manager.api.Configuration;
import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Device;
import org.asamk.signal.manager.api.DeviceLimitExceededException;
import org.asamk.signal.manager.api.DeviceLinkUrl; import org.asamk.signal.manager.api.DeviceLinkUrl;
import org.asamk.signal.manager.api.Group; import org.asamk.signal.manager.api.Group;
import org.asamk.signal.manager.api.GroupId; import org.asamk.signal.manager.api.GroupId;
@ -33,6 +35,7 @@ import org.asamk.signal.manager.api.IdentityVerificationCode;
import org.asamk.signal.manager.api.InactiveGroupLinkException; import org.asamk.signal.manager.api.InactiveGroupLinkException;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.InvalidDeviceLinkException; import org.asamk.signal.manager.api.InvalidDeviceLinkException;
import org.asamk.signal.manager.api.InvalidNumberException;
import org.asamk.signal.manager.api.InvalidStickerException; import org.asamk.signal.manager.api.InvalidStickerException;
import org.asamk.signal.manager.api.InvalidUsernameException; import org.asamk.signal.manager.api.InvalidUsernameException;
import org.asamk.signal.manager.api.LastGroupAdminException; import org.asamk.signal.manager.api.LastGroupAdminException;
@ -45,6 +48,7 @@ import org.asamk.signal.manager.api.NotPrimaryDeviceException;
import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.PendingAdminApprovalException; import org.asamk.signal.manager.api.PendingAdminApprovalException;
import org.asamk.signal.manager.api.PhoneNumberSharingMode; import org.asamk.signal.manager.api.PhoneNumberSharingMode;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.Profile; import org.asamk.signal.manager.api.Profile;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
@ -64,6 +68,8 @@ import org.asamk.signal.manager.api.UpdateGroup;
import org.asamk.signal.manager.api.UpdateProfile; import org.asamk.signal.manager.api.UpdateProfile;
import org.asamk.signal.manager.api.UserStatus; import org.asamk.signal.manager.api.UserStatus;
import org.asamk.signal.manager.api.UsernameLinkUrl; 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.ServiceEnvironmentConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.helper.AccountFileUpdater; import org.asamk.signal.manager.helper.AccountFileUpdater;
import org.asamk.signal.manager.helper.Context; import org.asamk.signal.manager.helper.Context;
@ -83,23 +89,25 @@ import org.asamk.signal.manager.storage.stickers.StickerPack;
import org.asamk.signal.manager.util.AttachmentUtils; import org.asamk.signal.manager.util.AttachmentUtils;
import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.MimeUtils; import org.asamk.signal.manager.util.MimeUtils;
import org.asamk.signal.manager.util.PhoneNumberFormatter;
import org.asamk.signal.manager.util.StickerUtils; import org.asamk.signal.manager.util.StickerUtils;
import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.usernames.BaseUsernameException; import org.signal.libsignal.usernames.BaseUsernameException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServicePreview; import org.whispersystems.signalservice.api.messages.SignalServicePreview;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException; import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException;
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.DeviceNameUtil;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.signalservice.internal.util.Util; import org.whispersystems.signalservice.internal.util.Util;
@ -124,13 +132,18 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers; 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 { public class ManagerImpl implements Manager {
@ -149,6 +162,7 @@ public class ManagerImpl implements Manager {
private final List<Runnable> closedListeners = new ArrayList<>(); private final List<Runnable> closedListeners = new ArrayList<>();
private final List<Runnable> addressChangedListeners = new ArrayList<>(); private final List<Runnable> addressChangedListeners = new ArrayList<>();
private final CompositeDisposable disposable = new CompositeDisposable(); private final CompositeDisposable disposable = new CompositeDisposable();
private final AtomicLong lastMessageTimestamp = new AtomicLong();
public ManagerImpl( public ManagerImpl(
SignalAccount account, SignalAccount account,
@ -159,15 +173,7 @@ public class ManagerImpl implements Manager {
) { ) {
this.account = account; this.account = account;
final var sessionLock = new SignalSessionLock() { final var sessionLock = new ReentrantSignalSessionLock();
private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
@Override
public Lock acquire() {
LEGACY_LOCK.lock();
return LEGACY_LOCK::unlock;
}
};
this.dependencies = new SignalDependencies(serviceEnvironmentConfig, this.dependencies = new SignalDependencies(serviceEnvironmentConfig,
userAgent, userAgent,
account.getCredentialsProvider(), account.getCredentialsProvider(),
@ -278,6 +284,33 @@ public class ManagerImpl implements Manager {
})); }));
} }
@Override
public Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) throws IOException {
final var registeredUsers = new HashMap<String, RecipientAddress>();
for (final var username : usernames) {
try {
final var recipientId = context.getRecipientHelper().resolveRecipientByUsernameOrLink(username, true);
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
registeredUsers.put(username, address);
} catch (UnregisteredRecipientException e) {
// ignore
}
}
return usernames.stream().collect(Collectors.toMap(n -> n, username -> {
final var user = registeredUsers.get(username);
final var serviceId = user == null ? null : user.serviceId().orElse(null);
final var profile = serviceId == null
? null
: context.getProfileHelper()
.getRecipientProfile(account.getRecipientResolver().resolveRecipient(serviceId));
return new UsernameStatus(username,
serviceId == null ? null : serviceId.getRawUuid(),
profile != null
&& profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.UNRESTRICTED);
}));
}
@Override @Override
public void updateAccountAttributes( public void updateAccountAttributes(
String deviceName, String deviceName,
@ -360,7 +393,15 @@ public class ManagerImpl implements Manager {
@Override @Override
public void setUsername(final String username) throws IOException, InvalidUsernameException { public void setUsername(final String username) throws IOException, InvalidUsernameException {
try { try {
context.getAccountHelper().reserveUsername(username); if (username.contains(".")) {
context.getAccountHelper().reserveExactUsername(username);
} else {
context.getAccountHelper().reserveUsernameFromNickname(username);
}
} catch (UsernameMalformedException e) {
throw new InvalidUsernameException("Username is malformed", e);
} catch (UsernameTakenException e) {
throw new InvalidUsernameException("Username is already registered", e);
} catch (BaseUsernameException e) { } catch (BaseUsernameException e) {
throw new InvalidUsernameException(e.getMessage() + " (" + e.getClass().getSimpleName() + ")", e); throw new InvalidUsernameException(e.getMessage() + " (" + e.getClass().getSimpleName() + ")", e);
} }
@ -373,8 +414,10 @@ public class ManagerImpl implements Manager {
@Override @Override
public void startChangeNumber( public void startChangeNumber(
String newNumber, boolean voiceVerification, String captcha String newNumber,
) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, NotPrimaryDeviceException { boolean voiceVerification,
String captcha
) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, NotPrimaryDeviceException, VerificationMethodNotAvailableException {
if (!account.isPrimaryDevice()) { if (!account.isPrimaryDevice()) {
throw new NotPrimaryDeviceException(); throw new NotPrimaryDeviceException();
} }
@ -383,8 +426,10 @@ public class ManagerImpl implements Manager {
@Override @Override
public void finishChangeNumber( public void finishChangeNumber(
String newNumber, String verificationCode, String pin String newNumber,
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException { String verificationCode,
String pin
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException {
if (!account.isPrimaryDevice()) { if (!account.isPrimaryDevice()) {
throw new NotPrimaryDeviceException(); throw new NotPrimaryDeviceException();
} }
@ -402,15 +447,22 @@ public class ManagerImpl implements Manager {
} }
@Override @Override
public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException { public void submitRateLimitRecaptchaChallenge(
captcha = captcha == null ? null : captcha.replace("signalcaptcha://", ""); String challenge,
String captcha
) throws IOException, CaptchaRejectedException {
captcha = captcha == null ? "" : captcha.replace("signalcaptcha://", "");
dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha); try {
handleResponseException(dependencies.getRateLimitChallengeApi().submitCaptchaChallenge(challenge, captcha));
} catch (org.whispersystems.signalservice.internal.push.exceptions.CaptchaRejectedException ignored) {
throw new CaptchaRejectedException();
}
} }
@Override @Override
public List<Device> getLinkedDevices() throws IOException { public List<Device> getLinkedDevices() throws IOException {
var devices = dependencies.getAccountManager().getDevices(); var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
account.setMultiDevice(devices.size() > 1); account.setMultiDevice(devices.size() > 1);
var identityKey = account.getAciIdentityKeyPair().getPrivateKey(); var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
return devices.stream().map(d -> { return devices.stream().map(d -> {
@ -431,12 +483,15 @@ public class ManagerImpl implements Manager {
} }
@Override @Override
public void removeLinkedDevices(int deviceId) throws IOException { public void removeLinkedDevices(int deviceId) throws IOException, NotPrimaryDeviceException {
if (!account.isPrimaryDevice()) {
throw new NotPrimaryDeviceException();
}
context.getAccountHelper().removeLinkedDevices(deviceId); context.getAccountHelper().removeLinkedDevices(deviceId);
} }
@Override @Override
public void addDeviceLink(DeviceLinkUrl linkUrl) throws IOException, InvalidDeviceLinkException, NotPrimaryDeviceException { public void addDeviceLink(DeviceLinkUrl linkUrl) throws IOException, InvalidDeviceLinkException, NotPrimaryDeviceException, DeviceLimitExceededException {
if (!account.isPrimaryDevice()) { if (!account.isPrimaryDevice()) {
throw new NotPrimaryDeviceException(); throw new NotPrimaryDeviceException();
} }
@ -461,7 +516,7 @@ public class ManagerImpl implements Manager {
@Override @Override
public List<Group> getGroups() { public List<Group> getGroups() {
return account.getGroupStore().getGroups().stream().map(this::toGroup).toList(); return context.getGroupHelper().getGroups().stream().map(this::toGroup).toList();
} }
private Group toGroup(final GroupInfo groupInfo) { private Group toGroup(final GroupInfo groupInfo) {
@ -474,7 +529,8 @@ public class ManagerImpl implements Manager {
@Override @Override
public SendGroupMessageResults quitGroup( public SendGroupMessageResults quitGroup(
GroupId groupId, Set<RecipientIdentifier.Single> groupAdmins GroupId groupId,
Set<RecipientIdentifier.Single> groupAdmins
) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException { ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException {
final var newAdmins = context.getRecipientHelper().resolveRecipients(groupAdmins); final var newAdmins = context.getRecipientHelper().resolveRecipients(groupAdmins);
return context.getGroupHelper().quitGroup(groupId, newAdmins); return context.getGroupHelper().quitGroup(groupId, newAdmins);
@ -492,7 +548,9 @@ public class ManagerImpl implements Manager {
@Override @Override
public Pair<GroupId, SendGroupMessageResults> createGroup( 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 { ) throws IOException, AttachmentInvalidException, UnregisteredRecipientException {
return context.getGroupHelper() return context.getGroupHelper()
.createGroup(name, .createGroup(name,
@ -502,7 +560,8 @@ public class ManagerImpl implements Manager {
@Override @Override
public SendGroupMessageResults updateGroup( public SendGroupMessageResults updateGroup(
final GroupId groupId, final UpdateGroup updateGroup final GroupId groupId,
final UpdateGroup updateGroup
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException { ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException {
return context.getGroupHelper() return context.getGroupHelper()
.updateGroup(groupId, .updateGroup(groupId,
@ -542,8 +601,28 @@ public class ManagerImpl implements Manager {
return context.getGroupHelper().joinGroup(inviteLinkUrl); return context.getGroupHelper().joinGroup(inviteLinkUrl);
} }
private long getNextMessageTimestamp() {
while (true) {
final var last = lastMessageTimestamp.get();
final var timestamp = System.currentTimeMillis();
if (last == timestamp) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
continue;
}
if (lastMessageTimestamp.compareAndSet(last, timestamp)) {
return timestamp;
}
}
}
private SendMessageResults sendMessage( private SendMessageResults sendMessage(
SignalServiceDataMessage.Builder messageBuilder, Set<RecipientIdentifier> recipients, boolean notifySelf SignalServiceDataMessage.Builder messageBuilder,
Set<RecipientIdentifier> recipients,
boolean notifySelf
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
return sendMessage(messageBuilder, recipients, notifySelf, Optional.empty()); return sendMessage(messageBuilder, recipients, notifySelf, Optional.empty());
} }
@ -555,7 +634,7 @@ public class ManagerImpl implements Manager {
Optional<Long> editTargetTimestamp Optional<Long> editTargetTimestamp
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>(); var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
long timestamp = System.currentTimeMillis(); long timestamp = getNextMessageTimestamp();
messageBuilder.withTimestamp(timestamp); messageBuilder.withTimestamp(timestamp);
for (final var recipient : recipients) { for (final var recipient : recipients) {
if (recipient instanceof RecipientIdentifier.NoteToSelf || ( if (recipient instanceof RecipientIdentifier.NoteToSelf || (
@ -591,10 +670,11 @@ public class ManagerImpl implements Manager {
} }
private SendMessageResults sendTypingMessage( private SendMessageResults sendTypingMessage(
SignalServiceTypingMessage.Action action, Set<RecipientIdentifier> recipients SignalServiceTypingMessage.Action action,
Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>(); var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
final var timestamp = System.currentTimeMillis(); final var timestamp = getNextMessageTimestamp();
for (var recipient : recipients) { for (var recipient : recipients) {
if (recipient instanceof RecipientIdentifier.Single single) { if (recipient instanceof RecipientIdentifier.Single single) {
final var message = new SignalServiceTypingMessage(action, timestamp, Optional.empty()); final var message = new SignalServiceTypingMessage(action, timestamp, Optional.empty());
@ -618,16 +698,15 @@ public class ManagerImpl implements Manager {
@Override @Override
public SendMessageResults sendTypingMessage( public SendMessageResults sendTypingMessage(
TypingAction action, Set<RecipientIdentifier> recipients TypingAction action,
Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
return sendTypingMessage(action.toSignalService(), recipients); return sendTypingMessage(action.toSignalService(), recipients);
} }
@Override @Override
public SendMessageResults sendReadReceipt( public SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) {
RecipientIdentifier.Single sender, List<Long> messageIds final var timestamp = getNextMessageTimestamp();
) {
final var timestamp = System.currentTimeMillis();
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ,
messageIds, messageIds,
timestamp); timestamp);
@ -636,10 +715,8 @@ public class ManagerImpl implements Manager {
} }
@Override @Override
public SendMessageResults sendViewedReceipt( public SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) {
RecipientIdentifier.Single sender, List<Long> messageIds final var timestamp = getNextMessageTimestamp();
) {
final var timestamp = System.currentTimeMillis();
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED,
messageIds, messageIds,
timestamp); timestamp);
@ -653,8 +730,15 @@ public class ManagerImpl implements Manager {
final SignalServiceReceiptMessage receiptMessage final SignalServiceReceiptMessage receiptMessage
) { ) {
try { try {
final var result = context.getSendHelper() final var recipientId = context.getRecipientHelper().resolveRecipient(sender);
.sendReceiptMessage(receiptMessage, context.getRecipientHelper().resolveRecipient(sender)); final var result = context.getSendHelper().sendReceiptMessage(receiptMessage, recipientId);
final var serviceId = account.getRecipientAddressResolver()
.resolveRecipientAddress(recipientId)
.serviceId();
if (serviceId.isPresent()) {
context.getSyncHelper().sendSyncReceiptMessage(serviceId.get(), receiptMessage);
}
return new SendMessageResults(timestamp, Map.of(sender, List.of(toSendMessageResult(result)))); return new SendMessageResults(timestamp, Map.of(sender, List.of(toSendMessageResult(result))));
} catch (UnregisteredRecipientException e) { } catch (UnregisteredRecipientException e) {
return new SendMessageResults(timestamp, return new SendMessageResults(timestamp,
@ -664,7 +748,9 @@ public class ManagerImpl implements Manager {
@Override @Override
public SendMessageResults sendMessage( 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 { ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
final var selfProfile = context.getProfileHelper().getSelfProfile(); final var selfProfile = context.getProfileHelper().getSelfProfile();
if (selfProfile == null || selfProfile.getDisplayName().isEmpty()) { if (selfProfile == null || selfProfile.getDisplayName().isEmpty()) {
@ -678,7 +764,9 @@ public class ManagerImpl implements Manager {
@Override @Override
public SendMessageResults sendEditMessage( 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 { ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
final var messageBuilder = SignalServiceDataMessage.newBuilder(); final var messageBuilder = SignalServiceDataMessage.newBuilder();
applyMessage(messageBuilder, message); applyMessage(messageBuilder, message);
@ -686,20 +774,43 @@ public class ManagerImpl implements Manager {
} }
private void applyMessage( private void applyMessage(
final SignalServiceDataMessage.Builder messageBuilder, final Message message final SignalServiceDataMessage.Builder messageBuilder,
final Message message
) throws AttachmentInvalidException, IOException, UnregisteredRecipientException, InvalidStickerException { ) throws AttachmentInvalidException, IOException, UnregisteredRecipientException, InvalidStickerException {
if (message.messageText().length() > 2000) { final var additionalAttachments = new ArrayList<SignalServiceAttachment>();
final var messageBytes = message.messageText().getBytes(StandardCharsets.UTF_8); if (Utf8.size(message.messageText()) > MAX_MESSAGE_SIZE_BYTES) {
final var textAttachment = AttachmentUtils.createAttachmentStream(new StreamDetails(new ByteArrayInputStream( final var result = splitByByteLength(message.messageText(), MAX_MESSAGE_SIZE_BYTES);
messageBytes), MimeUtils.LONG_TEXT, messageBytes.length), Optional.empty()); final var trimmed = result.getFirst();
messageBuilder.withBody(message.messageText().substring(0, 2000)); final var remainder = result.getSecond();
messageBuilder.withAttachment(context.getAttachmentHelper().uploadAttachment(textAttachment)); 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 { } else {
messageBuilder.withBody(message.messageText()); messageBuilder.withBody(message.messageText());
} }
if (!message.attachments().isEmpty()) { if (!message.attachments().isEmpty()) {
messageBuilder.withAttachments(context.getAttachmentHelper().uploadAttachments(message.attachments())); final var uploadedAttachments = context.getAttachmentHelper().uploadAttachments(message.attachments());
if (!additionalAttachments.isEmpty()) {
additionalAttachments.addAll(uploadedAttachments);
messageBuilder.withAttachments(additionalAttachments);
} else {
messageBuilder.withAttachments(uploadedAttachments);
}
} else if (!additionalAttachments.isEmpty()) {
messageBuilder.withAttachments(additionalAttachments);
} }
messageBuilder.withViewOnce(message.viewOnce());
if (!message.mentions().isEmpty()) { if (!message.mentions().isEmpty()) {
messageBuilder.withMentions(resolveMentions(message.mentions())); messageBuilder.withMentions(resolveMentions(message.mentions()));
} }
@ -743,11 +854,15 @@ public class ManagerImpl implements Manager {
if (streamDetails == null) { if (streamDetails == null) {
throw new InvalidStickerException("Missing local sticker file"); throw new InvalidStickerException("Missing local sticker file");
} }
final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
final var stickerAttachment = AttachmentUtils.createAttachmentStream(streamDetails,
Optional.empty(),
uploadSpec);
messageBuilder.withSticker(new SignalServiceDataMessage.Sticker(packId.serialize(), messageBuilder.withSticker(new SignalServiceDataMessage.Sticker(packId.serialize(),
stickerPack.packKey(), stickerPack.packKey(),
stickerId, stickerId,
manifestSticker.emoji(), manifestSticker.emoji(),
AttachmentUtils.createAttachmentStream(streamDetails, Optional.empty()))); stickerAttachment));
} }
if (!message.previews().isEmpty()) { if (!message.previews().isEmpty()) {
final var previews = new ArrayList<SignalServicePreview>(message.previews().size()); final var previews = new ArrayList<SignalServicePreview>(message.previews().size());
@ -785,7 +900,8 @@ public class ManagerImpl implements Manager {
@Override @Override
public SendMessageResults sendRemoteDeleteMessage( public SendMessageResults sendRemoteDeleteMessage(
long targetSentTimestamp, Set<RecipientIdentifier> recipients long targetSentTimestamp,
Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp);
final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete); final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
@ -793,6 +909,9 @@ public class ManagerImpl implements Manager {
if (recipient instanceof RecipientIdentifier.Uuid u) { if (recipient instanceof RecipientIdentifier.Uuid u) {
account.getMessageSendLogStore() account.getMessageSendLogStore()
.deleteEntryForRecipientNonGroup(targetSentTimestamp, ACI.from(u.uuid())); .deleteEntryForRecipientNonGroup(targetSentTimestamp, ACI.from(u.uuid()));
} else if (recipient instanceof RecipientIdentifier.Pni pni) {
account.getMessageSendLogStore()
.deleteEntryForRecipientNonGroup(targetSentTimestamp, PNI.from(pni.pni()));
} else if (recipient instanceof RecipientIdentifier.Single r) { } else if (recipient instanceof RecipientIdentifier.Single r) {
try { try {
final var recipientId = context.getRecipientHelper().resolveRecipient(r); final var recipientId = context.getRecipientHelper().resolveRecipient(r);
@ -834,7 +953,9 @@ public class ManagerImpl implements Manager {
@Override @Override
public SendMessageResults sendPaymentNotificationMessage( public SendMessageResults sendPaymentNotificationMessage(
byte[] receipt, String note, RecipientIdentifier.Single recipient byte[] receipt,
String note,
RecipientIdentifier.Single recipient
) throws IOException { ) throws IOException {
final var paymentNotification = new SignalServiceDataMessage.PaymentNotification(receipt, note); final var paymentNotification = new SignalServiceDataMessage.PaymentNotification(receipt, note);
final var payment = new SignalServiceDataMessage.Payment(paymentNotification, null); final var payment = new SignalServiceDataMessage.Payment(paymentNotification, null);
@ -877,7 +998,8 @@ public class ManagerImpl implements Manager {
@Override @Override
public SendMessageResults sendMessageRequestResponse( 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>>(); var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
for (final var recipient : recipients) { for (final var recipient : recipients) {
@ -940,19 +1062,30 @@ public class ManagerImpl implements Manager {
@Override @Override
public void setContactName( 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 { ) throws NotPrimaryDeviceException, UnregisteredRecipientException {
if (!account.isPrimaryDevice()) { if (!account.isPrimaryDevice()) {
throw new NotPrimaryDeviceException(); throw new NotPrimaryDeviceException();
} }
context.getContactHelper() context.getContactHelper()
.setContactName(context.getRecipientHelper().resolveRecipient(recipient), givenName, familyName); .setContactName(context.getRecipientHelper().resolveRecipient(recipient),
givenName,
familyName,
nickGivenName,
nickFamilyName,
note);
syncRemoteStorage(); syncRemoteStorage();
} }
@Override @Override
public void setContactsBlocked( public void setContactsBlocked(
Collection<RecipientIdentifier.Single> recipients, boolean blocked Collection<RecipientIdentifier.Single> recipients,
boolean blocked
) throws IOException, UnregisteredRecipientException { ) throws IOException, UnregisteredRecipientException {
if (recipients.isEmpty()) { if (recipients.isEmpty()) {
return; return;
@ -986,7 +1119,8 @@ public class ManagerImpl implements Manager {
@Override @Override
public void setGroupsBlocked( public void setGroupsBlocked(
final Collection<GroupId> groupIds, final boolean blocked final Collection<GroupId> groupIds,
final boolean blocked
) throws GroupNotFoundException, IOException { ) throws GroupNotFoundException, IOException {
if (groupIds.isEmpty()) { if (groupIds.isEmpty()) {
return; return;
@ -1012,7 +1146,8 @@ public class ManagerImpl implements Manager {
@Override @Override
public void setExpirationTimer( public void setExpirationTimer(
RecipientIdentifier.Single recipient, int messageExpirationTimer RecipientIdentifier.Single recipient,
int messageExpirationTimer
) throws IOException, UnregisteredRecipientException { ) throws IOException, UnregisteredRecipientException {
var recipientId = context.getRecipientHelper().resolveRecipient(recipient); var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
context.getContactHelper().setExpirationTimer(recipientId, messageExpirationTimer); context.getContactHelper().setExpirationTimer(recipientId, messageExpirationTimer);
@ -1174,7 +1309,9 @@ public class ManagerImpl implements Manager {
@Override @Override
public void receiveMessages( public void receiveMessages(
Optional<Duration> timeout, Optional<Integer> maxMessages, ReceiveMessageHandler handler Optional<Duration> timeout,
Optional<Integer> maxMessages,
ReceiveMessageHandler handler
) throws IOException, AlreadyReceivingException { ) throws IOException, AlreadyReceivingException {
receiveMessages(timeout.orElse(Duration.ofMinutes(1)), timeout.isPresent(), maxMessages.orElse(null), handler); receiveMessages(timeout.orElse(Duration.ofMinutes(1)), timeout.isPresent(), maxMessages.orElse(null), handler);
} }
@ -1194,7 +1331,10 @@ public class ManagerImpl implements Manager {
} }
private void receiveMessages( private void receiveMessages(
Duration timeout, boolean returnOnTimeout, Integer maxMessages, ReceiveMessageHandler handler Duration timeout,
boolean returnOnTimeout,
Integer maxMessages,
ReceiveMessageHandler handler
) throws IOException, AlreadyReceivingException { ) throws IOException, AlreadyReceivingException {
synchronized (messageHandlers) { synchronized (messageHandlers) {
if (isReceiving()) { if (isReceiving()) {
@ -1267,7 +1407,8 @@ public class ManagerImpl implements Manager {
s.getContact(), s.getContact(),
s.getProfileKey(), s.getProfileKey(),
s.getExpiringProfileKeyCredential(), s.getExpiringProfileKeyCredential(),
s.getProfile())) s.getProfile(),
s.getDiscoverable()))
.toList(); .toList();
} }
@ -1322,7 +1463,7 @@ public class ManagerImpl implements Manager {
final var scannableFingerprint = context.getIdentityHelper() final var scannableFingerprint = context.getIdentityHelper()
.computeSafetyNumberForScanning(identityInfo.getServiceId(), identityInfo.getIdentityKey()); .computeSafetyNumberForScanning(identityInfo.getServiceId(), identityInfo.getIdentityKey());
return new Identity(address.toApiRecipientAddress(), return new Identity(address.toApiRecipientAddress(),
identityInfo.getIdentityKey(), identityInfo.getIdentityKey().getPublicKey().serialize(),
context.getIdentityHelper() context.getIdentityHelper()
.computeSafetyNumber(identityInfo.getServiceId(), identityInfo.getIdentityKey()), .computeSafetyNumber(identityInfo.getServiceId(), identityInfo.getIdentityKey()),
scannableFingerprint == null ? null : scannableFingerprint.getSerialized(), scannableFingerprint == null ? null : scannableFingerprint.getSerialized(),
@ -1349,7 +1490,8 @@ public class ManagerImpl implements Manager {
@Override @Override
public boolean trustIdentityVerified( public boolean trustIdentityVerified(
RecipientIdentifier.Single recipient, IdentityVerificationCode verificationCode RecipientIdentifier.Single recipient,
IdentityVerificationCode verificationCode
) throws UnregisteredRecipientException { ) throws UnregisteredRecipientException {
return switch (verificationCode) { return switch (verificationCode) {
case IdentityVerificationCode.Fingerprint fingerprint -> trustIdentity(recipient, case IdentityVerificationCode.Fingerprint fingerprint -> trustIdentity(recipient,
@ -1368,12 +1510,13 @@ public class ManagerImpl implements Manager {
} }
private boolean trustIdentity( private boolean trustIdentity(
RecipientIdentifier.Single recipient, Function<RecipientId, Boolean> trustMethod RecipientIdentifier.Single recipient,
Function<RecipientId, Boolean> trustMethod
) throws UnregisteredRecipientException { ) throws UnregisteredRecipientException {
final var recipientId = context.getRecipientHelper().resolveRecipient(recipient); final var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
final var updated = trustMethod.apply(recipientId); final var updated = trustMethod.apply(recipientId);
if (updated && this.isReceiving()) { if (updated && this.isReceiving()) {
context.getReceiveHelper().setNeedsToRetryFailedMessages(true); account.setNeedsToRetryFailedMessages(true);
} }
return updated; return updated;
} }
@ -1464,7 +1607,8 @@ public class ManagerImpl implements Manager {
context.close(); context.close();
executor.close(); executor.close();
dependencies.getSignalWebSocket().disconnect(); dependencies.getAuthenticatedSignalWebSocket().disconnect();
dependencies.getUnauthenticatedSignalWebSocket().disconnect();
dependencies.getPushServiceSocket().close(); dependencies.getPushServiceSocket().close();
disposable.dispose(); disposable.dispose();

View file

@ -29,13 +29,13 @@ import org.asamk.signal.manager.util.KeyUtils;
import org.signal.libsignal.protocol.IdentityKeyPair; import org.signal.libsignal.protocol.IdentityKeyPair;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.registration.ProvisioningApi;
import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.DeviceNameUtil;
import org.whispersystems.signalservice.internal.push.ProvisioningSocket;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import java.io.IOException; import java.io.IOException;
@ -56,7 +56,7 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
private final Consumer<Manager> newManagerListener; private final Consumer<Manager> newManagerListener;
private final AccountsStore accountsStore; private final AccountsStore accountsStore;
private final SignalServiceAccountManager accountManager; private final ProvisioningApi provisioningApi;
private final IdentityKeyPair tempIdentityKey; private final IdentityKeyPair tempIdentityKey;
private final String password; private final String password;
@ -75,25 +75,30 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
tempIdentityKey = KeyUtils.generateIdentityKeyPair(); tempIdentityKey = KeyUtils.generateIdentityKeyPair();
password = KeyUtils.createPassword(); password = KeyUtils.createPassword();
GroupsV2Operations groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create( final var credentialsProvider = new DynamicCredentialsProvider(null,
serviceEnvironmentConfig.signalServiceConfiguration()), ServiceConfig.GROUP_MAX_SIZE); null,
accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.signalServiceConfiguration(), null,
new DynamicCredentialsProvider(null, null, null, password, SignalServiceAddress.DEFAULT_DEVICE_ID), password,
SignalServiceAddress.DEFAULT_DEVICE_ID);
final var pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
credentialsProvider,
userAgent, userAgent,
groupsV2Operations,
ServiceConfig.AUTOMATIC_NETWORK_RETRY); ServiceConfig.AUTOMATIC_NETWORK_RETRY);
final var provisioningSocket = new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
userAgent);
this.provisioningApi = new ProvisioningApi(pushServiceSocket, provisioningSocket, credentialsProvider);
} }
@Override @Override
public URI getDeviceLinkUri() throws TimeoutException, IOException { public URI getDeviceLinkUri() throws TimeoutException, IOException {
var deviceUuid = accountManager.getNewDeviceUuid(); var deviceUuid = provisioningApi.getNewDeviceUuid();
return new DeviceLinkUrl(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri(); return new DeviceLinkUrl(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
} }
@Override @Override
public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException { public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException {
var ret = accountManager.getNewDeviceRegistration(tempIdentityKey); var ret = provisioningApi.getNewDeviceRegistration(tempIdentityKey);
var number = ret.getNumber(); var number = ret.getNumber();
var aci = ret.getAci(); var aci = ret.getAci();
var pni = ret.getPni(); var pni = ret.getPni();
@ -140,7 +145,9 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
ret.getAciIdentity(), ret.getAciIdentity(),
ret.getPniIdentity(), ret.getPniIdentity(),
profileKey, profileKey,
ret.getMasterKey()); ret.getMasterKey(),
ret.getAccountEntropyPool(),
ret.getMediaRootBackupKey());
account.getConfigurationStore().setReadReceipts(ret.isReadReceipts()); account.getConfigurationStore().setReadReceipts(ret.isReadReceipts());
@ -148,7 +155,7 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI)); final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
logger.debug("Finishing new device registration"); logger.debug("Finishing new device registration");
var deviceId = accountManager.finishNewDeviceRegistration(ret.getProvisioningCode(), var deviceId = provisioningApi.finishNewDeviceRegistration(ret.getProvisioningCode(),
account.getAccountAttributes(null), account.getAccountAttributes(null),
aciPreKeys, aciPreKeys,
pniPreKeys); pniPreKeys);

View file

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

View file

@ -21,9 +21,11 @@ import org.asamk.signal.manager.RegistrationManager;
import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.UpdateProfile; import org.asamk.signal.manager.api.UpdateProfile;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.helper.AccountFileUpdater; import org.asamk.signal.manager.helper.AccountFileUpdater;
@ -31,14 +33,11 @@ import org.asamk.signal.manager.helper.PinHelper;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.NumberVerificationUtils; import org.asamk.signal.manager.util.NumberVerificationUtils;
import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.usernames.BaseUsernameException; import org.signal.libsignal.usernames.BaseUsernameException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.account.PreKeyCollection; import org.whispersystems.signalservice.api.account.PreKeyCollection;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.PNI; import org.whispersystems.signalservice.api.push.ServiceId.PNI;
@ -48,12 +47,12 @@ import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedExcep
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
import org.whispersystems.signalservice.api.svr.SecureValueRecovery; import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import java.io.IOException; import java.io.IOException;
import java.util.function.Consumer; import java.util.function.Consumer;
import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType; import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class RegistrationManagerImpl implements RegistrationManager { public class RegistrationManagerImpl implements RegistrationManager {
@ -64,9 +63,8 @@ public class RegistrationManagerImpl implements RegistrationManager {
private final ServiceEnvironmentConfig serviceEnvironmentConfig; private final ServiceEnvironmentConfig serviceEnvironmentConfig;
private final String userAgent; private final String userAgent;
private final Consumer<Manager> newManagerListener; private final Consumer<Manager> newManagerListener;
private final GroupsV2Operations groupsV2Operations;
private final SignalServiceAccountManager accountManager; private final SignalServiceAccountManager unauthenticatedAccountManager;
private final PinHelper pinHelper; private final PinHelper pinHelper;
private final AccountFileUpdater accountFileUpdater; private final AccountFileUpdater accountFileUpdater;
@ -85,26 +83,30 @@ public class RegistrationManagerImpl implements RegistrationManager {
this.userAgent = userAgent; this.userAgent = userAgent;
this.newManagerListener = newManagerListener; this.newManagerListener = newManagerListener;
groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration()), this.unauthenticatedAccountManager = SignalServiceAccountManager.createWithStaticCredentials(
ServiceConfig.GROUP_MAX_SIZE); serviceEnvironmentConfig.signalServiceConfiguration(),
this.accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.signalServiceConfiguration(), // Using empty UUID, because registering doesn't work otherwise
new DynamicCredentialsProvider( null,
// Using empty UUID, because registering doesn't work otherwise null,
null, null, account.getNumber(), account.getPassword(), SignalServiceAddress.DEFAULT_DEVICE_ID), account.getNumber(),
SignalServiceAddress.DEFAULT_DEVICE_ID,
account.getPassword(),
userAgent, userAgent,
groupsV2Operations, ServiceConfig.AUTOMATIC_NETWORK_RETRY,
ServiceConfig.AUTOMATIC_NETWORK_RETRY); ServiceConfig.GROUP_MAX_SIZE);
final var secureValueRecoveryV2 = serviceEnvironmentConfig.svr2Mrenclaves() final var secureValueRecovery = serviceEnvironmentConfig.svr2Mrenclaves()
.stream() .stream()
.map(mr -> (SecureValueRecovery) accountManager.getSecureValueRecoveryV2(mr)) .map(mr -> (SecureValueRecovery) this.unauthenticatedAccountManager.getSecureValueRecoveryV2(mr))
.toList(); .toList();
this.pinHelper = new PinHelper(secureValueRecoveryV2); this.pinHelper = new PinHelper(secureValueRecovery);
} }
@Override @Override
public void register( public void register(
boolean voiceVerification, String captcha boolean voiceVerification,
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException { String captcha,
final boolean forceRegister
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException {
if (account.isRegistered() if (account.isRegistered()
&& account.getServiceEnvironment() != null && account.getServiceEnvironment() != null
&& account.getServiceEnvironment() != serviceEnvironmentConfig.type()) { && account.getServiceEnvironment() != serviceEnvironmentConfig.type()) {
@ -112,21 +114,32 @@ public class RegistrationManagerImpl implements RegistrationManager {
} }
try { try {
if (!forceRegister) {
if (account.isRegistered()) {
throw new IOException("Account is already registered");
}
if (account.getAci() != null && attemptReactivateAccount()) {
return;
}
}
final var recoveryPassword = account.getRecoveryPassword(); final var recoveryPassword = account.getRecoveryPassword();
if (recoveryPassword != null && account.isPrimaryDevice() && attemptReregisterAccount(recoveryPassword)) { if (recoveryPassword != null && account.isPrimaryDevice() && attemptReregisterAccount(recoveryPassword)) {
return; return;
} }
if (account.getAci() != null && attemptReactivateAccount()) { final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
return; logger.trace("Creating verification session");
} String sessionId = NumberVerificationUtils.handleVerificationSession(registrationApi,
String sessionId = NumberVerificationUtils.handleVerificationSession(accountManager,
account.getSessionId(account.getNumber()), account.getSessionId(account.getNumber()),
id -> account.setSessionId(account.getNumber(), id), id -> account.setSessionId(account.getNumber(), id),
voiceVerification, voiceVerification,
captcha); captcha);
NumberVerificationUtils.requestVerificationCode(accountManager, sessionId, voiceVerification); logger.trace("Requesting verification code");
NumberVerificationUtils.requestVerificationCode(registrationApi, sessionId, voiceVerification);
logger.debug("Successfully requested verification code");
account.setRegistered(false);
} catch (DeprecatedVersionException e) { } catch (DeprecatedVersionException e) {
logger.debug("Signal-Server returned deprecated version exception", e); logger.debug("Signal-Server returned deprecated version exception", e);
throw e; throw e;
@ -135,8 +148,9 @@ public class RegistrationManagerImpl implements RegistrationManager {
@Override @Override
public void verifyAccount( public void verifyAccount(
String verificationCode, String pin String verificationCode,
) throws IOException, PinLockedException, IncorrectPinException { String pin
) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException {
if (account.isRegistered()) { if (account.isRegistered()) {
throw new IOException("Account is already registered"); throw new IOException("Account is already registered");
} }
@ -185,7 +199,8 @@ public class RegistrationManagerImpl implements RegistrationManager {
final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI)); final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI));
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI)); final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
final var response = Utils.handleResponseException(accountManager.registerAccount(null, final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
final var response = handleResponseException(registrationApi.registerAccount(null,
recoveryPassword, recoveryPassword,
account.getAccountAttributes(null), account.getAccountAttributes(null),
aciPreKeys, aciPreKeys,
@ -207,12 +222,14 @@ public class RegistrationManagerImpl implements RegistrationManager {
private boolean attemptReactivateAccount() { private boolean attemptReactivateAccount() {
try { try {
final var accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.signalServiceConfiguration(), final var dependencies = new SignalDependencies(serviceEnvironmentConfig,
account.getCredentialsProvider(),
userAgent, userAgent,
groupsV2Operations, account.getCredentialsProvider(),
ServiceConfig.AUTOMATIC_NETWORK_RETRY); account.getSignalServiceDataStore(),
accountManager.setAccountAttributes(account.getAccountAttributes(null)); null,
new ReentrantSignalSessionLock());
handleResponseException(dependencies.getAccountApi()
.setAccountAttributes(account.getAccountAttributes(null)));
account.setRegistered(true); account.setRegistered(true);
logger.info("Reactivated existing account, verify is not necessary."); logger.info("Reactivated existing account, verify is not necessary.");
if (newManagerListener != null) { if (newManagerListener != null) {
@ -238,12 +255,13 @@ public class RegistrationManagerImpl implements RegistrationManager {
final PreKeyCollection aciPreKeys, final PreKeyCollection aciPreKeys,
final PreKeyCollection pniPreKeys final PreKeyCollection pniPreKeys
) throws IOException { ) throws IOException {
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
try { try {
Utils.handleResponseException(accountManager.verifyAccount(verificationCode, sessionId)); handleResponseException(registrationApi.verifyAccount(sessionId, verificationCode));
} catch (AlreadyVerifiedException e) { } catch (AlreadyVerifiedException e) {
// Already verified so can continue registering // Already verified so can continue registering
} }
return Utils.handleResponseException(accountManager.registerAccount(sessionId, return handleResponseException(registrationApi.registerAccount(sessionId,
null, null,
account.getAccountAttributes(registrationLock), account.getAccountAttributes(registrationLock),
aciPreKeys, aciPreKeys,

View file

@ -2,35 +2,58 @@ package org.asamk.signal.manager.internal;
import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.signal.libsignal.net.Network;
import org.signal.libsignal.protocol.UsePqRatchet;
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceDataStore; import org.whispersystems.signalservice.api.SignalServiceDataStore;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.SignalWebSocket; import org.whispersystems.signalservice.api.account.AccountApi;
import org.whispersystems.signalservice.api.attachment.AttachmentApi;
import org.whispersystems.signalservice.api.cds.CdsApi;
import org.whispersystems.signalservice.api.certificate.CertificateApi;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.keys.KeysApi;
import org.whispersystems.signalservice.api.link.LinkDeviceApi;
import org.whispersystems.signalservice.api.message.MessageApi;
import org.whispersystems.signalservice.api.profiles.ProfileApi;
import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi;
import org.whispersystems.signalservice.api.registration.RegistrationApi;
import org.whispersystems.signalservice.api.services.ProfileService; 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.svr.SecureValueRecovery;
import org.whispersystems.signalservice.api.username.UsernameApi;
import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
import org.whispersystems.signalservice.api.websocket.WebSocketFactory; import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection; import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier; import java.util.function.Supplier;
public class SignalDependencies { public class SignalDependencies {
private static final Logger logger = LoggerFactory.getLogger(SignalDependencies.class);
private final Object LOCK = new Object(); private final Object LOCK = new Object();
private final ServiceEnvironmentConfig serviceEnvironmentConfig; private final ServiceEnvironmentConfig serviceEnvironmentConfig;
@ -43,17 +66,31 @@ public class SignalDependencies {
private boolean allowStories = true; private boolean allowStories = true;
private SignalServiceAccountManager accountManager; private SignalServiceAccountManager accountManager;
private AccountApi accountApi;
private RateLimitChallengeApi rateLimitChallengeApi;
private CdsApi cdsApi;
private UsernameApi usernameApi;
private GroupsV2Api groupsV2Api; private GroupsV2Api groupsV2Api;
private RegistrationApi registrationApi;
private LinkDeviceApi linkDeviceApi;
private StorageServiceApi storageServiceApi;
private CertificateApi certificateApi;
private AttachmentApi attachmentApi;
private MessageApi messageApi;
private KeysApi keysApi;
private GroupsV2Operations groupsV2Operations; private GroupsV2Operations groupsV2Operations;
private ClientZkOperations clientZkOperations; private ClientZkOperations clientZkOperations;
private PushServiceSocket pushServiceSocket; private PushServiceSocket pushServiceSocket;
private SignalWebSocket signalWebSocket; private Network libSignalNetwork;
private SignalWebSocket.AuthenticatedWebSocket authenticatedSignalWebSocket;
private SignalWebSocket.UnauthenticatedWebSocket unauthenticatedSignalWebSocket;
private SignalServiceMessageReceiver messageReceiver; private SignalServiceMessageReceiver messageReceiver;
private SignalServiceMessageSender messageSender; private SignalServiceMessageSender messageSender;
private List<SecureValueRecovery> secureValueRecoveryV2; private List<SecureValueRecovery> secureValueRecovery;
private ProfileService profileService; private ProfileService profileService;
private ProfileApi profileApi;
SignalDependencies( SignalDependencies(
final ServiceEnvironmentConfig serviceEnvironmentConfig, final ServiceEnvironmentConfig serviceEnvironmentConfig,
@ -75,9 +112,20 @@ public class SignalDependencies {
if (this.pushServiceSocket != null) { if (this.pushServiceSocket != null) {
this.pushServiceSocket.close(); this.pushServiceSocket.close();
this.pushServiceSocket = null; this.pushServiceSocket = null;
this.accountManager = null;
this.messageReceiver = null;
this.messageSender = null;
this.profileService = null;
this.groupsV2Api = null;
this.registrationApi = null;
this.secureValueRecovery = null;
}
if (this.authenticatedSignalWebSocket != null) {
this.authenticatedSignalWebSocket.forceNewWebSocket();
}
if (this.unauthenticatedSignalWebSocket != null) {
this.unauthenticatedSignalWebSocket.forceNewWebSocket();
} }
this.messageSender = null;
getSignalWebSocket().forceNewWebSockets();
} }
/** /**
@ -100,21 +148,50 @@ public class SignalDependencies {
() -> pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(), () -> pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
credentialsProvider, credentialsProvider,
userAgent, userAgent,
getClientZkProfileOperations(),
ServiceConfig.AUTOMATIC_NETWORK_RETRY)); ServiceConfig.AUTOMATIC_NETWORK_RETRY));
} }
public Network getLibSignalNetwork() {
return getOrCreate(() -> libSignalNetwork, () -> {
libSignalNetwork = new Network(serviceEnvironmentConfig.netEnvironment(), userAgent);
setSignalNetworkProxy(libSignalNetwork);
});
}
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() { public SignalServiceAccountManager getAccountManager() {
return getOrCreate(() -> accountManager, return getOrCreate(() -> accountManager,
() -> accountManager = new SignalServiceAccountManager(getPushServiceSocket(), () -> accountManager = new SignalServiceAccountManager(getAuthenticatedSignalWebSocket(),
null, getAccountApi(),
serviceEnvironmentConfig.signalServiceConfiguration(), getPushServiceSocket(),
credentialsProvider,
getGroupsV2Operations())); getGroupsV2Operations()));
} }
public SignalServiceAccountManager createUnauthenticatedAccountManager(String number, String password) { public SignalServiceAccountManager createUnauthenticatedAccountManager(String number, String password) {
return new SignalServiceAccountManager(getServiceEnvironmentConfig().signalServiceConfiguration(), return SignalServiceAccountManager.createWithStaticCredentials(getServiceEnvironmentConfig().signalServiceConfiguration(),
null, null,
null, null,
number, number,
@ -125,10 +202,67 @@ public class SignalDependencies {
ServiceConfig.GROUP_MAX_SIZE); ServiceConfig.GROUP_MAX_SIZE);
} }
public AccountApi getAccountApi() {
return getOrCreate(() -> accountApi, () -> accountApi = new AccountApi(getAuthenticatedSignalWebSocket()));
}
public RateLimitChallengeApi getRateLimitChallengeApi() {
return getOrCreate(() -> rateLimitChallengeApi,
() -> rateLimitChallengeApi = new RateLimitChallengeApi(getAuthenticatedSignalWebSocket()));
}
public CdsApi getCdsApi() {
return getOrCreate(() -> cdsApi, () -> cdsApi = new CdsApi(getAuthenticatedSignalWebSocket()));
}
public UsernameApi getUsernameApi() {
return getOrCreate(() -> usernameApi, () -> usernameApi = new UsernameApi(getUnauthenticatedSignalWebSocket()));
}
public GroupsV2Api getGroupsV2Api() { public GroupsV2Api getGroupsV2Api() {
return getOrCreate(() -> groupsV2Api, () -> groupsV2Api = getAccountManager().getGroupsV2Api()); return getOrCreate(() -> groupsV2Api, () -> groupsV2Api = getAccountManager().getGroupsV2Api());
} }
public RegistrationApi getRegistrationApi() {
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() { public GroupsV2Operations getGroupsV2Operations() {
return getOrCreate(() -> groupsV2Operations, return getOrCreate(() -> groupsV2Operations,
() -> groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration()), () -> groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration()),
@ -145,66 +279,79 @@ public class SignalDependencies {
return clientZkOperations.getProfileOperations(); return clientZkOperations.getProfileOperations();
} }
public SignalWebSocket getSignalWebSocket() { public SignalWebSocket.AuthenticatedWebSocket getAuthenticatedSignalWebSocket() {
return getOrCreate(() -> signalWebSocket, () -> { return getOrCreate(() -> authenticatedSignalWebSocket, () -> {
final var timer = new UptimeSleepTimer(); final var timer = new UptimeSleepTimer();
final var healthMonitor = new SignalWebSocketHealthMonitor(timer); final var healthMonitor = new SignalWebSocketHealthMonitor(timer);
final var webSocketFactory = new WebSocketFactory() {
@Override
public WebSocketConnection createWebSocket() {
return new WebSocketConnection("normal",
serviceEnvironmentConfig.signalServiceConfiguration(),
Optional.of(credentialsProvider),
userAgent,
healthMonitor,
allowStories);
}
@Override authenticatedSignalWebSocket = new SignalWebSocket.AuthenticatedWebSocket(() -> new OkHttpWebSocketConnection(
public WebSocketConnection createUnidentifiedWebSocket() { "normal",
return new WebSocketConnection("unidentified", serviceEnvironmentConfig.signalServiceConfiguration(),
serviceEnvironmentConfig.signalServiceConfiguration(), Optional.of(credentialsProvider),
Optional.empty(), userAgent,
userAgent, healthMonitor,
healthMonitor, allowStories), () -> true, timer, TimeUnit.SECONDS.toMillis(10));
allowStories); healthMonitor.monitor(authenticatedSignalWebSocket);
} });
}; }
signalWebSocket = new SignalWebSocket(webSocketFactory);
healthMonitor.monitor(signalWebSocket); public SignalWebSocket.UnauthenticatedWebSocket getUnauthenticatedSignalWebSocket() {
return getOrCreate(() -> unauthenticatedSignalWebSocket, () -> {
final var timer = new UptimeSleepTimer();
final var healthMonitor = new SignalWebSocketHealthMonitor(timer);
unauthenticatedSignalWebSocket = new SignalWebSocket.UnauthenticatedWebSocket(() -> new OkHttpWebSocketConnection(
"unidentified",
serviceEnvironmentConfig.signalServiceConfiguration(),
Optional.empty(),
userAgent,
healthMonitor,
allowStories), () -> true, timer, TimeUnit.SECONDS.toMillis(10));
healthMonitor.monitor(unauthenticatedSignalWebSocket);
}); });
} }
public SignalServiceMessageReceiver getMessageReceiver() { public SignalServiceMessageReceiver getMessageReceiver() {
return getOrCreate(() -> messageReceiver, return getOrCreate(() -> messageReceiver,
() -> messageReceiver = new SignalServiceMessageReceiver(pushServiceSocket)); () -> messageReceiver = new SignalServiceMessageReceiver(getPushServiceSocket()));
} }
public SignalServiceMessageSender getMessageSender() { public SignalServiceMessageSender getMessageSender() {
return getOrCreate(() -> messageSender, return getOrCreate(() -> messageSender,
() -> messageSender = new SignalServiceMessageSender(credentialsProvider, () -> messageSender = new SignalServiceMessageSender(getPushServiceSocket(),
dataStore, dataStore,
sessionLock, sessionLock,
getSignalWebSocket(), getAttachmentApi(),
getMessageApi(),
getKeysApi(),
Optional.empty(), Optional.empty(),
executor, executor,
ServiceConfig.MAX_ENVELOPE_SIZE, ServiceConfig.MAX_ENVELOPE_SIZE,
pushServiceSocket)); () -> true,
UsePqRatchet.NO));
} }
public List<SecureValueRecovery> getSecureValueRecoveryV2() { public List<SecureValueRecovery> getSecureValueRecovery() {
return getOrCreate(() -> secureValueRecoveryV2, return getOrCreate(() -> secureValueRecovery,
() -> secureValueRecoveryV2 = serviceEnvironmentConfig.svr2Mrenclaves() () -> secureValueRecovery = serviceEnvironmentConfig.svr2Mrenclaves()
.stream() .stream()
.map(mr -> (SecureValueRecovery) getAccountManager().getSecureValueRecoveryV2(mr)) .map(mr -> (SecureValueRecovery) getAccountManager().getSecureValueRecoveryV2(mr))
.toList()); .toList());
} }
public ProfileApi getProfileApi() {
return getOrCreate(() -> profileApi,
() -> profileApi = new ProfileApi(getAuthenticatedSignalWebSocket(),
getUnauthenticatedSignalWebSocket(),
getPushServiceSocket(),
getClientZkProfileOperations()));
}
public ProfileService getProfileService() { public ProfileService getProfileService() {
return getOrCreate(() -> profileService, return getOrCreate(() -> profileService,
() -> profileService = new ProfileService(getClientZkProfileOperations(), () -> profileService = new ProfileService(getClientZkProfileOperations(),
getMessageReceiver(), getAuthenticatedSignalWebSocket(),
getSignalWebSocket())); getUnauthenticatedSignalWebSocket()));
} }
public SignalServiceCipher getCipher(ServiceIdType serviceIdType) { public SignalServiceCipher getCipher(ServiceIdType serviceIdType) {

View file

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

View file

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

View file

@ -33,7 +33,7 @@ import java.util.UUID;
public class AccountDatabase extends Database { public class AccountDatabase extends Database {
private static final Logger logger = LoggerFactory.getLogger(AccountDatabase.class); private static final Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
private static final long DATABASE_VERSION = 24; private static final long DATABASE_VERSION = 27;
private AccountDatabase(final HikariDataSource dataSource) { private AccountDatabase(final HikariDataSource dataSource) {
super(logger, DATABASE_VERSION, dataSource); super(logger, DATABASE_VERSION, dataSource);
@ -556,7 +556,7 @@ public class AccountDatabase extends Database {
profile_capabilities TEXT profile_capabilities TEXT
) STRICT; ) STRICT;
INSERT INTO recipient2 (_id, aci, pni, storage_id, storage_record, number, username, unregistered_timestamp, profile_key, profile_key_credential, given_name, family_name, color, expiration_time, blocked, archived, profile_sharing, hidden, profile_last_update_timestamp, profile_given_name, profile_family_name, profile_about, profile_about_emoji, profile_avatar_url_path, profile_mobile_coin_address, profile_unidentified_access_mode, profile_capabilities) INSERT INTO recipient2 (_id, aci, pni, storage_id, storage_record, number, username, unregistered_timestamp, profile_key, profile_key_credential, given_name, family_name, color, expiration_time, blocked, archived, profile_sharing, hidden, profile_last_update_timestamp, profile_given_name, profile_family_name, profile_about, profile_about_emoji, profile_avatar_url_path, profile_mobile_coin_address, profile_unidentified_access_mode, profile_capabilities)
SELECT r._id, (SELECT t.address FROM tmp_mapping_table t WHERE t.uuid = r.uuid AND t.address not like 'PNI:%') aci, (SELECT t.address FROM tmp_mapping_table t WHERE t.uuid = r.pni AND t.address like 'PNI:%') pni, storage_id, storage_record, number, username, unregistered_timestamp, profile_key, profile_key_credential, given_name, family_name, color, expiration_time, blocked, archived, profile_sharing, hidden, profile_last_update_timestamp, profile_given_name, profile_family_name, profile_about, profile_about_emoji, profile_avatar_url_path, profile_mobile_coin_address, profile_unidentified_access_mode, profile_capabilities SELECT r._id, (SELECT t.address FROM tmp_mapping_table t WHERE t.uuid = r.uuid AND t.address not like 'PNI:%') aci, (SELECT t.address FROM tmp_mapping_table t WHERE t.uuid = r.pni AND t.address like 'PNI:%' AND (SELECT COUNT(pni) FROM recipient WHERE pni = r.pni) = 1) pni, storage_id, storage_record, number, username, unregistered_timestamp, profile_key, profile_key_credential, given_name, family_name, color, expiration_time, blocked, archived, profile_sharing, hidden, profile_last_update_timestamp, profile_given_name, profile_family_name, profile_about, profile_about_emoji, profile_avatar_url_path, profile_mobile_coin_address, profile_unidentified_access_mode, profile_capabilities
FROM recipient r; FROM recipient r;
DROP TABLE recipient; DROP TABLE recipient;
ALTER TABLE recipient2 RENAME TO recipient; ALTER TABLE recipient2 RENAME TO recipient;
@ -581,10 +581,38 @@ public class AccountDatabase extends Database {
"""); """);
} }
} }
if (oldVersion < 25) {
logger.debug("Updating database: Create nick_name and note columns");
try (final var statement = connection.createStatement()) {
statement.executeUpdate("""
ALTER TABLE recipient ADD nick_name_given_name TEXT;
ALTER TABLE recipient ADD nick_name_family_name TEXT;
ALTER TABLE recipient ADD note TEXT;
""");
}
}
if (oldVersion < 26) {
logger.debug("Updating database: Create discoverable and profile_phone_number_sharing columns");
try (final var statement = connection.createStatement()) {
statement.executeUpdate("""
ALTER TABLE recipient ADD discoverable INTEGER;
ALTER TABLE recipient ADD profile_phone_number_sharing TEXT;
""");
}
}
if (oldVersion < 27) {
logger.debug("Updating database: Create expiration_time_version column");
try (final var statement = connection.createStatement()) {
statement.executeUpdate("""
ALTER TABLE recipient ADD expiration_time_version INTEGER DEFAULT 1 NOT NULL;
""");
}
}
} }
private static void createUuidMappingTable( private static void createUuidMappingTable(
final Connection connection, final Statement statement final Connection connection,
final Statement statement
) throws SQLException { ) throws SQLException {
statement.executeUpdate(""" statement.executeUpdate("""
CREATE TABLE tmp_mapping_table ( CREATE TABLE tmp_mapping_table (

View file

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

View file

@ -24,7 +24,8 @@ public abstract class Database implements AutoCloseable {
} }
public static <T extends Database> T initDatabase( public static <T extends Database> T initDatabase(
File databaseFile, Function<HikariDataSource, T> newDatabase File databaseFile,
Function<HikariDataSource, T> newDatabase
) throws SQLException { ) throws SQLException {
HikariDataSource dataSource = null; HikariDataSource dataSource = null;
@ -94,10 +95,12 @@ public abstract class Database implements AutoCloseable {
sqliteConfig.setTransactionMode(SQLiteConfig.TransactionMode.IMMEDIATE); sqliteConfig.setTransactionMode(SQLiteConfig.TransactionMode.IMMEDIATE);
HikariConfig config = new HikariConfig(); 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.setDataSourceProperties(sqliteConfig.toProperties());
config.setMinimumIdle(1); config.setMinimumIdle(1);
config.setConnectionInitSql("PRAGMA foreign_keys=ON"); config.setConnectionTimeout(90_000);
config.setMaximumPoolSize(50);
config.setMaxLifetime(0);
return new HikariDataSource(config); 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.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.AccountEntropyPool;
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore; import org.whispersystems.signalservice.api.SignalServiceAccountDataStore;
import org.whispersystems.signalservice.api.SignalServiceDataStore; import org.whispersystems.signalservice.api.SignalServiceDataStore;
import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.account.AccountAttributes;
import org.whispersystems.signalservice.api.account.PreKeyCollection; 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.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.ServiceId; 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 Logger logger = LoggerFactory.getLogger(SignalAccount.class);
private static final int MINIMUM_STORAGE_VERSION = 1; private static final int MINIMUM_STORAGE_VERSION = 1;
private static final int CURRENT_STORAGE_VERSION = 9; private static final int CURRENT_STORAGE_VERSION = 10;
private final Object LOCK = new Object(); private final Object LOCK = new Object();
@ -138,6 +140,8 @@ public class SignalAccount implements Closeable {
private String registrationLockPin; private String registrationLockPin;
private MasterKey pinMasterKey; private MasterKey pinMasterKey;
private StorageKey storageKey; private StorageKey storageKey;
private AccountEntropyPool accountEntropyPool;
private MediaRootBackupKey mediaRootBackupKey;
private ProfileKey profileKey; private ProfileKey profileKey;
private Settings settings; private Settings settings;
@ -149,6 +153,9 @@ public class SignalAccount implements Closeable {
private final KeyValueEntry<Long> lastReceiveTimestamp = new KeyValueEntry<>("last-receive-timestamp", private final KeyValueEntry<Long> lastReceiveTimestamp = new KeyValueEntry<>("last-receive-timestamp",
long.class, long.class,
0L); 0L);
private final KeyValueEntry<Boolean> needsToRetryFailedMessages = new KeyValueEntry<>("retry-failed-messages",
Boolean.class,
true);
private final KeyValueEntry<byte[]> cdsiToken = new KeyValueEntry<>("cdsi-token", byte[].class); private final KeyValueEntry<byte[]> cdsiToken = new KeyValueEntry<>("cdsi-token", byte[].class);
private final KeyValueEntry<Long> lastRecipientsRefresh = new KeyValueEntry<>("last-recipients-refresh", private final KeyValueEntry<Long> lastRecipientsRefresh = new KeyValueEntry<>("last-recipients-refresh",
long.class); long.class);
@ -186,7 +193,10 @@ public class SignalAccount implements Closeable {
} }
public static SignalAccount load( public static SignalAccount load(
File dataPath, String accountPath, boolean waitForLock, final Settings settings File dataPath,
String accountPath,
boolean waitForLock,
final Settings settings
) throws IOException { ) throws IOException {
logger.trace("Opening account file"); logger.trace("Opening account file");
final var fileName = getFileName(dataPath, accountPath); final var fileName = getFileName(dataPath, accountPath);
@ -282,7 +292,9 @@ public class SignalAccount implements Closeable {
final IdentityKeyPair aciIdentity, final IdentityKeyPair aciIdentity,
final IdentityKeyPair pniIdentity, final IdentityKeyPair pniIdentity,
final ProfileKey profileKey, final ProfileKey profileKey,
final MasterKey masterKey final MasterKey masterKey,
final AccountEntropyPool accountEntropyPool,
final MediaRootBackupKey mediaRootBackupKey
) { ) {
this.deviceId = 0; this.deviceId = 0;
this.number = number; this.number = number;
@ -297,8 +309,15 @@ public class SignalAccount implements Closeable {
this.pniAccountData.setIdentityKeyPair(pniIdentity); this.pniAccountData.setIdentityKeyPair(pniIdentity);
this.registered = false; this.registered = false;
this.isMultiDevice = true; this.isMultiDevice = true;
getKeyValueStore().storeEntry(lastReceiveTimestamp, 0L); 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); getKeyValueStore().storeEntry(storageManifestVersion, -1L);
this.setStorageManifest(null); this.setStorageManifest(null);
this.storageKey = null; this.storageKey = null;
@ -313,7 +332,9 @@ public class SignalAccount implements Closeable {
} }
public void finishLinking( 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.registered = true;
this.deviceId = deviceId; this.deviceId = deviceId;
@ -331,6 +352,7 @@ public class SignalAccount implements Closeable {
final PreKeyCollection pniPreKeys final PreKeyCollection pniPreKeys
) { ) {
this.pinMasterKey = masterKey; this.pinMasterKey = masterKey;
this.accountEntropyPool = null;
getKeyValueStore().storeEntry(storageManifestVersion, -1L); getKeyValueStore().storeEntry(storageManifestVersion, -1L);
this.setStorageManifest(null); this.setStorageManifest(null);
this.storageKey = null; this.storageKey = null;
@ -342,7 +364,7 @@ public class SignalAccount implements Closeable {
this.pniAccountData.setServiceId(pni); this.pniAccountData.setServiceId(pni);
init(); init();
this.registrationLockPin = pin; this.registrationLockPin = pin;
getKeyValueStore().storeEntry(lastReceiveTimestamp, 0L); setLastReceiveTimestamp(0L);
save(); save();
setPreKeys(ServiceIdType.ACI, aciPreKeys); setPreKeys(ServiceIdType.ACI, aciPreKeys);
@ -372,7 +394,9 @@ public class SignalAccount implements Closeable {
} }
private void mergeRecipients( private void mergeRecipients(
final Connection connection, RecipientId recipientId, RecipientId toBeMergedRecipientId final Connection connection,
RecipientId recipientId,
RecipientId toBeMergedRecipientId
) throws SQLException { ) throws SQLException {
getMessageCache().mergeRecipients(recipientId, toBeMergedRecipientId); getMessageCache().mergeRecipients(recipientId, toBeMergedRecipientId);
getGroupStore().mergeRecipients(connection, recipientId, toBeMergedRecipientId); getGroupStore().mergeRecipients(connection, recipientId, toBeMergedRecipientId);
@ -435,9 +459,7 @@ public class SignalAccount implements Closeable {
return f.exists() && !f.isDirectory() && f.length() > 0L; return f.exists() && !f.isDirectory() && f.length() > 0L;
} }
private void load( private void load(File dataPath, String accountPath, final Settings settings) throws IOException {
File dataPath, String accountPath, final Settings settings
) throws IOException {
logger.trace("Loading account file {}", accountPath); logger.trace("Loading account file {}", accountPath);
this.dataPath = dataPath; this.dataPath = dataPath;
this.accountPath = accountPath; this.accountPath = accountPath;
@ -491,6 +513,12 @@ public class SignalAccount implements Closeable {
if (storage.storageKey != null) { if (storage.storageKey != null) {
storageKey = new StorageKey(base64.decode(storage.storageKey)); 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) { if (storage.profileKey != null) {
try { try {
profileKey = new ProfileKey(base64.decode(storage.profileKey)); profileKey = new ProfileKey(base64.decode(storage.profileKey));
@ -590,7 +618,7 @@ public class SignalAccount implements Closeable {
isMultiDevice = rootNode.get("isMultiDevice").asBoolean(); isMultiDevice = rootNode.get("isMultiDevice").asBoolean();
} }
if (rootNode.hasNonNull("lastReceiveTimestamp")) { if (rootNode.hasNonNull("lastReceiveTimestamp")) {
getKeyValueStore().storeEntry(lastReceiveTimestamp, rootNode.get("lastReceiveTimestamp").asLong()); setLastReceiveTimestamp(rootNode.get("lastReceiveTimestamp").asLong());
} }
int registrationId = 0; int registrationId = 0;
if (rootNode.hasNonNull("registrationId")) { if (rootNode.hasNonNull("registrationId")) {
@ -783,7 +811,8 @@ public class SignalAccount implements Closeable {
} }
private void loadLegacyStores( private void loadLegacyStores(
final JsonNode rootNode, final LegacyJsonSignalProtocolStore legacySignalProtocolStore final JsonNode rootNode,
final LegacyJsonSignalProtocolStore legacySignalProtocolStore
) { ) {
var legacyRecipientStoreNode = rootNode.get("recipientStore"); var legacyRecipientStoreNode = rootNode.get("recipientStore");
if (legacyRecipientStoreNode != null) { if (legacyRecipientStoreNode != null) {
@ -798,6 +827,7 @@ public class SignalAccount implements Closeable {
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyPreKeyStore() != null) { if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyPreKeyStore() != null) {
logger.debug("Migrating legacy pre key store."); logger.debug("Migrating legacy pre key store.");
aciAccountData.getPreKeyStore().removeAllPreKeys();
for (var entry : legacySignalProtocolStore.getLegacyPreKeyStore().getPreKeys().entrySet()) { for (var entry : legacySignalProtocolStore.getLegacyPreKeyStore().getPreKeys().entrySet()) {
try { try {
aciAccountData.getPreKeyStore().storePreKey(entry.getKey(), new PreKeyRecord(entry.getValue())); aciAccountData.getPreKeyStore().storePreKey(entry.getKey(), new PreKeyRecord(entry.getValue()));
@ -809,6 +839,7 @@ public class SignalAccount implements Closeable {
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySignedPreKeyStore() != null) { if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySignedPreKeyStore() != null) {
logger.debug("Migrating legacy signed pre key store."); logger.debug("Migrating legacy signed pre key store.");
aciAccountData.getSignedPreKeyStore().removeAllSignedPreKeys();
for (var entry : legacySignalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) { for (var entry : legacySignalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) {
try { try {
aciAccountData.getSignedPreKeyStore() aciAccountData.getSignedPreKeyStore()
@ -854,10 +885,14 @@ public class SignalAccount implements Closeable {
final var recipientId = getRecipientStore().resolveRecipientTrusted(contact.getAddress()); final var recipientId = getRecipientStore().resolveRecipientTrusted(contact.getAddress());
getContactStore().storeContact(recipientId, getContactStore().storeContact(recipientId,
new Contact(contact.name, new Contact(contact.name,
null,
null,
null,
null, null,
null, null,
contact.color, contact.color,
contact.messageExpirationTime, contact.messageExpirationTime,
1,
0, 0,
false, false,
contact.blocked, contact.blocked,
@ -892,9 +927,6 @@ public class SignalAccount implements Closeable {
if (profile != null) { if (profile != null) {
final var capabilities = new HashSet<Profile.Capability>(); final var capabilities = new HashSet<Profile.Capability>();
if (profile.getCapabilities() != null) { if (profile.getCapabilities() != null) {
if (profile.getCapabilities().gv1Migration) {
capabilities.add(Profile.Capability.gv1Migration);
}
if (profile.getCapabilities().storage) { if (profile.getCapabilities().storage) {
capabilities.add(Profile.Capability.storage); capabilities.add(Profile.Capability.storage);
} }
@ -911,7 +943,8 @@ public class SignalAccount implements Closeable {
: profile.getUnidentifiedAccess() != null : profile.getUnidentifiedAccess() != null
? Profile.UnidentifiedAccessMode.ENABLED ? Profile.UnidentifiedAccessMode.ENABLED
: Profile.UnidentifiedAccessMode.DISABLED, : Profile.UnidentifiedAccessMode.DISABLED,
capabilities); capabilities,
null);
getProfileStore().storeProfile(recipientId, newProfile); getProfileStore().storeProfile(recipientId, newProfile);
} }
} }
@ -935,6 +968,7 @@ public class SignalAccount implements Closeable {
getContactStore().storeContact(recipientId, getContactStore().storeContact(recipientId,
Contact.newBuilder(contact) Contact.newBuilder(contact)
.withMessageExpirationTime(thread.messageExpirationTime) .withMessageExpirationTime(thread.messageExpirationTime)
.withMessageExpirationTimeVersion(1)
.build()); .build());
} }
} else { } else {
@ -955,6 +989,7 @@ public class SignalAccount implements Closeable {
synchronized (fileChannel) { synchronized (fileChannel) {
final var base64 = Base64.getEncoder(); final var base64 = Base64.getEncoder();
final var storage = new Storage(CURRENT_STORAGE_VERSION, final var storage = new Storage(CURRENT_STORAGE_VERSION,
System.currentTimeMillis(),
serviceEnvironment.name(), serviceEnvironment.name(),
registered, registered,
number, number,
@ -968,6 +1003,8 @@ public class SignalAccount implements Closeable {
registrationLockPin, registrationLockPin,
pinMasterKey == null ? null : base64.encodeToString(pinMasterKey.serialize()), pinMasterKey == null ? null : base64.encodeToString(pinMasterKey.serialize()),
storageKey == null ? null : base64.encodeToString(storageKey.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()), profileKey == null ? null : base64.encodeToString(profileKey.serialize()),
usernameLink == null ? null : base64.encodeToString(usernameLink.getEntropy()), usernameLink == null ? null : base64.encodeToString(usernameLink.getEntropy()),
usernameLink == null ? null : usernameLink.getServerId().toString()); usernameLink == null ? null : usernameLink.getServerId().toString());
@ -1333,6 +1370,7 @@ public class SignalAccount implements Closeable {
public void setUsernameLink(final UsernameLinkComponents usernameLink) { public void setUsernameLink(final UsernameLinkComponents usernameLink) {
this.usernameLink = usernameLink; this.usernameLink = usernameLink;
save();
} }
public ServiceEnvironment getServiceEnvironment() { public ServiceEnvironment getServiceEnvironment() {
@ -1428,6 +1466,10 @@ public class SignalAccount implements Closeable {
return selfRecipientId; return selfRecipientId;
} }
public Profile getSelfRecipientProfile() {
return recipientStore.getProfile(selfRecipientId);
}
public String getSessionId(final String forNumber) { public String getSessionId(final String forNumber) {
final var keyValueStore = getKeyValueStore(); final var keyValueStore = getKeyValueStore();
final var sessionNumber = keyValueStore.getEntry(verificationSessionNumber); final var sessionNumber = keyValueStore.getEntry(verificationSessionNumber);
@ -1498,16 +1540,28 @@ public class SignalAccount implements Closeable {
public MasterKey getPinBackedMasterKey() { public MasterKey getPinBackedMasterKey() {
if (registrationLockPin == null) { if (registrationLockPin == null) {
return null; return null;
} else if (!isPrimaryDevice()) {
return getMasterKey();
} }
return pinMasterKey; return getOrCreatePinMasterKey();
} }
public MasterKey getOrCreatePinMasterKey() { public MasterKey getOrCreatePinMasterKey() {
if (pinMasterKey == null) { final var key = getMasterKey();
pinMasterKey = KeyUtils.createMasterKey(); if (key != null) {
save(); 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) { public void setMasterKey(MasterKey masterKey) {
@ -1515,14 +1569,19 @@ public class SignalAccount implements Closeable {
return; return;
} }
this.pinMasterKey = masterKey; this.pinMasterKey = masterKey;
if (masterKey != null) {
this.storageKey = null;
}
save(); save();
} }
public StorageKey getOrCreateStorageKey() { public StorageKey getOrCreateStorageKey() {
if (pinMasterKey != null) { if (storageKey != null) {
return pinMasterKey.deriveStorageServiceKey();
} else if (storageKey != null) {
return storageKey; return storageKey;
} else if (pinMasterKey != null) {
return pinMasterKey.deriveStorageServiceKey();
} else if (accountEntropyPool != null) {
return accountEntropyPool.deriveMasterKey().deriveStorageServiceKey();
} else if (!isPrimaryDevice() || !isMultiDevice()) { } else if (!isPrimaryDevice() || !isMultiDevice()) {
// Only upload storage, if a pin master key already exists or linked devices exist // Only upload storage, if a pin master key already exists or linked devices exist
return null; return null;
@ -1539,6 +1598,40 @@ public class SignalAccount implements Closeable {
save(); 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() { public String getRecoveryPassword() {
final var masterKey = getPinBackedMasterKey(); final var masterKey = getPinBackedMasterKey();
if (masterKey == null) { if (masterKey == null) {
@ -1561,7 +1654,7 @@ public class SignalAccount implements Closeable {
return Optional.empty(); return Optional.empty();
} }
try (var inputStream = new FileInputStream(storageManifestFile)) { try (var inputStream = new FileInputStream(storageManifestFile)) {
return Optional.of(SignalStorageManifest.deserialize(inputStream.readAllBytes())); return Optional.of(SignalStorageManifest.Companion.deserialize(inputStream.readAllBytes()));
} catch (IOException e) { } catch (IOException e) {
logger.warn("Failed to read local storage manifest.", e); logger.warn("Failed to read local storage manifest.", e);
return Optional.empty(); return Optional.empty();
@ -1650,6 +1743,14 @@ public class SignalAccount implements Closeable {
getKeyValueStore().storeEntry(lastReceiveTimestamp, value); getKeyValueStore().storeEntry(lastReceiveTimestamp, value);
} }
public void setNeedsToRetryFailedMessages(final boolean value) {
getKeyValueStore().storeEntry(needsToRetryFailedMessages, value);
}
public boolean getNeedsToRetryFailedMessages() {
return getKeyValueStore().getEntry(needsToRetryFailedMessages);
}
public boolean isUnrestrictedUnidentifiedAccess() { public boolean isUnrestrictedUnidentifiedAccess() {
return Boolean.TRUE.equals(getKeyValueStore().getEntry(unrestrictedUnidentifiedAccess)); return Boolean.TRUE.equals(getKeyValueStore().getEntry(unrestrictedUnidentifiedAccess));
} }
@ -1846,6 +1947,7 @@ public class SignalAccount implements Closeable {
public record Storage( public record Storage(
int version, int version,
long timestamp,
String serviceEnvironment, String serviceEnvironment,
boolean registered, boolean registered,
String number, String number,
@ -1859,6 +1961,8 @@ public class SignalAccount implements Closeable {
String registrationLockPin, String registrationLockPin,
String pinMasterKey, String pinMasterKey,
String storageKey, String storageKey,
String accountEntropyPool,
String mediaRootBackupKey,
String profileKey, String profileKey,
String usernameLinkEntropy, String usernameLinkEntropy,
String usernameLinkServerId String usernameLinkServerId

View file

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

View file

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

View file

@ -1,6 +1,7 @@
package org.asamk.signal.manager.storage.accounts; package org.asamk.signal.manager.storage.accounts;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.ServiceEnvironment; import org.asamk.signal.manager.api.ServiceEnvironment;
@ -10,7 +11,6 @@ import org.asamk.signal.manager.util.IOUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -41,7 +41,9 @@ public class AccountsStore {
private final AccountLoader accountLoader; private final AccountLoader accountLoader;
public AccountsStore( public AccountsStore(
final File dataPath, final ServiceEnvironment serviceEnvironment, final AccountLoader accountLoader final File dataPath,
final ServiceEnvironment serviceEnvironment,
final AccountLoader accountLoader
) throws IOException { ) throws IOException {
this.dataPath = dataPath; this.dataPath = dataPath;
this.serviceEnvironment = getServiceEnvironmentString(serviceEnvironment); this.serviceEnvironment = getServiceEnvironmentString(serviceEnvironment);
@ -179,7 +181,7 @@ public class AccountsStore {
return Arrays.stream(files) return Arrays.stream(files)
.filter(File::isFile) .filter(File::isFile)
.map(File::getName) .map(File::getName)
.filter(file -> PhoneNumberFormatter.isValidNumber(file, null)) .filter(file -> PhoneNumberUtil.getInstance().isPossibleNumber(file, null))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
@ -202,7 +204,9 @@ public class AccountsStore {
} }
private AccountsStorage upgradeAccountsFile( private AccountsStorage upgradeAccountsFile(
final FileChannel fileChannel, final AccountsStorage storage, final int accountsVersion final FileChannel fileChannel,
final AccountsStorage storage,
final int accountsVersion
) { ) {
try { try {
List<AccountsStorage.Account> newAccounts = storage.accounts(); List<AccountsStorage.Account> newAccounts = storage.accounts();

View file

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

View file

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

View file

@ -121,7 +121,10 @@ public class GroupStore {
} }
public void storeStorageRecord( 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 { ) throws SQLException {
final var groupTable = groupId instanceof GroupIdV1 ? TABLE_GROUP_V1 : TABLE_GROUP_V2; final var groupTable = groupId instanceof GroupIdV1 ? TABLE_GROUP_V1 : TABLE_GROUP_V2;
final var deleteSql = ( final var deleteSql = (
@ -229,24 +232,29 @@ public class GroupStore {
public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) { public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) {
try (final var connection = database.getConnection()) { try (final var connection = database.getConnection()) {
var group = getGroup(connection, groupId); return getOrCreateGroupV1(connection, groupId);
if (group != null) {
return group;
}
if (getGroupV2ByV1Id(connection, groupId) == null) {
return new GroupInfoV1(groupId);
}
return null;
} catch (SQLException e) { } catch (SQLException e) {
throw new RuntimeException("Failed read from group store", e); throw new RuntimeException("Failed read from group store", e);
} }
} }
public GroupInfoV1 getOrCreateGroupV1(final Connection connection, final GroupIdV1 groupId) throws SQLException {
var group = getGroup(connection, groupId);
if (group != null) {
return group;
}
if (getGroupV2ByV1Id(connection, groupId) == null) {
return new GroupInfoV1(groupId);
}
return null;
}
public GroupInfoV2 getGroupOrPartialMigrate( public GroupInfoV2 getGroupOrPartialMigrate(
Connection connection, final GroupMasterKey groupMasterKey Connection connection,
final GroupMasterKey groupMasterKey
) throws SQLException { ) throws SQLException {
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
final var groupId = GroupUtils.getGroupIdV2(groupSecretParams); final var groupId = GroupUtils.getGroupIdV2(groupSecretParams);
@ -254,9 +262,7 @@ public class GroupStore {
return getGroupOrPartialMigrate(connection, groupMasterKey, groupId); return getGroupOrPartialMigrate(connection, groupMasterKey, groupId);
} }
public GroupInfoV2 getGroupOrPartialMigrate( public GroupInfoV2 getGroupOrPartialMigrate(final GroupMasterKey groupMasterKey, final GroupIdV2 groupId) {
final GroupMasterKey groupMasterKey, final GroupIdV2 groupId
) {
try (final var connection = database.getConnection()) { try (final var connection = database.getConnection()) {
return getGroupOrPartialMigrate(connection, groupMasterKey, groupId); return getGroupOrPartialMigrate(connection, groupMasterKey, groupId);
} catch (SQLException e) { } catch (SQLException e) {
@ -265,9 +271,11 @@ public class GroupStore {
} }
private GroupInfoV2 getGroupOrPartialMigrate( private GroupInfoV2 getGroupOrPartialMigrate(
Connection connection, final GroupMasterKey groupMasterKey, final GroupIdV2 groupId Connection connection,
final GroupMasterKey groupMasterKey,
final GroupIdV2 groupId
) throws SQLException { ) throws SQLException {
switch (getGroup(groupId)) { switch (getGroup(connection, (GroupId) groupId)) {
case GroupInfoV1 groupInfoV1 -> { case GroupInfoV1 groupInfoV1 -> {
// Received a v2 group message for a v1 group, we need to locally migrate the group // Received a v2 group message for a v1 group, we need to locally migrate the group
deleteGroup(connection, groupInfoV1.getGroupId()); deleteGroup(connection, groupInfoV1.getGroupId());
@ -321,7 +329,9 @@ public class GroupStore {
} }
public void mergeRecipients( public void mergeRecipients(
final Connection connection, final RecipientId recipientId, final RecipientId toBeMergedRecipientId final Connection connection,
final RecipientId recipientId,
final RecipientId toBeMergedRecipientId
) throws SQLException { ) throws SQLException {
final var sql = ( final var sql = (
""" """
@ -356,7 +366,9 @@ public class GroupStore {
} }
public void updateStorageIds( 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 { ) throws SQLException {
final var sql = ( final var sql = (
""" """
@ -381,9 +393,7 @@ public class GroupStore {
} }
} }
public void updateStorageId( public void updateStorageId(Connection connection, GroupId groupId, StorageId storageId) throws SQLException {
Connection connection, GroupId groupId, StorageId storageId
) throws SQLException {
final var sqlV1 = ( final var sqlV1 = (
""" """
UPDATE %s UPDATE %s
@ -456,7 +466,9 @@ public class GroupStore {
} }
private void insertOrReplaceGroup( private void insertOrReplaceGroup(
final Connection connection, Long internalId, final GroupInfo group final Connection connection,
Long internalId,
final GroupInfo group
) throws SQLException { ) throws SQLException {
if (group instanceof GroupInfoV1 groupV1) { if (group instanceof GroupInfoV1 groupV1) {
if (internalId != null) { if (internalId != null) {
@ -733,7 +745,7 @@ public class GroupStore {
final var expirationTime = resultSet.getInt("expiration_time"); final var expirationTime = resultSet.getInt("expiration_time");
final var blocked = resultSet.getBoolean("blocked"); final var blocked = resultSet.getBoolean("blocked");
final var archived = resultSet.getBoolean("archived"); final var archived = resultSet.getBoolean("archived");
final var storagRecord = resultSet.getBytes("storage_record"); final var storageRecord = resultSet.getBytes("storage_record");
return new GroupInfoV1(GroupId.v1(groupId), return new GroupInfoV1(GroupId.v1(groupId),
groupIdV2 == null ? null : GroupId.v2(groupIdV2), groupIdV2 == null ? null : GroupId.v2(groupIdV2),
name, name,
@ -742,7 +754,7 @@ public class GroupStore {
expirationTime, expirationTime,
blocked, blocked,
archived, archived,
storagRecord); storageRecord);
} }
private GroupInfoV2 getGroupV2ByV1Id(final Connection connection, final GroupIdV1 groupId) throws SQLException { private GroupInfoV2 getGroupV2ByV1Id(final Connection connection, final GroupIdV1 groupId) throws SQLException {

View file

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

View file

@ -11,9 +11,7 @@ public class IdentityInfo {
private final TrustLevel trustLevel; private final TrustLevel trustLevel;
private final long addedTimestamp; private final long addedTimestamp;
IdentityInfo( IdentityInfo(final String address, IdentityKey identityKey, TrustLevel trustLevel, long addedTimestamp) {
final String address, IdentityKey identityKey, TrustLevel trustLevel, long addedTimestamp
) {
this.address = address; this.address = address;
this.identityKey = identityKey; this.identityKey = identityKey;
this.trustLevel = trustLevel; 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.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.state.IdentityKeyStore.Direction; import org.signal.libsignal.protocol.state.IdentityKeyStore.Direction;
import org.signal.libsignal.protocol.state.IdentityKeyStore.IdentityChange;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId;
@ -49,7 +50,9 @@ public class IdentityKeyStore {
} }
public IdentityKeyStore( public IdentityKeyStore(
final Database database, final TrustNewIdentity trustNewIdentity, RecipientStore recipientStore final Database database,
final TrustNewIdentity trustNewIdentity,
RecipientStore recipientStore
) { ) {
this.database = database; this.database = database;
this.trustNewIdentity = trustNewIdentity; this.trustNewIdentity = trustNewIdentity;
@ -60,19 +63,21 @@ public class IdentityKeyStore {
return identityChanges; return identityChanges;
} }
public boolean saveIdentity(final ServiceId serviceId, final IdentityKey identityKey) { public IdentityChange saveIdentity(final ServiceId serviceId, final IdentityKey identityKey) {
return saveIdentity(serviceId.toString(), identityKey); return saveIdentity(serviceId.toString(), identityKey);
} }
public boolean saveIdentity( public IdentityChange saveIdentity(
final Connection connection, final ServiceId serviceId, final IdentityKey identityKey final Connection connection,
final ServiceId serviceId,
final IdentityKey identityKey
) throws SQLException { ) throws SQLException {
return saveIdentity(connection, serviceId.toString(), identityKey); return saveIdentity(connection, serviceId.toString(), identityKey);
} }
boolean saveIdentity(final String address, final IdentityKey identityKey) { IdentityChange saveIdentity(final String address, final IdentityKey identityKey) {
if (isRetryingDecryption) { if (isRetryingDecryption) {
return false; return IdentityChange.NEW_OR_UNCHANGED;
} }
try (final var connection = database.getConnection()) { try (final var connection = database.getConnection()) {
return saveIdentity(connection, address, identityKey); return saveIdentity(connection, address, identityKey);
@ -81,18 +86,24 @@ public class IdentityKeyStore {
} }
} }
private boolean saveIdentity( private IdentityChange saveIdentity(
final Connection connection, final String address, final IdentityKey identityKey final Connection connection,
final String address,
final IdentityKey identityKey
) throws SQLException { ) throws SQLException {
final var identityInfo = loadIdentity(connection, address); final var identityInfo = loadIdentity(connection, address);
if (identityInfo != null && identityInfo.getIdentityKey().equals(identityKey)) { if (identityInfo == null) {
saveNewIdentity(connection, address, identityKey, true);
return IdentityChange.NEW_OR_UNCHANGED;
}
if (identityInfo.getIdentityKey().equals(identityKey)) {
// Identity already exists, not updating the trust level // Identity already exists, not updating the trust level
logger.trace("Not storing new identity for recipient {}, identity already stored", address); logger.trace("Not storing new identity for recipient {}, identity already stored", address);
return false; return IdentityChange.NEW_OR_UNCHANGED;
} }
saveNewIdentity(connection, address, identityKey, identityInfo == null); saveNewIdentity(connection, address, identityKey, false);
return true; return IdentityChange.REPLACED_EXISTING;
} }
public void setRetryingDecryption(final boolean retryingDecryption) { public void setRetryingDecryption(final boolean retryingDecryption) {
@ -230,9 +241,7 @@ public class IdentityKeyStore {
logger.debug("Complete identities migration took {}ms", (System.nanoTime() - start) / 1000000); logger.debug("Complete identities migration took {}ms", (System.nanoTime() - start) / 1000000);
} }
private IdentityInfo loadIdentity( private IdentityInfo loadIdentity(final Connection connection, final String address) throws SQLException {
final Connection connection, final String address
) throws SQLException {
final var sql = ( final var sql = (
""" """
SELECT i.address, i.identity_key, i.added_timestamp, i.trust_level 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+)"); static final Pattern identityFileNamePattern = Pattern.compile("(\\d+)");
private static List<IdentityInfo> getIdentities( 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(); final var files = identitiesPath.listFiles();
if (files == null) { if (files == null) {
@ -66,7 +68,9 @@ public class LegacyIdentityKeyStore {
} }
private static IdentityInfo loadIdentityLocked( 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); final var file = getIdentityFile(recipientId, identitiesPath);
if (!file.exists()) { if (!file.exists()) {

View file

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

View file

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

View file

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

View file

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

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