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

View file

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

View file

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

View file

@ -5,8 +5,8 @@
<option name="GENERATE_FINAL_LOCALS" value="true" />
<option name="GENERATE_FINAL_PARAMETERS" value="true" />
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="50" />
<option name="NAMES_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="99" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="com" withSubpackages="true" static="false" />

View file

@ -2,6 +2,232 @@
## [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
**Attention**: Now requires Java 21 and libsignal-client version 0.39.2
@ -471,7 +697,7 @@
- Improve exit code for message sending.
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).
- 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
- Enable more security options for systemd service file
- Rename sandbox to staging environment, to match the upstream name.
@ -612,7 +838,8 @@
### Added
- 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 `--delete` parameter for `quitGroup`, to delete the local group data
- 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/).
It supports registering, verifying, sending and receiving messages.
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.
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)) .
For the JSON-RPC interface there's also a simple [example client](https://github.com/AsamK/signal-cli/tree/master/client), written in Rust.
signal-cli needs to be kept up-to-date to keep up with Signal-Server changes.
The official Signal clients expire after three months and then the Signal-Server can make incompatible changes.
So signal-cli releases older than three months may not work correctly.
## Installation
You can [build signal-cli](#building) yourself or use
@ -55,8 +59,15 @@ of all country codes.)
signal-cli -a ACCOUNT register
You can register Signal using a landline number. In this case you can skip SMS verification process and jump directly
to the voice call verification by adding the `--voice` switch at the end of above register command.
You can register Signal using a landline number. In this case, you need to follow the procedure below:
* Attempt a SMS verification process first (`signal-cli -a ACCOUNT register`)
* You will get an error `400 (InvalidTransportModeException)`, this is normal
* Wait 60 seconds
* Attempt a voice call verification by adding the `--voice` switch and wait for the call:
```sh
signal-cli -a ACCOUNT register --voice
```
Registering may require solving a CAPTCHA
challenge: [Registration with captcha](https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha)
@ -72,6 +83,12 @@ of all country codes.)
signal-cli -a ACCOUNT send -m "This is a message" RECIPIENT
```
* Send a message to a username, usernames need to be prefixed with `u:`
```sh
signal-cli -a ACCOUNT send -m "This is a message" u:USERNAME.000
```
* Pipe the message content from another process.
uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT
@ -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
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:
./gradlew nativeCompile

View file

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

View file

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

View file

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

1327
client/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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":"[[B"
},
{
"name":"com.sun.security.auth.module.UnixSystem",
"fields":[{"name":"gid"}, {"name":"groups"}, {"name":"uid"}, {"name":"username"}]
@ -12,12 +18,19 @@
},
{
"name":"java.lang.Class",
"methods":[{"name":"getCanonicalName","parameterTypes":[] }]
"methods":[{"name":"getCanonicalName","parameterTypes":[] }, {"name":"getClassLoader","parameterTypes":[] }]
},
{
"name":"java.lang.ClassLoader",
"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",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
@ -26,24 +39,46 @@
"name":"java.lang.IllegalStateException",
"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.String"
},
{
"name":"java.lang.Thread",
"methods":[{"name":"currentThread","parameterTypes":[] }, {"name":"getStackTrace","parameterTypes":[] }]
},
{
"name":"java.lang.Throwable",
"methods":[{"name":"getMessage","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }]
"methods":[{"name":"getMessage","parameterTypes":[] }, {"name":"setStackTrace","parameterTypes":["java.lang.StackTraceElement[]"] }, {"name":"toString","parameterTypes":[] }]
},
{
"name":"java.lang.UnsatisfiedLinkError",
"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",
"methods":[{"name":"<init>","parameterTypes":["long","long"] }, {"name":"getLeastSignificantBits","parameterTypes":[] }, {"name":"getMostSignificantBits","parameterTypes":[] }]
},
{
"name":"jdk.internal.loader.ClassLoaders$AppClassLoader"
},
{
"name":"jdk.internal.loader.ClassLoaders$PlatformClassLoader"
},
@ -59,6 +94,42 @@
"name":"org.graalvm.jniutils.JNIExceptionWrapperEntryPoints",
"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",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
@ -136,6 +207,9 @@
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore$Direction",
"fields":[{"name":"RECEIVING"}, {"name":"SENDING"}]
},
{
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore$IdentityChange"
},
{
"name":"org.signal.libsignal.protocol.state.KyberPreKeyRecord",
"fields":[{"name":"unsafeHandle"}]
@ -165,6 +239,10 @@
{
"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",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
@ -173,6 +251,10 @@
"name":"org.signal.libsignal.usernames.CannotBeEmptyException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.usernames.DiscriminatorCannotBeZeroException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.usernames.MissingSeparatorException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]

View file

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

View file

@ -39,6 +39,48 @@
{
"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;"
},
@ -103,6 +145,13 @@
"name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"com.squareup.wire.Message",
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
},
{
"name":"com.squareup.wire.ProtoAdapter"
},
{
"name":"com.squareup.wire.internal.ImmutableList",
"allDeclaredFields":true,
@ -161,7 +210,7 @@
"name":"com.zaxxer.hikari.HikariConfig",
"allDeclaredFields":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",
@ -188,9 +237,14 @@
{
"name":"java.io.FilePermission"
},
{
"name":"java.io.OutputStream"
},
{
"name":"java.io.Serializable",
"allDeclaredMethods":true
"allDeclaredFields":true,
"allDeclaredMethods":true,
"allDeclaredClasses":true
},
{
"name":"java.lang.Boolean",
@ -203,6 +257,9 @@
"name":"java.lang.Class",
"methods":[{"name":"getPermittedSubclasses","parameterTypes":[] }, {"name":"getRecordComponents","parameterTypes":[] }, {"name":"isRecord","parameterTypes":[] }, {"name":"isSealed","parameterTypes":[] }]
},
{
"name":"java.lang.ClassValue"
},
{
"name":"java.lang.Comparable",
"allDeclaredMethods":true
@ -245,7 +302,8 @@
"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",
@ -367,6 +425,11 @@
"allDeclaredFields":true,
"allDeclaredMethods":true
},
{
"name":"java.util.AbstractMap",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"java.util.ArrayList",
"allDeclaredMethods":true,
@ -381,6 +444,45 @@
"allDeclaredMethods":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",
"allDeclaredMethods":true,
@ -395,7 +497,8 @@
"methods":[{"name":"getUnicodeLocaleType","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.util.Map"
"name":"java.util.Map",
"queryAllDeclaredMethods":true
},
{
"name":"java.util.Optional",
@ -436,6 +539,10 @@
"name":"java.util.concurrent.atomic.Striped64",
"fields":[{"name":"base"}, {"name":"cellsBusy"}]
},
{
"name":"java.util.concurrent.atomic.Striped64$Cell",
"fields":[{"name":"value"}]
},
{
"name":"javax.security.auth.x500.X500Principal",
"methods":[{"name":"<init>","parameterTypes":["sun.security.x509.X500Name"] }]
@ -509,6 +616,9 @@
{
"name":"kotlin.String"
},
{
"name":"kotlin.Unit"
},
{
"name":"kotlin.collections.AbstractCollection",
"allDeclaredFields":true,
@ -561,6 +671,13 @@
{
"name":"long[]"
},
{
"name":"okhttp3.internal.connection.RealConnectionPool",
"fields":[{"name":"addressStates"}]
},
{
"name":"okio.BufferedSink"
},
{
"name":"okio.ByteString"
},
@ -568,7 +685,7 @@
"name":"org.asamk.Signal",
"allDeclaredMethods":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",
@ -647,7 +764,9 @@
},
{
"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",
@ -689,7 +808,7 @@
"allDeclaredFields":true,
"allDeclaredMethods":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",
@ -789,6 +908,34 @@
"queryAllDeclaredConstructors":true,
"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",
"allDeclaredFields":true,
@ -845,6 +992,27 @@
"queryAllDeclaredConstructors":true,
"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",
"allDeclaredFields":true,
@ -871,7 +1039,7 @@
"allDeclaredFields":true,
"allDeclaredMethods":true,
"allDeclaredConstructors":true,
"methods":[{"name":"display","parameterTypes":[] }, {"name":"family","parameterTypes":[] }, {"name":"given","parameterTypes":[] }, {"name":"middle","parameterTypes":[] }, {"name":"prefix","parameterTypes":[] }, {"name":"suffix","parameterTypes":[] }]
"methods":[{"name":"display","parameterTypes":[] }, {"name":"family","parameterTypes":[] }, {"name":"given","parameterTypes":[] }, {"name":"middle","parameterTypes":[] }, {"name":"nickname","parameterTypes":[] }, {"name":"prefix","parameterTypes":[] }, {"name":"suffix","parameterTypes":[] }]
},
{
"name":"org.asamk.signal.json.JsonContactPhone",
@ -906,7 +1074,7 @@
"allDeclaredFields":true,
"allDeclaredMethods":true,
"allDeclaredConstructors":true,
"methods":[{"name":"groupId","parameterTypes":[] }, {"name":"type","parameterTypes":[] }]
"methods":[{"name":"groupId","parameterTypes":[] }, {"name":"groupName","parameterTypes":[] }, {"name":"revision","parameterTypes":[] }, {"name":"type","parameterTypes":[] }]
},
{
"name":"org.asamk.signal.json.JsonMention",
@ -920,7 +1088,7 @@
"allDeclaredFields":true,
"allDeclaredMethods":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",
@ -1128,7 +1296,7 @@
"allDeclaredFields":true,
"queryAllDeclaredMethods":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",
@ -1245,6 +1413,12 @@
"name":"org.asamk.signal.manager.storage.profiles.LegacyProfileStore$ProfileStoreDeserializer",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.asamk.signal.manager.storage.profiles.LegacySignalProfile",
"allDeclaredFields":true,
"allDeclaredMethods":true,
"allDeclaredConstructors":true
},
{
"name":"org.asamk.signal.manager.storage.profiles.LegacySignalProfileEntry",
"allDeclaredFields":true,
@ -1380,6 +1554,14 @@
"name":"org.bouncycastle.jcajce.provider.asymmetric.COMPOSITE$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.CONTEXT$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.CompositeSignatures$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.DH$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
@ -1436,14 +1618,30 @@
"name":"org.bouncycastle.jcajce.provider.asymmetric.LMS$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.MLDSA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.MLKEM$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.NTRU$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.NoSig$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.SLHDSA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.SPHINCSPlus$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
@ -1799,6 +1997,10 @@
"name":"org.bouncycastle.pqc.jcajce.provider.XMSS$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.freedesktop.dbus.connections.base.GlobalHandler",
"queryAllDeclaredMethods":true
},
{
"name":"org.freedesktop.dbus.errors.ServiceUnknown",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
@ -1831,7 +2033,7 @@
"name":"org.freedesktop.dbus.interfaces.Properties",
"allDeclaredMethods":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",
@ -1852,7 +2054,10 @@
"name":"org.signal.libsignal.protocol.IdentityKey"
},
{
"name":"org.signal.libsignal.protocol.ServiceId"
"name":"org.signal.libsignal.protocol.ServiceId",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{
"name":"org.signal.libsignal.protocol.SignalProtocolAddress"
@ -2096,6 +2301,9 @@
"name":"org.signal.storageservice.protos.groups.local.DecryptedTimer",
"fields":[{"name":"duration_"}]
},
{
"name":"org.slf4j.Logger"
},
{
"name":"org.sqlite.JDBC"
},
@ -2111,7 +2319,7 @@
"allDeclaredFields":true,
"allDeclaredMethods":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",
@ -2138,6 +2346,20 @@
{
"name":"org.whispersystems.signalservice.api.groupsv2.TemporalCredential[]"
},
{
"name":"org.whispersystems.signalservice.api.keys.OneTimePreKeyCounts",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","int","kotlin.jvm.internal.DefaultConstructorMarker"] }]
},
{
"name":"org.whispersystems.signalservice.api.messages.calls.HangupMessage",
"allDeclaredFields":true,
@ -2198,14 +2420,21 @@
"name":"org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite",
"allDeclaredFields":true,
"allDeclaredMethods":true,
"allDeclaredConstructors":true
"allDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","byte[]","byte[]","byte[]","byte[]","byte[]","boolean","boolean","byte[]","java.util.List"] }, {"name":"getAbout","parameterTypes":[] }, {"name":"getAboutEmoji","parameterTypes":[] }, {"name":"getAvatar","parameterTypes":[] }, {"name":"getBadgeIds","parameterTypes":[] }, {"name":"getCommitment","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPaymentAddress","parameterTypes":[] }, {"name":"getPhoneNumberSharing","parameterTypes":[] }, {"name":"getSameAvatar","parameterTypes":[] }, {"name":"getVersion","parameterTypes":[] }]
},
{
"name":"org.whispersystems.signalservice.api.provisioning.ProvisioningMessage",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{
"name":"org.whispersystems.signalservice.api.push.ServiceId",
"allDeclaredFields":true,
"queryAllDeclaredMethods":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"
@ -2215,7 +2444,7 @@
"allDeclaredFields":true,
"queryAllDeclaredMethods":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"
@ -2243,6 +2472,12 @@
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String"] }]
},
{
"name":"org.whispersystems.signalservice.api.ratelimit.SubmitRecaptchaChallengePayload",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true
},
{
"name":"org.whispersystems.signalservice.api.storage.StorageAuthResponse",
"allDeclaredFields":true,
@ -2250,6 +2485,13 @@
"allDeclaredConstructors":true,
"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",
"allDeclaredFields":true,
@ -2350,6 +2592,13 @@
"name":"org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse",
"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",
"allDeclaredFields":true,
@ -2364,6 +2613,10 @@
"allDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.whispersystems.signalservice.internal.push.ByteArrayDeserializerBase64",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.whispersystems.signalservice.internal.push.CdsiAuthResponse",
"allDeclaredFields":true,
@ -2475,6 +2728,7 @@
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"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,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","java.lang.String","java.lang.String","org.whispersystems.signalservice.internal.push.WhoAmIResponse$Entitlements"] }, {"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","java.lang.String","java.lang.String","org.whispersystems.signalservice.internal.push.WhoAmIResponse$Entitlements","int","kotlin.jvm.internal.DefaultConstructorMarker"] }]
},
{
"name":"org.whispersystems.signalservice.internal.push.WhoAmIResponse$BackupEntitlement",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.lang.Long","java.lang.Long"] }]
},
{
"name":"org.whispersystems.signalservice.internal.push.WhoAmIResponse$BadgeEntitlement",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.Boolean","java.lang.Long"] }, {"name":"<init>","parameterTypes":["java.lang.String","java.lang.Boolean","java.lang.Long","int","kotlin.jvm.internal.DefaultConstructorMarker"] }]
},
{
"name":"org.whispersystems.signalservice.internal.push.WhoAmIResponse$Entitlements",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.util.List","org.whispersystems.signalservice.internal.push.WhoAmIResponse$BackupEntitlement"] }, {"name":"<init>","parameterTypes":["java.util.List","org.whispersystems.signalservice.internal.push.WhoAmIResponse$BackupEntitlement","int","kotlin.jvm.internal.DefaultConstructorMarker"] }]
},
{
"name":"org.whispersystems.signalservice.internal.serialize.protos.AddressProto",
@ -2726,7 +3001,26 @@
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord",
"allDeclaredFields":true
"allDeclaredFields":true,
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$BackupTierHistory"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$Builder"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$Companion"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$IAPSubscriberData"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$NotificationProfileManualOverride"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$PhoneNumberSharingMode"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$PinnedConversation",
@ -2740,17 +3034,53 @@
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$UsernameLink",
"allDeclaredFields":true
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.AvatarColor"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord",
"allDeclaredFields":true,
"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
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record",
"allDeclaredFields":true
"allDeclaredFields":true,
"fields":[{"name":"archived"}, {"name":"blocked"}, {"name":"id"}, {"name":"markedUnread"}, {"name":"mutedUntilTimestamp"}, {"name":"whitelisted"}],
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record$Builder"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record$Companion"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record",
"allDeclaredFields":true
"allDeclaredFields":true,
"fields":[{"name":"archived"}, {"name":"avatarColor"}, {"name":"blocked"}, {"name":"dontNotifyForMentionsIfMuted"}, {"name":"hideStory"}, {"name":"markedUnread"}, {"name":"masterKey"}, {"name":"mutedUntilTimestamp"}, {"name":"storySendMode"}, {"name":"whitelisted"}],
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record$Builder"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record$Companion"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record$StorySendMode"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord",
@ -2760,6 +3090,9 @@
"name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord$Identifier",
"fields":[{"name":"raw_"}, {"name":"type_"}]
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.OptionalBool"
},
{
"name":"org.whispersystems.signalservice.internal.storage.protos.Payments",
"allDeclaredFields":true

View file

@ -180,10 +180,14 @@
"pattern":"\\Qkotlin/ranges/ranges.kotlin_builtins\\E"
}, {
"pattern":"\\Qkotlin/reflect/reflect.kotlin_builtins\\E"
}, {
"pattern":"\\Qlibsignal_jni.dylib\\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"
}, {
@ -194,6 +198,8 @@
"pattern":"\\Qorg/sqlite/native/Linux/x86_64/libsqlitejdbc.so\\E"
}, {
"pattern":"\\Qsignal_jni.dll\\E"
}, {
"pattern":"\\Qsignal_jni_amd64.dll\\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
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
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

14
gradlew vendored
View file

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

6
gradlew.bat vendored
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 familyName,
String nickName,
String nickNameGivenName,
String nickNameFamilyName,
String note,
String color,
int messageExpirationTime,
int messageExpirationTimeVersion,
long muteUntil,
boolean hideStory,
boolean isBlocked,
@ -21,8 +25,12 @@ public record Contact(
this(builder.givenName,
builder.familyName,
builder.nickName,
builder.nickNameGivenName,
builder.nickNameFamilyName,
builder.note,
builder.color,
builder.messageExpirationTime,
builder.messageExpirationTimeVersion,
builder.muteUntil,
builder.hideStory,
builder.isBlocked,
@ -41,8 +49,12 @@ public record Contact(
builder.givenName = copy.givenName();
builder.familyName = copy.familyName();
builder.nickName = copy.nickName();
builder.nickNameGivenName = copy.nickNameGivenName();
builder.nickNameFamilyName = copy.nickNameFamilyName();
builder.note = copy.note();
builder.color = copy.color();
builder.messageExpirationTime = copy.messageExpirationTime();
builder.messageExpirationTimeVersion = copy.messageExpirationTimeVersion();
builder.muteUntil = copy.muteUntil();
builder.hideStory = copy.hideStory();
builder.isBlocked = copy.isBlocked();
@ -73,8 +85,12 @@ public record Contact(
private String givenName;
private String familyName;
private String nickName;
private String nickNameGivenName;
private String nickNameFamilyName;
private String note;
private String color;
private int messageExpirationTime;
private int messageExpirationTimeVersion = 1;
private long muteUntil;
private boolean hideStory;
private boolean isBlocked;
@ -105,6 +121,21 @@ public record Contact(
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) {
color = val;
return this;
@ -115,6 +146,11 @@ public record Contact(
return this;
}
public Builder withMessageExpirationTimeVersion(final int val) {
messageExpirationTimeVersion = val;
return this;
}
public Builder withMuteUntil(final long val) {
muteUntil = val;
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.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import java.net.URI;
@ -37,7 +36,7 @@ public record DeviceLinkUrl(String deviceIdentifier, ECPublicKey deviceKey) {
}
ECPublicKey deviceKey;
try {
deviceKey = Curve.decodePoint(publicKeyBytes, 0);
deviceKey = new ECPublicKey(publicKeyBytes);
} catch (InvalidKeyException e) {
throw new InvalidDeviceLinkException("Invalid device link", e);
}

View file

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

View file

@ -1,17 +1,10 @@
package org.asamk.signal.manager.api;
import org.signal.libsignal.protocol.IdentityKey;
public record Identity(
RecipientAddress recipient,
IdentityKey identityKey,
byte[] fingerprint,
String safetyNumber,
byte[] scannableSafetyNumber,
TrustLevel trustLevel,
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 InvalidNumberException(String message) {
super(message);
}
InvalidNumberException(String message, Throwable e) {
super(message, e);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
package org.asamk.signal.manager.api;
import org.asamk.signal.manager.util.PhoneNumberFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.UUID;
@ -24,31 +24,37 @@ public sealed interface RecipientIdentifier {
sealed interface Single extends RecipientIdentifier {
static Single fromString(String identifier, String localNumber) throws InvalidNumberException {
try {
if (UuidUtil.isUuid(identifier)) {
return new Uuid(UUID.fromString(identifier));
}
if (identifier.startsWith("u:")) {
return new Username(identifier.substring(2));
}
final var normalizedNumber = PhoneNumberFormatter.formatNumber(identifier, localNumber);
if (!normalizedNumber.equals(identifier)) {
final Logger logger = LoggerFactory.getLogger(RecipientIdentifier.class);
logger.debug("Normalized number {} to {}.", identifier, normalizedNumber);
}
return new Number(normalizedNumber);
} catch (org.whispersystems.signalservice.api.util.InvalidNumberException e) {
throw new InvalidNumberException(e.getMessage(), e);
if (UuidUtil.isUuid(identifier)) {
return new Uuid(UUID.fromString(identifier));
}
if (identifier.startsWith("PNI:")) {
final var pni = identifier.substring(4);
if (!UuidUtil.isUuid(pni)) {
throw new InvalidNumberException("Invalid PNI");
}
return new Pni(UUID.fromString(pni));
}
if (identifier.startsWith("u:")) {
return new Username(identifier.substring(2));
}
final var normalizedNumber = PhoneNumberFormatter.formatNumber(identifier, localNumber);
if (!normalizedNumber.equals(identifier)) {
final Logger logger = LoggerFactory.getLogger(RecipientIdentifier.class);
logger.debug("Normalized number {} to {}.", identifier, normalizedNumber);
}
return new Number(normalizedNumber);
}
static Single fromAddress(RecipientAddress address) {
if (address.number().isPresent()) {
return new Number(address.number().get());
} else if (address.uuid().isPresent()) {
return new Uuid(address.uuid().get());
} else if (address.aci().isPresent()) {
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()) {
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 {
@Override
@ -80,7 +99,7 @@ public sealed interface RecipientIdentifier {
@Override
public RecipientAddress toPartialRecipientAddress() {
return new RecipientAddress(null, number);
return new RecipientAddress(number);
}
}
@ -93,7 +112,7 @@ public sealed interface RecipientIdentifier {
@Override
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;
import org.signal.libsignal.net.Network.Environment;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.HttpProxy;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
@ -27,12 +28,14 @@ class LiveConfig {
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
.decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF");
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
private static final String SVR2_MRENCLAVE = "a6622ad4656e1abcd0bc0ff17c229477747d2ded0495c4ebee7ed35c1789fa97";
private static final String SVR2_MRENCLAVE_DEPRECATED = "6ee1042f9e20f880326686dd4ba50c25359f01e9f733eeba4382bca001d45094";
private static final String SVR2_MRENCLAVE_LEGACY_LEGACY = "9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570";
private static final String SVR2_MRENCLAVE_LEGACY = "093be9ea32405e85ae28dbb48eb668aebeb7dbe29517b9b86ad4bec4dfe0e6a6";
private static final String SVR2_MRENCLAVE = "29cd63c87bea751e3bfd0fbd401279192e2e5c99948b4ee9437eafc4968355fb";
private static final String URL = "https://chat.signal.org";
private static final String CDN_URL = "https://cdn.signal.org";
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 SIGNAL_CDSI_URL = "https://cdsi.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<SignalProxy> proxy = Optional.empty();
private static final Optional<HttpProxy> systemProxy = Optional.empty();
private static final byte[] zkGroupServerPublicParams = Base64.getDecoder()
.decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/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()
.decode("AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN");
private static final byte[] backupServerPublicParams = Base64.getDecoder()
.decode("AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O");
private static final Environment LIBSIGNAL_NET_ENV = Environment.PRODUCTION;
static SignalServiceConfiguration createDefaultServiceConfiguration(
final List<Interceptor> interceptors
) {
@ -56,21 +62,25 @@ class LiveConfig {
Map.of(0,
new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)},
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 SignalCdsiUrl[]{new SignalCdsiUrl(SIGNAL_CDSI_URL, TRUST_STORE)},
new SignalSvr2Url[]{new SignalSvr2Url(SIGNAL_SVR2_URL, TRUST_STORE, null, null)},
interceptors,
dns,
proxy,
systemProxy,
zkGroupServerPublicParams,
genericServerPublicParams,
backupServerPublicParams);
backupServerPublicParams,
false);
}
static ECPublicKey getUnidentifiedSenderTrustRoot() {
try {
return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0);
return new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
@ -78,10 +88,11 @@ class LiveConfig {
static ServiceEnvironmentConfig getServiceEnvironmentConfig(List<Interceptor> interceptors) {
return new ServiceEnvironmentConfig(LIVE,
LIBSIGNAL_NET_ENV,
createDefaultServiceConfiguration(interceptors),
getUnidentifiedSenderTrustRoot(),
CDSI_MRENCLAVE,
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_DEPRECATED));
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_LEGACY_LEGACY));
}
private LiveConfig() {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -114,7 +114,7 @@ public class Context implements AutoCloseable {
}
PinHelper getPinHelper() {
return getOrCreate(() -> pinHelper, () -> pinHelper = new PinHelper(dependencies.getSecureValueRecoveryV2()));
return getOrCreate(() -> pinHelper, () -> pinHelper = new PinHelper(dependencies.getSecureValueRecovery()));
}
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.profiles.ProfileKey;
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.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
import org.slf4j.Logger;
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.ReceivedGroupSendEndorsements;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
@ -79,6 +82,16 @@ public class GroupHelper {
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) {
var group = getGroup(groupId);
return group != null && group.isBlocked();
@ -100,11 +113,14 @@ public class GroupHelper {
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(
final GroupMasterKey groupMasterKey, final int revision, final byte[] signedGroupChange
final GroupMasterKey groupMasterKey,
final int revision,
final byte[] signedGroupChange
) {
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
@ -127,9 +143,10 @@ public class GroupHelper {
}
if (group == null) {
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);
}
} catch (NotAGroupMemberException ignored) {
@ -150,8 +167,41 @@ public class GroupHelper {
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(
String name, Set<RecipientId> members, String avatarFile
String name,
Set<RecipientId> members,
String avatarFile
) throws IOException, AttachmentInvalidException {
final var selfRecipientId = account.getSelfRecipientId();
if (members != null && members.contains(selfRecipientId)) {
@ -175,7 +225,7 @@ public class GroupHelper {
final var gv2 = gv2Pair.first();
final var decryptedGroup = gv2Pair.second();
gv2.setGroup(decryptedGroup);
gv2.setGroup(handleDecryptedGroupResponse(gv2, decryptedGroup));
gv2.setProfileSharingEnabled(true);
if (avatarBytes != null) {
context.getAvatarStore()
@ -271,7 +321,7 @@ public class GroupHelper {
var group = getGroupForUpdating(groupId);
if (group instanceof GroupInfoV2 groupInfoV2) {
Pair<DecryptedGroup, GroupChange> groupChangePair;
Pair<DecryptedGroup, GroupChangeResponse> groupChangePair;
try {
groupChangePair = context.getGroupV2Helper().updateSelfProfileKey(groupInfoV2);
} catch (ConflictException e) {
@ -280,7 +330,9 @@ public class GroupHelper {
groupChangePair = context.getGroupV2Helper().updateSelfProfileKey(groupInfoV2);
}
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) {
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);
final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(),
groupJoinInfo.revision + 1,
groupChange.encode());
changeResponse.groupChange == null ? null : changeResponse.groupChange.encode());
final var groupChange = handleGroupChangeResponse(group, changeResponse);
if (group.getGroup() == null) {
// Only requested member, can't send update to group members
@ -316,7 +369,8 @@ public class GroupHelper {
}
public SendGroupMessageResults quitGroup(
final GroupId groupId, final Set<RecipientId> newAdmins
final GroupId groupId,
final Set<RecipientId> newAdmins
) throws IOException, LastGroupAdminException, NotAGroupMemberException, GroupNotFoundException {
var group = getGroupForUpdating(groupId);
if (group instanceof GroupInfoV1) {
@ -349,9 +403,7 @@ public class GroupHelper {
context.getJobExecutor().enqueueJob(new SyncStorageJob());
}
public SendGroupMessageResults sendGroupInfoRequest(
GroupIdV1 groupId, RecipientId recipientId
) throws IOException {
public SendGroupMessageResults sendGroupInfoRequest(GroupIdV1 groupId, RecipientId recipientId) throws IOException {
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize());
var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build());
@ -361,7 +413,8 @@ public class GroupHelper {
}
public SendGroupMessageResults sendGroupInfoMessage(
GroupIdV1 groupId, RecipientId recipientId
GroupIdV1 groupId,
RecipientId recipientId
) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
GroupInfoV1 g;
var group = getGroupForUpdating(groupId);
@ -382,34 +435,46 @@ public class GroupHelper {
private GroupInfo getGroup(GroupId groupId, boolean forceUpdate) {
final var group = account.getGroupStore().getGroup(groupId);
if (group instanceof GroupInfoV2 groupInfoV2) {
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);
}
}
fillOrUpdateGroup(group, forceUpdate);
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) {
try {
context.getAvatarStore()
@ -421,7 +486,9 @@ public class GroupHelper {
}
private void retrieveGroupV2Avatar(
GroupSecretParams groupSecretParams, String cdnKey, OutputStream outputStream
GroupSecretParams groupSecretParams,
String cdnKey,
OutputStream outputStream
) throws IOException {
var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
@ -478,15 +545,19 @@ public class GroupHelper {
) throws NotAGroupMemberException {
final var revisionWeWereAdded = context.getGroupV2Helper().findRevisionWeWereAdded(newDecryptedGroup);
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);
final var newProfileKeys = new HashMap<RecipientId, ProfileKey>();
while (true) {
final var page = context.getGroupV2Helper().getDecryptedGroupHistoryPage(groupSecretParams, fromRevision);
page.getResults()
final var page = context.getGroupV2Helper()
.getDecryptedGroupHistoryPage(groupSecretParams, fromRevision, sendEndorsementsExpirationMs);
if (page == null) {
break;
}
page.getChangeLogs()
.stream()
.map(DecryptedGroupHistoryEntry::getChange)
.filter(Optional::isPresent)
.map(Optional::get)
.map(DecryptedGroupChangeLog::getChange)
.filter(Objects::nonNull)
.map(context.getGroupV2Helper()::getAuthoritativeProfileKeyFromChange)
.filter(Objects::nonNull)
.forEach(p -> {
@ -495,13 +566,16 @@ public class GroupHelper {
final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId);
newProfileKeys.put(recipientId, profileKey);
});
if (!page.getPagingData().hasMorePages()) {
if (!page.getPagingData().getHasMorePages()) {
break;
}
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 {
@ -520,7 +594,10 @@ public class GroupHelper {
}
private SendGroupMessageResults updateGroupV1(
final GroupInfoV1 gv1, final String name, final Set<RecipientId> members, final byte[] avatarFile
final GroupInfoV1 gv1,
final String name,
final Set<RecipientId> members,
final byte[] avatarFile
) throws IOException, AttachmentInvalidException {
updateGroupV1Details(gv1, name, members, avatarFile);
@ -533,7 +610,10 @@ public class GroupHelper {
}
private void updateGroupV1Details(
final GroupInfoV1 g, final String name, final Collection<RecipientId> members, final byte[] avatarFile
final GroupInfoV1 g,
final String name,
final Collection<RecipientId> members,
final byte[] avatarFile
) throws IOException {
if (name != null) {
g.name = name;
@ -552,7 +632,8 @@ public class GroupHelper {
* Change the expiration timer for a group
*/
private void setExpirationTimer(
GroupInfoV1 groupInfoV1, int messageExpirationTimer
GroupInfoV1 groupInfoV1,
int messageExpirationTimer
) throws NotAGroupMemberException, GroupNotFoundException, IOException, GroupSendingNotAllowedException {
groupInfoV1.messageExpirationTime = messageExpirationTimer;
account.getGroupStore().updateGroup(groupInfoV1);
@ -586,7 +667,9 @@ public class GroupHelper {
final var groupV2Helper = context.getGroupV2Helper();
if (group.isPendingMember(account.getSelfRecipientId())) {
var groupGroupChangePair = groupV2Helper.acceptInvite(group);
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
handleGroupChangeResponse(group, groupGroupChangePair.second()));
}
if (members != null) {
@ -594,14 +677,18 @@ public class GroupHelper {
requestingMembers.retainAll(group.getRequestingMembers());
if (!requestingMembers.isEmpty()) {
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);
newMembers.removeAll(group.getMembers());
newMembers.removeAll(group.getRequestingMembers());
if (!newMembers.isEmpty()) {
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
if (!existingRemoveMembers.isEmpty()) {
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);
pendingRemoveMembers.retainAll(group.getPendingMembers());
if (!pendingRemoveMembers.isEmpty()) {
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);
requestingRemoveMembers.retainAll(group.getRequestingMembers());
if (!requestingRemoveMembers.isEmpty()) {
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);
result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
groupGroupChangePair.second());
handleGroupChangeResponse(group, groupGroupChangePair.second()));
}
}
}
@ -656,7 +749,7 @@ public class GroupHelper {
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false);
result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
groupGroupChangePair.second());
handleGroupChangeResponse(group, groupGroupChangePair.second()));
}
}
}
@ -666,7 +759,9 @@ public class GroupHelper {
newlyBannedMembers.removeAll(group.getBannedMembers());
if (!newlyBannedMembers.isEmpty()) {
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());
if (!existingUnbanMembers.isEmpty()) {
var groupGroupChangePair = groupV2Helper.unbanMembers(group, existingUnbanMembers);
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
handleGroupChangeResponse(group, groupGroupChangePair.second()));
}
}
if (resetGroupLink) {
var groupGroupChangePair = groupV2Helper.resetGroupLinkPassword(group);
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
result = sendUpdateGroupV2Message(group,
groupGroupChangePair.first(),
handleGroupChangeResponse(group, groupGroupChangePair.second()));
}
if (groupLinkState != null) {
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) {
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) {
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) {
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) {
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) {
@ -715,7 +824,9 @@ public class GroupHelper {
context.getAvatarStore()
.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;
@ -735,7 +846,8 @@ public class GroupHelper {
}
private SendGroupMessageResults quitGroupV2(
final GroupInfoV2 groupInfoV2, final Set<RecipientId> newAdmins
final GroupInfoV2 groupInfoV2,
final Set<RecipientId> newAdmins
) throws LastGroupAdminException, IOException {
final var currentAdmins = groupInfoV2.getAdminMembers();
newAdmins.removeAll(currentAdmins);
@ -751,7 +863,8 @@ public class GroupHelper {
groupInfoV2.setGroup(groupGroupChangePair.first());
account.getGroupStore().updateGroup(groupInfoV2);
var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().encode());
var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2,
handleGroupChangeResponse(groupInfoV2, groupGroupChangePair.second()).encode());
return sendGroupMessage(messageBuilder,
groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId()),
groupInfoV2.getDistributionId());
@ -788,7 +901,9 @@ public class GroupHelper {
}
private SendGroupMessageResults sendUpdateGroupV2Message(
GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
GroupInfoV2 group,
DecryptedGroup newDecryptedGroup,
GroupChange groupChange
) throws IOException {
final var selfRecipientId = account.getSelfRecipientId();
final var members = group.getMembersIncludingPendingWithout(selfRecipientId);

View file

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

View file

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

View file

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

View file

@ -21,9 +21,7 @@ public class PinHelper {
this.secureValueRecoveries = secureValueRecoveries;
}
public void setRegistrationLockPin(
String pin, MasterKey masterKey
) throws IOException {
public void setRegistrationLockPin(String pin, MasterKey masterKey) throws IOException {
IOException exception = null;
for (final var secureValueRecovery : secureValueRecoveries) {
try {
@ -32,11 +30,11 @@ public class PinHelper {
case SecureValueRecovery.BackupResponse.Success success -> {
}
case SecureValueRecovery.BackupResponse.ServerRejected serverRejected ->
logger.warn("Backup svr2 failed: ServerRejected");
logger.warn("Backup svr failed: ServerRejected");
case SecureValueRecovery.BackupResponse.EnclaveNotFound enclaveNotFound ->
logger.warn("Backup svr2 failed: EnclaveNotFound");
logger.warn("Backup svr failed: EnclaveNotFound");
case SecureValueRecovery.BackupResponse.ExposeFailure exposeFailure ->
logger.warn("Backup svr2 failed: ExposeFailure");
logger.warn("Backup svr failed: ExposeFailure");
case SecureValueRecovery.BackupResponse.ApplicationError error ->
throw new IOException(error.getException());
case SecureValueRecovery.BackupResponse.NetworkError error -> throw error.getException();
@ -82,14 +80,19 @@ public class PinHelper {
}
public SecureValueRecovery.RestoreResponse.Success getRegistrationLockData(
String pin, LockedException lockedException
String pin,
LockedException lockedException
) throws IOException, IncorrectPinException {
var svr2Credentials = lockedException.getSvr2Credentials();
if (svr2Credentials != null) {
IOException exception = null;
for (final var secureValueRecovery : secureValueRecoveries) {
try {
return getRegistrationLockData(secureValueRecovery, svr2Credentials, pin);
final var lockData = getRegistrationLockData(secureValueRecovery, svr2Credentials, pin);
if (lockData == null) {
continue;
}
return lockData;
} catch (IOException e) {
exception = e;
}
@ -103,9 +106,11 @@ public class PinHelper {
}
public SecureValueRecovery.RestoreResponse.Success getRegistrationLockData(
SecureValueRecovery secureValueRecovery, AuthCredentials authCredentials, String pin
SecureValueRecovery secureValueRecovery,
AuthCredentials authCredentials,
String pin
) throws IOException, IncorrectPinException {
final var restoreResponse = secureValueRecovery.restoreDataPreRegistration(authCredentials, pin);
final var restoreResponse = secureValueRecovery.restoreDataPreRegistration(authCredentials, null, pin);
switch (restoreResponse) {
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.NetworkResultUtil;
import org.whispersystems.signalservice.api.account.PreKeyUpload;
import org.whispersystems.signalservice.api.keys.OneTimePreKeyCounts;
import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.internal.push.OneTimePreKeyCounts;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import java.io.IOException;
import java.util.List;
import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_STALE_AGE;
import static org.asamk.signal.manager.config.ServiceConfig.SIGNED_PREKEY_ROTATE_AGE;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class PreKeyHelper {
@ -29,9 +32,7 @@ public class PreKeyHelper {
private final SignalAccount account;
private final SignalDependencies dependencies;
public PreKeyHelper(
final SignalAccount account, final SignalDependencies dependencies
) {
public PreKeyHelper(final SignalAccount account, final SignalDependencies dependencies) {
this.account = account;
this.dependencies = dependencies;
}
@ -78,11 +79,12 @@ public class PreKeyHelper {
}
private boolean refreshPreKeysIfNecessary(
final ServiceIdType serviceIdType, final IdentityKeyPair identityKeyPair
final ServiceIdType serviceIdType,
final IdentityKeyPair identityKeyPair
) throws IOException {
OneTimePreKeyCounts preKeyCounts;
try {
preKeyCounts = dependencies.getAccountManager().getPreKeyCounts(serviceIdType);
preKeyCounts = handleResponseException(dependencies.getKeysApi().getAvailablePreKeyCounts(serviceIdType));
} catch (AuthorizationFailedException e) {
logger.debug("Failed to get pre key count, ignoring: " + e.getClass().getSimpleName());
preKeyCounts = new OneTimePreKeyCounts(0, 0);
@ -143,7 +145,7 @@ public class PreKeyHelper {
kyberPreKeyRecords);
var needsReset = false;
try {
dependencies.getAccountManager().setPreKeys(preKeyUpload);
NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeys(preKeyUpload));
try {
if (preKeyRecords != null) {
account.addPreKeys(serviceIdType, preKeyRecords);
@ -171,6 +173,11 @@ public class PreKeyHelper {
} catch (AuthorizationFailedException e) {
// This can happen when the primary device has changed phone number
logger.warn("Failed to updated pre keys: {}", e.getMessage());
} catch (NonSuccessfulResponseCodeException e) {
if (serviceIdType != ServiceIdType.PNI || e.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;
}
@ -215,7 +222,8 @@ public class PreKeyHelper {
}
private List<KyberPreKeyRecord> generateKyberPreKeys(
ServiceIdType serviceIdType, final IdentityKeyPair identityKeyPair
ServiceIdType serviceIdType,
final IdentityKeyPair identityKeyPair
) {
final var accountData = account.getAccountData(serviceIdType);
final var offset = accountData.getPreKeyMetadata().getNextKyberPreKeyId();
@ -240,7 +248,9 @@ public class PreKeyHelper {
}
private KyberPreKeyRecord generateLastResortKyberPreKey(
ServiceIdType serviceIdType, IdentityKeyPair identityKeyPair, final int offset
ServiceIdType serviceIdType,
IdentityKeyPair identityKeyPair,
final int offset
) {
final var accountData = account.getAccountData(serviceIdType);
final var signedPreKeyId = accountData.getPreKeyMetadata().getNextKyberPreKeyId() + offset;

View file

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

View file

@ -11,10 +11,10 @@ import org.asamk.signal.manager.storage.messageCache.CachedMessage;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalWebSocket;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException;
@ -28,7 +28,6 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class ReceiveHelper {
@ -41,7 +40,6 @@ public class ReceiveHelper {
private final Context context;
private ReceiveConfig receiveConfig = new ReceiveConfig(false, false, false);
private boolean needsToRetryFailedMessages = false;
private boolean hasCaughtUpWithOldMessages = false;
private boolean isWaitingForMessage = false;
private boolean shouldStop = false;
@ -59,10 +57,6 @@ public class ReceiveHelper {
dependencies.setAllowStories(!receiveConfig.ignoreStories());
}
public void setNeedsToRetryFailedMessages(final boolean needsToRetryFailedMessages) {
this.needsToRetryFailedMessages = needsToRetryFailedMessages;
}
public void setAuthenticationFailureListener(final Callable authenticationFailureListener) {
this.authenticationFailureListener = authenticationFailureListener;
}
@ -88,22 +82,25 @@ public class ReceiveHelper {
}
public void receiveMessages(
Duration timeout, boolean returnOnTimeout, Integer maxMessages, Manager.ReceiveMessageHandler handler
Duration timeout,
boolean returnOnTimeout,
Integer maxMessages,
Manager.ReceiveMessageHandler handler
) throws IOException {
needsToRetryFailedMessages = true;
account.setNeedsToRetryFailedMessages(true);
hasCaughtUpWithOldMessages = false;
// Use a Map here because java Set doesn't have a get method ...
Map<HandleAction, HandleAction> queuedActions = new HashMap<>();
final var signalWebSocket = dependencies.getSignalWebSocket();
final var webSocketStateDisposable = Observable.merge(signalWebSocket.getUnidentifiedWebSocketState(),
signalWebSocket.getWebSocketState())
final var signalWebSocket = dependencies.getAuthenticatedSignalWebSocket();
final var webSocketStateDisposable = signalWebSocket.getState()
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation())
.distinctUntilChanged()
.subscribe(this::onWebSocketStateChange);
signalWebSocket.connect();
signalWebSocket.registerKeepAliveToken("receive");
try {
receiveMessagesInternal(signalWebSocket, timeout, returnOnTimeout, maxMessages, handler, queuedActions);
@ -111,6 +108,7 @@ public class ReceiveHelper {
hasCaughtUpWithOldMessages = false;
handleQueuedActions(queuedActions.keySet());
queuedActions.clear();
signalWebSocket.removeKeepAliveToken("receive");
signalWebSocket.disconnect();
webSocketStateDisposable.dispose();
shouldStop = false;
@ -118,7 +116,7 @@ public class ReceiveHelper {
}
private void receiveMessagesInternal(
final SignalWebSocket signalWebSocket,
final SignalWebSocket.AuthenticatedWebSocket signalWebSocket,
Duration timeout,
boolean returnOnTimeout,
Integer maxMessages,
@ -130,9 +128,8 @@ public class ReceiveHelper {
isWaitingForMessage = false;
while (!shouldStop && remainingMessages != 0) {
if (needsToRetryFailedMessages) {
if (account.getNeedsToRetryFailedMessages()) {
retryFailedReceivedMessages(handler);
needsToRetryFailedMessages = false;
}
SignalServiceEnvelope envelope;
final CachedMessage[] cachedMessage = {null};
@ -235,9 +232,9 @@ public class ReceiveHelper {
if (exception instanceof UntrustedIdentityException) {
logger.debug("Keeping message with untrusted identity in message cache");
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()
.resolveRecipient(ACI.from(address.uuid().get()));
.resolveRecipient(ACI.parseOrThrow(address.aci().get()));
try {
cachedMessage[0] = account.getMessageCache()
.replaceSender(cachedMessage[0], recipientId);
@ -266,10 +263,12 @@ public class ReceiveHelper {
}
}
handleQueuedActions(queuedActions);
account.setNeedsToRetryFailedMessages(false);
}
private List<HandleAction> retryFailedReceivedMessage(
final Manager.ReceiveMessageHandler handler, final CachedMessage cachedMessage
final Manager.ReceiveMessageHandler handler,
final CachedMessage cachedMessage
) {
var envelope = cachedMessage.loadEnvelope();
if (envelope == null) {
@ -282,8 +281,8 @@ public class ReceiveHelper {
final var exception = result.second();
if (exception instanceof UntrustedIdentityException) {
if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 30) {
// Envelope is more than a month old, cleaning up.
if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 14) {
// Envelope is more than two weeks old, cleaning up.
cachedMessage.delete();
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.UnregisteredRecipientException;
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.storage.SignalAccount;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.cds.CdsiV2Service;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException;
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
import org.whispersystems.signalservice.api.services.CdsiV2Service;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import java.io.IOException;
import java.util.Collection;
@ -25,8 +26,10 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import static org.asamk.signal.manager.config.ServiceConfig.MAXIMUM_ONE_OFF_REQUEST_SIZE;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class RecipientHelper {
@ -34,12 +37,10 @@ public class RecipientHelper {
private final SignalAccount account;
private final SignalDependencies dependencies;
private final ServiceEnvironmentConfig serviceEnvironmentConfig;
public RecipientHelper(final Context context) {
this.account = context.getAccount();
this.dependencies = context.getDependencies();
this.serviceEnvironmentConfig = dependencies.getServiceEnvironmentConfig();
}
public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) {
@ -68,7 +69,7 @@ public class RecipientHelper {
.toSignalServiceAddress();
}
public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException {
public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException, IOException {
final var recipientIds = new HashSet<RecipientId>(recipients.size());
for (var number : recipients) {
final var recipientId = resolveRecipient(number);
@ -78,10 +79,11 @@ public class RecipientHelper {
}
public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) {
return account.getRecipientResolver().resolveRecipient(ACI.from(uuidRecipient.uuid()));
} else if (recipient instanceof RecipientIdentifier.Number numberRecipient) {
final var number = numberRecipient.number();
if (recipient instanceof RecipientIdentifier.Uuid(UUID uuid)) {
return account.getRecipientResolver().resolveRecipient(ACI.from(uuid));
} else if (recipient instanceof RecipientIdentifier.Pni(UUID pni)) {
return account.getRecipientResolver().resolveRecipient(PNI.from(pni));
} else if (recipient instanceof RecipientIdentifier.Number(String number)) {
return account.getRecipientStore().resolveRecipientByNumber(number, () -> {
try {
return getRegisteredUserByNumber(number);
@ -89,38 +91,70 @@ public class RecipientHelper {
return null;
}
});
} else if (recipient instanceof RecipientIdentifier.Username usernameRecipient) {
var username = usernameRecipient.username();
} else if (recipient instanceof RecipientIdentifier.Username(String username)) {
try {
UsernameLinkUrl usernameLinkUrl = UsernameLinkUrl.fromUri(username);
final var components = usernameLinkUrl.getComponents();
final var encryptedUsername = dependencies.getAccountManager()
.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);
return resolveRecipientByUsernameOrLink(username, false);
} catch (Exception e) {
return null;
}
final String finalUsername = username;
return account.getRecipientStore().resolveRecipientByUsername(finalUsername, () -> {
try {
return getRegisteredUserByUsername(finalUsername);
} catch (Exception e) {
return null;
}
});
}
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) {
try {
return Optional.of(resolveRecipient(recipient));
} catch (UnregisteredRecipientException e) {
if (recipient instanceof RecipientIdentifier.Number r) {
return account.getRecipientStore().resolveRecipientByNumberOptional(r.number());
if (recipient instanceof RecipientIdentifier.Number(String number)) {
return account.getRecipientStore().resolveRecipientByNumberOptional(number);
} else {
return Optional.empty();
}
@ -156,7 +190,8 @@ public class RecipientHelper {
}
private Map<String, RegisteredUser> getRegisteredUsers(
final Set<String> numbers, final boolean isPartialRefresh
final Set<String> numbers,
final boolean isPartialRefresh
) throws IOException {
Map<String, RegisteredUser> registeredUsers = getRegisteredUsersV2(numbers, isPartialRefresh);
@ -166,7 +201,8 @@ public class RecipientHelper {
final var unregisteredUsers = new HashSet<>(numbers);
unregisteredUsers.removeAll(registeredUsers.keySet());
account.getRecipientStore().markUnregistered(unregisteredUsers);
account.getRecipientStore().markUndiscoverablePossiblyUnregistered(unregisteredUsers);
account.getRecipientStore().markDiscoverable(registeredUsers.keySet());
return registeredUsers;
}
@ -176,17 +212,18 @@ public class RecipientHelper {
try {
aciMap = getRegisteredUsers(Set.of(number), true);
} 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);
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();
}
private Map<String, RegisteredUser> getRegisteredUsersV2(
final Set<String> numbers, boolean isPartialRefresh
final Set<String> numbers,
boolean isPartialRefresh
) throws IOException {
final var previousNumbers = isPartialRefresh ? Set.<String>of() : account.getCdsiStore().getAllNumbers();
final var newNumbers = new HashSet<>(numbers) {{
@ -206,13 +243,13 @@ public class RecipientHelper {
final CdsiV2Service.Response response;
try {
response = dependencies.getAccountManager()
.getRegisteredUsersWithCdsi(previousNumbers,
response = handleResponseException(dependencies.getCdsApi()
.getRegisteredUsers(token.isEmpty() ? Set.of() : previousNumbers,
newNumbers,
account.getRecipientStore().getServiceIdToProfileKeyMap(),
token,
serviceEnvironmentConfig.cdsiMrenclave(),
null,
dependencies.getLibSignalNetwork(),
newToken -> {
if (isPartialRefresh) {
account.getCdsiStore().updateAfterPartialCdsQuery(newNumbers);
@ -228,8 +265,8 @@ public class RecipientHelper {
account.setCdsiToken(newToken);
account.setLastRecipientsRefresh(System.currentTimeMillis());
}
});
} catch (CdsiInvalidTokenException e) {
}));
} catch (CdsiInvalidTokenException | CdsiInvalidArgumentException e) {
account.setCdsiToken(null);
account.getCdsiStore().clearAll();
throw e;
@ -245,10 +282,6 @@ public class RecipientHelper {
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 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.recipients.RecipientId;
import org.asamk.signal.manager.storage.sendLog.MessageSendLogEntry;
import org.jetbrains.annotations.Nullable;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.InvalidRegistrationIdException;
import org.signal.libsignal.protocol.NoSessionException;
@ -22,9 +23,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
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.UnidentifiedAccessPair;
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.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage;
@ -84,8 +86,8 @@ public class SendHelper {
account.getContactStore().storeContact(recipientId, contact);
}
final var expirationTime = contact.messageExpirationTime();
messageBuilder.withExpiration(expirationTime);
messageBuilder.withExpiration(contact.messageExpirationTime());
messageBuilder.withExpireTimerVersion(contact.messageExpirationTimeVersion());
if (!contact.isBlocked()) {
final var profileKey = account.getProfileKey().serialize();
@ -123,7 +125,8 @@ public class SendHelper {
}
public SendMessageResult sendReceiptMessage(
final SignalServiceReceiptMessage receiptMessage, final RecipientId recipientId
final SignalServiceReceiptMessage receiptMessage,
final RecipientId recipientId
) {
final var messageSendLogStore = account.getMessageSendLogStore();
final var result = handleSendMessage(recipientId,
@ -155,7 +158,9 @@ public class SendHelper {
}
public SendMessageResult sendRetryReceipt(
DecryptionErrorMessage errorMessage, RecipientId recipientId, Optional<GroupId> groupId
DecryptionErrorMessage errorMessage,
RecipientId recipientId,
Optional<GroupId> groupId
) {
logger.debug("Sending retry receipt for {} to {}, device: {}",
errorMessage.getTimestamp(),
@ -181,12 +186,13 @@ public class SendHelper {
}
public SendMessageResult sendSelfMessage(
SignalServiceDataMessage.Builder messageBuilder, Optional<Long> editTargetTimestamp
SignalServiceDataMessage.Builder messageBuilder,
Optional<Long> editTargetTimestamp
) {
final var recipientId = account.getSelfRecipientId();
final var contact = account.getContactStore().getContact(recipientId);
final var expirationTime = contact != null ? contact.messageExpirationTime() : 0;
messageBuilder.withExpiration(expirationTime);
messageBuilder.withExpiration(contact != null ? contact.messageExpirationTime() : 0);
messageBuilder.withExpireTimerVersion(contact != null ? contact.messageExpirationTimeVersion() : 1);
var message = messageBuilder.build();
return sendSelfMessage(message, editTargetTimestamp);
@ -199,31 +205,20 @@ public class SendHelper {
return SendMessageResult.success(account.getSelfAddress(), List.of(), false, false, 0, Optional.empty());
}
try {
return messageSender.sendSyncMessage(message, context.getUnidentifiedAccessHelper().getAccessForSync());
} catch (UnregisteredUserException e) {
return messageSender.sendSyncMessage(message);
} catch (Throwable e) {
var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
return SendMessageResult.unregisteredFailure(address);
} catch (ProofRequiredException e) {
var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
return SendMessageResult.proofRequiredFailure(address, e);
} catch (RateLimitException e) {
var address = context.getRecipientHelper().resolveSignalServiceAddress(account.getSelfRecipientId());
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);
try {
return SignalServiceMessageSender.mapSendErrorToSendResult(e, System.currentTimeMillis(), address);
} catch (IOException ex) {
logger.warn("Failed to send message due to IO exception: {}", e.getMessage());
logger.debug("Exception", e);
return SendMessageResult.networkFailure(address);
}
}
}
public SendMessageResult sendTypingMessage(
SignalServiceTypingMessage message, RecipientId recipientId
) {
public SendMessageResult sendTypingMessage(SignalServiceTypingMessage message, RecipientId recipientId) {
final var result = handleSendMessage(recipientId,
(messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendTyping(List.of(
address), List.of(unidentifiedAccess), message, null).getFirst());
@ -232,7 +227,8 @@ public class SendHelper {
}
public List<SendMessageResult> sendGroupTypingMessage(
SignalServiceTypingMessage message, GroupId groupId
SignalServiceTypingMessage message,
GroupId groupId
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
final var g = getGroupForSending(groupId);
if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
@ -245,7 +241,9 @@ public class SendHelper {
}
public SendMessageResult resendMessage(
final RecipientId recipientId, final long timestamp, final MessageSendLogEntry messageSendLogEntry
final RecipientId recipientId,
final long timestamp,
final MessageSendLogEntry messageSendLogEntry
) {
logger.trace("Resending message {} to {}", timestamp, recipientId);
if (messageSendLogEntry.groupId().isEmpty()) {
@ -261,7 +259,7 @@ public class SendHelper {
}
final var groupId = messageSendLogEntry.groupId().get();
final var group = account.getGroupStore().getGroup(groupId);
final var group = context.getGroupHelper().getGroup(groupId);
if (group == null) {
logger.debug("Could not find a matching group for the groupId {}! Skipping message send.",
@ -380,10 +378,11 @@ public class SendHelper {
() -> false,
urgent,
editTargetTimestamp.get());
final SenderKeySenderHandler senderKeySender = (distId, recipients, unidentifiedAccess, isRecipientUpdate) -> messageSender.sendGroupDataMessage(
final SenderKeySenderHandler senderKeySender = (distId, recipients, unidentifiedAccess, groupSendEndorsements, isRecipientUpdate) -> messageSender.sendGroupDataMessage(
distId,
recipients,
unidentifiedAccess,
groupSendEndorsements,
isRecipientUpdate,
contentHint,
message,
@ -436,9 +435,11 @@ public class SendHelper {
unidentifiedAccess,
message,
() -> false),
(distId, recipients, unidentifiedAccess, isRecipientUpdate) -> messageSender.sendGroupTyping(distId,
(distId, recipients, unidentifiedAccess, groupSendEndorsements, isRecipientUpdate) -> messageSender.sendGroupTyping(
distId,
recipients,
unidentifiedAccess,
groupSendEndorsements,
message),
recipientIds,
distributionId);
@ -523,23 +524,11 @@ public class SendHelper {
}
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 recipientList = new ArrayList<>(recipientIds);
final var profiles = context.getProfileHelper().getRecipientProfiles(recipientList).iterator();
for (final var recipientId : recipientList) {
final var profile = profiles.next();
if (profile == null || !profile.getCapabilities().contains(Profile.Capability.senderKey)) {
continue;
}
final var access = context.getUnidentifiedAccessHelper().getAccessFor(recipientId);
if (access.isEmpty() || access.get().getTargetUnidentifiedAccess().isEmpty()) {
final var access = context.getUnidentifiedAccessHelper().getSealedSenderAccessFor(recipientId);
if (access == null) {
continue;
}
@ -568,13 +557,16 @@ public class SendHelper {
}
private List<SendMessageResult> sendGroupMessageInternalWithLegacy(
final LegacySenderHandler sender, final Set<RecipientId> recipientIds, final boolean isRecipientUpdate
final LegacySenderHandler sender,
final Set<RecipientId> recipientIds,
final boolean isRecipientUpdate
) throws IOException {
final var recipientIdList = new ArrayList<>(recipientIds);
final var addresses = recipientIdList.stream()
.map(context.getRecipientHelper()::resolveSignalServiceAddress)
.toList();
final var unidentifiedAccesses = context.getUnidentifiedAccessHelper().getAccessFor(recipientIdList);
final var unidentifiedAccesses = context.getUnidentifiedAccessHelper()
.getSealedSenderAccessFor(recipientIdList);
try {
final var results = sender.send(addresses, unidentifiedAccesses, isRecipientUpdate);
@ -613,15 +605,14 @@ public class SendHelper {
List<UnidentifiedAccess> unidentifiedAccesses = context.getUnidentifiedAccessHelper()
.getAccessFor(recipientIdList)
.stream()
.map(Optional::get)
.map(UnidentifiedAccessPair::getTargetUnidentifiedAccess)
.map(Optional::get)
.toList();
final GroupSendEndorsements groupSendEndorsements = null;//TODO
try {
List<SendMessageResult> results = sender.send(distributionId,
addresses,
unidentifiedAccesses,
groupSendEndorsements,
isRecipientUpdate);
final var successCount = results.stream().filter(SendMessageResult::isSuccess).count();
@ -660,7 +651,9 @@ public class SendHelper {
}
private SendMessageResult sendMessage(
SignalServiceDataMessage message, RecipientId recipientId, Optional<Long> editTargetTimestamp
SignalServiceDataMessage message,
RecipientId recipientId,
Optional<Long> editTargetTimestamp
) {
final var messageSendLogStore = account.getMessageSendLogStore();
final var urgent = true;
@ -696,7 +689,7 @@ public class SendHelper {
try {
return s.send(messageSender,
address,
context.getUnidentifiedAccessHelper().getAccessFor(recipientId),
context.getUnidentifiedAccessHelper().getSealedSenderAccessFor(recipientId),
includePniSignature);
} catch (UnregisteredUserException e) {
final RecipientId newRecipientId;
@ -708,22 +701,17 @@ public class SendHelper {
address = context.getRecipientHelper().resolveSignalServiceAddress(newRecipientId);
return s.send(messageSender,
address,
context.getUnidentifiedAccessHelper().getAccessFor(newRecipientId),
context.getUnidentifiedAccessHelper().getSealedSenderAccessFor(newRecipientId),
includePniSignature);
}
} catch (UnregisteredUserException e) {
return SendMessageResult.unregisteredFailure(address);
} catch (ProofRequiredException e) {
return SendMessageResult.proofRequiredFailure(address, e);
} catch (RateLimitException e) {
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) {
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);
} catch (Throwable e) {
try {
return SignalServiceMessageSender.mapSendErrorToSendResult(e, System.currentTimeMillis(), address);
} catch (IOException ex) {
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(
SignalServiceMessageSender messageSender,
SignalServiceAddress address,
Optional<UnidentifiedAccessPair> unidentifiedAccess,
@Nullable SealedSenderAccess unidentifiedAccess,
boolean includePniSignature
) throws IOException, UnregisteredUserException, ProofRequiredException, RateLimitException, org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
}
@ -795,6 +783,7 @@ public class SendHelper {
DistributionId distributionId,
List<SignalServiceAddress> recipients,
List<UnidentifiedAccess> unidentifiedAccess,
GroupSendEndorsements groupSendEndorsements,
boolean isRecipientUpdate
) throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException, InvalidRegistrationIdException;
}
@ -803,7 +792,7 @@ public class SendHelper {
List<SendMessageResult> send(
List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
List<SealedSenderAccess> unidentifiedAccess,
boolean isRecipientUpdate
) throws IOException, UntrustedIdentityException;
}

View file

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

View file

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

View file

@ -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.RecipientId;
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.MimeUtils;
import org.jetbrains.annotations.NotNull;
@ -18,11 +17,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
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.ConfigurationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
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.DeviceContactsOutputStream;
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.KeysMessage;
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.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
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.internal.push.SyncMessage;
@ -67,17 +70,12 @@ public class SyncHelper {
requestSyncData(SyncMessage.Request.Type.BLOCKED);
requestSyncData(SyncMessage.Request.Type.CONFIGURATION);
requestSyncKeys();
requestSyncPniIdentity();
}
public void requestSyncKeys() {
requestSyncData(SyncMessage.Request.Type.KEYS);
}
public void requestSyncPniIdentity() {
requestSyncData(SyncMessage.Request.Type.PNI_IDENTITY);
}
public SendMessageResult sendSyncFetchProfileMessage() {
return context.getSendHelper()
.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
@ -88,6 +86,22 @@ public class SyncHelper {
.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 {
var groupsFile = IOUtils.createTempFile();
@ -115,10 +129,12 @@ public class SyncHelper {
if (groupsFile.exists() && groupsFile.length() > 0) {
try (var groupsFileStream = new FileInputStream(groupsFile)) {
final var uploadSpec = context.getDependencies().getMessageSender().getResumableUploadSpec();
var attachmentStream = SignalServiceAttachment.newStreamBuilder()
.withStream(groupsFileStream)
.withContentType(MimeUtils.OCTET_STREAM)
.withLength(groupsFile.length())
.withResumableUploadSpec(uploadSpec)
.build();
context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
@ -144,7 +160,14 @@ public class SyncHelper {
final var contact = contactPair.second();
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) {
@ -152,16 +175,25 @@ public class SyncHelper {
final var address = account.getSelfRecipientAddress();
final var recipientId = account.getSelfRecipientId();
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) {
try (var contactsFileStream = new FileInputStream(contactsFile)) {
final var uploadSpec = context.getDependencies().getMessageSender().getResumableUploadSpec();
var attachmentStream = SignalServiceAttachment.newStreamBuilder()
.withStream(contactsFileStream)
.withContentType(MimeUtils.OCTET_STREAM)
.withLength(contactsFile.length())
.withResumableUploadSpec(uploadSpec)
.build();
context.getSendHelper()
@ -179,39 +211,25 @@ public class SyncHelper {
}
@NotNull
private DeviceContact getDeviceContact(
final RecipientAddress address, final RecipientId recipientId, final Contact contact
) throws IOException {
var currentIdentity = address.serviceId().isEmpty()
? null
: account.getIdentityKeyStore().getIdentityInfo(address.serviceId().get());
VerifiedMessage verifiedMessage = null;
if (currentIdentity != null) {
verifiedMessage = new VerifiedMessage(address.toSignalServiceAddress(),
currentIdentity.getIdentityKey(),
currentIdentity.getTrustLevel().toVerifiedState(),
currentIdentity.getDateAddedTimestamp());
}
var profileKey = account.getProfileStore().getProfileKey(recipientId);
private DeviceContact getDeviceContact(final RecipientAddress address, final Contact contact) throws IOException {
return new DeviceContact(address.aci(),
address.number(),
Optional.ofNullable(contact == null ? null : contact.getName()),
createContactAvatarAttachment(address),
Optional.ofNullable(contact == null ? null : contact.color()),
Optional.ofNullable(verifiedMessage),
Optional.ofNullable(profileKey),
contact != null && contact.isBlocked(),
Optional.ofNullable(contact == null ? null : contact.messageExpirationTime()),
Optional.empty(),
contact != null && contact.isArchived());
Optional.ofNullable(contact == null ? null : contact.messageExpirationTimeVersion()),
Optional.empty());
}
public SendMessageResult sendBlockedList() {
var addresses = new ArrayList<SignalServiceAddress>();
var addresses = new ArrayList<BlockedListMessage.Individual>();
for (var record : account.getContactStore().getContacts()) {
if (record.second().isBlocked()) {
addresses.add(context.getRecipientHelper().resolveSignalServiceAddress(record.first()));
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(record.first());
if (address.aci().isPresent() || address.number().isPresent()) {
addresses.add(new BlockedListMessage.Individual(address.aci().orElse(null),
address.number().orElse(null)));
}
}
}
var groupIds = new ArrayList<byte[]>();
@ -225,7 +243,9 @@ public class SyncHelper {
}
public SendMessageResult sendVerifiedMessage(
SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel
SignalServiceAddress destination,
IdentityKey identityKey,
TrustLevel trustLevel
) {
var verifiedMessage = new VerifiedMessage(destination,
identityKey,
@ -235,13 +255,16 @@ public class SyncHelper {
}
public SendMessageResult sendKeysMessage() {
var keysMessage = new KeysMessage(Optional.ofNullable(account.getOrCreateStorageKey()),
Optional.ofNullable(account.getOrCreatePinMasterKey()));
var keysMessage = new KeysMessage(account.getOrCreateStorageKey(),
account.getOrCreatePinMasterKey(),
account.getOrCreateAccountEntropyPool(),
account.getOrCreateMediaRootBackupKey());
return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage));
}
public SendMessageResult sendStickerOperationsMessage(
List<StickerPack> installStickers, List<StickerPack> removeStickers
List<StickerPack> installStickers,
List<StickerPack> removeStickers
) {
var installStickerMessages = installStickers.stream().map(s -> getStickerPackOperationMessage(s, true));
var removeStickerMessages = removeStickers.stream().map(s -> getStickerPackOperationMessage(s, false));
@ -251,7 +274,8 @@ public class SyncHelper {
}
private static StickerPackOperationMessage getStickerPackOperationMessage(
final StickerPack s, final boolean installed
final StickerPack s,
final boolean installed
) {
return new StickerPackOperationMessage(s.packId().serialize(),
s.packKey(),
@ -317,7 +341,7 @@ public class SyncHelper {
c = s.read();
} catch (IOException e) {
if (e.getMessage() != null && e.getMessage().contains("Missing contact address!")) {
logger.warn("Sync contacts contained invalid contact, ignoring: {}", e.getMessage());
logger.debug("Sync contacts contained invalid contact, ignoring: {}", e.getMessage());
continue;
} else {
throw e;
@ -327,9 +351,6 @@ public class SyncHelper {
break;
}
final var address = new RecipientAddress(c.getAci(), Optional.empty(), c.getE164(), Optional.empty());
if (address.matches(account.getSelfRecipientAddress()) && c.getProfileKey().isPresent()) {
account.setProfileKey(c.getProfileKey().get());
}
final var recipientId = account.getRecipientTrustedResolver().resolveRecipientTrusted(address);
var contact = account.getContactStore().getContact(recipientId);
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
@ -341,41 +362,36 @@ public class SyncHelper {
builder.withGivenName(c.getName().get());
builder.withFamilyName(null);
}
if (c.getColor().isPresent()) {
builder.withColor(c.getColor().get());
}
if (c.getProfileKey().isPresent()) {
account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get());
}
if (c.getVerified().isPresent()) {
final var verifiedMessage = c.getVerified().get();
account.getIdentityKeyStore()
.setIdentityTrustLevel(verifiedMessage.getDestination().getServiceId(),
verifiedMessage.getIdentityKey(),
TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
}
if (c.getExpirationTimer().isPresent()) {
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());
if (c.getAvatar().isPresent()) {
downloadContactAvatar(c.getAvatar().get(), address);
storeContactAvatar(c.getAvatar().get(), address);
}
}
}
public SendMessageResult sendMessageRequestResponse(
final MessageRequestResponse.Type type, final GroupId groupId
) {
public SendMessageResult sendMessageRequestResponse(final MessageRequestResponse.Type type, final GroupId groupId) {
final var response = MessageRequestResponseMessage.forGroup(groupId.serialize(), localToRemoteType(type));
return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response));
}
public SendMessageResult sendMessageRequestResponse(
final MessageRequestResponse.Type type, final RecipientId recipientId
final MessageRequestResponse.Type type,
final RecipientId recipientId
) {
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
if (address.serviceId().isEmpty()) {
@ -396,20 +412,22 @@ public class SyncHelper {
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);
if (streamDetails == null) {
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 {
context.getAvatarStore()
.storeContactAvatar(address,
outputStream -> context.getAttachmentHelper().retrieveAttachment(avatar, outputStream));
outputStream -> IOUtils.copyStream(avatar.getInputStream(), outputStream));
} catch (IOException e) {
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.storage.SignalAccount;
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.SenderCertificate;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class UnidentifiedAccessHelper {
private static final Logger logger = LoggerFactory.getLogger(UnidentifiedAccessHelper.class);
@ -42,63 +44,55 @@ public class UnidentifiedAccessHelper {
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();
}
public Optional<UnidentifiedAccessPair> getAccessFor(RecipientId recipient) {
private @Nullable UnidentifiedAccess getAccessFor(RecipientId recipient) {
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);
if (recipientUnidentifiedAccessKey == null) {
logger.trace("Unidentified access not available for {}", recipientId);
return Optional.empty();
return null;
}
var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(noRefresh);
if (selfUnidentifiedAccessKey == null) {
logger.trace("Unidentified access not available for self");
return Optional.empty();
return null;
}
var senderCertificate = getSenderCertificateFor(recipientId);
if (senderCertificate == null) {
logger.trace("Unidentified access not available due to missing sender certificate");
return Optional.empty();
return null;
}
try {
return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(recipientUnidentifiedAccessKey,
senderCertificate,
false), new UnidentifiedAccess(selfUnidentifiedAccessKey, senderCertificate, false)));
return new UnidentifiedAccess(recipientUnidentifiedAccessKey, senderCertificate, false);
} catch (InvalidCertificateException e) {
return Optional.empty();
}
}
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();
return null;
}
}
private byte[] getSenderCertificateFor(final RecipientId recipientId) {
final var sharingMode = account.getConfigurationStore().getPhoneNumberSharingMode();
if (sharingMode == null || sharingMode == PhoneNumberSharingMode.EVERYBODY || (
if (sharingMode == PhoneNumberSharingMode.EVERYBODY || (
sharingMode == PhoneNumberSharingMode.CONTACTS
&& account.getContactStore().getContact(recipientId) != null
)) {
@ -117,11 +111,12 @@ public class UnidentifiedAccessHelper {
return privacySenderCertificate.getSerialized();
}
try {
final var certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy();
final var certificate = handleResponseException(dependencies.getCertificateApi()
.getSenderCertificateForPhoneNumberPrivacy());
privacySenderCertificate = new SenderCertificate(certificate);
return certificate;
} catch (IOException | InvalidCertificateException e) {
logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage());
logger.warn("Failed to get sender certificate (pnp), ignoring: {}", e.getMessage());
return null;
}
}
@ -133,7 +128,7 @@ public class UnidentifiedAccessHelper {
return senderCertificate.getSerialized();
}
try {
final var certificate = dependencies.getAccountManager().getSenderCertificate();
final var certificate = handleResponseException(dependencies.getCertificateApi().getSenderCertificate());
this.senderCertificate = new SenderCertificate(certificate);
return certificate;
} catch (IOException | InvalidCertificateException e) {
@ -166,7 +161,8 @@ public class UnidentifiedAccessHelper {
}
private static byte[] getTargetUnidentifiedAccessKey(
final Profile targetProfile, final ProfileKey theirProfileKey
final Profile targetProfile,
final ProfileKey theirProfileKey
) {
return switch (targetProfile.getUnidentifiedAccessMode()) {
case ENABLED -> theirProfileKey == null ? null : UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);

View file

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

View file

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

View file

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

View file

@ -21,9 +21,11 @@ import org.asamk.signal.manager.RegistrationManager;
import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.UpdateProfile;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.helper.AccountFileUpdater;
@ -31,14 +33,11 @@ import org.asamk.signal.manager.helper.PinHelper;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.NumberVerificationUtils;
import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.usernames.BaseUsernameException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.account.PreKeyCollection;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
@ -48,12 +47,12 @@ import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedExcep
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import java.io.IOException;
import java.util.function.Consumer;
import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class RegistrationManagerImpl implements RegistrationManager {
@ -64,9 +63,8 @@ public class RegistrationManagerImpl implements RegistrationManager {
private final ServiceEnvironmentConfig serviceEnvironmentConfig;
private final String userAgent;
private final Consumer<Manager> newManagerListener;
private final GroupsV2Operations groupsV2Operations;
private final SignalServiceAccountManager accountManager;
private final SignalServiceAccountManager unauthenticatedAccountManager;
private final PinHelper pinHelper;
private final AccountFileUpdater accountFileUpdater;
@ -85,26 +83,30 @@ public class RegistrationManagerImpl implements RegistrationManager {
this.userAgent = userAgent;
this.newManagerListener = newManagerListener;
groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration()),
ServiceConfig.GROUP_MAX_SIZE);
this.accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.signalServiceConfiguration(),
new DynamicCredentialsProvider(
// Using empty UUID, because registering doesn't work otherwise
null, null, account.getNumber(), account.getPassword(), SignalServiceAddress.DEFAULT_DEVICE_ID),
this.unauthenticatedAccountManager = SignalServiceAccountManager.createWithStaticCredentials(
serviceEnvironmentConfig.signalServiceConfiguration(),
// Using empty UUID, because registering doesn't work otherwise
null,
null,
account.getNumber(),
SignalServiceAddress.DEFAULT_DEVICE_ID,
account.getPassword(),
userAgent,
groupsV2Operations,
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
final var secureValueRecoveryV2 = serviceEnvironmentConfig.svr2Mrenclaves()
ServiceConfig.AUTOMATIC_NETWORK_RETRY,
ServiceConfig.GROUP_MAX_SIZE);
final var secureValueRecovery = serviceEnvironmentConfig.svr2Mrenclaves()
.stream()
.map(mr -> (SecureValueRecovery) accountManager.getSecureValueRecoveryV2(mr))
.map(mr -> (SecureValueRecovery) this.unauthenticatedAccountManager.getSecureValueRecoveryV2(mr))
.toList();
this.pinHelper = new PinHelper(secureValueRecoveryV2);
this.pinHelper = new PinHelper(secureValueRecovery);
}
@Override
public void register(
boolean voiceVerification, String captcha
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException {
boolean voiceVerification,
String captcha,
final boolean forceRegister
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException {
if (account.isRegistered()
&& account.getServiceEnvironment() != null
&& account.getServiceEnvironment() != serviceEnvironmentConfig.type()) {
@ -112,21 +114,32 @@ public class RegistrationManagerImpl implements RegistrationManager {
}
try {
if (!forceRegister) {
if (account.isRegistered()) {
throw new IOException("Account is already registered");
}
if (account.getAci() != null && attemptReactivateAccount()) {
return;
}
}
final var recoveryPassword = account.getRecoveryPassword();
if (recoveryPassword != null && account.isPrimaryDevice() && attemptReregisterAccount(recoveryPassword)) {
return;
}
if (account.getAci() != null && attemptReactivateAccount()) {
return;
}
String sessionId = NumberVerificationUtils.handleVerificationSession(accountManager,
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
logger.trace("Creating verification session");
String sessionId = NumberVerificationUtils.handleVerificationSession(registrationApi,
account.getSessionId(account.getNumber()),
id -> account.setSessionId(account.getNumber(), id),
voiceVerification,
captcha);
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) {
logger.debug("Signal-Server returned deprecated version exception", e);
throw e;
@ -135,8 +148,9 @@ public class RegistrationManagerImpl implements RegistrationManager {
@Override
public void verifyAccount(
String verificationCode, String pin
) throws IOException, PinLockedException, IncorrectPinException {
String verificationCode,
String pin
) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException {
if (account.isRegistered()) {
throw new IOException("Account is already registered");
}
@ -185,7 +199,8 @@ public class RegistrationManagerImpl implements RegistrationManager {
final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI));
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,
account.getAccountAttributes(null),
aciPreKeys,
@ -207,12 +222,14 @@ public class RegistrationManagerImpl implements RegistrationManager {
private boolean attemptReactivateAccount() {
try {
final var accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.signalServiceConfiguration(),
account.getCredentialsProvider(),
final var dependencies = new SignalDependencies(serviceEnvironmentConfig,
userAgent,
groupsV2Operations,
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
accountManager.setAccountAttributes(account.getAccountAttributes(null));
account.getCredentialsProvider(),
account.getSignalServiceDataStore(),
null,
new ReentrantSignalSessionLock());
handleResponseException(dependencies.getAccountApi()
.setAccountAttributes(account.getAccountAttributes(null)));
account.setRegistered(true);
logger.info("Reactivated existing account, verify is not necessary.");
if (newManagerListener != null) {
@ -238,12 +255,13 @@ public class RegistrationManagerImpl implements RegistrationManager {
final PreKeyCollection aciPreKeys,
final PreKeyCollection pniPreKeys
) throws IOException {
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
try {
Utils.handleResponseException(accountManager.verifyAccount(verificationCode, sessionId));
handleResponseException(registrationApi.verifyAccount(sessionId, verificationCode));
} catch (AlreadyVerifiedException e) {
// Already verified so can continue registering
}
return Utils.handleResponseException(accountManager.registerAccount(sessionId,
return handleResponseException(registrationApi.registerAccount(sessionId,
null,
account.getAccountAttributes(registrationLock),
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.ServiceEnvironmentConfig;
import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.signal.libsignal.net.Network;
import org.signal.libsignal.protocol.UsePqRatchet;
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceDataStore;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.SignalWebSocket;
import org.whispersystems.signalservice.api.account.AccountApi;
import org.whispersystems.signalservice.api.attachment.AttachmentApi;
import org.whispersystems.signalservice.api.cds.CdsApi;
import org.whispersystems.signalservice.api.certificate.CertificateApi;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.keys.KeysApi;
import org.whispersystems.signalservice.api.link.LinkDeviceApi;
import org.whispersystems.signalservice.api.message.MessageApi;
import org.whispersystems.signalservice.api.profiles.ProfileApi;
import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi;
import org.whispersystems.signalservice.api.registration.RegistrationApi;
import org.whispersystems.signalservice.api.services.ProfileService;
import org.whispersystems.signalservice.api.storage.StorageServiceApi;
import org.whispersystems.signalservice.api.storage.StorageServiceRepository;
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
import org.whispersystems.signalservice.api.username.UsernameApi;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
import org.whispersystems.signalservice.api.websocket.WebSocketFactory;
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
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.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
public class SignalDependencies {
private static final Logger logger = LoggerFactory.getLogger(SignalDependencies.class);
private final Object LOCK = new Object();
private final ServiceEnvironmentConfig serviceEnvironmentConfig;
@ -43,17 +66,31 @@ public class SignalDependencies {
private boolean allowStories = true;
private SignalServiceAccountManager accountManager;
private AccountApi accountApi;
private RateLimitChallengeApi rateLimitChallengeApi;
private CdsApi cdsApi;
private UsernameApi usernameApi;
private GroupsV2Api groupsV2Api;
private RegistrationApi registrationApi;
private LinkDeviceApi linkDeviceApi;
private StorageServiceApi storageServiceApi;
private CertificateApi certificateApi;
private AttachmentApi attachmentApi;
private MessageApi messageApi;
private KeysApi keysApi;
private GroupsV2Operations groupsV2Operations;
private ClientZkOperations clientZkOperations;
private PushServiceSocket pushServiceSocket;
private SignalWebSocket signalWebSocket;
private Network libSignalNetwork;
private SignalWebSocket.AuthenticatedWebSocket authenticatedSignalWebSocket;
private SignalWebSocket.UnauthenticatedWebSocket unauthenticatedSignalWebSocket;
private SignalServiceMessageReceiver messageReceiver;
private SignalServiceMessageSender messageSender;
private List<SecureValueRecovery> secureValueRecoveryV2;
private List<SecureValueRecovery> secureValueRecovery;
private ProfileService profileService;
private ProfileApi profileApi;
SignalDependencies(
final ServiceEnvironmentConfig serviceEnvironmentConfig,
@ -75,9 +112,20 @@ public class SignalDependencies {
if (this.pushServiceSocket != null) {
this.pushServiceSocket.close();
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(),
credentialsProvider,
userAgent,
getClientZkProfileOperations(),
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() {
return getOrCreate(() -> accountManager,
() -> accountManager = new SignalServiceAccountManager(getPushServiceSocket(),
null,
serviceEnvironmentConfig.signalServiceConfiguration(),
credentialsProvider,
() -> accountManager = new SignalServiceAccountManager(getAuthenticatedSignalWebSocket(),
getAccountApi(),
getPushServiceSocket(),
getGroupsV2Operations()));
}
public SignalServiceAccountManager createUnauthenticatedAccountManager(String number, String password) {
return new SignalServiceAccountManager(getServiceEnvironmentConfig().signalServiceConfiguration(),
return SignalServiceAccountManager.createWithStaticCredentials(getServiceEnvironmentConfig().signalServiceConfiguration(),
null,
null,
number,
@ -125,10 +202,67 @@ public class SignalDependencies {
ServiceConfig.GROUP_MAX_SIZE);
}
public AccountApi getAccountApi() {
return getOrCreate(() -> accountApi, () -> accountApi = new AccountApi(getAuthenticatedSignalWebSocket()));
}
public RateLimitChallengeApi getRateLimitChallengeApi() {
return getOrCreate(() -> rateLimitChallengeApi,
() -> rateLimitChallengeApi = new RateLimitChallengeApi(getAuthenticatedSignalWebSocket()));
}
public CdsApi getCdsApi() {
return getOrCreate(() -> cdsApi, () -> cdsApi = new CdsApi(getAuthenticatedSignalWebSocket()));
}
public UsernameApi getUsernameApi() {
return getOrCreate(() -> usernameApi, () -> usernameApi = new UsernameApi(getUnauthenticatedSignalWebSocket()));
}
public GroupsV2Api getGroupsV2Api() {
return getOrCreate(() -> groupsV2Api, () -> groupsV2Api = getAccountManager().getGroupsV2Api());
}
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() {
return getOrCreate(() -> groupsV2Operations,
() -> groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration()),
@ -145,66 +279,79 @@ public class SignalDependencies {
return clientZkOperations.getProfileOperations();
}
public SignalWebSocket getSignalWebSocket() {
return getOrCreate(() -> signalWebSocket, () -> {
public SignalWebSocket.AuthenticatedWebSocket getAuthenticatedSignalWebSocket() {
return getOrCreate(() -> authenticatedSignalWebSocket, () -> {
final var timer = new UptimeSleepTimer();
final var healthMonitor = new SignalWebSocketHealthMonitor(timer);
final var webSocketFactory = new WebSocketFactory() {
@Override
public WebSocketConnection createWebSocket() {
return new WebSocketConnection("normal",
serviceEnvironmentConfig.signalServiceConfiguration(),
Optional.of(credentialsProvider),
userAgent,
healthMonitor,
allowStories);
}
@Override
public WebSocketConnection createUnidentifiedWebSocket() {
return new WebSocketConnection("unidentified",
serviceEnvironmentConfig.signalServiceConfiguration(),
Optional.empty(),
userAgent,
healthMonitor,
allowStories);
}
};
signalWebSocket = new SignalWebSocket(webSocketFactory);
healthMonitor.monitor(signalWebSocket);
authenticatedSignalWebSocket = new SignalWebSocket.AuthenticatedWebSocket(() -> new OkHttpWebSocketConnection(
"normal",
serviceEnvironmentConfig.signalServiceConfiguration(),
Optional.of(credentialsProvider),
userAgent,
healthMonitor,
allowStories), () -> true, timer, TimeUnit.SECONDS.toMillis(10));
healthMonitor.monitor(authenticatedSignalWebSocket);
});
}
public SignalWebSocket.UnauthenticatedWebSocket getUnauthenticatedSignalWebSocket() {
return getOrCreate(() -> unauthenticatedSignalWebSocket, () -> {
final var timer = new UptimeSleepTimer();
final var healthMonitor = new SignalWebSocketHealthMonitor(timer);
unauthenticatedSignalWebSocket = new SignalWebSocket.UnauthenticatedWebSocket(() -> new OkHttpWebSocketConnection(
"unidentified",
serviceEnvironmentConfig.signalServiceConfiguration(),
Optional.empty(),
userAgent,
healthMonitor,
allowStories), () -> true, timer, TimeUnit.SECONDS.toMillis(10));
healthMonitor.monitor(unauthenticatedSignalWebSocket);
});
}
public SignalServiceMessageReceiver getMessageReceiver() {
return getOrCreate(() -> messageReceiver,
() -> messageReceiver = new SignalServiceMessageReceiver(pushServiceSocket));
() -> messageReceiver = new SignalServiceMessageReceiver(getPushServiceSocket()));
}
public SignalServiceMessageSender getMessageSender() {
return getOrCreate(() -> messageSender,
() -> messageSender = new SignalServiceMessageSender(credentialsProvider,
() -> messageSender = new SignalServiceMessageSender(getPushServiceSocket(),
dataStore,
sessionLock,
getSignalWebSocket(),
getAttachmentApi(),
getMessageApi(),
getKeysApi(),
Optional.empty(),
executor,
ServiceConfig.MAX_ENVELOPE_SIZE,
pushServiceSocket));
() -> true,
UsePqRatchet.NO));
}
public List<SecureValueRecovery> getSecureValueRecoveryV2() {
return getOrCreate(() -> secureValueRecoveryV2,
() -> secureValueRecoveryV2 = serviceEnvironmentConfig.svr2Mrenclaves()
public List<SecureValueRecovery> getSecureValueRecovery() {
return getOrCreate(() -> secureValueRecovery,
() -> secureValueRecovery = serviceEnvironmentConfig.svr2Mrenclaves()
.stream()
.map(mr -> (SecureValueRecovery) getAccountManager().getSecureValueRecoveryV2(mr))
.toList());
}
public ProfileApi getProfileApi() {
return getOrCreate(() -> profileApi,
() -> profileApi = new ProfileApi(getAuthenticatedSignalWebSocket(),
getUnauthenticatedSignalWebSocket(),
getPushServiceSocket(),
getClientZkProfileOperations()));
}
public ProfileService getProfileService() {
return getOrCreate(() -> profileService,
() -> profileService = new ProfileService(getClientZkProfileOperations(),
getMessageReceiver(),
getSignalWebSocket()));
getAuthenticatedSignalWebSocket(),
getUnauthenticatedSignalWebSocket()));
}
public SignalServiceCipher getCipher(ServiceIdType serviceIdType) {

View file

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

View file

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

View file

@ -33,7 +33,7 @@ import java.util.UUID;
public class AccountDatabase extends Database {
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) {
super(logger, DATABASE_VERSION, dataSource);
@ -73,16 +73,16 @@ public class AccountDatabase extends Database {
uuid BLOB UNIQUE,
profile_key BLOB,
profile_key_credential BLOB,
given_name TEXT,
family_name TEXT,
color TEXT,
expiration_time INTEGER NOT NULL DEFAULT 0,
blocked INTEGER NOT NULL DEFAULT FALSE,
archived INTEGER NOT NULL DEFAULT FALSE,
profile_sharing INTEGER NOT NULL DEFAULT FALSE,
profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
profile_given_name TEXT,
profile_family_name TEXT,
@ -244,7 +244,7 @@ public class AccountDatabase extends Database {
WHERE uuid IS NOT NULL;
DROP TABLE identity;
ALTER TABLE identity2 RENAME TO identity;
DROP INDEX msl_recipient_index;
ALTER TABLE message_send_log ADD COLUMN uuid BLOB;
UPDATE message_send_log
@ -254,7 +254,7 @@ public class AccountDatabase extends Database {
DELETE FROM message_send_log WHERE uuid IS NULL;
ALTER TABLE message_send_log DROP COLUMN recipient_id;
CREATE INDEX msl_recipient_index ON message_send_log (uuid, device_id, content_id);
CREATE TABLE sender_key2 (
_id INTEGER PRIMARY KEY,
uuid BLOB NOT NULL,
@ -270,7 +270,7 @@ public class AccountDatabase extends Database {
WHERE uuid IS NOT NULL;
DROP TABLE sender_key;
ALTER TABLE sender_key2 RENAME TO sender_key;
CREATE TABLE sender_key_shared2 (
_id INTEGER PRIMARY KEY,
uuid BLOB NOT NULL,
@ -285,7 +285,7 @@ public class AccountDatabase extends Database {
WHERE uuid IS NOT NULL;
DROP TABLE sender_key_shared;
ALTER TABLE sender_key_shared2 RENAME TO sender_key_shared;
CREATE TABLE session2 (
_id INTEGER PRIMARY KEY,
account_id_type INTEGER NOT NULL,
@ -378,7 +378,7 @@ public class AccountDatabase extends Database {
WHERE address IS NOT NULL;
DROP TABLE identity;
ALTER TABLE identity2 RENAME TO identity;
CREATE TABLE message_send_log2 (
_id INTEGER PRIMARY KEY,
content_id INTEGER NOT NULL REFERENCES message_send_log_content (_id) ON DELETE CASCADE,
@ -395,7 +395,7 @@ public class AccountDatabase extends Database {
ALTER TABLE message_send_log2 RENAME TO message_send_log;
CREATE INDEX msl_recipient_index ON message_send_log (address, device_id, content_id);
CREATE INDEX msl_content_index ON message_send_log (content_id);
CREATE TABLE sender_key2 (
_id INTEGER PRIMARY KEY,
address TEXT NOT NULL,
@ -411,7 +411,7 @@ public class AccountDatabase extends Database {
WHERE address IS NOT NULL;
DROP TABLE sender_key;
ALTER TABLE sender_key2 RENAME TO sender_key;
CREATE TABLE sender_key_shared2 (
_id INTEGER PRIMARY KEY,
address TEXT NOT NULL,
@ -426,7 +426,7 @@ public class AccountDatabase extends Database {
WHERE address IS NOT NULL;
DROP TABLE sender_key_shared;
ALTER TABLE sender_key_shared2 RENAME TO sender_key_shared;
CREATE TABLE session2 (
_id INTEGER PRIMARY KEY,
account_id_type INTEGER NOT NULL,
@ -441,7 +441,7 @@ public class AccountDatabase extends Database {
WHERE address IS NOT NULL;
DROP TABLE session;
ALTER TABLE session2 RENAME TO session;
DROP TABLE tmp_mapping_table;
""");
}
@ -531,12 +531,12 @@ public class AccountDatabase extends Database {
unregistered_timestamp INTEGER,
profile_key BLOB,
profile_key_credential BLOB,
given_name TEXT,
family_name TEXT,
nick_name TEXT,
color TEXT,
expiration_time INTEGER NOT NULL DEFAULT 0,
mute_until INTEGER NOT NULL DEFAULT 0,
blocked INTEGER NOT NULL DEFAULT FALSE,
@ -544,7 +544,7 @@ public class AccountDatabase extends Database {
profile_sharing INTEGER NOT NULL DEFAULT FALSE,
hide_story INTEGER NOT NULL DEFAULT FALSE,
hidden INTEGER NOT NULL DEFAULT FALSE,
profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
profile_given_name TEXT,
profile_family_name TEXT,
@ -556,11 +556,11 @@ public class AccountDatabase extends Database {
profile_capabilities TEXT
) 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)
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;
DROP TABLE recipient;
ALTER TABLE recipient2 RENAME TO recipient;
DROP TABLE tmp_mapping_table;
""");
}
@ -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(
final Connection connection, final Statement statement
final Connection connection,
final Statement statement
) throws SQLException {
statement.executeUpdate("""
CREATE TABLE tmp_mapping_table (

View file

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

View file

@ -24,7 +24,8 @@ public abstract class Database implements AutoCloseable {
}
public static <T extends Database> T initDatabase(
File databaseFile, Function<HikariDataSource, T> newDatabase
File databaseFile,
Function<HikariDataSource, T> newDatabase
) throws SQLException {
HikariDataSource dataSource = null;
@ -94,10 +95,12 @@ public abstract class Database implements AutoCloseable {
sqliteConfig.setTransactionMode(SQLiteConfig.TransactionMode.IMMEDIATE);
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:sqlite:" + databaseFile);
config.setJdbcUrl("jdbc:sqlite:" + databaseFile + "?foreign_keys=ON&journal_mode=wal");
config.setDataSourceProperties(sqliteConfig.toProperties());
config.setMinimumIdle(1);
config.setConnectionInitSql("PRAGMA foreign_keys=ON");
config.setConnectionTimeout(90_000);
config.setMaximumPoolSize(50);
config.setMaxLifetime(0);
return new HikariDataSource(config);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,6 +8,7 @@ import org.asamk.signal.manager.storage.recipients.RecipientStore;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.state.IdentityKeyStore.Direction;
import org.signal.libsignal.protocol.state.IdentityKeyStore.IdentityChange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.ServiceId;
@ -49,7 +50,9 @@ public class IdentityKeyStore {
}
public IdentityKeyStore(
final Database database, final TrustNewIdentity trustNewIdentity, RecipientStore recipientStore
final Database database,
final TrustNewIdentity trustNewIdentity,
RecipientStore recipientStore
) {
this.database = database;
this.trustNewIdentity = trustNewIdentity;
@ -60,19 +63,21 @@ public class IdentityKeyStore {
return identityChanges;
}
public boolean saveIdentity(final ServiceId serviceId, final IdentityKey identityKey) {
public IdentityChange saveIdentity(final ServiceId serviceId, final IdentityKey identityKey) {
return saveIdentity(serviceId.toString(), identityKey);
}
public boolean saveIdentity(
final Connection connection, final ServiceId serviceId, final IdentityKey identityKey
public IdentityChange saveIdentity(
final Connection connection,
final ServiceId serviceId,
final IdentityKey identityKey
) throws SQLException {
return saveIdentity(connection, serviceId.toString(), identityKey);
}
boolean saveIdentity(final String address, final IdentityKey identityKey) {
IdentityChange saveIdentity(final String address, final IdentityKey identityKey) {
if (isRetryingDecryption) {
return false;
return IdentityChange.NEW_OR_UNCHANGED;
}
try (final var connection = database.getConnection()) {
return saveIdentity(connection, address, identityKey);
@ -81,18 +86,24 @@ public class IdentityKeyStore {
}
}
private boolean saveIdentity(
final Connection connection, final String address, final IdentityKey identityKey
private IdentityChange saveIdentity(
final Connection connection,
final String address,
final IdentityKey identityKey
) throws SQLException {
final var identityInfo = loadIdentity(connection, address);
if (identityInfo != null && identityInfo.getIdentityKey().equals(identityKey)) {
if (identityInfo == null) {
saveNewIdentity(connection, address, identityKey, true);
return IdentityChange.NEW_OR_UNCHANGED;
}
if (identityInfo.getIdentityKey().equals(identityKey)) {
// Identity already exists, not updating the trust level
logger.trace("Not storing new identity for recipient {}, identity already stored", address);
return false;
return IdentityChange.NEW_OR_UNCHANGED;
}
saveNewIdentity(connection, address, identityKey, identityInfo == null);
return true;
saveNewIdentity(connection, address, identityKey, false);
return IdentityChange.REPLACED_EXISTING;
}
public void setRetryingDecryption(final boolean retryingDecryption) {
@ -230,9 +241,7 @@ public class IdentityKeyStore {
logger.debug("Complete identities migration took {}ms", (System.nanoTime() - start) / 1000000);
}
private IdentityInfo loadIdentity(
final Connection connection, final String address
) throws SQLException {
private IdentityInfo loadIdentity(final Connection connection, final String address) throws SQLException {
final var sql = (
"""
SELECT i.address, i.identity_key, i.added_timestamp, i.trust_level

View file

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

View file

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

View file

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

View file

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

View file

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

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