Compare commits

...

352 Commits

Author SHA1 Message Date
Ravi Khadiwala
c93972a322 Update to the latest version of the spam filter 2023-12-19 18:33:45 -06:00
Ravi Khadiwala
ca47a7b663 handle new RegistrationService proto error 2023-12-19 18:19:26 -06:00
Jonathan Klabunde Tomer
9d3d4a3698 Add phone-number-sharing field to versioned profile
Co-authored-by: Katherine <katherine@signal.org>
2023-12-19 11:20:04 -08:00
Chris Eager
3b509bf820 Add command to remove expired linked devices 2023-12-19 13:11:26 -06:00
Jon Chambers
5b7f91827a Remove signed pre-keys transactionally when removing devices 2023-12-19 14:11:05 -05:00
Chris Eager
a44491714c Update to the latest version of the spam filter 2023-12-18 14:55:39 -06:00
Chris Eager
06800043a9 Set TLS keystore password in secondary persistent services 2023-12-15 13:39:58 -06:00
Chris Eager
3090de56b8 Set TLS keystore password from secrets configuration 2023-12-15 12:47:27 -06:00
Katherine
a37acd1f42 Add ttl for braintree writes to onetime donation table 2023-12-15 13:37:35 -05:00
Jonathan Klabunde Tomer
372e3f83d2 Update libsignal to 0.37.0 2023-12-15 10:36:59 -08:00
Chris Eager
de260a2bef Update to the latest version of the spam filter 2023-12-15 12:23:01 -06:00
Jonathan Klabunde Tomer
e9a130f976 add versioned zkconfig secrets to sample secrets bundle 2023-12-14 17:38:49 -08:00
Jonathan Klabunde Tomer
43f17414ff Make key-setting methods asynchronous again 2023-12-14 16:49:16 -08:00
Jon Chambers
b259eea8ce Refactor/clarify account creation/reclamation process 2023-12-14 16:48:57 -08:00
Katherine
9cfc2ba09a Persist onetime donation payment success timestamps for Braintree transactions 2023-12-14 16:48:29 -08:00
Chris Eager
bb347999ce Propagate another subscription processor error to clients 2023-12-14 15:40:08 -05:00
Katherine
3548c3df15 Calculate onetime badge expiration from payment success timestamp 2023-12-14 15:39:46 -05:00
Jon Chambers
1167d0ac2e Make key-setting methods synchronous 2023-12-13 17:49:55 -05:00
Jon Chambers
f738bc97e7 Revert "Retire the "migrate signed pre-keys" command"
This reverts commit c7cc3002d5.
2023-12-13 17:49:55 -05:00
Jon Chambers
3f9edfe597 Revert "Treat the stand-alone signed pre-keys table as the source of truth for signed pre-keys"
This reverts commit feb933b4df.
2023-12-13 17:49:55 -05:00
Jon Chambers
a024949311 Revert "Retire "migrate signed pre-keys" configuration"
This reverts commit 44145073f1.
2023-12-13 17:49:55 -05:00
Jon Chambers
609c901867 Refactor key-fetching to be reactive 2023-12-13 12:46:48 -05:00
Jon Chambers
4ce060a963 Count wildcard "get keys" requests by platform 2023-12-13 12:46:48 -05:00
Jon Chambers
c4ca0fee40 Synchronize access to responseItems when assembling a "get keys" response 2023-12-13 12:46:48 -05:00
Chris Eager
8d4acf0330 Remove ForkJoinPool.managedBlock in favor of async updates 2023-12-13 10:18:04 -05:00
Jon Chambers
28a981f29f Assume that all devices have signed pre-keys 2023-12-13 10:17:51 -05:00
Jon Chambers
c29113d17a Reject requests with missing device capabilities 2023-12-12 11:37:44 -05:00
Katherine
951f978447 Use start of subscription period as fallback timestamp 2023-12-12 11:34:44 -05:00
Jon Chambers
07899f35bd Return DeviceController#linkDevice to synchronous processing 2023-12-12 11:34:23 -05:00
Katherine
3cbbf37468 Use payment success timestamp to calculate recurring donation badge expiration 2023-12-12 10:01:20 -05:00
Jon Chambers
6c7a3df5ae Retire non-atomic device-linking pathways 2023-12-12 09:53:51 -05:00
Jon Chambers
2054ab2771 Revert "Count requests with missing device capabilities"
This reverts commit 6cdf8ebd2c.
2023-12-12 09:53:51 -05:00
Jon Chambers
44145073f1 Retire "migrate signed pre-keys" configuration 2023-12-12 09:53:34 -05:00
Jon Chambers
feb933b4df Treat the stand-alone signed pre-keys table as the source of truth for signed pre-keys 2023-12-11 11:39:54 -05:00
Jon Chambers
c7cc3002d5 Retire the "migrate signed pre-keys" command 2023-12-11 11:07:00 -05:00
xiaolou86
049b901d63 Fix typos 2023-12-09 10:04:12 -05:00
Jon Chambers
3cf1b92dfc Do not set one-time pre-keys if the lists of new keys are empty 2023-12-08 14:03:08 -05:00
Jon Chambers
5b0fcbe854 Always expose sequential fluxes to account crawlers 2023-12-08 13:43:42 -05:00
Jon Chambers
cca747a1f6 Treat transaction conflicts during transactional account updates as contested optimistic locks 2023-12-08 12:13:23 -05:00
Jon Chambers
417d99a17e Check story rate limits in parallel 2023-12-08 12:13:08 -05:00
Jon Chambers
e9708b9259 Use random UUIDs throughout MessageControllerTest 2023-12-08 12:13:08 -05:00
Jon Chambers
e5d3be16b0 Fetch destination accounts in parallel when sending multi-recipient messages 2023-12-08 12:13:08 -05:00
Jordan Rose
2ab3c97ee8 Replace MultiRecipientMessage parsing with libsignal's implementation
Co-authored-by: Jonathan Klabunde Tomer <jkt@signal.org>
2023-12-08 11:52:47 -05:00
Jon Chambers
f20d3043d6 Process key migrations sequentially to better control concurrency 2023-12-07 21:42:49 -05:00
Jonathan Klabunde Tomer
4efda89358 multisend cleanup 2023-12-07 12:23:02 -08:00
Jon Chambers
1fb88271e5 Invalidate cache entries for accounts after successfully adding devices 2023-12-07 13:27:26 -05:00
Jon Chambers
a843780f68 Add a (failing!) test for device-linking 2023-12-07 13:27:26 -05:00
Jon Chambers
5ad83da4e0 Remove the now-unused admin logger 2023-12-07 12:58:43 -05:00
Jon Chambers
949cc9e214 Update to the latest version of the spam filter 2023-12-07 12:58:23 -05:00
Jon Chambers
50d92265ea Add devices to accounts transactionally 2023-12-07 11:19:40 -05:00
Chris Eager
e084a9f2b6 Remove PUT/DELETE methods from RemoteConfigController 2023-12-07 10:54:19 -05:00
Jon Chambers
664f9f36e1 Use padded base64 encoding for outbound device names 2023-12-07 10:53:56 -05:00
Jon Chambers
4c9efdb936 Revert "Revert "Represent device names as byte arrays""
This reverts commit 45848e7bfe.
2023-12-07 10:53:56 -05:00
Jon Chambers
45848e7bfe Revert "Represent device names as byte arrays"
This reverts commit 5ae2e5281a.
2023-12-06 17:05:04 -05:00
Ravi Khadiwala
4fa10e5783 Fix NPE in request metrics when a finished request is missing a response 2023-12-06 15:11:41 -05:00
Ravi Khadiwala
fc0bc85f4d Add some extra request validation to /v1/archives/keys 2023-12-06 15:11:27 -05:00
Jon Chambers
5ae2e5281a Represent device names as byte arrays 2023-12-06 15:10:45 -05:00
Jon Chambers
34a943832a Align push notification types and delivery priorities 2023-12-06 12:40:16 -05:00
Jon Chambers
db17693ba7 Revert "Temporarily make registration challenge notifications "noisy""
This reverts commit 9069c5abb6.
2023-12-06 12:40:16 -05:00
Jon Chambers
6cdf8ebd2c Count requests with missing device capabilities 2023-12-06 10:40:50 -05:00
Ravi Khadiwala
072b470f46 Be more specific about encodings in /v1/archive docs 2023-12-06 10:40:20 -05:00
Ravi Khadiwala
78b2df2ecc Use long instead of int for epoch seconds 2023-12-06 10:40:06 -05:00
Jon Chambers
51a825f25c Update to the latest version of the spam filter 2023-12-06 10:39:04 -05:00
Jon Chambers
00e72a30c9 Assume all accounts have primary devices 2023-12-06 10:28:19 -05:00
Ravi Khadiwala
69990c23a5 Avoid test dep in integration-test 2023-12-05 16:35:51 -06:00
Jon Chambers
df421e0182 Update signed pre-keys in transactions 2023-12-05 14:20:16 -05:00
Jon Chambers
ede9297139 Disallow identity key changes 2023-12-05 14:14:24 -05:00
Jon Chambers
85383fe581 Remove an unused "store keys" method 2023-12-05 14:14:24 -05:00
Jon Chambers
4cca7aa4bd Normalize identity types throughout KeysController 2023-12-05 14:14:24 -05:00
Jon Chambers
e2037dea6c Rename PreKeyState to SetKeysRequest 2023-12-05 14:14:24 -05:00
Jon Chambers
f10f772e94 Convert PreKeyState to a record 2023-12-05 14:14:24 -05:00
Jon Chambers
9ecfe15ac4 Use multiline strings for PreKeyState documentation 2023-12-05 14:14:24 -05:00
Jon Chambers
5f0726af8a Perform cleanup operations before overwriting an existing account record 2023-12-05 12:18:09 -05:00
Ravi Khadiwala
331bbdd4e6 replace deprecated apache RandomUtils 2023-12-05 10:48:18 -06:00
Jon Chambers
37e3bcfc3e Move "remove device" logic into AccountsManager 2023-12-05 11:44:58 -05:00
Jon Chambers
4f42c10d60 Disallow sync messages to PNIs 2023-12-05 11:44:27 -05:00
Jonathan Klabunde Tomer
20392a567b Revert "multisend cleanup"
This reverts commit c03249b411.
2023-12-01 14:39:31 -08:00
Jonathan Klabunde Tomer
c03249b411 multisend cleanup 2023-11-30 15:50:36 -08:00
gram-signal
22e6584402 Add auth controller for SVR3 to /v3/backup. 2023-11-30 15:50:21 -08:00
Chris Eager
c18aca9215 Update to the latest version of the spam filter 2023-11-29 16:08:31 -06:00
Jon Chambers
aa23a5422a Don't modify registration IDs from the "set account attributes" endpoint 2023-11-29 16:56:47 -05:00
Jon Chambers
01fde4f9ca Require PNI-associated registration IDs for new devices 2023-11-29 16:56:47 -05:00
Jon Chambers
3980dec123 Revert "Add metrics to check presence of PNI-associated registration IDs"
This reverts commit 1da3f96d10.
2023-11-29 16:56:47 -05:00
Jon Chambers
c97f837f45 Revert "Instrument registration ID changes via the "set account attributes" endpoint"
This reverts commit 8fbc1dac74.
2023-11-29 16:56:47 -05:00
Chris Eager
9c54d2407b Update tests 2023-11-29 15:48:24 -06:00
Chris Eager
a027c4ce1f Conditionally use request.remoteAddr instead of X-Forwarded-For 2023-11-29 15:48:24 -06:00
Chris Eager
b1fd025ea6 Use EpochSecondSerializer for UserRemoteConfigList.serverEpochTime 2023-11-29 15:47:10 -06:00
Jonathan Klabunde Tomer
a05a230085 use Account-specific json serializer when reserving username hash 2023-11-29 13:40:06 -08:00
Jon Chambers
8fbc1dac74 Instrument registration ID changes via the "set account attributes" endpoint 2023-11-29 10:26:41 -05:00
Katherine
f46842c6c9 Validate registration IDs 2023-11-28 15:43:35 -08:00
Ravi Khadiwala
8b95bb0c03 Only run integration tests if integration bucket is configured 2023-11-28 15:10:49 -06:00
ravi-signal
202dd8e92d Add copy endpoint to ArchiveController
Co-authored-by: Jonathan Klabunde Tomer <125505367+jkt-signal@users.noreply.github.com>
Co-authored-by: Chris Eager <79161849+eager-signal@users.noreply.github.com>
2023-11-28 11:45:41 -06:00
Jon Chambers
1da3f96d10 Add metrics to check presence of PNI-associated registration IDs 2023-11-28 12:26:15 -05:00
Chris Eager
5f6fe4d670 Add NoneHealthResponder to replace default responder 2023-11-27 14:38:47 -06:00
Chris Eager
a74438d1ee Add test for concurrent in-flight outbound messages on WebSocket queue processing
This also elevates the implicit default concurrency (via reactor’s `Queues.SMALL_BUFFER_SIZE`) to be explicit.
2023-11-27 14:37:46 -06:00
Jon Chambers
c8033f875d Create accounts transactionally 2023-11-27 09:57:43 -05:00
Jon Chambers
07c04006df Avoid blocking calls in async account updates 2023-11-27 09:57:43 -05:00
Jon Chambers
521900c048 Always require atomic account creation 2023-11-27 09:52:57 -05:00
Jon Chambers
9069c5abb6 Temporarily make registration challenge notifications "noisy" 2023-11-22 10:25:59 -05:00
Jon Chambers
ff7a5f471b Always use "conserve power" priority for challenge notifications 2023-11-21 17:58:32 -05:00
Ravi Khadiwala
42a47406cc Fix platform tag for push challenge metric 2023-11-21 17:57:32 -05:00
Katherine
de10b6de7b Add metric to count invalid registration ID when setting account attributes 2023-11-20 14:25:27 -08:00
Chris Eager
d6ade0e1ac Send HTTP 408 on idle timeout 2023-11-20 16:24:54 -06:00
Ravi Khadiwala
e04b5e5c9f run integration-test job daily at 19:30 UTC 2023-11-20 15:31:45 -06:00
Ravi Khadiwala
15a6c46d47 use a configured number in integration test 2023-11-20 15:02:53 -06:00
Jonathan Klabunde Tomer
cb1fc734c2 report exceptions in fanned-out sends of multi-recipient messages 2023-11-20 10:46:26 -08:00
Katherine Yen
db7f18aae7 Add missing boolean parameter to getSubscriptionStatus method call 2023-11-16 19:28:10 -05:00
Katherine
7fbc327591 For Braintree, return active subscription status if latest transaction also succeeded 2023-11-16 19:15:46 -05:00
Chris Eager
84b56ae1b2 Upgrade to dropwizard 3.0 2023-11-16 17:57:48 -06:00
Katherine
041aa8639a Enforce story ratelimit 2023-11-16 12:36:43 -05:00
Katherine
216ac72ad0 Use Braintree subscription status alongside transaction status to determine 204 and 402 responses for /v1/subscription/{subscriberId}/receipt_credentials 2023-11-16 12:36:19 -05:00
Jon Chambers
c85ddaeb9c Make DynamoDB client connection pool sizes configurable 2023-11-16 12:29:43 -05:00
Jonathan Klabunde Tomer
e09dec330a Update to the latest version of the spam filter 2023-11-15 17:34:57 -08:00
Jonathan Klabunde Tomer
8f7bae54fe When persisting messages fails due to a full queue in DynamoDB, automatically unlink one device to free up room.
Co-authored-by: Chris Eager <79161849+eager-signal@users.noreply.github.com>
2023-11-15 17:15:17 -08:00
Jon Chambers
ce60f13320 Don't track IPv4/IPv6 metrics at the application level
We can measure this at the infrastructure level without observing IPs directly.
2023-11-15 17:18:54 -05:00
Jon Chambers
1ac0140666 Remove a couple of now-unused variables 2023-11-15 17:12:08 -05:00
Jon Chambers
6cc8b147a9 Remove unused X-Forwarded-For parameters 2023-11-15 17:09:47 -05:00
Jon Chambers
e078161e2f Consistently use HttpHeaders.X_FORWARDED_FOR 2023-11-15 17:09:36 -05:00
Jonathan Klabunde Tomer
7764185c57 return explicit Response rather than Void from async controllers with no expected body content 2023-11-14 21:57:25 -08:00
Katherine
d4ef2adf0a Remove low priority attempt login notification workaround for old iOS clients 2023-11-13 12:06:55 -05:00
Jonathan Klabunde Tomer
a83378a44e add an option to replace username ciphertext without rotating the link handle 2023-11-13 09:01:54 -08:00
ravi-signal
a4a4204762 Allow re-registered accounts to reclaim their usernames 2023-11-13 10:41:23 -06:00
Jon Chambers
acd1140ef6 Make registration challenge APNs payloads "silent" 2023-11-13 08:39:58 -08:00
Jon Chambers
fbf71c93ff Make rate limit challenge APNs payloads "silent" 2023-11-13 08:39:58 -08:00
Katherine
38bc0c466a Change sepaMaximumEuros field to number in JSON response 2023-11-10 10:16:03 -08:00
Katherine
71e4351743 Add sepaMaximumEuros field to subscription configuration 2023-11-10 09:13:51 -08:00
Katherine
387e4b94b4 Expand charge object on Stripe subscription to surface charge failure information 2023-11-10 09:12:59 -08:00
Katherine
201c76b861 Add charge failure details to /v1/subscription/{subscriberId}/receipt_credentials 402 response 2023-11-08 10:54:14 -08:00
Chris Eager
1c3aa87ca6 Update to the latest version of the spam filter 2023-11-06 10:11:41 -06:00
Sergey Skrobotov
db63ff6b88 gRPC validations 2023-11-03 11:30:48 -07:00
Katherine
115431a486 Un-hardcode payment activation flag 2023-11-03 11:27:34 -07:00
Jonathan Klabunde Tomer
d47ff9b7c7 don't make empty transactions 2023-11-02 16:20:19 -07:00
Chris Eager
b0818148cf Update to the latest version of the spam filter 2023-11-02 11:48:50 -05:00
Chris Eager
2bc4412d66 Encapsulate device ID in ProvisioningAddress 2023-11-02 11:48:10 -05:00
Chris Eager
6a428b4da9 Convert Device.id from long to byte 2023-11-02 11:48:10 -05:00
Jonathan Klabunde Tomer
7299067829 Don't attempt to update PNI PQ prekeys for disabled devices 2023-11-01 16:55:55 -07:00
Chris Eager
5659cb2820 Update to the latest version of the spam filter 2023-11-01 15:53:26 -05:00
Chris Eager
570aa4b9e2 Remove several unused classes 2023-11-01 15:46:10 -05:00
Chris Eager
c4079a3b11 Update to the latest version of the spam filter 2023-11-01 10:07:42 -05:00
Ravi Khadiwala
6b38b538f1 Add ArchiveController
Adds endpoints for creating and managing backup objects with ZK
anonymous credentials.
2023-10-30 14:02:19 -05:00
Chris Eager
ba139dddd8 Use all devices when checking limit 2023-10-30 12:40:06 -05:00
Chris Eager
38b581a231 Update to the latest version of the spam filter 2023-10-27 10:22:50 -05:00
Chris Eager
3c2675b41a Update libphonenumber to 8.13.23 2023-10-27 09:26:40 -05:00
Chris Eager
0f5c62ade5 Set max threads = min threads on command executor services 2023-10-27 09:26:32 -05:00
Jon Chambers
54bc3bce96 Add an authentication-required gRPC service for working with accounts 2023-10-25 14:47:20 -04:00
Jon Chambers
3d92e5b8a9 Explicitly stop and start managed dependencies 2023-10-24 16:50:02 -04:00
Chris Eager
325d145ac3 Update to the latest version of the spam filter 2023-10-24 14:33:31 -05:00
Chris Eager
b0654a416a Update maven plugins 2023-10-24 14:32:55 -05:00
Chris Eager
19930ec2e4 Update dependencies
- AWS: 2.20.130 → 2.21.5
- Braintree: 3.25.0 → 3.27.0
- commons-csv: 2.13.0 → 2.14.0
- dropwizard: 2.1.7 → 2.1.9
- Google libraries BOM: 26.22.0 → 26.25.0
- grpc: 1.56.1 → 1.58.0
- kotlin: 1.9.0 → 1.9.10
- protobuf: 3.23.2 → 3.24.3
- stripe: 23.1.1 → 23.10.0
- junit-pioneer: 2.0.1 → 2.1.0
- firebase-admin: 9.1.1 → 9.2.0
- swagger-jaxrs2: 2.2.8 → 2.2.17
- java-uuid-generator: 4.2.0 → 4.3.0
- log4j: 2.17.0 → 2.21.0
- reactor-bom: 2022.0.10 → 2022.0.12
2023-10-24 14:32:55 -05:00
Jon Chambers
e4de6bf4a7 Only update devices that aren't already disabled 2023-10-24 15:29:03 -04:00
Jon Chambers
21125c2f5a Update to the latest version of the spam filter 2023-10-20 16:38:04 -04:00
Katherine Yen
6f166425fe Fix bank mandate test 2023-10-20 16:19:31 -04:00
Chris Eager
cf2353bcf9 Remove InstrumentedExecutorService wrapping 2023-10-20 15:14:35 -05:00
Jon Chambers
744eb58071 Discard old chunk-based account crawler machinery 2023-10-20 16:09:17 -04:00
Jon Chambers
9d47a6f41f Introduce a reactive push notification feedback processor 2023-10-20 16:09:17 -04:00
Jonathan Klabunde Tomer
4f4c23b12f Update to the latest version of the spam filter 2023-10-20 09:39:46 -07:00
Jonathan Klabunde Tomer
fb02815c27 Update to the latest version of the spam filter 2023-10-20 09:12:37 -07:00
Jonathan Klabunde Tomer
fd19299ae0 Accept a captcha score threshold for challenges from the spam filter 2023-10-20 09:09:22 -07:00
Jon Chambers
9c053e20da Drop Util#isEmpty/Util#nonEmpty in favor of StringUtils 2023-10-20 12:04:15 -04:00
Jon Chambers
19d7b5c65d Drop Util#wait 2023-10-20 12:04:15 -04:00
Jon Chambers
7b9d8829da Remove entirely unused Util methods 2023-10-20 12:04:15 -04:00
Jon Chambers
3505ac498c Update to the latest version of the spam filter 2023-10-20 10:52:43 -04:00
Jon Chambers
f0ab52eb5d Rename "master device" to "primary device" 2023-10-20 10:52:13 -04:00
Jon Chambers
e8cebad27e Avoid modifying original Account instances when constructing JSON for updates 2023-10-20 10:51:50 -04:00
Jon Chambers
6441d5838d Clear username links in the same transaction when clearing username hashes 2023-10-20 10:51:50 -04:00
Jon Chambers
ac0c8b1e9a Introduce a canonical constant for UAK length 2023-10-20 10:50:44 -04:00
Katherine
8ec062fbef Define an endpoint to set the default payment method for iDEAL subscriptions 2023-10-19 10:29:40 -07:00
Katherine
5990a100db Add charge failure details to /v1/subscription/boost/receipt_credential 402 response 2023-10-19 10:21:26 -07:00
Jon Chambers
bc35278684 Drop the old AccountCleaner 2023-10-19 10:34:24 -04:00
Jon Chambers
c3c7329ebb Add a single-shot command for removing expired accounts 2023-10-19 10:34:24 -04:00
Jon Chambers
6fd1c84126 Make command namespace available to subclasses 2023-10-19 10:34:24 -04:00
Jon Chambers
0100f0fcc9 Migrate a username links test from AccountsTest to AccountsManagerUsernameIntegrationTest 2023-10-18 10:20:48 -04:00
Jon Chambers
0cdc32cf65 Really REALLY fix instrumentation for re-registration of recently-deleted accounts 2023-10-18 10:15:03 -04:00
Jon Chambers
601e9eebbd Implement an anonymous account service for looking up accounts 2023-10-18 10:14:52 -04:00
Jon Chambers
eaa868cf06 Add a remote address interceptor to base gRPC tests 2023-10-18 10:14:52 -04:00
Jon Chambers
f55504c665 Add utility methods for rate-limiting by remote address 2023-10-18 10:14:52 -04:00
Katherine Yen
b2ff016cc1 Add back story ratelimiter with counter but do not enforce 2023-10-17 12:22:17 -04:00
Jon Chambers
33b4f17945 Make username-related operations asynchronous 2023-10-17 12:21:52 -04:00
Jon Chambers
e310a3560b Remove unused configuration for the legacy Secure Backup Service 2023-10-17 12:21:14 -04:00
Jon Chambers
162b27323e Fix instrumentation for re-registration of recently-deleted accounts 2023-10-17 12:20:58 -04:00
Jon Chambers
ae976ef8d6 Retire legacy Secure Value Recovery plumbing 2023-10-13 15:32:41 -04:00
Katherine
c6b4e2b71d Support iDEAL 2023-10-12 09:54:05 -07:00
Jon Chambers
33c8bbd0ce Trim stale capabilities from the profiles gRPC service 2023-10-12 12:52:32 -04:00
Jon Chambers
f2a3b8dba4 Treat APNs team/key IDs as secrets so they can change atomically with the key itself 2023-10-12 12:52:13 -04:00
Katherine
207ae6129b Add paymentMethod and paymentProcessing fields to GET /v1/subscription/{subscriberId} endpoint 2023-10-10 09:56:50 -07:00
Katherine
e1aa734c40 Define endpoint to get localized bank mandate text 2023-10-05 09:53:33 -07:00
Jonathan Klabunde Tomer
9b1b03bbfa Update to the latest version of the spam filter 2023-10-05 09:46:27 -07:00
Jon Chambers
bb7e0528c4 Make account deletion an asynchronous operation 2023-10-04 10:44:50 -04:00
Jonathan Klabunde Tomer
010eadcd10 UnlinkDeviceCommand improvements 2023-10-03 15:14:02 -07:00
Katherine
c43e0b54f2 Exclude SEPA_DEBIT as a supported payment method for certain iOS client versions 2023-10-03 11:34:52 -07:00
Chris Eager
6522b74e20 Remove obsolete metrics 2023-10-03 11:42:25 -05:00
Chris Eager
8c7975d89a Clear presence only if the connection’s displacement listener is still present 2023-10-03 11:42:25 -05:00
Chris Eager
407070c9fc Unsubscribe from keyspace notifications only if queue still maps to the listener 2023-10-03 11:42:25 -05:00
Katherine
7821a3cd61 Accommodate PayPal with SEPA changes 2023-09-28 10:28:17 -07:00
Katherine
a00c2fcfdb Support SEPA 2023-09-28 08:26:01 -07:00
Jonathan Klabunde Tomer
9cd21d1326 count ItemCollectionSizeLimitExceededExceptions persisting messages 2023-09-27 10:58:28 -07:00
Jonathan Klabunde Tomer
aaba95f9b8 return null for empty username hash in AccountIdentityResponse 2023-09-27 10:58:04 -07:00
Chris Eager
8d1135a2a3 Refine RegistrationController logic
Local device transfer on iOS uses the `409` status code to prompt the
transfer UI. This needs to happen before sending a `423` and locking
an existing account, since the device transfer
includes the local device database verbatim.
2023-09-25 15:54:31 -05:00
Jon Chambers
f9fabbedce Convert SubscriptionController request/response entities to records 2023-09-25 12:32:49 -07:00
Chris Eager
16012e6ffe Remove obsolete ManagedPeriodicWork 2023-09-25 12:15:17 -07:00
Jon Chambers
d10a132b0c Remove unused methods in SubscriptionController 2023-09-25 12:14:56 -07:00
Sergey Skrobotov
0b3af7d824 gRPC API for external services credentials service 2023-09-25 12:14:49 -07:00
Sergey Skrobotov
d0fdae3df7 Enable header-based auth for WebSocket connections 2023-09-25 12:14:40 -07:00
Ravi Khadiwala
a263611746 editorconfig: keep_simple_classes_in_one_line 2023-09-25 10:10:44 -05:00
Chris Eager
0e989419c6 Add metric for late removal of message availability and displacement listeners 2023-09-19 12:04:24 -05:00
ravi-signal
0fa8276d2d retry hCaptcha errors
Co-authored-by: Jon Chambers <63609320+jon-signal@users.noreply.github.com>
2023-09-14 16:07:35 -05:00
Ravi Khadiwala
b594986241 Set an idle timeout on registration gRPC client 2023-09-14 16:06:49 -05:00
Sergey Skrobotov
9f3ffa3707 gRPC API for payments service 2023-09-14 11:12:00 -07:00
Jonathan Klabunde Tomer
8e598c19dc don't attempt to update KEM prekeys if we have no PQ-enabled devices 2023-09-14 11:11:22 -07:00
Katherine
2601d6e906 Convert some fields on CreateProfileRequest and VersionedProfileResponse to byte arrays 2023-09-13 14:00:03 -07:00
Jon Chambers
de41088051 Update to WireMock 2.35.1 2023-09-13 16:56:15 -04:00
Jon Chambers
f2752b2a02 Update to the latest version of the spam filter 2023-09-13 16:02:46 -04:00
Jon Chambers
f0544fab89 Update recently-deleted accounts table transactionally as part of account mutations 2023-09-13 16:02:19 -04:00
Jon Chambers
1b9bf01ab1 Absorb DeletedAccounts into Accounts 2023-09-13 16:02:19 -04:00
Ravi Khadiwala
9945367fa1 Update to the latest version of the spam filter 2023-09-11 15:19:10 -05:00
Katherine
cbc3887226 Define identity key check endpoint in keys anonymous service 2023-09-11 11:57:00 -07:00
Ravi Khadiwala
c11b74e9c0 Update to the latest version of the spam filter 2023-09-11 13:37:07 -05:00
Jon Chambers
2b764c2abd Don't allow callers to unlink their primary device 2023-09-11 14:29:48 -04:00
Jon Chambers
845fc338d7 Add a (failing) test for removing primary devices from accounts 2023-09-11 14:29:48 -04:00
Sergey Skrobotov
977243ebfd DRY gRPC tests, refactor error mapping 2023-09-08 17:12:08 -07:00
Chris Eager
29ca544c95 Revert "Set suppressCancel=true in Mono.fromFuture"
This reverts commit 8348263fab.
2023-09-07 17:03:33 -05:00
Ravi Khadiwala
94b41d3a2c Fixup default rate limits
A previous refactor left the default rate limits off by a factor of 60.
2023-09-07 16:07:42 -05:00
Chris Eager
92bb783cbb Use static exception instance when a connection is closed 2023-09-07 16:06:16 -05:00
Chris Eager
8348263fab Set suppressCancel=true in Mono.fromFuture 2023-09-07 16:06:03 -05:00
Ravi Khadiwala
48f633de11 Fix type for comparison in integration test 2023-09-07 14:41:29 -05:00
Ravi Khadiwala
b3b9a629f3 Update to the latest version of the spam filter 2023-09-07 11:18:48 -05:00
Ravi Khadiwala
5934b7344a Remove unused captcha configuration 2023-09-07 11:16:32 -05:00
Chris Eager
a9a2e40fed Move onErrorResume to individual sendMessage Mono 2023-09-07 11:15:57 -05:00
Chris Eager
656326355a Invert String.equals() to prevent NullPointerException 2023-09-07 11:14:36 -05:00
Chris Eager
b89e2e5355 Propagate certain subscription processor errors to client responses 2023-09-06 15:57:14 -05:00
Chris Eager
2d187abf13 Handle WebSocket sendMessage errors with onErrorResume 2023-09-06 15:53:01 -05:00
Chris Eager
b701412295 Update maven-wrapper.properties 2023-09-06 15:48:27 -05:00
Jonathan Klabunde Tomer
b4dad81220 Update to the latest version of the spam filter 2023-09-05 13:55:07 -07:00
Jonathan Klabunde Tomer
6bccdad998 Update to the latest version of the spam filter 2023-09-05 10:23:39 -07:00
Chris Eager
ecd6b0174a Add timeouts to crawl chunk join()s 2023-08-31 15:03:19 -05:00
Chris Eager
a1e534a515 Add default request timeout to FaultTolerantHttpClient 2023-08-31 15:03:19 -05:00
Sergey Skrobotov
ebbe19ba63 Add missing copyright headers and reorder some imports 2023-08-30 16:07:53 -07:00
Katherine Yen
6a37b73463 Profile gRPC: Define getExpiringProfileKeyCredential endpoint 2023-08-30 14:56:43 -07:00
Katherine Yen
dd18fcaea2 Profile gRPC: Define getVersionedProfile endpoint 2023-08-30 14:47:11 -07:00
Katherine Yen
5afc058f90 Profile gRPC: Define getUnversionedProfile endpoint 2023-08-30 14:24:43 -07:00
Jon Chambers
5e221fa9a3 Tests for validation of Kyber keys on PNI change/key distribution events
Co-authored-by: Jonathan Klabunde Tomer <jkt@signal.org>
2023-08-30 14:07:33 -07:00
Jon Chambers
0e0cb4d422 Drop the non-normalized account crawler 2023-08-30 13:55:41 -04:00
Jonathan Klabunde Tomer
09f6d60ae9 Update to the latest version of the spam filter 2023-08-29 15:52:42 -07:00
Jonathan Klabunde Tomer
9577d552c6 pass challenge type to rate limit reset listeners 2023-08-29 15:19:49 -07:00
Chris Eager
093f17dce2 Update to stripe-java 23.1.1 2023-08-29 15:18:16 -07:00
Jon Chambers
6089f49b9c Add a gRPC interceptor for getting client addresses 2023-08-29 15:18:06 -07:00
Sergey Skrobotov
cfb910e87e Adding copyright headers to proto files 2023-08-28 14:39:33 -07:00
Ravi Khadiwala
376cffc61d Update to the latest version of the spam filter 2023-08-25 16:49:05 -05:00
Chris Eager
d338ba5152 Convert some KeysController methods return CompletableFutures 2023-08-24 11:59:28 -05:00
Chris Eager
f181397664 Add test for round-trip AccountsManager JSON serialization 2023-08-24 11:18:01 -05:00
Chris Eager
708f23a2ee Remove deprecated identity key and signed pre-key methods 2023-08-24 11:18:01 -05:00
Chris Eager
2d1a979eba Update libphonenumber to 8.13.19 2023-08-24 11:07:18 -05:00
Chris Eager
ee0be92967 Update to the latest version of the spam filter 2023-08-24 11:06:30 -05:00
Chris Eager
7536b75508 Remove unused test fixtures 2023-08-24 11:06:11 -05:00
Jonathan Klabunde Tomer
7237ae6c54 check that pq last-resort prekeys, if submitted, match device list 2023-08-24 09:04:29 -07:00
Sergey Skrobotov
ca05753a3e adding 400 response documentation to the API call 2023-08-23 13:20:07 -07:00
Chris Eager
9ca8503eac Downgrade to stripe-java 22.30.0 2023-08-22 16:31:46 -05:00
Jon Chambers
754f71ce00 Add a gRPC service for working with devices 2023-08-22 16:31:02 -05:00
Jon Chambers
619b05e56c Add utility a method for requiring authentication with the account's primary device 2023-08-22 16:31:02 -05:00
Jon Chambers
8b13826949 Convert DeviceInfo and DeviceInfoList to a record 2023-08-22 16:31:02 -05:00
Jon Chambers
a96ee57c7e Defer asynchronous actions when deriving Mono instances from futures 2023-08-22 16:28:02 -05:00
Jon Chambers
ff1ef90a6d Defer actions taken after rate limit checks 2023-08-22 16:28:02 -05:00
Chris Eager
22905fa8ee Downgrade logstash-logback-encoder to 7.3 2023-08-21 12:44:02 -05:00
Chris Eager
9e218ddd1c Update to the latest version of the spam filter 2023-08-21 11:42:11 -05:00
Chris Eager
6f0462622b Update maven and various plugins 2023-08-21 11:34:08 -05:00
Chris Eager
2f17161163 Update various dependencies 2023-08-21 11:34:08 -05:00
Ravi Khadiwala
17d48b95ac keep lettuce metrics; strip remote tags 2023-08-18 16:28:19 -05:00
Chris Eager
eeea97e2fe Return a single OAuth2 credentials JSON 2023-08-18 16:16:31 -05:00
Chris Eager
360e101660 Update to the latest version of the spam filter 2023-08-18 16:13:30 -05:00
Jon Chambers
3501a944a3 Update to the latest version of the spam filter 2023-08-18 11:49:11 -04:00
Jon Chambers
76305190a2 Temporarily restore explicit service/version/environment/host tags 2023-08-17 18:30:59 -04:00
Jon Chambers
ab83990170 Send latency metrics as distributions 2023-08-17 17:10:16 -04:00
Jon Chambers
8103a22026 Submit Micrometer metrics via dogstatsd instead of the Datadog API 2023-08-17 17:01:36 -04:00
Jonathan Klabunde Tomer
1f8e4713ef limit concurrency of async DynamoDB ops 2023-08-17 13:56:09 -07:00
Katherine Yen
ff9fe2c1be Remove record equality test 2023-08-17 13:55:27 -07:00
Jon Chambers
7f37c8ee5e Retire now-unused HTTP transport configuration for Datadog metric reporter 2023-08-17 16:53:53 -04:00
Jon Chambers
ed0a723fef Include underlying exceptions when logging failures to write exit files 2023-08-17 12:32:45 -04:00
Jon Chambers
5c31ef43c9 Send an HTTP/440 response instead of an HTTP/502 if an upstream provider rejects a "send verification code" request 2023-08-17 12:15:00 -04:00
Katherine Yen
43fd8518c0 Add missing java.util.Base64 import to ProfileController 2023-08-16 14:02:53 -07:00
Katherine Yen
19a08f01e8 Write certain profile data as bytes instead of strings to dynamo and represent those fields as byte arrays on VersionedProfile 2023-08-16 13:45:16 -07:00
Jonathan Klabunde Tomer
33498cf147 Update to the latest version of the spam filter 2023-08-16 10:19:00 -07:00
Jon Chambers
beeb85cf8d Update to the latest version of the spam filter 2023-08-15 14:21:00 -04:00
Jon Chambers
ccd860207b Make MessagesManager#clear asynchronous 2023-08-15 14:08:16 -04:00
Jon Chambers
2c835b5c51 Make message deletion from DynamoDB asynchronous 2023-08-15 14:08:16 -04:00
Jon Chambers
5caa951c61 Make MessagesCache#clear methods asynchronous 2023-08-15 14:08:16 -04:00
Jon Chambers
4d8c4d6693 Also delete APNs VOIP tokens when clearing APNs tokens 2023-08-15 14:08:00 -04:00
Jon Chambers
a9d0574ea8 Remove most @Timed annotations 2023-08-15 14:06:31 -04:00
Jonathan Klabunde Tomer
3954494eae Update to the latest version of the spam filter 2023-08-11 15:11:58 -07:00
Ravi Khadiwala
ed6a2c55eb adjust lettuce metric denial for post-transform name 2023-08-11 09:43:41 -05:00
Ravi Khadiwala
b6ee074149 fix captcha shortening url path resolution 2023-08-10 16:01:56 -05:00
Ravi Khadiwala
f6b3500e92 remove most high cardinality lettuce metrics 2023-08-10 16:01:16 -05:00
Katherine Yen
a71dc48b9b Prepare to read profile data stored as byte arrays 2023-08-10 14:00:35 -07:00
Katherine Yen
bc5eed48c3 Add authentication interceptor to profile gRPC service 2023-08-10 13:59:46 -07:00
Jon Chambers
2ecf3cb303 Revert "Don't immediately require PNI-associated keys for "atomic" device linking"
This reverts commit 4ec97cf006.
2023-08-10 16:59:35 -04:00
Jon Chambers
bed33d042a Revert "Require PNI-associated keys if the target account has a PNI identity key"
This reverts commit 1dde612855.
2023-08-10 16:59:35 -04:00
Jonathan Klabunde Tomer
d7975626be Update to the latest version of the spam filter 2023-08-10 09:58:26 -07:00
Ravi Khadiwala
3ac7aba6b2 Add a captcha short-code expander 2023-08-09 12:41:31 -05:00
Jon Chambers
1dde612855 Require PNI-associated keys if the target account has a PNI identity key 2023-08-09 12:10:56 -04:00
Jon Chambers
4ec97cf006 Don't immediately require PNI-associated keys for "atomic" device linking 2023-08-09 12:10:56 -04:00
Jon Chambers
d51c6fd2f8 Convert Device.Capabilities to a record 2023-08-08 15:38:37 -04:00
Jon Chambers
d868e3075c Retire fully-adopted device capabilities 2023-08-08 15:38:37 -04:00
Jon Chambers
ae61ee5486 Retire AnalyzeDeviceCapabilitiesCommand 2023-08-08 15:38:37 -04:00
Katherine Yen
58fd9ddb27 Count profile data that cannot be parsed as base64 2023-08-08 10:54:25 -07:00
Katherine Yen
a953cb33b7 Define ProfileController protobufs and setProfile endpoint 2023-08-08 10:53:11 -07:00
Jon Chambers
95b90e7c5a Add a preliminary gRPC service for dealing with calling credentials 2023-08-08 12:46:55 -04:00
Jon Chambers
6a3ecb2881 Convert TurnToken to a record 2023-08-08 12:46:55 -04:00
Jon Chambers
6cf4241283 Add a reactive method for checking rate limits by UUID 2023-08-08 12:46:55 -04:00
Jon Chambers
42141e51a1 Use ACIs instead of E164s for TURN URI overrides 2023-08-08 12:46:55 -04:00
Jon Chambers
b01945ff50 Clarify parameterized tests by modifying prototype request objects; remove spurious warning suppressions 2023-08-08 10:33:29 -04:00
Jon Chambers
a131f2116f Retire verification code storage machinery 2023-08-04 17:26:55 -04:00
Jon Chambers
625637b888 Stop checking for stored verification codes when linking devices 2023-08-04 17:26:55 -04:00
Jon Chambers
c873f62025 Produce verification tokens instead of stored verification codes for linking devices 2023-08-04 16:04:47 -04:00
Jon Chambers
43d91e5bd6 Convert VerificationCode to a record 2023-08-04 16:04:47 -04:00
Jon Chambers
5c4c729703 Disallow reuse of device verification tokens 2023-08-04 13:40:37 -05:00
Jon Chambers
308da3343d Accept signed tokens in addition to randomly-generated codes for authorizing device linking 2023-08-04 13:40:37 -05:00
Chris Eager
48c7572dd5 Add CommandStopListener 2023-08-04 13:29:35 -05:00
Ravi Khadiwala
dc5f35460b Update to the latest version of the spam filter 2023-08-04 11:38:33 -05:00
Jon Chambers
69ea9b0296 Add a request counter tagged by client version 2023-08-04 12:16:48 -04:00
Jon Chambers
969c6884c0 Add a command for analyzing device capabilities 2023-08-04 12:14:08 -04:00
Jon Chambers
fcf311aab3 Retire the PendingAccounts table 2023-08-04 12:13:57 -04:00
ravi-signal
888879dfb2 Estimate message byte limit exceeded error count 2023-08-04 11:10:58 -05:00
Chris Eager
e003197f77 Update to protobuf-java 3.23.3 2023-08-03 17:09:31 -05:00
Chris Eager
f57910cd97 Update to dropwizard 2.1.7, jackson 2.13.5 2023-08-03 16:18:27 -05:00
Chris Eager
d85e25dba0 Update to the latest version of the spam filter 2023-08-02 16:17:22 -05:00
Chris Eager
89a4034fc6 Remove s3-upload from deploy phase 2023-08-02 16:16:27 -05:00
Chris Eager
f53743d287 Add configuration for Datadog UDP transport 2023-08-02 13:54:15 -05:00
Jon Chambers
2d132128e1 Switched to a composed request object model for anonymous keys gRPC operations 2023-07-28 14:20:24 -05:00
Chris Eager
6e5ffbe7b5 Restore aci field to BatchIdentityCheckRequest 2023-07-28 14:16:48 -05:00
Jonathan Klabunde Tomer
a81c9681a0 Update to the latest version of the spam filter 2023-07-26 14:57:18 -07:00
Ravi Khadiwala
baf98accd0 acquire lock before checking message listeners in MessagesCache 2023-07-26 16:45:53 -04:00
Ravi Khadiwala
901c950ee6 Add metrics to keyspace-notifier executor 2023-07-26 16:45:53 -04:00
Ravi Khadiwala
50ac7f9dc2 adjust messageDeletionAsyncExecutor core pool size 2023-07-26 16:45:41 -04:00
Jon Chambers
c2ea4a5290 Update to the latest version of the spam filter 2023-07-26 16:45:13 -04:00
Jon Chambers
b691b8d37d Log successful client version refreshes 2023-07-26 16:41:54 -04:00
Jon Chambers
4ead8527c8 Use ClientReleasesManager when deciding whether to add client version tags 2023-07-26 16:41:54 -04:00
Jon Chambers
6f4801fd6f Add a manager class for checking "liveness" of client versions 2023-07-26 16:41:54 -04:00
Jon Chambers
10689843b0 Add a repository for client release information 2023-07-26 16:41:54 -04:00
Chris Eager
60cc0c482e Add @Produces to PUT /v1/accounts/apn 2023-07-26 16:35:23 -04:00
Jon Chambers
e1a5105c28 Revert "Restore max concurrency when migrating pre-keys"
This reverts commit ed8a1ed579.
2023-07-26 12:56:35 -04:00
Jon Chambers
ed8a1ed579 Restore max concurrency when migrating pre-keys 2023-07-26 12:34:32 -04:00
Jon Chambers
c3fd2e2284 Retry key storage attempts when migrating signed pre-keys 2023-07-26 12:34:32 -04:00
Chris Eager
872ef5d0a0 Add environment variable to toggle tcp appender 2023-07-24 13:13:13 -05:00
Chris Eager
b44599cd59 Remove unused jedis library 2023-07-24 10:54:34 -04:00
Jordan Rose
7a5dcc700e Add support for AuthCredentialAsPni with pniAsServiceId=true
Update to libsignal 0.30.0, and add a new query param to
/v1/certificate/auth/group, "pniAsServiceId=true", that uses the new
encoding of PNIs in zkgroup rather than encoding PNIs the same way as
ACIs, as we have been doing.

Also includes all the adjustments for the libsignal 0.30.0 update.
2023-07-24 10:53:59 -04:00
ravi-signal
705fb93e45 Add v4 attachment controller
Add AttachmentControllerV4 which can be configured to generate upload
forms for a TUS based CDN
2023-07-21 12:09:45 -05:00
Jon Chambers
9df923d916 Update keys gRPC endpoint to use service identifiers 2023-07-21 13:03:01 -04:00
Chris Eager
dc1cb9093a Remove unused code 2023-07-21 11:08:32 -05:00
Jon Chambers
e32043ae79 Remove outdated documentation 2023-07-21 10:24:17 -04:00
Jon Chambers
881c921d56 Update to the latest version of the spam filter 2023-07-21 09:44:53 -04:00
Jon Chambers
abb32bd919 Introduce "service identifiers" 2023-07-21 09:34:10 -04:00
Katherine Yen
4a6c7152cf Update to the latest version of the spam filter 2023-07-20 14:37:12 -07:00
Sergey Skrobotov
cf92007f66 Moving Account serialization logic to storage-specific classes 2023-07-20 14:28:07 -07:00
544 changed files with 28320 additions and 13759 deletions

View File

@@ -158,7 +158,7 @@ ij_java_keep_indents_on_empty_lines = false
ij_java_keep_line_breaks = true
ij_java_keep_multiple_expressions_in_one_line = false
ij_java_keep_simple_blocks_in_one_line = false
ij_java_keep_simple_classes_in_one_line = false
ij_java_keep_simple_classes_in_one_line = true
ij_java_keep_simple_lambdas_in_one_line = false
ij_java_keep_simple_methods_in_one_line = false
ij_java_label_indent_absolute = false

View File

@@ -1,9 +1,13 @@
name: Integration Tests
on: [workflow_dispatch]
on:
schedule:
- cron: '30 19 * * MON-FRI'
workflow_dispatch:
jobs:
build:
if: ${{ vars.INTEGRATION_TESTS_BUCKET != '' }}
runs-on: ubuntu-latest
permissions:
id-token: write

View File

@@ -4,6 +4,6 @@
<extension>
<groupId>fr.brouillard.oss</groupId>
<artifactId>jgitver-maven-plugin</artifactId>
<version>1.7.1</version>
<version>1.9.0</version>
</extension>
</extensions>

Binary file not shown.

View File

@@ -14,5 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
distributionSha256Sum=e896b60329a71b719d77bb4388b251a50aebcd73c62f69d510c858ce360afe0f
wrapperSha256Sum=e63a53cfb9c4d291ebe3c2b0edacb7622bbc480326beaa5a0456e412f52f066a

View File

@@ -23,7 +23,7 @@
<plugin>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-maven-plugin</artifactId>
<version>2.2.8</version>
<version>${swagger.version}</version>
<configuration>
<outputFileName>signal-server-openapi</outputFileName>
<outputPath>${project.build.directory}/openapi</outputPath>

View File

@@ -1,94 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2022 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>TextSecureServer</artifactId>
<groupId>org.whispersystems.textsecure</groupId>
<version>JGITVER</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>event-logger</artifactId>
<dependencies>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-logging</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<exclusions>
<exclusion>
<groupId>org.jetbrains</groupId>
<!--
depends on an outdated version (13.0) for JDK 6 compatibility, but its safe to override
https://youtrack.jetbrains.com/issue/KT-25047
-->
<artifactId>annotations</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-serialization-json</artifactId>
<version>${kotlinx-serialization.version}</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<configuration>
<compilerPlugins>
<plugin>kotlinx-serialization</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-serialization</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<configuration>
<!-- we don't want jib to execute on this module -->
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,40 +0,0 @@
/*
* Copyright 2022 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.event
import java.util.Collections
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
val module = SerializersModule {
polymorphic(Event::class) {
subclass(RemoteConfigSetEvent::class)
subclass(RemoteConfigDeleteEvent::class)
}
}
val jsonFormat = Json { serializersModule = module }
sealed interface Event
@Serializable
data class RemoteConfigSetEvent(
val identity: String,
val name: String,
val percentage: Int,
val defaultValue: String? = null,
val value: String? = null,
val hashKey: String? = null,
val uuids: Collection<String> = Collections.emptyList(),
) : Event
@Serializable
data class RemoteConfigDeleteEvent(
val identity: String,
val name: String,
) : Event

View File

@@ -1,41 +0,0 @@
/*
* Copyright 2022 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.event
import com.google.cloud.logging.LogEntry
import com.google.cloud.logging.Logging
import com.google.cloud.logging.MonitoredResourceUtil
import com.google.cloud.logging.Payload.JsonPayload
import com.google.cloud.logging.Severity
import com.google.protobuf.Struct
import com.google.protobuf.util.JsonFormat
import kotlinx.serialization.encodeToString
interface AdminEventLogger {
fun logEvent(event: Event, labels: Map<String, String>?)
fun logEvent(event: Event) = logEvent(event, null)
}
class NoOpAdminEventLogger : AdminEventLogger {
override fun logEvent(event: Event, labels: Map<String, String>?) {}
}
class GoogleCloudAdminEventLogger(private val logging: Logging, private val projectId: String, private val logName: String) : AdminEventLogger {
override fun logEvent(event: Event, labels: Map<String, String>?) {
val structBuilder = Struct.newBuilder()
JsonFormat.parser().merge(jsonFormat.encodeToString(event), structBuilder)
val struct = structBuilder.build()
val logEntryBuilder = LogEntry.newBuilder(JsonPayload.of(struct))
.setLogName(logName)
.setSeverity(Severity.NOTICE)
.setResource(MonitoredResourceUtil.getResource(projectId, "project"));
if (labels != null) {
logEntryBuilder.setLabels(labels);
}
logging.write(listOf(logEntryBuilder.build()))
}
}

View File

@@ -1,22 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.event
import com.google.cloud.logging.Logging
import org.junit.jupiter.api.Test
import org.mockito.Mockito.mock
class GoogleCloudAdminEventLoggerTest {
@Test
fun logEvent() {
val logging = mock(Logging::class.java)
val logger = GoogleCloudAdminEventLogger(logging, "my-project", "test")
val event = RemoteConfigDeleteEvent("token", "test")
logger.logEvent(event)
}
}

View File

@@ -28,7 +28,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
<version>3.1.2</version>
<configuration>
<excludes>
<exclude>**</exclude>
@@ -39,7 +39,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.0.0</version>
<version>3.1.2</version>
<configuration>
<additionalClasspathElements>
<additionalClasspathElement>${project.basedir}/.libs/software.amazon.awssdk-sso.jar</additionalClasspathElement>

View File

@@ -17,6 +17,7 @@ import java.net.URL;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Base64;
@@ -24,7 +25,6 @@ import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Executors;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.tuple.Pair;
@@ -67,38 +67,8 @@ public final class Operations {
}
public static TestUser newRegisteredUser(final String number) {
final byte[] registrationPassword = RandomUtils.nextBytes(32);
final String accountPassword = Base64.getEncoder().encodeToString(RandomUtils.nextBytes(32));
final TestUser user = TestUser.create(number, accountPassword, registrationPassword);
final AccountAttributes accountAttributes = user.accountAttributes();
INTEGRATION_TOOLS.populateRecoveryPassword(number, registrationPassword).join();
// register account
final RegistrationRequest registrationRequest = new RegistrationRequest(
null, registrationPassword, accountAttributes, true, false,
Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty());
final AccountIdentityResponse registrationResponse = apiPost("/v1/registration", registrationRequest)
.authorized(number, accountPassword)
.executeExpectSuccess(AccountIdentityResponse.class);
user.setAciUuid(registrationResponse.uuid());
user.setPniUuid(registrationResponse.pni());
// upload pre-key
final TestUser.PreKeySetPublicView preKeySetPublicView = user.preKeys(Device.MASTER_ID, false);
apiPut("/v2/keys", preKeySetPublicView)
.authorized(user, Device.MASTER_ID)
.executeExpectSuccess();
return user;
}
public static TestUser newRegisteredUserAtomic(final String number) {
final byte[] registrationPassword = RandomUtils.nextBytes(32);
final String accountPassword = Base64.getEncoder().encodeToString(RandomUtils.nextBytes(32));
final byte[] registrationPassword = randomBytes(32);
final String accountPassword = Base64.getEncoder().encodeToString(randomBytes(32));
final TestUser user = TestUser.create(number, accountPassword, registrationPassword);
final AccountAttributes accountAttributes = user.accountAttributes();
@@ -113,13 +83,12 @@ public final class Operations {
registrationPassword,
accountAttributes,
true,
true,
Optional.of(new IdentityKey(aciIdentityKeyPair.getPublicKey())),
Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey())),
Optional.of(generateSignedECPreKey(1, aciIdentityKeyPair)),
Optional.of(generateSignedECPreKey(2, pniIdentityKeyPair)),
Optional.of(generateSignedKEMPreKey(3, aciIdentityKeyPair)),
Optional.of(generateSignedKEMPreKey(4, pniIdentityKeyPair)),
new IdentityKey(aciIdentityKeyPair.getPublicKey()),
new IdentityKey(pniIdentityKeyPair.getPublicKey()),
generateSignedECPreKey(1, aciIdentityKeyPair),
generateSignedECPreKey(2, pniIdentityKeyPair),
generateSignedKEMPreKey(3, aciIdentityKeyPair),
generateSignedKEMPreKey(4, pniIdentityKeyPair),
Optional.empty(),
Optional.empty());
@@ -133,6 +102,13 @@ public final class Operations {
return user;
}
public record PrescribedVerificationNumber(String number, String verificationCode) {}
public static PrescribedVerificationNumber prescribedVerificationNumber() {
return new PrescribedVerificationNumber(
CONFIG.prescribedRegistrationNumber(),
CONFIG.prescribedRegistrationCode());
}
public static void deleteUser(final TestUser user) {
apiDelete("/v1/accounts/me").authorized(user).executeExpectSuccess();
}
@@ -180,6 +156,12 @@ public final class Operations {
}
}
private static byte[] randomBytes(int numBytes) {
final byte[] bytes = new byte[numBytes];
new SecureRandom().nextBytes(bytes);
return bytes;
}
public static RequestBuilder apiGet(final String endpoint) {
return new RequestBuilder(HttpRequest.newBuilder().GET(), endpoint);
}
@@ -233,10 +215,10 @@ public final class Operations {
}
public RequestBuilder authorized(final TestUser user) {
return authorized(user, Device.MASTER_ID);
return authorized(user, Device.PRIMARY_ID);
}
public RequestBuilder authorized(final TestUser user, final long deviceId) {
public RequestBuilder authorized(final TestUser user, final byte deviceId) {
final String username = "%s.%d".formatted(user.aciUuid().toString(), deviceId);
return authorized(username, user.accountPassword());
}

View File

@@ -16,13 +16,13 @@ import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
public class TestDevice {
private final long deviceId;
private final byte deviceId;
private final Map<Integer, Pair<IdentityKeyPair, SignedPreKeyRecord>> signedPreKeys = new ConcurrentHashMap<>();
public static TestDevice create(
final long deviceId,
final byte deviceId,
final IdentityKeyPair aciIdentityKeyPair,
final IdentityKeyPair pniIdentityKeyPair) {
final TestDevice device = new TestDevice(deviceId);
@@ -31,11 +31,11 @@ public class TestDevice {
return device;
}
public TestDevice(final long deviceId) {
public TestDevice(final byte deviceId) {
this.deviceId = deviceId;
}
public long deviceId() {
public byte deviceId() {
return deviceId;
}

View File

@@ -9,17 +9,19 @@ import static java.util.Objects.requireNonNull;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.security.SecureRandom;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang3.RandomUtils;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.IdentityKeyPair;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
import org.signal.libsignal.protocol.util.KeyHelper;
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.storage.Device;
@@ -27,9 +29,11 @@ public class TestUser {
private final int registrationId;
private final int pniRegistrationId;
private final IdentityKeyPair aciIdentityKey;
private final Map<Long, TestDevice> devices = new ConcurrentHashMap<>();
private final Map<Byte, TestDevice> devices = new ConcurrentHashMap<>();
private final byte[] unidentifiedAccessKey;
@@ -53,11 +57,14 @@ public class TestUser {
final IdentityKeyPair pniIdentityKey = IdentityKeyPair.generate();
// registration id
final int registrationId = KeyHelper.generateRegistrationId(false);
final int pniRegistrationId = KeyHelper.generateRegistrationId(false);
// uak
final byte[] unidentifiedAccessKey = RandomUtils.nextBytes(16);
final byte[] unidentifiedAccessKey = new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH];
new SecureRandom().nextBytes(unidentifiedAccessKey);
return new TestUser(
registrationId,
pniRegistrationId,
aciIdentityKey,
phoneNumber,
pniIdentityKey,
@@ -68,6 +75,7 @@ public class TestUser {
public TestUser(
final int registrationId,
final int pniRegistrationId,
final IdentityKeyPair aciIdentityKey,
final String phoneNumber,
final IdentityKeyPair pniIdentityKey,
@@ -75,13 +83,14 @@ public class TestUser {
final String accountPassword,
final byte[] registrationPassword) {
this.registrationId = registrationId;
this.pniRegistrationId = pniRegistrationId;
this.aciIdentityKey = aciIdentityKey;
this.phoneNumber = phoneNumber;
this.pniIdentityKey = pniIdentityKey;
this.unidentifiedAccessKey = unidentifiedAccessKey;
this.accountPassword = accountPassword;
this.registrationPassword = registrationPassword;
devices.put(Device.MASTER_ID, TestDevice.create(Device.MASTER_ID, aciIdentityKey, pniIdentityKey));
devices.put(Device.PRIMARY_ID, TestDevice.create(Device.PRIMARY_ID, aciIdentityKey, pniIdentityKey));
}
public int registrationId() {
@@ -117,7 +126,7 @@ public class TestUser {
}
public AccountAttributes accountAttributes() {
return new AccountAttributes(true, registrationId, "", "", true, new Device.DeviceCapabilities())
return new AccountAttributes(true, registrationId, pniRegistrationId, "".getBytes(StandardCharsets.UTF_8), "", true, new Device.DeviceCapabilities(false, false, false, false))
.withUnidentifiedAccessKey(unidentifiedAccessKey)
.withRecoveryPassword(registrationPassword);
}
@@ -146,7 +155,7 @@ public class TestUser {
this.registrationPassword = registrationPassword;
}
public PreKeySetPublicView preKeys(final long deviceId, final boolean pni) {
public PreKeySetPublicView preKeys(final byte deviceId, final boolean pni) {
final IdentityKeyPair identity = pni
? pniIdentityKey
: aciIdentityKey;

View File

@@ -10,5 +10,7 @@ import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguratio
public record Config(String domain,
String rootCert,
DynamoDbClientConfiguration dynamoDbClientConfiguration,
DynamoDbTables dynamoDbTables) {
DynamoDbTables dynamoDbTables,
String prescribedRegistrationNumber,
String prescribedRegistrationCode) {
}

View File

@@ -18,10 +18,10 @@ import org.signal.libsignal.usernames.Username;
import org.whispersystems.textsecuregcm.entities.AccountIdentifierResponse;
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
import org.whispersystems.textsecuregcm.entities.ConfirmUsernameHashRequest;
import org.whispersystems.textsecuregcm.entities.EncryptedUsername;
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest;
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse;
import org.whispersystems.textsecuregcm.entities.UsernameHashResponse;
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
public class AccountTest {
@@ -40,7 +40,7 @@ public class AccountTest {
@Test
public void testCreateAccountAtomic() throws Exception {
final TestUser user = Operations.newRegisteredUserAtomic("+19995550201");
final TestUser user = Operations.newRegisteredUser("+19995550201");
try {
final Pair<Integer, AccountIdentityResponse> execute = Operations.apiGet("/v1/accounts/whoami")
.authorized(user)
@@ -107,7 +107,7 @@ public class AccountTest {
final AccountIdentifierResponse accountIdentifierResponse = Operations
.apiGet("/v1/accounts/username_hash/" + Base64.getUrlEncoder().encodeToString(reservedHash))
.executeExpectSuccess(AccountIdentifierResponse.class);
assertEquals(user.aciUuid(), accountIdentifierResponse.uuid());
assertEquals(new AciServiceIdentifier(user.aciUuid()), accountIdentifierResponse.uuid());
// try authorized
Operations
.apiGet("/v1/accounts/username_hash/" + Base64.getUrlEncoder().encodeToString(reservedHash))

View File

@@ -11,8 +11,7 @@ import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.api.Test;
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
@@ -21,24 +20,15 @@ import org.whispersystems.textsecuregcm.storage.Device;
public class MessagingTest {
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testSendMessageUnsealed(final boolean atomicAccountCreation) throws Exception {
final TestUser userA;
final TestUser userB;
if (atomicAccountCreation) {
userA = Operations.newRegisteredUserAtomic("+19995550102");
userB = Operations.newRegisteredUserAtomic("+19995550103");
} else {
userA = Operations.newRegisteredUser("+19995550104");
userB = Operations.newRegisteredUser("+19995550105");
}
@Test
public void testSendMessageUnsealed() {
final TestUser userA = Operations.newRegisteredUser("+19995550102");
final TestUser userB = Operations.newRegisteredUser("+19995550103");
try {
final byte[] expectedContent = "Hello, World!".getBytes(StandardCharsets.UTF_8);
final String contentBase64 = Base64.getEncoder().encodeToString(expectedContent);
final IncomingMessage message = new IncomingMessage(1, Device.MASTER_ID, userB.registrationId(), contentBase64);
final IncomingMessage message = new IncomingMessage(1, Device.PRIMARY_ID, userB.registrationId(), contentBase64);
final IncomingMessageList messages = new IncomingMessageList(List.of(message), false, true, System.currentTimeMillis());
final Pair<Integer, SendMessageResponse> sendMessage = Operations

View File

@@ -19,7 +19,10 @@ public class RegistrationTest {
public void testRegistration() throws Exception {
final UpdateVerificationSessionRequest originalRequest = new UpdateVerificationSessionRequest(
"test", UpdateVerificationSessionRequest.PushTokenType.FCM, null, null, null, null);
final CreateVerificationSessionRequest input = new CreateVerificationSessionRequest("+19995550102", originalRequest);
final Operations.PrescribedVerificationNumber params = Operations.prescribedVerificationNumber();
final CreateVerificationSessionRequest input = new CreateVerificationSessionRequest(params.number(),
originalRequest);
final VerificationSessionResponse verificationSessionResponse = Operations
.apiPost("/v1/verification/session", input)
@@ -46,7 +49,8 @@ public class RegistrationTest {
.executeExpectSuccess(VerificationSessionResponse.class);
// verify code
final SubmitVerificationCodeRequest submitVerificationCodeRequest = new SubmitVerificationCodeRequest("265402");
final SubmitVerificationCodeRequest submitVerificationCodeRequest = new SubmitVerificationCodeRequest(
params.verificationCode());
final VerificationSessionResponse codeVerified = Operations
.apiPut("/v1/verification/session/%s/code".formatted(sessionId), submitVerificationCodeRequest)
.executeExpectSuccess(VerificationSessionResponse.class);

218
mvnw vendored
View File

@@ -19,7 +19,7 @@
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Maven Start Up Batch script
# Apache Maven Wrapper startup batch script, version 3.2.0
#
# Required ENV vars:
# ------------------
@@ -27,7 +27,6 @@
#
# Optional ENV vars
# -----------------
# M2_HOME - location of maven2's installed home dir
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@@ -54,7 +53,7 @@ fi
cygwin=false;
darwin=false;
mingw=false
case "`uname`" in
case "$(uname)" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
@@ -62,9 +61,9 @@ case "`uname`" in
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
export JAVA_HOME="`/usr/libexec/java_home`"
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
else
export JAVA_HOME="/Library/Java/Home"
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
fi
fi
;;
@@ -72,68 +71,38 @@ esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=`java-config --jre-home`
JAVA_HOME=$(java-config --jre-home)
fi
fi
if [ -z "$M2_HOME" ] ; then
## resolve links - $0 may be a link to maven's home
PRG="$0"
# need this for relative symlinks
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG="`dirname "$PRG"`/$link"
fi
done
saveddir=`pwd`
M2_HOME=`dirname "$PRG"`/..
# make it fully qualified
M2_HOME=`cd "$M2_HOME" && pwd`
cd "$saveddir"
# echo Using m2 at $M2_HOME
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --unix "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$M2_HOME" ] &&
M2_HOME="`(cd "$M2_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] &&
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="`which javac`"
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
javaExecutable="$(which javac)"
if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=`which readlink`
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
readLink=$(which readlink)
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
if $darwin ; then
javaHome="`dirname \"$javaExecutable\"`"
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
javaHome="$(dirname "\"$javaExecutable\"")"
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
else
javaExecutable="`readlink -f \"$javaExecutable\"`"
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
fi
javaHome="`dirname \"$javaExecutable\"`"
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
javaHome="$(dirname "\"$javaExecutable\"")"
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
@@ -149,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="`\\unset -f command; \\command -v java`"
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
fi
fi
@@ -163,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
@@ -184,96 +150,99 @@ find_maven_basedir() {
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=`cd "$wdir/.."; pwd`
wdir=$(cd "$wdir/.." || exit 1; pwd)
fi
# end of workaround
done
echo "${basedir}"
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
echo "$(tr -s '\n' ' ' < "$1")"
# Remove \r in case we run on Windows within Git Bash
# and check out the repository with auto CRLF management
# enabled. Otherwise, we may read lines that are delimited with
# \r\n and produce $'-Xarg\r' rather than -Xarg due to word
# splitting rules.
tr -s '\r\n' ' ' < "$1"
fi
}
BASE_DIR=`find_maven_basedir "$(pwd)"`
log() {
if [ "$MVNW_VERBOSE" = true ]; then
printf '%s\n' "$1"
fi
}
BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
log "$MAVEN_PROJECTBASEDIR"
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found .mvn/wrapper/maven-wrapper.jar"
fi
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
if [ -r "$wrapperJarPath" ]; then
log "Found $wrapperJarPath"
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
fi
log "Couldn't find $wrapperJarPath, downloading it ..."
if [ -n "$MVNW_REPOURL" ]; then
jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
else
jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
fi
while IFS="=" read key value; do
case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
while IFS="=" read -r key value; do
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
safeValue=$(echo "$value" | tr -d '\r')
case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
esac
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
if [ "$MVNW_VERBOSE" = true ]; then
echo "Downloading from: $jarUrl"
fi
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
log "Downloading from: $wrapperUrl"
if $cygwin; then
wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
fi
if command -v wget > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found wget ... using wget"
fi
log "Found wget ... using wget"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
else
wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
fi
elif command -v curl > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found curl ... using curl"
fi
log "Found curl ... using curl"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
curl -o "$wrapperJarPath" "$jarUrl" -f
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
else
curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
fi
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Falling back to using Java to download"
fi
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
log "Falling back to using Java to download"
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
# For Cygwin, switch paths to Windows format before running javac
if $cygwin; then
javaClass=`cygpath --path --windows "$javaClass"`
javaSource=$(cygpath --path --windows "$javaSource")
javaClass=$(cygpath --path --windows "$javaClass")
fi
if [ -e "$javaClass" ]; then
if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Compiling MavenWrapperDownloader.java ..."
fi
# Compiling the Java class
("$JAVA_HOME/bin/javac" "$javaClass")
if [ -e "$javaSource" ]; then
if [ ! -e "$javaClass" ]; then
log " - Compiling MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/javac" "$javaSource")
fi
if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
# Running the downloader
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Running MavenWrapperDownloader.java ..."
fi
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
if [ -e "$javaClass" ]; then
log " - Running MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
fi
fi
fi
@@ -282,35 +251,58 @@ fi
# End of extension
##########################################################################################
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
if [ "$MVNW_VERBOSE" = true ]; then
echo $MAVEN_PROJECTBASEDIR
# If specified, validate the SHA-256 sum of the Maven wrapper jar file
wrapperSha256Sum=""
while IFS="=" read -r key value; do
case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
esac
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
if [ -n "$wrapperSha256Sum" ]; then
wrapperSha256Result=false
if command -v sha256sum > /dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
wrapperSha256Result=true
fi
elif command -v shasum > /dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
wrapperSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
exit 1
fi
if [ $wrapperSha256Result = false ]; then
echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
exit 1
fi
fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --path --windows "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
fi
# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
export MAVEN_CMD_LINE_ARGS
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
# shellcheck disable=SC2086 # safe args
exec "$JAVACMD" \
$MAVEN_OPTS \
$MAVEN_DEBUG_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.home=${M2_HOME}" \
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

31
mvnw.cmd vendored
View File

@@ -18,13 +18,12 @@
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Maven Start Up Batch script
@REM Apache Maven Wrapper startup batch script, version 3.2.0
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM M2_HOME - location of maven2's installed home dir
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@@ -120,10 +119,10 @@ SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@@ -134,11 +133,11 @@ if exist %WRAPPER_JAR% (
)
) else (
if not "%MVNW_REPOURL%" == "" (
SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
)
if "%MVNW_VERBOSE%" == "true" (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
echo Downloading from: %DOWNLOAD_URL%
echo Downloading from: %WRAPPER_URL%
)
powershell -Command "&{"^
@@ -146,7 +145,7 @@ if exist %WRAPPER_JAR% (
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
"}"^
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
"}"
if "%MVNW_VERBOSE%" == "true" (
echo Finished downloading %WRAPPER_JAR%
@@ -154,6 +153,24 @@ if exist %WRAPPER_JAR% (
)
@REM End of extension
@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
SET WRAPPER_SHA_256_SUM=""
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
)
IF NOT %WRAPPER_SHA_256_SUM%=="" (
powershell -Command "&{"^
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
" exit 1;"^
"}"^
"}"
if ERRORLEVEL 1 goto error
)
@REM Provide a "standardized" way to retrieve the CLI args that will
@REM work with both Windows and non-Windows executions.
set MAVEN_CMD_LINE_ARGS=%*

123
pom.xml
View File

@@ -31,50 +31,53 @@
<modules>
<module>api-doc</module>
<module>event-logger</module>
<module>integration-tests</module>
<module>service</module>
<module>websocket-resources</module>
</modules>
<properties>
<aws.sdk2.version>2.19.8</aws.sdk2.version>
<braintree.version>3.19.0</braintree.version>
<commons-csv.version>1.9.0</commons-csv.version>
<commons-io.version>2.9.0</commons-io.version>
<dropwizard.version>2.0.34</dropwizard.version>
<aws.sdk2.version>2.21.5</aws.sdk2.version>
<braintree.version>3.27.0</braintree.version>
<commons-csv.version>1.10.0</commons-csv.version>
<commons-io.version>2.14.0</commons-io.version>
<dropwizard.version>3.0.4</dropwizard.version>
<dropwizard-metrics-datadog.version>1.1.13</dropwizard-metrics-datadog.version>
<google-cloud-libraries.version>26.1.3</google-cloud-libraries.version>
<grpc.version>1.51.1</grpc.version> <!-- this should be kept in sync with the value from Googles libraries-bom -->
<gson.version>2.9.0</gson.version>
<jackson.version>2.13.4</jackson.version>
<google-cloud-libraries.version>26.25.0</google-cloud-libraries.version>
<grpc.version>1.58.0</grpc.version> <!-- should be kept in sync with the value from Google libraries-bom -->
<gson.version>2.10.1</gson.version>
<!-- several libraries (AWS, Google Cloud) use Apache http components transitively, and we need to align them -->
<httpcore.version>4.4.16</httpcore.version>
<httpclient.version>4.5.14</httpclient.version>
<jackson.version>2.16.0</jackson.version>
<jaxb.version>2.3.1</jaxb.version>
<jedis.version>2.9.0</jedis.version>
<kotlin.version>1.8.0</kotlin.version>
<kotlinx-serialization.version>1.4.1</kotlinx-serialization.version>
<lettuce.version>6.2.4.RELEASE</lettuce.version>
<libphonenumber.version>8.12.54</libphonenumber.version>
<logstash.logback.version>7.2</logstash.logback.version>
<junit-pioneer.version>2.1.0</junit-pioneer.version>
<jsr305.version>3.0.2</jsr305.version>
<kotlin.version>1.9.10</kotlin.version>
<kotlinx-serialization.version>1.5.1</kotlinx-serialization.version>
<lettuce.version>6.2.6.RELEASE</lettuce.version>
<libphonenumber.version>8.13.23</libphonenumber.version>
<logstash.logback.version>7.3</logstash.logback.version>
<log4j-bom.version>2.21.0</log4j-bom.version>
<luajava.version>3.4.0</luajava.version>
<micrometer.version>1.10.3</micrometer.version>
<mockito.version>4.11.0</mockito.version>
<netty.version>4.1.82.Final</netty.version>
<opentest4j.version>1.2.0</opentest4j.version>
<protobuf.version>3.21.7</protobuf.version>
<micrometer.version>1.10.10</micrometer.version>
<netty.version>4.1.96.Final</netty.version>
<opentest4j.version>1.3.0</opentest4j.version>
<protobuf.version>3.24.3</protobuf.version> <!-- should be kept in sync with the value from Google libraries-bom -->
<pushy.version>0.15.2</pushy.version>
<reactive.grpc.version>1.2.4</reactive.grpc.version>
<reactor-bom.version>2022.0.12</reactor-bom.version> <!-- 3.5.x, see https://github.com/reactor/reactor#bom-versioning-scheme -->
<resilience4j.version>1.7.0</resilience4j.version>
<semver4j.version>3.1.0</semver4j.version>
<slf4j.version>1.7.30</slf4j.version>
<stripe.version>21.2.0</stripe.version>
<slf4j.version>2.0.9</slf4j.version>
<stripe.version>23.10.0</stripe.version>
<swagger.version>2.2.17</swagger.version>
<vavr.version>0.10.4</vavr.version>
<!-- 17.0.7_7-jre-jammy -->
<docker.image.sha256>ddf36656dc8920621fddf4928bdcb4b98c0d0e7bc9672f0cea8115c10ad5cbc6</docker.image.sha256>
<!-- 17.0.8_7-jre-jammy -->
<docker.image.sha256>b8af44d6a7e0615a7486d7307dd54bba23ff24e3aea14893fd2795e8c436d44e</docker.image.sha256>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<deploy.skip.s3.upload>false</deploy.skip.s3.upload>
</properties>
<groupId>org.whispersystems.textsecure</groupId>
@@ -147,7 +150,7 @@
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-bom</artifactId>
<version>2022.0.3</version> <!-- 3.5.x, see https://github.com/reactor/reactor#bom-versioning-scheme -->
<version>${reactor-bom.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@@ -224,18 +227,6 @@
<version>${jaxb.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.opentest4j</groupId>
<artifactId>opentest4j</artifactId>
@@ -253,11 +244,6 @@
<version>${slf4j.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
@@ -266,7 +252,7 @@
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.2</version>
<version>9.5</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -279,6 +265,11 @@
<artifactId>braintree-java</artifactId>
<version>${braintree.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>${jsr305.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
@@ -293,15 +284,25 @@
<dependency>
<groupId>org.signal</groupId>
<artifactId>libsignal-server</artifactId>
<version>0.26.0</version>
<version>0.37.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-bom</artifactId>
<version>2.17.1</version>
<version>${log4j-bom.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>${httpcore.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${httpclient.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
@@ -313,9 +314,10 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>2.35.0</version>
<groupId>org.wiremock</groupId>
<!-- use standalone until Dropwizard 4 + jakarta.* -->
<artifactId>wiremock-standalone</artifactId>
<version>3.3.1</version>
<scope>test</scope>
<exclusions>
<exclusion>
@@ -331,7 +333,6 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -347,7 +348,7 @@
<dependency>
<groupId>org.junit-pioneer</groupId>
<artifactId>junit-pioneer</artifactId>
<version>1.9.1</version>
<version>${junit-pioneer.version}</version>
<scope>test</scope>
</dependency>
@@ -389,7 +390,7 @@
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.3.1</version>
<version>3.4.0</version>
</plugin>
</plugins>
</pluginManagement>
@@ -430,7 +431,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<version>3.11.0</version>
<configuration>
<release>17</release>
</configuration>
@@ -439,7 +440,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
@@ -452,7 +453,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.2</version>
<version>3.3.0</version>
<executions>
<execution>
<id>copy</id>
@@ -472,7 +473,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<version>3.1.2</version>
<configuration>
<systemProperties>
<property>
@@ -486,7 +487,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.0.0-M3</version>
<version>3.3.0</version>
<executions>
<execution>
<goals>
@@ -507,7 +508,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>3.0.0-M1</version>
<version>3.1.1</version>
<configuration>
<skip>true</skip>
</configuration>
@@ -516,7 +517,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>3.0.0-M1</version>
<version>3.1.1</version>
<configuration>
<skip>true</skip>
</configuration>

View File

@@ -11,6 +11,11 @@ directoryV2.client.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789
svr2.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth tokens for Signal users
svr2.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth identity tokens for Signal users
svr3.userAuthenticationTokenSharedSecret: cbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR3 to generate auth tokens for Signal users
svr3.userIdTokenSharedSecret: dbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR3 to generate auth identity tokens for Signal users
tus.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG=
awsAttachments.accessKey: test
awsAttachments.accessSecret: test
@@ -44,6 +49,8 @@ gcpAttachments.rsaSigningKey: |
AAAAAAAA
-----END PRIVATE KEY-----
apn.teamId: team-id
apn.keyId: key-id
apn.signingKey: |
-----BEGIN PRIVATE KEY-----
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
@@ -65,11 +72,13 @@ hCaptcha.apiKey: unset
storageService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
backupService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
zkConfig.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
zkConfig-libsignal-0.36.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
zkConfig-libsignal-0.37.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
genericZkConfig.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
callingZkConfig.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
backupsZkConfig.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
paymentsService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= # base64-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users
paymentsService.fixerApiKey: unset
@@ -82,3 +91,7 @@ currentReportingKey.secret: AAAAAAAAAAA=
currentReportingKey.salt: AAAAAAAAAAA=
turn.secret: AAAAAAAAAAA=
linkDevice.secret: AAAAAAAAAAA=
tlsKeyStore.password: unset

View File

@@ -22,8 +22,9 @@ metrics:
tags:
- "env:staging"
- "service:chat"
transport:
apiKey: secret://datadog.apiKey
udpTransport:
statsdHost: localhost
port: 8125
excludesAttributes:
- m1_rate
- m5_rate
@@ -39,30 +40,22 @@ metrics:
- ^lettuce\..+$
reportOnStop: true
adminEventLoggingConfiguration:
credentials: |
{
"key": "value"
}
secondaryCredentials: |
{
"key": "value"
}
projectId: some-project-id
logName: some-log-name
grpcPort: 8080
tlsKeyStore:
password: secret://tlsKeyStore.password
stripe:
apiKey: secret://stripe.apiKey
idempotencyKeyGenerator: secret://stripe.idempotencyKeyGenerator
boostDescription: >
Example
supportedCurrencies:
- xts
# - ...
# - Nth supported currency
supportedCurrenciesByPaymentMethod:
CARD:
- usd
- eur
SEPA_DEBIT:
- eur
braintree:
merchantId: unset
@@ -73,10 +66,9 @@ braintree:
merchantAccounts:
# ISO 4217 currency code and its corresponding sub-merchant account
'xts': unset
supportedCurrencies:
- xts
# - ...
# - Nth supported currency
supportedCurrenciesByPaymentMethod:
PAYPAL:
- usd
dynamoDbClientConfiguration:
region: us-west-2 # AWS Region
@@ -87,7 +79,12 @@ dynamoDbTables:
phoneNumberTableName: Example_Accounts_PhoneNumbers
phoneNumberIdentifierTableName: Example_Accounts_PhoneNumberIdentifiers
usernamesTableName: Example_Accounts_Usernames
scanPageSize: 100
backups:
tableName: Example_Backups
backupMedia:
tableName: Example_BackupMedia
clientReleases:
tableName: Example_ClientReleases
deletedAccounts:
tableName: Example_DeletedAccounts
deletedAccountsLock:
@@ -107,10 +104,9 @@ dynamoDbTables:
messages:
tableName: Example_Messages
expiration: P30D # Duration of time until rows expire
pendingAccounts:
tableName: Example_PendingAccounts
pendingDevices:
tableName: Example_PendingDevices
onetimeDonations:
tableName: Example_OnetimeDonations
expiration: P90D
phoneNumberIdentifiers:
tableName: Example_PhoneNumberIdentifiers
profiles:
@@ -180,6 +176,34 @@ svr2:
AAAAAAAAAAAAAAAAAAAA
-----END CERTIFICATE-----
svr3:
uri: svr3.example.com
userAuthenticationTokenSharedSecret: secret://svr3.userAuthenticationTokenSharedSecret
userIdTokenSharedSecret: secret://svr3.userIdTokenSharedSecret
svrCaCertificates:
- |
-----BEGIN CERTIFICATE-----
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
AAAAAAAAAAAAAAAAAAAA
-----END CERTIFICATE-----
messageCache: # Redis server configuration for message store cache
persistDelayMinutes: 1
@@ -202,14 +226,15 @@ gcpAttachments: # GCP Storage configuration
pathPrefix:
rsaSigningKey: secret://gcpAttachments.rsaSigningKey
accountDatabaseCrawler:
chunkSize: 10 # accounts per run
tus:
uploadUri: https://example.org/upload
userAuthenticationTokenSharedSecret: secret://tus.userAuthenticationTokenSharedSecret
apn: # Apple Push Notifications configuration
sandbox: true
bundleId: com.example.textsecuregcm
keyId: unset
teamId: unset
keyId: secret://apn.keyId
teamId: secret://apn.teamId
signingKey: secret://apn.signingKey
fcm: # FCM configuration
@@ -221,8 +246,34 @@ cdn:
bucket: cdn # S3 Bucket name
region: us-west-2 # AWS region
datadog:
apiKey: secret://datadog.apiKey
clientCdn:
attachmentUrls:
2: https://cdn2.example.com/attachments/
caCertificates:
- |
-----BEGIN CERTIFICATE-----
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
AAAAAAAAAAAAAAAAAAAA
-----END CERTIFICATE-----
dogstatsd:
environment: dev
unidentifiedDelivery:
@@ -233,11 +284,13 @@ unidentifiedDelivery:
recaptcha:
projectPath: projects/example
credentialConfigurationJson: "{ }" # service account configuration for backend authentication
secondaryCredentialConfigurationJson: "{ }" # service account configuration for backend authentication
hCaptcha:
apiKey: secret://hCaptcha.apiKey
shortCode:
baseUrl: https://example.com/shortcodes/
storageService:
uri: storage.example.com
userAuthenticationTokenSharedSecret: secret://storageService.userAuthenticationTokenSharedSecret
@@ -265,39 +318,15 @@ storageService:
AAAAAAAAAAAAAAAAAAAA
-----END CERTIFICATE-----
backupService:
uri: backup.example.com
userAuthenticationTokenSharedSecret: secret://backupService.userAuthenticationTokenSharedSecret
backupCaCertificates:
- |
-----BEGIN CERTIFICATE-----
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
AAAAAAAAAAAAAAAAAAAA
-----END CERTIFICATE-----
zkConfig:
serverPublic: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
serverSecret: secret://zkConfig.serverSecret
genericZkConfig:
serverSecret: secret://genericZkConfig.serverSecret
callingZkConfig:
serverSecret: secret://callingZkConfig.serverSecret
backupsZkConfig:
serverSecret: secret://backupsZkConfig.serverSecret
appConfig:
application: example
@@ -305,17 +334,6 @@ appConfig:
configuration: example
remoteConfig:
authorizedUsers:
- # 1st authorized user
- # 2nd authorized user
- # ...
- # Nth authorized user
requiredHostedDomain: example.com
audiences:
- # 1st audience
- # 2nd audience
- # ...
- # Nth audience
globalConfig: # keys and values that are given to clients on GET /v1/config
EXAMPLE_KEY: VALUE
@@ -354,6 +372,7 @@ badges:
'1': TEST
subscription: # configuration for Stripe subscriptions
badgeExpiration: P30D
badgeGracePeriod: P15D
levels:
500:
@@ -367,6 +386,7 @@ subscription: # configuration for Stripe subscriptions
BRAINTREE: plan_example # braintree Plan ID
oneTimeDonations:
sepaMaximumEuros: '10000'
boost:
level: 1
expiration: P90D
@@ -395,10 +415,6 @@ registrationService:
{
"example": "example"
}
secondaryCredentialConfigurationJson: |
{
"example": "example"
}
identityTokenAudience: https://registration.example.com
registrationCaCertificate: | # Registration service TLS certificate trust root
-----BEGIN CERTIFICATE-----
@@ -425,3 +441,9 @@ registrationService:
turn:
secret: secret://turn.secret
commandStopListener:
path: /example/path
linkDevice:
secret: secret://linkDevice.secret

View File

@@ -10,11 +10,17 @@
<modelVersion>4.0.0</modelVersion>
<artifactId>service</artifactId>
<properties>
<firebase-admin.version>9.2.0</firebase-admin.version>
<java-uuid-generator.version>4.3.0</java-uuid-generator.version>
<sqlite4java.version>1.0.392</sqlite4java.version>
</properties>
<dependencies>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-jaxrs2</artifactId>
<version>2.2.8</version>
<version>${swagger.version}</version>
<exclusions>
<!-- org.yaml:snakeyaml is causing a dependency convergence error -->
<exclusion>
@@ -36,11 +42,6 @@
<artifactId>jakarta.ws.rs-api</artifactId>
</dependency>
<dependency>
<groupId>org.whispersystems.textsecure</groupId>
<artifactId>event-logger</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.whispersystems.textsecure</groupId>
<artifactId>websocket-resources</artifactId>
@@ -159,12 +160,6 @@
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-testing</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
@@ -189,7 +184,7 @@
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-api</artifactId>
<artifactId>websocket-jetty-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
@@ -208,7 +203,7 @@
<dependency>
<groupId>com.google.firebase</groupId>
<artifactId>firebase-admin</artifactId>
<version>9.1.1</version>
<version>${firebase-admin.version}</version>
</dependency>
<dependency>
@@ -255,7 +250,7 @@
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-datadog</artifactId>
<artifactId>micrometer-registry-statsd</artifactId>
</dependency>
<dependency>
<groupId>org.coursera</groupId>
@@ -291,6 +286,14 @@
<artifactId>reactor-grpc-stub</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>apache-client</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>netty-nio-client</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sts</artifactId>
@@ -323,11 +326,6 @@
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
@@ -371,12 +369,6 @@
<groupId>org.glassfish.jersey.test-framework</groupId>
<artifactId>jersey-test-framework-core</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
@@ -387,17 +379,13 @@
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</exclusion>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.almworks.sqlite4java</groupId>
<artifactId>sqlite4java</artifactId>
<version>1.0.392</version>
<version>${sqlite4java.version}</version>
<scope>test</scope>
</dependency>
@@ -434,20 +422,20 @@
<dependency>
<groupId>com.fasterxml.uuid</groupId>
<artifactId>java-uuid-generator</artifactId>
<version>4.0.1</version>
<version>${java-uuid-generator.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>DynamoDBLocal</artifactId>
<version>1.21.1</version>
<version>1.23.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.github.ganadist.sqlite4java</groupId>
<artifactId>libsqlite4java-osx-aarch64</artifactId>
<version>1.0.392</version>
<version>${sqlite4java.version}</version>
<type>dylib</type>
<scope>test</scope>
</dependency>
@@ -475,7 +463,14 @@
<dependency>
<groupId>com.apollographql.apollo3</groupId>
<artifactId>apollo-api-jvm</artifactId>
<version>3.7.1</version>
<version>3.8.2</version>
<exclusions>
<exclusion>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
@@ -488,7 +483,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<version>3.5.1</version>
<configuration>
<createDependencyReducedPom>true</createDependencyReducedPom>
<filters>
@@ -523,7 +518,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<version>3.6.0</version>
<configuration>
<descriptors>
<descriptor>assembly.xml</descriptor>
@@ -543,7 +538,7 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>properties-maven-plugin</artifactId>
<version>1.0.0</version>
<version>1.2.0</version>
<executions>
<execution>
<id>read-deploy-configuration</id>
@@ -558,28 +553,6 @@
</executions>
</plugin>
<plugin>
<groupId>com.bazaarvoice.maven.plugins</groupId>
<artifactId>s3-upload-maven-plugin</artifactId>
<version>2.0.1</version>
<configuration>
<source>${project.build.directory}/${project.build.finalName}-bin.tar.gz</source>
<bucketName>${deploy.bucketName}</bucketName>
<region>${deploy.bucketRegion}</region>
<destination>${project.build.finalName}-bin.tar.gz</destination>
<doNotUpload>${deploy.skip.s3.upload}</doNotUpload>
</configuration>
<executions>
<execution>
<id>deploy-to-s3</id>
<phase>deploy</phase>
<goals>
<goal>s3-upload</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
@@ -665,7 +638,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
<version>3.1.2</version>
<configuration>
<!-- work around PATCH not being a supported method on HttpUrlConnection -->
<argLine>--add-opens=java.base/java.net=ALL-UNNAMED</argLine>
@@ -687,7 +660,7 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
<executions>
<execution>
<id>check-all-service-config</id>

View File

@@ -5,7 +5,8 @@
package org.whispersystems.textsecuregcm;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.dropwizard.Configuration;
import io.dropwizard.core.Configuration;
import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
@@ -14,8 +15,7 @@ import java.util.Map;
import java.util.Set;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.AccountDatabaseCrawlerConfiguration;
import org.whispersystems.textsecuregcm.configuration.AdminEventLoggingConfiguration;
import org.whispersystems.textsecuregcm.attachments.TusConfiguration;
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
import org.whispersystems.textsecuregcm.configuration.AppConfigConfiguration;
import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration;
@@ -23,15 +23,20 @@ import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguratio
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
import org.whispersystems.textsecuregcm.configuration.BraintreeConfiguration;
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration;
import org.whispersystems.textsecuregcm.configuration.ClientCdnConfiguration;
import org.whispersystems.textsecuregcm.configuration.ClientReleaseConfiguration;
import org.whispersystems.textsecuregcm.configuration.CommandStopListenerConfiguration;
import org.whispersystems.textsecuregcm.configuration.DirectoryV2Configuration;
import org.whispersystems.textsecuregcm.configuration.DogstatsdConfiguration;
import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguration;
import org.whispersystems.textsecuregcm.configuration.DynamoDbTables;
import org.whispersystems.textsecuregcm.configuration.FcmConfiguration;
import org.whispersystems.textsecuregcm.configuration.GcpAttachmentsConfiguration;
import org.whispersystems.textsecuregcm.configuration.GenericZkConfig;
import org.whispersystems.textsecuregcm.configuration.HCaptchaConfiguration;
import org.whispersystems.textsecuregcm.configuration.LinkDeviceSecretConfiguration;
import org.whispersystems.textsecuregcm.configuration.MaxDeviceConfiguration;
import org.whispersystems.textsecuregcm.configuration.MessageByteLimitCardinalityEstimatorConfiguration;
import org.whispersystems.textsecuregcm.configuration.MessageCacheConfiguration;
import org.whispersystems.textsecuregcm.configuration.OneTimeDonationConfiguration;
import org.whispersystems.textsecuregcm.configuration.PaymentsServiceConfiguration;
@@ -41,12 +46,14 @@ import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
import org.whispersystems.textsecuregcm.configuration.RegistrationServiceConfiguration;
import org.whispersystems.textsecuregcm.configuration.RemoteConfigConfiguration;
import org.whispersystems.textsecuregcm.configuration.ReportMessageConfiguration;
import org.whispersystems.textsecuregcm.configuration.SecureBackupServiceConfiguration;
import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfiguration;
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration;
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery3Configuration;
import org.whispersystems.textsecuregcm.configuration.ShortCodeExpanderConfiguration;
import org.whispersystems.textsecuregcm.configuration.SpamFilterConfiguration;
import org.whispersystems.textsecuregcm.configuration.StripeConfiguration;
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
import org.whispersystems.textsecuregcm.configuration.TlsKeyStoreConfiguration;
import org.whispersystems.textsecuregcm.configuration.TurnSecretConfiguration;
import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration;
import org.whispersystems.textsecuregcm.configuration.ZkConfig;
@@ -59,7 +66,7 @@ public class WhisperServerConfiguration extends Configuration {
@NotNull
@Valid
@JsonProperty
private AdminEventLoggingConfiguration adminEventLoggingConfiguration;
private TlsKeyStoreConfiguration tlsKeyStore;
@NotNull
@Valid
@@ -99,7 +106,12 @@ public class WhisperServerConfiguration extends Configuration {
@NotNull
@Valid
@JsonProperty
private DatadogConfiguration datadog;
private ClientCdnConfiguration clientCdn;
@NotNull
@Valid
@JsonProperty
private DogstatsdConfiguration dogstatsd = new DogstatsdConfiguration();
@NotNull
@Valid
@@ -125,11 +137,10 @@ public class WhisperServerConfiguration extends Configuration {
@Valid
@JsonProperty
private SecureValueRecovery2Configuration svr2;
@NotNull
@Valid
@JsonProperty
private AccountDatabaseCrawlerConfiguration accountDatabaseCrawler;
private SecureValueRecovery3Configuration svr3;
@NotNull
@Valid
@@ -199,12 +210,12 @@ public class WhisperServerConfiguration extends Configuration {
@Valid
@NotNull
@JsonProperty
private SecureStorageServiceConfiguration storageService;
private ShortCodeExpanderConfiguration shortCode;
@Valid
@NotNull
@JsonProperty
private SecureBackupServiceConfiguration backupService;
private SecureStorageServiceConfiguration storageService;
@Valid
@NotNull
@@ -224,7 +235,12 @@ public class WhisperServerConfiguration extends Configuration {
@Valid
@NotNull
@JsonProperty
private GenericZkConfig genericZkConfig;
private GenericZkConfig callingZkConfig;
@Valid
@NotNull
@JsonProperty
private GenericZkConfig backupsZkConfig;
@Valid
@NotNull
@@ -270,15 +286,41 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private TurnSecretConfiguration turn;
@Valid
@NotNull
@JsonProperty
private TusConfiguration tus;
@Valid
@NotNull
@JsonProperty
private int grpcPort;
public AdminEventLoggingConfiguration getAdminEventLoggingConfiguration() {
return adminEventLoggingConfiguration;
@Valid
@NotNull
@JsonProperty
private ClientReleaseConfiguration clientRelease = new ClientReleaseConfiguration(Duration.ofHours(4));
@Valid
@NotNull
@JsonProperty
private MessageByteLimitCardinalityEstimatorConfiguration messageByteLimitCardinalityEstimator = new MessageByteLimitCardinalityEstimatorConfiguration(Duration.ofDays(1));
@Valid
@NotNull
@JsonProperty
private CommandStopListenerConfiguration commandStopListener;
@Valid
@NotNull
@JsonProperty
private LinkDeviceSecretConfiguration linkDevice;
public TlsKeyStoreConfiguration getTlsKeyStoreConfiguration() {
return tlsKeyStore;
}
public StripeConfiguration getStripe() {
return stripe;
}
@@ -303,6 +345,10 @@ public class WhisperServerConfiguration extends Configuration {
return hCaptcha;
}
public ShortCodeExpanderConfiguration getShortCodeRetrieverConfiguration() {
return shortCode;
}
public WebSocketConfiguration getWebSocketConfiguration() {
return webSocket;
}
@@ -327,9 +373,13 @@ public class WhisperServerConfiguration extends Configuration {
return metricsCluster;
}
public SecureValueRecovery2Configuration getSvr2Configuration() {
return svr2;
}
public SecureValueRecovery3Configuration getSvr3Configuration() {
return svr3;
}
public DirectoryV2Configuration getDirectoryV2Configuration() {
return directoryV2;
@@ -339,10 +389,6 @@ public class WhisperServerConfiguration extends Configuration {
return storageService;
}
public AccountDatabaseCrawlerConfiguration getAccountDatabaseCrawlerConfiguration() {
return accountDatabaseCrawler;
}
public MessageCacheConfiguration getMessageCacheConfiguration() {
return messageCache;
}
@@ -375,8 +421,12 @@ public class WhisperServerConfiguration extends Configuration {
return cdn;
}
public DatadogConfiguration getDatadogConfiguration() {
return datadog;
public ClientCdnConfiguration getClientCdn() {
return clientCdn;
}
public DogstatsdConfiguration getDatadogConfiguration() {
return dogstatsd;
}
public UnidentifiedDeliveryConfiguration getDeliveryCertificate() {
@@ -398,10 +448,6 @@ public class WhisperServerConfiguration extends Configuration {
return results;
}
public SecureBackupServiceConfiguration getSecureBackupServiceConfiguration() {
return backupService;
}
public PaymentsServiceConfiguration getPaymentsServiceConfiguration() {
return paymentsService;
}
@@ -414,8 +460,12 @@ public class WhisperServerConfiguration extends Configuration {
return zkConfig;
}
public GenericZkConfig getGenericZkConfig() {
return genericZkConfig;
public GenericZkConfig getCallingZkConfig() {
return callingZkConfig;
}
public GenericZkConfig getBackupsZkConfig() {
return backupsZkConfig;
}
public RemoteConfigConfiguration getRemoteConfigConfiguration() {
@@ -454,8 +504,27 @@ public class WhisperServerConfiguration extends Configuration {
return turn;
}
public TusConfiguration getTus() {
return tus;
}
public int getGrpcPort() {
return grpcPort;
}
public ClientReleaseConfiguration getClientReleaseConfiguration() {
return clientRelease;
}
public MessageByteLimitCardinalityEstimatorConfiguration getMessageByteLimitCardinalityEstimator() {
return messageByteLimitCardinalityEstimator;
}
public CommandStopListenerConfiguration getCommandStopListener() {
return commandStopListener;
}
public LinkDeviceSecretConfiguration getLinkDeviceSecretConfiguration() {
return linkDevice;
}
}

View File

@@ -7,22 +7,19 @@ package org.whispersystems.textsecuregcm;
import static com.codahale.metrics.MetricRegistry.name;
import static java.util.Objects.requireNonNull;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
import com.google.api.client.http.apache.v2.ApacheHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.logging.LoggingOptions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import io.dropwizard.Application;
import io.dropwizard.auth.AuthFilter;
import io.dropwizard.auth.PolymorphicAuthDynamicFeature;
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
import io.dropwizard.auth.basic.BasicCredentialAuthFilter;
import io.dropwizard.auth.basic.BasicCredentials;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import io.dropwizard.core.Application;
import io.dropwizard.core.server.DefaultServerFactory;
import io.dropwizard.core.setup.Bootstrap;
import io.dropwizard.core.setup.Environment;
import io.dropwizard.jetty.HttpsConnectorFactory;
import io.grpc.ServerBuilder;
import io.grpc.ServerInterceptors;
import io.lettuce.core.metrics.MicrometerCommandLatencyRecorder;
@@ -31,9 +28,7 @@ import io.lettuce.core.resource.ClientResources;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.binder.grpc.MetricCollectingServerInterceptor;
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
import java.io.ByteArrayInputStream;
import java.net.http.HttpClient;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.Duration;
import java.util.Collections;
@@ -51,9 +46,8 @@ import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletRegistration;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
import org.glassfish.jersey.server.ServerProperties;
import org.signal.event.AdminEventLogger;
import org.signal.event.GoogleCloudAdminEventLogger;
import org.signal.i18n.HeaderControlledResourceBundleLookup;
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
import org.signal.libsignal.zkgroup.ServerSecretParams;
@@ -63,6 +57,8 @@ import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation;
import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.attachments.GcsAttachmentGenerator;
import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.BaseAccountAuthenticator;
@@ -75,20 +71,28 @@ import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.auth.WebsocketRefreshApplicationEventListener;
import org.whispersystems.textsecuregcm.auth.grpc.BasicCredentialAuthenticationInterceptor;
import org.whispersystems.textsecuregcm.backup.BackupAuthManager;
import org.whispersystems.textsecuregcm.backup.BackupManager;
import org.whispersystems.textsecuregcm.backup.BackupsDb;
import org.whispersystems.textsecuregcm.backup.Cdn3BackupCredentialGenerator;
import org.whispersystems.textsecuregcm.backup.Cdn3RemoteStorageManager;
import org.whispersystems.textsecuregcm.badges.ConfiguredProfileBadgeConverter;
import org.whispersystems.textsecuregcm.badges.ResourceBundleLevelTranslator;
import org.whispersystems.textsecuregcm.captcha.CaptchaChecker;
import org.whispersystems.textsecuregcm.captcha.HCaptchaClient;
import org.whispersystems.textsecuregcm.captcha.RecaptchaClient;
import org.whispersystems.textsecuregcm.captcha.RegistrationCaptchaManager;
import org.whispersystems.textsecuregcm.captcha.ShortCodeExpander;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretStore;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretsModule;
import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.controllers.AccountControllerV2;
import org.whispersystems.textsecuregcm.controllers.ArchiveController;
import org.whispersystems.textsecuregcm.controllers.ArtController;
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV2;
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV3;
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV4;
import org.whispersystems.textsecuregcm.controllers.CallLinkController;
import org.whispersystems.textsecuregcm.controllers.CertificateController;
import org.whispersystems.textsecuregcm.controllers.ChallengeController;
@@ -103,28 +107,39 @@ import org.whispersystems.textsecuregcm.controllers.ProfileController;
import org.whispersystems.textsecuregcm.controllers.ProvisioningController;
import org.whispersystems.textsecuregcm.controllers.RegistrationController;
import org.whispersystems.textsecuregcm.controllers.RemoteConfigController;
import org.whispersystems.textsecuregcm.controllers.SecureBackupController;
import org.whispersystems.textsecuregcm.controllers.SecureStorageController;
import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery2Controller;
import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery3Controller;
import org.whispersystems.textsecuregcm.controllers.StickerController;
import org.whispersystems.textsecuregcm.controllers.SubscriptionController;
import org.whispersystems.textsecuregcm.controllers.VerificationController;
import org.whispersystems.textsecuregcm.currency.CoinMarketCapClient;
import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager;
import org.whispersystems.textsecuregcm.currency.FixerClient;
import org.whispersystems.textsecuregcm.grpc.GrpcServerManagedWrapper;
import org.whispersystems.textsecuregcm.grpc.UserAgentInterceptor;
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.filters.RemoteDeprecationFilter;
import org.whispersystems.textsecuregcm.filters.RequestStatisticsFilter;
import org.whispersystems.textsecuregcm.filters.TimestampResponseFilter;
import org.whispersystems.textsecuregcm.grpc.KeysGrpcService;
import org.whispersystems.textsecuregcm.grpc.AcceptLanguageInterceptor;
import org.whispersystems.textsecuregcm.grpc.AccountsAnonymousGrpcService;
import org.whispersystems.textsecuregcm.grpc.AccountsGrpcService;
import org.whispersystems.textsecuregcm.grpc.ErrorMappingInterceptor;
import org.whispersystems.textsecuregcm.grpc.ExternalServiceCredentialsAnonymousGrpcService;
import org.whispersystems.textsecuregcm.grpc.ExternalServiceCredentialsGrpcService;
import org.whispersystems.textsecuregcm.grpc.GrpcServerManagedWrapper;
import org.whispersystems.textsecuregcm.grpc.KeysAnonymousGrpcService;
import org.whispersystems.textsecuregcm.grpc.KeysGrpcService;
import org.whispersystems.textsecuregcm.grpc.PaymentsGrpcService;
import org.whispersystems.textsecuregcm.grpc.ProfileAnonymousGrpcService;
import org.whispersystems.textsecuregcm.grpc.ProfileGrpcService;
import org.whispersystems.textsecuregcm.grpc.UserAgentInterceptor;
import org.whispersystems.textsecuregcm.limits.CardinalityEstimator;
import org.whispersystems.textsecuregcm.limits.PushChallengeManager;
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.GrpcStatusRuntimeExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.InvalidWebsocketAddressExceptionMapper;
@@ -133,6 +148,7 @@ import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptio
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.ServerRejectedExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.SubscriptionProcessorExceptionMapper;
import org.whispersystems.textsecuregcm.metrics.MetricsApplicationEventListener;
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
import org.whispersystems.textsecuregcm.metrics.ReportedMessageMetricsListener;
@@ -153,10 +169,10 @@ import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
import org.whispersystems.textsecuregcm.s3.PolicySigner;
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
import org.whispersystems.textsecuregcm.spam.FilterSpam;
import org.whispersystems.textsecuregcm.spam.PushChallengeConfigProvider;
import org.whispersystems.textsecuregcm.spam.RateLimitChallengeListener;
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
import org.whispersystems.textsecuregcm.spam.ScoreThresholdProvider;
@@ -165,13 +181,15 @@ import org.whispersystems.textsecuregcm.storage.AccountLockManager;
import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ChangeNumberManager;
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
import org.whispersystems.textsecuregcm.storage.ClientReleases;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
import org.whispersystems.textsecuregcm.storage.KeysManager;
import org.whispersystems.textsecuregcm.storage.MessagesCache;
import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.OneTimeDonationsManager;
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
import org.whispersystems.textsecuregcm.storage.Profiles;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
@@ -183,11 +201,10 @@ import org.whispersystems.textsecuregcm.storage.RemoteConfigs;
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
import org.whispersystems.textsecuregcm.storage.VerificationCodeStore;
import org.whispersystems.textsecuregcm.storage.VerificationSessionManager;
import org.whispersystems.textsecuregcm.storage.VerificationSessions;
import org.whispersystems.textsecuregcm.subscriptions.BankMandateTranslator;
import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager;
import org.whispersystems.textsecuregcm.subscriptions.StripeManager;
import org.whispersystems.textsecuregcm.util.DynamoDbFromConfig;
@@ -201,10 +218,12 @@ import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
import org.whispersystems.textsecuregcm.workers.AssignUsernameCommand;
import org.whispersystems.textsecuregcm.workers.CertificateCommand;
import org.whispersystems.textsecuregcm.workers.CheckDynamicConfigurationCommand;
import org.whispersystems.textsecuregcm.workers.CrawlAccountsCommand;
import org.whispersystems.textsecuregcm.workers.DeleteUserCommand;
import org.whispersystems.textsecuregcm.workers.MessagePersisterServiceCommand;
import org.whispersystems.textsecuregcm.workers.MigrateSignedECPreKeysCommand;
import org.whispersystems.textsecuregcm.workers.ProcessPushNotificationFeedbackCommand;
import org.whispersystems.textsecuregcm.workers.RemoveExpiredAccountsCommand;
import org.whispersystems.textsecuregcm.workers.RemoveExpiredLinkedDevicesCommand;
import org.whispersystems.textsecuregcm.workers.ScheduledApnPushNotificationSenderServiceCommand;
import org.whispersystems.textsecuregcm.workers.ServerVersionCommand;
import org.whispersystems.textsecuregcm.workers.SetRequestLoggingEnabledTask;
@@ -223,6 +242,7 @@ import software.amazon.awssdk.auth.credentials.WebIdentityTokenFileCredentialsPr
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.S3Client;
public class WhisperServerService extends Application<WhisperServerConfiguration> {
@@ -256,10 +276,12 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
bootstrap.addCommand(new SetUserDiscoverabilityCommand());
bootstrap.addCommand(new AssignUsernameCommand());
bootstrap.addCommand(new UnlinkDeviceCommand());
bootstrap.addCommand(new CrawlAccountsCommand());
bootstrap.addCommand(new ScheduledApnPushNotificationSenderServiceCommand());
bootstrap.addCommand(new MessagePersisterServiceCommand());
bootstrap.addCommand(new MigrateSignedECPreKeysCommand());
bootstrap.addCommand(new RemoveExpiredAccountsCommand(Clock.systemUTC()));
bootstrap.addCommand(new ProcessPushNotificationFeedbackCommand(Clock.systemUTC()));
bootstrap.addCommand(new RemoveExpiredLinkedDevicesCommand());
}
@Override
@@ -276,16 +298,26 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
MetricsUtil.configureRegistries(config, environment);
final boolean useSecondaryCredentialsJson = Optional.ofNullable(
System.getenv("SIGNAL_USE_SECONDARY_CREDENTIALS_JSON"))
final boolean useRemoteAddress = Optional.ofNullable(
System.getenv("SIGNAL_USE_REMOTE_ADDRESS"))
.isPresent();
if (config.getServerFactory() instanceof DefaultServerFactory defaultServerFactory) {
defaultServerFactory.getApplicationConnectors()
.forEach(connectorFactory -> {
if (connectorFactory instanceof HttpsConnectorFactory h) {
h.setKeyStorePassword(config.getTlsKeyStoreConfiguration().password().value());
}
});
}
HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup =
new HeaderControlledResourceBundleLookup();
ConfiguredProfileBadgeConverter profileBadgeConverter = new ConfiguredProfileBadgeConverter(
clock, config.getBadges(), headerControlledResourceBundleLookup);
ResourceBundleLevelTranslator resourceBundleLevelTranslator = new ResourceBundleLevelTranslator(
headerControlledResourceBundleLookup);
BankMandateTranslator bankMandateTranslator = new BankMandateTranslator(headerControlledResourceBundleLookup);
DynamoDbAsyncClient dynamoDbAsyncClient = DynamoDbFromConfig.asyncClient(config.getDynamoDbClientConfiguration(),
AWSSDK_CREDENTIALS_PROVIDER);
@@ -293,9 +325,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
DynamoDbClient dynamoDbClient = DynamoDbFromConfig.client(config.getDynamoDbClientConfiguration(),
AWSSDK_CREDENTIALS_PROVIDER);
DeletedAccounts deletedAccounts = new DeletedAccounts(dynamoDbClient,
config.getDynamoDbTables().getDeletedAccounts().getTableName());
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager =
new DynamicConfigurationManager<>(config.getAppConfig().getApplication(),
config.getAppConfig().getEnvironment(),
@@ -306,7 +335,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
Metrics.gaugeCollectionSize(name(getClass(), "messageDeletionQueueSize"), Collections.emptyList(),
messageDeletionQueue);
ExecutorService messageDeletionAsyncExecutor = environment.lifecycle()
.executorService(name(getClass(), "messageDeletionAsyncExecutor-%d")).maxThreads(16)
.executorService(name(getClass(), "messageDeletionAsyncExecutor-%d"))
.minThreads(2)
.maxThreads(2)
.allowCoreThreadTimeOut(true)
.workQueue(messageDeletionQueue).build();
Accounts accounts = new Accounts(
@@ -316,13 +348,15 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
config.getDynamoDbTables().getAccounts().getPhoneNumberTableName(),
config.getDynamoDbTables().getAccounts().getPhoneNumberIdentifierTableName(),
config.getDynamoDbTables().getAccounts().getUsernamesTableName(),
config.getDynamoDbTables().getAccounts().getScanPageSize());
config.getDynamoDbTables().getDeletedAccounts().getTableName());
ClientReleases clientReleases = new ClientReleases(dynamoDbAsyncClient,
config.getDynamoDbTables().getClientReleases().getTableName());
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(dynamoDbClient,
config.getDynamoDbTables().getPhoneNumberIdentifiers().getTableName());
Profiles profiles = new Profiles(dynamoDbClient, dynamoDbAsyncClient,
config.getDynamoDbTables().getProfiles().getTableName());
KeysManager keys = new KeysManager(
dynamoDbAsyncClient,
KeysManager keysManager = new KeysManager(
dynamoDbAsyncClient,
config.getDynamoDbTables().getEcKeys().getTableName(),
config.getDynamoDbTables().getKemKeys().getTableName(),
config.getDynamoDbTables().getEcSignedPreKeys().getTableName(),
@@ -339,10 +373,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
ReportMessageDynamoDb reportMessageDynamoDb = new ReportMessageDynamoDb(dynamoDbClient,
config.getDynamoDbTables().getReportMessage().getTableName(),
config.getReportMessageConfiguration().getReportTtl());
VerificationCodeStore pendingAccounts = new VerificationCodeStore(dynamoDbClient,
config.getDynamoDbTables().getPendingAccounts().getTableName());
VerificationCodeStore pendingDevices = new VerificationCodeStore(dynamoDbClient,
config.getDynamoDbTables().getPendingDevices().getTableName());
RegistrationRecoveryPasswords registrationRecoveryPasswords = new RegistrationRecoveryPasswords(
config.getDynamoDbTables().getRegistrationRecovery().getTableName(),
config.getDynamoDbTables().getRegistrationRecovery().getExpiration(),
@@ -381,9 +411,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
.scheduledExecutorService(name(getClass(), "recurringJob-%d")).threads(6).build();
ScheduledExecutorService websocketScheduledExecutor = environment.lifecycle()
.scheduledExecutorService(name(getClass(), "websocket-%d")).threads(8).build();
ExecutorService keyspaceNotificationDispatchExecutor = environment.lifecycle()
.executorService(name(getClass(), "keyspaceNotification-%d")).maxThreads(16)
.workQueue(keyspaceNotificationDispatchQueue).build();
ExecutorService keyspaceNotificationDispatchExecutor = ExecutorServiceMetrics.monitor(Metrics.globalRegistry,
environment.lifecycle()
.executorService(name(getClass(), "keyspaceNotification-%d"))
.maxThreads(16)
.workQueue(keyspaceNotificationDispatchQueue)
.build(),
MetricsUtil.name(getClass(), "keyspaceNotificationExecutor"),
MetricsUtil.PREFIX);
ExecutorService apnSenderExecutor = environment.lifecycle().executorService(name(getClass(), "apnSender-%d"))
.maxThreads(1).minThreads(1).build();
ExecutorService fcmSenderExecutor = environment.lifecycle().executorService(name(getClass(), "fcmSender-%d"))
@@ -396,6 +431,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
.scheduledExecutorService(name(getClass(), "secureValueRecoveryServiceRetry-%d")).threads(1).build();
ScheduledExecutorService storageServiceRetryExecutor = environment.lifecycle()
.scheduledExecutorService(name(getClass(), "storageServiceRetry-%d")).threads(1).build();
ScheduledExecutorService hcaptchaRetryExecutor = environment.lifecycle()
.scheduledExecutorService(name(getClass(), "hCaptchaRetry-%d")).threads(1).build();
ScheduledExecutorService remoteStorageExecutor = environment.lifecycle()
.scheduledExecutorService(name(getClass(), "remoteStorageRetry-%d")).threads(1).build();
Scheduler messageDeliveryScheduler = Schedulers.fromExecutorService(
ExecutorServiceMetrics.monitor(Metrics.globalRegistry,
@@ -429,26 +468,25 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
.maxThreads(2)
.minThreads(2)
.build();
ExecutorService accountLockExecutor = environment.lifecycle()
.executorService(name(getClass(), "accountLock-%d"))
.minThreads(8)
.maxThreads(8)
.build();
ExecutorService clientPresenceExecutor = environment.lifecycle()
.executorService(name(getClass(), "clientPresence-%d"))
.minThreads(8)
.maxThreads(8)
.build();
ScheduledExecutorService subscriptionProcessorRetryExecutor = environment.lifecycle()
.scheduledExecutorService(name(getClass(), "subscriptionProcessorRetry-%d")).threads(1).build();
final AdminEventLogger adminEventLogger = new GoogleCloudAdminEventLogger(
LoggingOptions.newBuilder().setProjectId(config.getAdminEventLoggingConfiguration().projectId())
.setCredentials(GoogleCredentials.fromStream(new ByteArrayInputStream(
useSecondaryCredentialsJson
? config.getAdminEventLoggingConfiguration().secondaryCredentials().getBytes(StandardCharsets.UTF_8)
: config.getAdminEventLoggingConfiguration().credentials().getBytes(StandardCharsets.UTF_8))))
.build().getService(),
config.getAdminEventLoggingConfiguration().projectId(),
config.getAdminEventLoggingConfiguration().logName());
StripeManager stripeManager = new StripeManager(config.getStripe().apiKey().value(), subscriptionProcessorExecutor,
config.getStripe().idempotencyKeyGenerator().value(), config.getStripe().boostDescription(), config.getStripe()
.supportedCurrencies());
config.getStripe().idempotencyKeyGenerator().value(), config.getStripe().boostDescription(), config.getStripe().supportedCurrenciesByPaymentMethod());
BraintreeManager braintreeManager = new BraintreeManager(config.getBraintree().merchantId(),
config.getBraintree().publicKey(), config.getBraintree().privateKey().value(),
config.getBraintree().environment(),
config.getBraintree().supportedCurrencies(), config.getBraintree().merchantAccounts(),
config.getBraintree().supportedCurrenciesByPaymentMethod(), config.getBraintree().merchantAccounts(),
config.getBraintree().graphqlUrl(), config.getBraintree().circuitBreaker(), subscriptionProcessorExecutor,
subscriptionProcessorRetryExecutor);
@@ -456,14 +494,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
config.getDirectoryV2Configuration().getDirectoryV2ClientConfiguration());
ExternalServiceCredentialsGenerator storageCredentialsGenerator = SecureStorageController.credentialsGenerator(
config.getSecureStorageServiceConfiguration());
ExternalServiceCredentialsGenerator backupCredentialsGenerator = SecureBackupController.credentialsGenerator(
config.getSecureBackupServiceConfiguration());
ExternalServiceCredentialsGenerator paymentsCredentialsGenerator = PaymentsController.credentialsGenerator(
config.getPaymentsServiceConfiguration());
ExternalServiceCredentialsGenerator artCredentialsGenerator = ArtController.credentialsGenerator(
config.getArtServiceConfiguration());
ExternalServiceCredentialsGenerator svr2CredentialsGenerator = SecureValueRecovery2Controller.credentialsGenerator(
config.getSvr2Configuration());
config.getSvr2Configuration());
ExternalServiceCredentialsGenerator svr3CredentialsGenerator = SecureValueRecovery3Controller.credentialsGenerator(
config.getSvr3Configuration());
dynamicConfigurationManager.start();
@@ -476,27 +514,24 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
RegistrationServiceClient registrationServiceClient = new RegistrationServiceClient(
config.getRegistrationServiceConfiguration().host(),
config.getRegistrationServiceConfiguration().port(),
useSecondaryCredentialsJson
? config.getRegistrationServiceConfiguration().secondaryCredentialConfigurationJson()
: config.getRegistrationServiceConfiguration().credentialConfigurationJson(),
config.getRegistrationServiceConfiguration().credentialConfigurationJson(),
config.getRegistrationServiceConfiguration().identityTokenAudience(),
config.getRegistrationServiceConfiguration().registrationCaCertificate(),
registrationCallbackExecutor);
SecureBackupClient secureBackupClient = new SecureBackupClient(backupCredentialsGenerator,
secureValueRecoveryServiceExecutor, secureValueRecoveryServiceRetryExecutor,
config.getSecureBackupServiceConfiguration());
SecureValueRecovery2Client secureValueRecovery2Client = new SecureValueRecovery2Client(svr2CredentialsGenerator,
secureValueRecoveryServiceExecutor, secureValueRecoveryServiceRetryExecutor, config.getSvr2Configuration());
SecureStorageClient secureStorageClient = new SecureStorageClient(storageCredentialsGenerator,
storageServiceExecutor, storageServiceRetryExecutor, config.getSecureStorageServiceConfiguration());
ClientPresenceManager clientPresenceManager = new ClientPresenceManager(clientPresenceCluster, recurringJobExecutor,
keyspaceNotificationDispatchExecutor);
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
StoredVerificationCodeManager pendingDevicesManager = new StoredVerificationCodeManager(pendingDevices);
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheCluster);
MessagesCache messagesCache = new MessagesCache(messagesCluster, messagesCluster,
keyspaceNotificationDispatchExecutor, messageDeliveryScheduler, messageDeletionAsyncExecutor, clock);
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster, dynamicConfigurationManager);
ClientReleaseManager clientReleaseManager = new ClientReleaseManager(clientReleases,
recurringJobExecutor,
config.getClientReleaseConfiguration().refreshInterval(),
Clock.systemUTC());
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster, clientReleaseManager);
ReportMessageManager reportMessageManager = new ReportMessageManager(reportMessageDynamoDb, rateLimitersCluster,
config.getReportMessageConfiguration().getCounterTtl());
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, reportMessageManager,
@@ -504,10 +539,11 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
AccountLockManager accountLockManager = new AccountLockManager(dynamoDbClient,
config.getDynamoDbTables().getDeletedAccountsLock().getTableName());
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
accountLockManager, deletedAccounts, keys, messagesManager, profilesManager,
pendingAccountsManager, secureStorageClient, secureBackupClient, secureValueRecovery2Client,
accountLockManager, keysManager, messagesManager, profilesManager,
secureStorageClient, secureValueRecovery2Client,
clientPresenceManager,
experimentEnrollmentManager, registrationRecoveryPasswordsManager, clock);
experimentEnrollmentManager, registrationRecoveryPasswordsManager, accountLockExecutor, clientPresenceExecutor,
clock);
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
APNSender apnSender = new APNSender(apnSenderExecutor, config.getApnConfiguration());
FcmSender fcmSender = new FcmSender(fcmSenderExecutor, config.getFcmConfiguration().credentials().value());
@@ -525,6 +561,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
config.getDynamoDbTables().getIssuedReceipts().getExpiration(),
dynamoDbAsyncClient,
config.getDynamoDbTables().getIssuedReceipts().getGenerator());
OneTimeDonationsManager oneTimeDonationsManager = new OneTimeDonationsManager(
config.getDynamoDbTables().getOnetimeDonations().getTableName(), config.getDynamoDbTables().getOnetimeDonations().getExpiration(), dynamoDbAsyncClient);
RedeemedReceiptsManager redeemedReceiptsManager = new RedeemedReceiptsManager(clock,
config.getDynamoDbTables().getRedeemedReceipts().getTableName(),
dynamoDbAsyncClient,
@@ -533,7 +571,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
config.getDynamoDbTables().getSubscriptions().getTableName(), dynamoDbAsyncClient);
final RegistrationLockVerificationManager registrationLockVerificationManager = new RegistrationLockVerificationManager(
accountsManager, clientPresenceManager, backupCredentialsGenerator, svr2CredentialsGenerator, registrationRecoveryPasswordsManager, pushNotificationManager, rateLimiters);
accountsManager, clientPresenceManager, svr2CredentialsGenerator, registrationRecoveryPasswordsManager, pushNotificationManager, rateLimiters);
final PhoneVerificationTokenManager phoneVerificationTokenManager = new PhoneVerificationTokenManager(
registrationServiceClient, registrationRecoveryPasswordsManager);
@@ -552,17 +590,25 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
final TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager,
config.getTurnSecretConfiguration().secret().value());
final CardinalityEstimator messageByteLimitCardinalityEstimator = new CardinalityEstimator(
rateLimitersCluster,
"message_byte_limit",
config.getMessageByteLimitCardinalityEstimator().period());
RecaptchaClient recaptchaClient = new RecaptchaClient(
config.getRecaptchaConfiguration().projectPath(),
useSecondaryCredentialsJson
? config.getRecaptchaConfiguration().secondaryCredentialConfigurationJson()
: config.getRecaptchaConfiguration().credentialConfigurationJson(),
config.getRecaptchaConfiguration().credentialConfigurationJson(),
dynamicConfigurationManager);
HttpClient hcaptchaHttpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2)
HCaptchaClient hCaptchaClient = new HCaptchaClient(
config.getHCaptchaConfiguration().getApiKey().value(),
hcaptchaRetryExecutor,
config.getHCaptchaConfiguration().getCircuitBreaker(),
config.getHCaptchaConfiguration().getRetry(),
dynamicConfigurationManager);
HttpClient shortCodeRetrieverHttpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(10)).build();
HCaptchaClient hCaptchaClient = new HCaptchaClient(config.getHCaptchaConfiguration().apiKey().value(), hcaptchaHttpClient,
dynamicConfigurationManager);
CaptchaChecker captchaChecker = new CaptchaChecker(List.of(recaptchaClient, hCaptchaClient));
ShortCodeExpander shortCodeRetriever = new ShortCodeExpander(shortCodeRetrieverHttpClient, config.getShortCodeRetrieverConfiguration().baseUrl());
CaptchaChecker captchaChecker = new CaptchaChecker(shortCodeRetriever, List.of(recaptchaClient, hCaptchaClient));
PushChallengeManager pushChallengeManager = new PushChallengeManager(pushNotificationManager,
pushChallengeDynamoDb);
@@ -575,7 +621,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
FixerClient fixerClient = new FixerClient(currencyClient, config.getPaymentsServiceConfiguration().fixerApiKey().value());
CoinMarketCapClient coinMarketCapClient = new CoinMarketCapClient(currencyClient, config.getPaymentsServiceConfiguration().coinMarketCapApiKey().value(), config.getPaymentsServiceConfiguration().coinMarketCapCurrencyIds());
CurrencyConversionManager currencyManager = new CurrencyConversionManager(fixerClient, coinMarketCapClient,
cacheCluster, config.getPaymentsServiceConfiguration().paymentCurrencies(), Clock.systemUTC());
cacheCluster, config.getPaymentsServiceConfiguration().paymentCurrencies(), recurringJobExecutor, Clock.systemUTC());
environment.lifecycle().manage(apnSender);
environment.lifecycle().manage(apnPushNotificationScheduler);
@@ -584,6 +630,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.lifecycle().manage(clientPresenceManager);
environment.lifecycle().manage(currencyManager);
environment.lifecycle().manage(registrationServiceClient);
environment.lifecycle().manage(clientReleaseManager);
final RegistrationCaptchaManager registrationCaptchaManager = new RegistrationCaptchaManager(captchaChecker,
rateLimiters, config.getTestDevices(), dynamicConfigurationManager);
@@ -596,17 +643,49 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
.credentialsProvider(cdnCredentialsProvider)
.region(Region.of(config.getCdnConfiguration().region()))
.build();
S3AsyncClient asyncCdnS3Client = S3AsyncClient.builder()
.credentialsProvider(cdnCredentialsProvider)
.region(Region.of(config.getCdnConfiguration().region()))
.build();
final GcsAttachmentGenerator gcsAttachmentGenerator = new GcsAttachmentGenerator(
config.getGcpAttachmentsConfiguration().domain(),
config.getGcpAttachmentsConfiguration().email(),
config.getGcpAttachmentsConfiguration().maxSizeInBytes(),
config.getGcpAttachmentsConfiguration().pathPrefix(),
config.getGcpAttachmentsConfiguration().rsaSigningKey().value());
PostPolicyGenerator profileCdnPolicyGenerator = new PostPolicyGenerator(config.getCdnConfiguration().region(),
config.getCdnConfiguration().bucket(), config.getCdnConfiguration().accessKey().value());
PolicySigner profileCdnPolicySigner = new PolicySigner(config.getCdnConfiguration().accessSecret().value(),
config.getCdnConfiguration().region());
ServerSecretParams zkSecretParams = new ServerSecretParams(config.getZkConfig().serverSecret().value());
GenericServerSecretParams genericZkSecretParams = new GenericServerSecretParams(config.getGenericZkConfig().serverSecret().value());
GenericServerSecretParams callingGenericZkSecretParams = new GenericServerSecretParams(config.getCallingZkConfig().serverSecret().value());
GenericServerSecretParams backupsGenericZkSecretParams = new GenericServerSecretParams(config.getBackupsZkConfig().serverSecret().value());
ServerZkProfileOperations zkProfileOperations = new ServerZkProfileOperations(zkSecretParams);
ServerZkAuthOperations zkAuthOperations = new ServerZkAuthOperations(zkSecretParams);
ServerZkReceiptOperations zkReceiptOperations = new ServerZkReceiptOperations(zkSecretParams);
Cdn3BackupCredentialGenerator cdn3BackupCredentialGenerator = new Cdn3BackupCredentialGenerator(config.getTus());
BackupAuthManager backupAuthManager = new BackupAuthManager(dynamicConfigurationManager, rateLimiters, accountsManager, backupsGenericZkSecretParams, clock);
BackupsDb backupsDb = new BackupsDb(
dynamoDbAsyncClient,
config.getDynamoDbTables().getBackups().getTableName(),
config.getDynamoDbTables().getBackupMedia().getTableName(),
clock);
BackupManager backupManager = new BackupManager(
backupsDb,
backupsGenericZkSecretParams,
cdn3BackupCredentialGenerator,
new Cdn3RemoteStorageManager(
remoteStorageExecutor,
config.getClientCdn().getCircuitBreaker(),
config.getClientCdn().getRetry(),
config.getClientCdn().getCaCertificates()),
config.getClientCdn().getAttachmentUrls(),
clock);
AuthFilter<BasicCredentials, AuthenticatedAccount> accountAuthFilter = new BasicCredentialAuthFilter.Builder<AuthenticatedAccount>().setAuthenticator(
accountAuthenticator).buildAuthFilter();
AuthFilter<BasicCredentials, DisabledPermittedAuthenticatedAccount> disabledPermittedAccountAuthFilter = new BasicCredentialAuthFilter.Builder<DisabledPermittedAuthenticatedAccount>().setAuthenticator(
@@ -616,10 +695,16 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
new BasicCredentialAuthenticationInterceptor(new BaseAccountAuthenticator(accountsManager));
final ServerBuilder<?> grpcServer = ServerBuilder.forPort(config.getGrpcPort())
// TODO: specialize metrics with user-agent platform
.intercept(new MetricCollectingServerInterceptor(Metrics.globalRegistry))
.addService(ServerInterceptors.intercept(new KeysGrpcService(accountsManager, keys, rateLimiters), basicCredentialAuthenticationInterceptor))
.addService(new KeysAnonymousGrpcService(accountsManager, keys));
.addService(ServerInterceptors.intercept(new AccountsGrpcService(accountsManager, rateLimiters, usernameHashZkProofVerifier, registrationRecoveryPasswordsManager), basicCredentialAuthenticationInterceptor))
.addService(new AccountsAnonymousGrpcService(accountsManager, rateLimiters))
.addService(ExternalServiceCredentialsGrpcService.createForAllExternalServices(config, rateLimiters))
.addService(ExternalServiceCredentialsAnonymousGrpcService.create(accountsManager, config))
.addService(ServerInterceptors.intercept(new KeysGrpcService(accountsManager, keysManager, rateLimiters), basicCredentialAuthenticationInterceptor))
.addService(new KeysAnonymousGrpcService(accountsManager, keysManager))
.addService(new PaymentsGrpcService(currencyManager))
.addService(ServerInterceptors.intercept(new ProfileGrpcService(clock, accountsManager, profilesManager, dynamicConfigurationManager,
config.getBadges(), asyncCdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, profileBadgeConverter, rateLimiters, zkProfileOperations, config.getCdnConfiguration().bucket()), basicCredentialAuthenticationInterceptor))
.addService(new ProfileAnonymousGrpcService(accountsManager, profilesManager, profileBadgeConverter, zkProfileOperations));
RemoteDeprecationFilter remoteDeprecationFilter = new RemoteDeprecationFilter(dynamicConfigurationManager);
environment.servlets()
@@ -629,14 +714,19 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
// Note: interceptors run in the reverse order they are added; the remote deprecation filter
// depends on the user-agent context so it has to come first here!
// http://grpc.github.io/grpc-java/javadoc/io/grpc/ServerBuilder.html#intercept-io.grpc.ServerInterceptor-
grpcServer.intercept(remoteDeprecationFilter);
grpcServer.intercept(new UserAgentInterceptor());
grpcServer
// TODO: specialize metrics with user-agent platform
.intercept(new MetricCollectingServerInterceptor(Metrics.globalRegistry))
.intercept(new ErrorMappingInterceptor())
.intercept(new AcceptLanguageInterceptor())
.intercept(remoteDeprecationFilter)
.intercept(new UserAgentInterceptor());
environment.lifecycle().manage(new GrpcServerManagedWrapper(grpcServer.build()));
environment.jersey().register(new RequestStatisticsFilter(TrafficSource.HTTP));
environment.jersey().register(MultiRecipientMessageProvider.class);
environment.jersey().register(new MetricsApplicationEventListener(TrafficSource.HTTP));
environment.jersey().register(new MetricsApplicationEventListener(TrafficSource.HTTP, clientReleaseManager));
environment.jersey()
.register(new PolymorphicAuthDynamicFeature<>(ImmutableMap.of(AuthenticatedAccount.class, accountAuthFilter,
DisabledPermittedAuthenticatedAccount.class, disabledPermittedAccountAuthFilter)));
@@ -647,16 +737,16 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
///
WebSocketEnvironment<AuthenticatedAccount> webSocketEnvironment = new WebSocketEnvironment<>(environment,
config.getWebSocketConfiguration(), 90000);
config.getWebSocketConfiguration(), Duration.ofMillis(90000));
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(accountAuthenticator));
webSocketEnvironment.setConnectListener(
new AuthenticatedConnectListener(receiptSender, messagesManager, pushNotificationManager,
clientPresenceManager, websocketScheduledExecutor, messageDeliveryScheduler, dynamicConfigurationManager));
clientPresenceManager, websocketScheduledExecutor, messageDeliveryScheduler, clientReleaseManager));
webSocketEnvironment.jersey()
.register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
webSocketEnvironment.jersey().register(new RequestStatisticsFilter(TrafficSource.WEBSOCKET));
webSocketEnvironment.jersey().register(MultiRecipientMessageProvider.class);
webSocketEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET));
webSocketEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET, clientReleaseManager));
webSocketEnvironment.jersey().register(new KeepAliveController(clientPresenceManager));
// these should be common, but use @Auth DisabledPermittedAccount, which isnt supported yet on websocket
@@ -665,7 +755,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
turnTokenGenerator,
registrationRecoveryPasswordsManager, usernameHashZkProofVerifier));
environment.jersey().register(new KeysController(rateLimiters, keys, accountsManager));
environment.jersey().register(new KeysController(rateLimiters, keysManager, accountsManager));
boolean registeredSpamFilter = false;
ReportSpamTokenProvider reportSpamTokenProvider = null;
@@ -717,45 +807,49 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
new AccountControllerV2(accountsManager, changeNumberManager, phoneVerificationTokenManager,
registrationLockVerificationManager, rateLimiters),
new ArtController(rateLimiters, artCredentialsGenerator),
new AttachmentControllerV2(rateLimiters, config.getAwsAttachmentsConfiguration().accessKey().value(), config.getAwsAttachmentsConfiguration().accessSecret().value(), config.getAwsAttachmentsConfiguration().region(), config.getAwsAttachmentsConfiguration().bucket()),
new AttachmentControllerV3(rateLimiters, config.getGcpAttachmentsConfiguration().domain(), config.getGcpAttachmentsConfiguration().email(), config.getGcpAttachmentsConfiguration().maxSizeInBytes(), config.getGcpAttachmentsConfiguration().pathPrefix(), config.getGcpAttachmentsConfiguration().rsaSigningKey().value()),
new CallLinkController(rateLimiters, genericZkSecretParams),
new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().certificate().value(), config.getDeliveryCertificate().ecPrivateKey(), config.getDeliveryCertificate().expiresDays()), zkAuthOperations, genericZkSecretParams, clock),
new ChallengeController(rateLimitChallengeManager),
new DeviceController(pendingDevicesManager, accountsManager, messagesManager, keys, rateLimiters, config.getMaxDevices()),
new AttachmentControllerV2(rateLimiters, config.getAwsAttachmentsConfiguration().accessKey().value(),
config.getAwsAttachmentsConfiguration().accessSecret().value(),
config.getAwsAttachmentsConfiguration().region(), config.getAwsAttachmentsConfiguration().bucket()),
new AttachmentControllerV3(rateLimiters, gcsAttachmentGenerator),
new AttachmentControllerV4(rateLimiters, gcsAttachmentGenerator, new TusAttachmentGenerator(config.getTus()),
experimentEnrollmentManager),
new ArchiveController(backupAuthManager, backupManager),
new CallLinkController(rateLimiters, callingGenericZkSecretParams),
new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().certificate().value(),
config.getDeliveryCertificate().ecPrivateKey(), config.getDeliveryCertificate().expiresDays()),
zkAuthOperations, callingGenericZkSecretParams, clock),
new ChallengeController(rateLimitChallengeManager, useRemoteAddress),
new DeviceController(config.getLinkDeviceSecretConfiguration().secret().value(), accountsManager,
rateLimiters, rateLimitersCluster, config.getMaxDevices(), clock),
new DirectoryV2Controller(directoryV2CredentialsGenerator),
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
ReceiptCredentialPresentation::new),
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, deletedAccounts,
messagesManager, pushNotificationManager, reportMessageManager, multiRecipientMessageExecutor,
messageDeliveryScheduler, reportSpamTokenProvider, dynamicConfigurationManager),
new MessageController(rateLimiters, messageByteLimitCardinalityEstimator, messageSender, receiptSender,
accountsManager, messagesManager, pushNotificationManager, reportMessageManager,
multiRecipientMessageExecutor, messageDeliveryScheduler, reportSpamTokenProvider, clientReleaseManager,
dynamicConfigurationManager),
new PaymentsController(currencyManager, paymentsCredentialsGenerator),
new ProfileController(clock, rateLimiters, accountsManager, profilesManager, dynamicConfigurationManager,
profileBadgeConverter, config.getBadges(), cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner,
config.getCdnConfiguration().bucket(), zkProfileOperations, batchIdentityCheckExecutor),
new ProvisioningController(rateLimiters, provisioningManager),
new RegistrationController(accountsManager, phoneVerificationTokenManager, registrationLockVerificationManager,
keys, rateLimiters),
new RemoteConfigController(remoteConfigsManager, adminEventLogger,
config.getRemoteConfigConfiguration().authorizedUsers(),
config.getRemoteConfigConfiguration().requiredHostedDomain(),
config.getRemoteConfigConfiguration().audiences(),
new GoogleIdTokenVerifier.Builder(new ApacheHttpTransport(), new GsonFactory()),
config.getRemoteConfigConfiguration().globalConfig()),
new SecureBackupController(backupCredentialsGenerator, accountsManager),
rateLimiters),
new RemoteConfigController(remoteConfigsManager, config.getRemoteConfigConfiguration().globalConfig(), clock),
new SecureStorageController(storageCredentialsGenerator),
new SecureValueRecovery2Controller(svr2CredentialsGenerator, accountsManager),
new SecureValueRecovery3Controller(svr3CredentialsGenerator, accountsManager),
new StickerController(rateLimiters, config.getCdnConfiguration().accessKey().value(),
config.getCdnConfiguration().accessSecret().value(), config.getCdnConfiguration().region(),
config.getCdnConfiguration().bucket()),
new VerificationController(registrationServiceClient, new VerificationSessionManager(verificationSessions),
pushNotificationManager, registrationCaptchaManager, registrationRecoveryPasswordsManager, rateLimiters,
accountsManager, clock)
accountsManager, useRemoteAddress, dynamicConfigurationManager, clock)
);
if (config.getSubscription() != null && config.getOneTimeDonations() != null) {
commonControllers.add(new SubscriptionController(clock, config.getSubscription(), config.getOneTimeDonations(),
subscriptionManager, stripeManager, braintreeManager, zkReceiptOperations, issuedReceiptsManager, profileBadgeConverter,
resourceBundleLevelTranslator));
subscriptionManager, stripeManager, braintreeManager, zkReceiptOperations, issuedReceiptsManager, oneTimeDonationsManager,
profileBadgeConverter, resourceBundleLevelTranslator, bankMandateTranslator));
}
for (Object controller : commonControllers) {
@@ -764,10 +858,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
}
WebSocketEnvironment<AuthenticatedAccount> provisioningEnvironment = new WebSocketEnvironment<>(environment,
webSocketEnvironment.getRequestLog(), 60000);
webSocketEnvironment.getRequestLog(), Duration.ofMillis(60000));
provisioningEnvironment.jersey().register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
provisioningEnvironment.setConnectListener(new ProvisioningConnectListener(provisioningManager));
provisioningEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET));
provisioningEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET, clientReleaseManager));
provisioningEnvironment.jersey().register(new KeepAliveController(clientPresenceManager));
registerCorsFilter(environment);
@@ -778,6 +872,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
webSocketEnvironment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
provisioningEnvironment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
JettyWebSocketServletContainerInitializer.configure(environment.getApplicationContext(), null);
WebSocketResourceProviderFactory<AuthenticatedAccount> webSocketServlet = new WebSocketResourceProviderFactory<>(
webSocketEnvironment, AuthenticatedAccount.class, config.getWebSocketConfiguration());
WebSocketResourceProviderFactory<AuthenticatedAccount> provisioningServlet = new WebSocketResourceProviderFactory<>(
@@ -803,9 +899,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
private void registerProviders(Environment environment,
WebSocketEnvironment<AuthenticatedAccount> webSocketEnvironment,
WebSocketEnvironment<AuthenticatedAccount> provisioningEnvironment) {
environment.jersey().register(ScoreThresholdProvider.ScoreThresholdFeature.class);
webSocketEnvironment.jersey().register(ScoreThresholdProvider.ScoreThresholdFeature.class);
provisioningEnvironment.jersey().register(ScoreThresholdProvider.ScoreThresholdFeature.class);
List.of(
ScoreThresholdProvider.ScoreThresholdFeature.class,
PushChallengeConfigProvider.PushChallengeConfigFeature.class)
.forEach(feature -> {
environment.jersey().register(feature);
webSocketEnvironment.jersey().register(feature);
provisioningEnvironment.jersey().register(feature);
});
}
private void registerExceptionMappers(Environment environment,
@@ -815,6 +916,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
List.of(
new LoggingUnhandledExceptionMapper(),
new CompletionExceptionMapper(),
new GrpcStatusRuntimeExceptionMapper(),
new IOExceptionMapper(),
new RateLimitExceededExceptionMapper(),
new InvalidWebsocketAddressExceptionMapper(),
@@ -823,6 +925,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
new ImpossiblePhoneNumberExceptionMapper(),
new NonNormalizedPhoneNumberExceptionMapper(),
new RegistrationServiceSenderExceptionMapper(),
new SubscriptionProcessorExceptionMapper(),
new JsonMappingExceptionMapper()
).forEach(exceptionMapper -> {
environment.jersey().register(exceptionMapper);

View File

@@ -0,0 +1,15 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.attachments;
import java.util.Map;
public interface AttachmentGenerator {
record Descriptor(Map<String, String> headers, String signedUploadLocation) {}
Descriptor generateAttachment(final String key);
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.attachments;
import org.whispersystems.textsecuregcm.gcp.CanonicalRequest;
import org.whispersystems.textsecuregcm.gcp.CanonicalRequestGenerator;
import org.whispersystems.textsecuregcm.gcp.CanonicalRequestSigner;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.spec.InvalidKeySpecException;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Map;
public class GcsAttachmentGenerator implements AttachmentGenerator {
@Nonnull
private final CanonicalRequestGenerator canonicalRequestGenerator;
@Nonnull
private final CanonicalRequestSigner canonicalRequestSigner;
public GcsAttachmentGenerator(@Nonnull String domain, @Nonnull String email,
int maxSizeInBytes, @Nonnull String pathPrefix, @Nonnull String rsaSigningKey)
throws IOException, InvalidKeyException, InvalidKeySpecException {
this.canonicalRequestGenerator = new CanonicalRequestGenerator(domain, email, maxSizeInBytes, pathPrefix);
this.canonicalRequestSigner = new CanonicalRequestSigner(rsaSigningKey);
}
@Override
public Descriptor generateAttachment(final String key) {
final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
final CanonicalRequest canonicalRequest = canonicalRequestGenerator.createFor(key, now);
return new Descriptor(getHeaderMap(canonicalRequest), getSignedUploadLocation(canonicalRequest));
}
private String getSignedUploadLocation(@Nonnull CanonicalRequest canonicalRequest) {
return "https://" + canonicalRequest.getDomain() + canonicalRequest.getResourcePath()
+ '?' + canonicalRequest.getCanonicalQuery()
+ "&X-Goog-Signature=" + canonicalRequestSigner.sign(canonicalRequest);
}
private static Map<String, String> getHeaderMap(@Nonnull CanonicalRequest canonicalRequest) {
return Map.of(
"host", canonicalRequest.getDomain(),
"x-goog-content-length-range", "1," + canonicalRequest.getMaxSizeInBytes(),
"x-goog-resumable", "start");
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.attachments;
import org.apache.http.HttpHeaders;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
import org.whispersystems.textsecuregcm.util.HeaderUtils;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.util.Base64;
import java.util.Map;
public class TusAttachmentGenerator implements AttachmentGenerator {
private static final String ATTACHMENTS = "attachments";
final ExternalServiceCredentialsGenerator credentialsGenerator;
final String tusUri;
public TusAttachmentGenerator(final TusConfiguration cfg) {
this.tusUri = cfg.uploadUri();
this.credentialsGenerator = credentialsGenerator(Clock.systemUTC(), cfg);
}
private static ExternalServiceCredentialsGenerator credentialsGenerator(final Clock clock, final TusConfiguration cfg) {
return ExternalServiceCredentialsGenerator
.builder(cfg.userAuthenticationTokenSharedSecret())
.prependUsername(false)
.withClock(clock)
.build();
}
@Override
public Descriptor generateAttachment(final String key) {
final ExternalServiceCredentials credentials = credentialsGenerator.generateFor(ATTACHMENTS + "/" + key);
final String b64Key = Base64.getEncoder().encodeToString(key.getBytes(StandardCharsets.UTF_8));
final Map<String, String> headers = Map.of(
HttpHeaders.AUTHORIZATION, HeaderUtils.basicAuthHeader(credentials),
"Upload-Metadata", String.format("filename %s", b64Key)
);
return new Descriptor(headers, tusUri + "/" + ATTACHMENTS);
}
}

View File

@@ -0,0 +1,15 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.attachments;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
import org.whispersystems.textsecuregcm.util.ExactlySize;
import javax.validation.constraints.NotEmpty;
public record TusConfiguration(
@ExactlySize(32) SecretBytes userAuthenticationTokenSharedSecret,
@NotEmpty String uploadUri
){}

View File

@@ -47,7 +47,7 @@ public class AuthEnablementRefreshRequirementProvider implements WebsocketRefres
}
@VisibleForTesting
static Map<Long, Boolean> buildDevicesEnabledMap(final Account account) {
static Map<Byte, Boolean> buildDevicesEnabledMap(final Account account) {
return account.getDevices().stream().collect(Collectors.toMap(Device::getId, Device::isEnabled));
}
@@ -68,17 +68,17 @@ public class AuthEnablementRefreshRequirementProvider implements WebsocketRefres
}
@Override
public List<Pair<UUID, Long>> handleRequestFinished(final RequestEvent requestEvent) {
public List<Pair<UUID, Byte>> handleRequestFinished(final RequestEvent requestEvent) {
// Now that the request is finished, check whether `isEnabled` changed for any of the devices. If the value did
// change or if a devices was added or removed, all devices must disconnect and reauthenticate.
if (requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED) != null) {
@SuppressWarnings("unchecked") final Map<Long, Boolean> initialDevicesEnabled =
(Map<Long, Boolean>) requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED);
@SuppressWarnings("unchecked") final Map<Byte, Boolean> initialDevicesEnabled =
(Map<Byte, Boolean>) requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED);
return accountsManager.getByAccountIdentifier((UUID) requestEvent.getContainerRequest().getProperty(ACCOUNT_UUID)).map(account -> {
final Set<Long> deviceIdsToDisplace;
final Map<Long, Boolean> currentDevicesEnabled = buildDevicesEnabledMap(account);
final Set<Byte> deviceIdsToDisplace;
final Map<Byte, Boolean> currentDevicesEnabled = buildDevicesEnabledMap(account);
if (!initialDevicesEnabled.equals(currentDevicesEnabled)) {
deviceIdsToDisplace = new HashSet<>(initialDevicesEnabled.keySet());

View File

@@ -0,0 +1,10 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import org.whispersystems.textsecuregcm.backup.BackupTier;
public record AuthenticatedBackupUser(byte[] backupId, BackupTier backupTier) {}

View File

@@ -32,9 +32,6 @@ public class BaseAccountAuthenticator {
private static final String AUTHENTICATION_SUCCEEDED_TAG_NAME = "succeeded";
private static final String AUTHENTICATION_FAILURE_REASON_TAG_NAME = "reason";
private static final String ENABLED_TAG_NAME = "enabled";
private static final String AUTHENTICATION_HAS_STORY_CAPABILITY = "hasStoryCapability";
private static final String STORY_ADOPTION_COUNTER_NAME = name(BaseAccountAuthenticator.class, "storyAdoption");
private static final String DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME = name(BaseAccountAuthenticator.class, "daysSinceLastSeen");
private static final String IS_PRIMARY_DEVICE_TAG = "isPrimary";
@@ -55,18 +52,18 @@ public class BaseAccountAuthenticator {
this.clock = clock;
}
static Pair<String, Long> getIdentifierAndDeviceId(final String basicUsername) {
static Pair<String, Byte> getIdentifierAndDeviceId(final String basicUsername) {
final String identifier;
final long deviceId;
final byte deviceId;
final int deviceIdSeparatorIndex = basicUsername.indexOf(DEVICE_ID_SEPARATOR);
if (deviceIdSeparatorIndex == -1) {
identifier = basicUsername;
deviceId = Device.MASTER_ID;
deviceId = Device.PRIMARY_ID;
} else {
identifier = basicUsername.substring(0, deviceIdSeparatorIndex);
deviceId = Long.parseLong(basicUsername.substring(deviceIdSeparatorIndex + 1));
deviceId = Byte.parseByte(basicUsername.substring(deviceIdSeparatorIndex + 1));
}
return new Pair<>(identifier, deviceId);
@@ -75,13 +72,12 @@ public class BaseAccountAuthenticator {
public Optional<AuthenticatedAccount> authenticate(BasicCredentials basicCredentials, boolean enabledRequired) {
boolean succeeded = false;
String failureReason = null;
boolean hasStoryCapability = false;
try {
final UUID accountUuid;
final long deviceId;
final byte deviceId;
{
final Pair<String, Long> identifierAndDeviceId = getIdentifierAndDeviceId(basicCredentials.getUsername());
final Pair<String, Byte> identifierAndDeviceId = getIdentifierAndDeviceId(basicCredentials.getUsername());
accountUuid = UUID.fromString(identifierAndDeviceId.first());
deviceId = identifierAndDeviceId.second();
@@ -94,8 +90,6 @@ public class BaseAccountAuthenticator {
return Optional.empty();
}
hasStoryCapability = account.map(Account::isStoriesSupported).orElse(false);
Optional<Device> device = account.get().getDevice(deviceId);
if (device.isEmpty()) {
@@ -119,7 +113,7 @@ public class BaseAccountAuthenticator {
} else {
Metrics.counter(ENABLED_NOT_REQUIRED_AUTHENTICATION_COUNTER_NAME,
ENABLED_TAG_NAME, String.valueOf(device.get().isEnabled() && account.get().isEnabled()),
IS_PRIMARY_DEVICE_TAG, String.valueOf(device.get().isMaster()))
IS_PRIMARY_DEVICE_TAG, String.valueOf(device.get().isPrimary()))
.increment();
}
@@ -150,9 +144,6 @@ public class BaseAccountAuthenticator {
}
Metrics.counter(AUTHENTICATION_COUNTER_NAME, tags).increment();
Tags storyTags = Tags.of(AUTHENTICATION_HAS_STORY_CAPABILITY, String.valueOf(hasStoryCapability));
Metrics.counter(STORY_ADOPTION_COUNTER_NAME, storyTags).increment();
}
}
@@ -171,7 +162,7 @@ public class BaseAccountAuthenticator {
// (1) each account will only update last-seen at most once per day
// (2) these updates will occur throughout the day rather than all occurring at UTC midnight.
if (device.getLastSeen() < todayInMillisWithOffset) {
Metrics.summary(DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME, IS_PRIMARY_DEVICE_TAG, String.valueOf(device.isMaster()))
Metrics.summary(DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME, IS_PRIMARY_DEVICE_TAG, String.valueOf(device.isPrimary()))
.record(Duration.ofMillis(todayInMillisWithOffset - device.getLastSeen()).toDays());
return accountsManager.updateDeviceLastSeen(account, device, Util.todayInMillis(clock));

View File

@@ -11,10 +11,10 @@ import org.whispersystems.textsecuregcm.util.Pair;
public class BasicAuthorizationHeader {
private final String username;
private final long deviceId;
private final byte deviceId;
private final String password;
private BasicAuthorizationHeader(final String username, final long deviceId, final String password) {
private BasicAuthorizationHeader(final String username, final byte deviceId, final String password) {
this.username = username;
this.deviceId = deviceId;
this.password = password;
@@ -59,9 +59,9 @@ public class BasicAuthorizationHeader {
final String usernameComponent = credentials.substring(0, credentialSeparatorIndex);
final String username;
final long deviceId;
final byte deviceId;
{
final Pair<String, Long> identifierAndDeviceId =
final Pair<String, Byte> identifierAndDeviceId =
BaseAccountAuthenticator.getIdentifierAndDeviceId(usernameComponent);
username = identifierAndDeviceId.first();

View File

@@ -13,6 +13,7 @@ import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPrivateKey;
import org.whispersystems.textsecuregcm.entities.MessageProtos.SenderCertificate;
import org.whispersystems.textsecuregcm.entities.MessageProtos.ServerCertificate;
import org.whispersystems.textsecuregcm.identity.IdentityType;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
@@ -32,11 +33,11 @@ public class CertificateGenerator {
public byte[] createFor(Account account, Device device, boolean includeE164) throws InvalidKeyException {
SenderCertificate.Certificate.Builder builder = SenderCertificate.Certificate.newBuilder()
.setSenderDevice(Math.toIntExact(device.getId()))
.setExpires(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(expiresDays))
.setIdentityKey(ByteString.copyFrom(account.getIdentityKey().serialize()))
.setSigner(serverCertificate)
.setSenderUuid(account.getUuid().toString());
.setSenderDevice(Math.toIntExact(device.getId()))
.setExpires(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(expiresDays))
.setIdentityKey(ByteString.copyFrom(account.getIdentityKey(IdentityType.ACI).serialize()))
.setSigner(serverCertificate)
.setSenderUuid(account.getUuid().toString());
if (includeE164) {
builder.setSender(account.getNumber());

View File

@@ -16,7 +16,7 @@ public class CombinedUnidentifiedSenderAccessKeys {
public CombinedUnidentifiedSenderAccessKeys(String header) {
try {
this.combinedUnidentifiedSenderAccessKeys = Base64.getDecoder().decode(header);
if (this.combinedUnidentifiedSenderAccessKeys == null || this.combinedUnidentifiedSenderAccessKeys.length != 16) {
if (this.combinedUnidentifiedSenderAccessKeys == null || this.combinedUnidentifiedSenderAccessKeys.length != UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH) {
throw new WebApplicationException("Invalid combined unidentified sender access keys", Status.UNAUTHORIZED);
}
} catch (IllegalArgumentException e) {

View File

@@ -29,7 +29,7 @@ public class OptionalAccess {
verify(requestAccount, accessKey, targetAccount);
if (!deviceSelector.equals("*")) {
long deviceId = Long.parseLong(deviceSelector);
byte deviceId = Byte.parseByte(deviceSelector);
Optional<Device> targetDevice = targetAccount.get().getDevice(deviceId);

View File

@@ -26,7 +26,7 @@ public class PhoneNumberChangeRefreshRequirementProvider implements WebsocketRef
}
@Override
public List<Pair<UUID, Long>> handleRequestFinished(final RequestEvent requestEvent) {
public List<Pair<UUID, Byte>> handleRequestFinished(final RequestEvent requestEvent) {
final String initialNumber = (String) requestEvent.getContainerRequest().getProperty(INITIAL_NUMBER_KEY);
if (initialNumber != null) {

View File

@@ -18,6 +18,7 @@ import java.util.List;
import javax.annotation.Nullable;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.StringUtils;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.entities.PhoneVerificationRequest;
import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure;
@@ -30,7 +31,6 @@ import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
import org.whispersystems.textsecuregcm.util.Util;
public class RegistrationLockVerificationManager {
public enum Flow {
@@ -54,7 +54,6 @@ public class RegistrationLockVerificationManager {
private final AccountsManager accounts;
private final ClientPresenceManager clientPresenceManager;
private final ExternalServiceCredentialsGenerator svr1CredentialGenerator;
private final ExternalServiceCredentialsGenerator svr2CredentialGenerator;
private final RateLimiters rateLimiters;
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
@@ -62,14 +61,12 @@ public class RegistrationLockVerificationManager {
public RegistrationLockVerificationManager(
final AccountsManager accounts, final ClientPresenceManager clientPresenceManager,
final ExternalServiceCredentialsGenerator svr1CredentialGenerator,
final ExternalServiceCredentialsGenerator svr2CredentialGenerator,
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
final PushNotificationManager pushNotificationManager,
final RateLimiters rateLimiters) {
this.accounts = accounts;
this.clientPresenceManager = clientPresenceManager;
this.svr1CredentialGenerator = svr1CredentialGenerator;
this.svr2CredentialGenerator = svr2CredentialGenerator;
this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
this.pushNotificationManager = pushNotificationManager;
@@ -109,7 +106,7 @@ public class RegistrationLockVerificationManager {
throw new RuntimeException("Unexpected status: " + existingRegistrationLock.getStatus());
}
if (!Util.isEmpty(clientRegistrationLock)) {
if (StringUtils.isNotEmpty(clientRegistrationLock)) {
rateLimiters.getPinLimiter().validate(account.getNumber());
}
@@ -141,7 +138,6 @@ public class RegistrationLockVerificationManager {
// Freezing the existing account credentials will definitively start the reglock timeout.
// Until the timeout, the current reglock can still be supplied,
// along with phone number verification, to restore access.
final ExternalServiceCredentials existingSvr1Credentials = svr1CredentialGenerator.generateForUuid(account.getUuid());
final ExternalServiceCredentials existingSvr2Credentials = svr2CredentialGenerator.generateForUuid(account.getUuid());
final Account updatedAccount;
@@ -161,7 +157,7 @@ public class RegistrationLockVerificationManager {
registrationRecoveryPasswordsManager.removeForNumber(updatedAccount.getNumber());
}
final List<Long> deviceIds = updatedAccount.getDevices().stream().map(Device::getId).toList();
final List<Byte> deviceIds = updatedAccount.getDevices().stream().map(Device::getId).toList();
clientPresenceManager.disconnectAllPresences(updatedAccount.getUuid(), deviceIds);
try {
@@ -173,7 +169,6 @@ public class RegistrationLockVerificationManager {
throw new WebApplicationException(Response.status(FAILURE_HTTP_STATUS)
.entity(new RegistrationLockFailure(existingRegistrationLock.getTimeRemaining().toMillis(),
existingRegistrationLock.needsFailureCredentials() ? existingSvr1Credentials : null,
existingRegistrationLock.needsFailureCredentials() ? existingSvr2Credentials : null))
.build());
}

View File

@@ -11,7 +11,7 @@ import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Optional;
import javax.annotation.Nullable;
import org.whispersystems.textsecuregcm.util.Util;
import org.apache.commons.lang3.StringUtils;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class StoredRegistrationLock {
@@ -73,7 +73,7 @@ public class StoredRegistrationLock {
}
public boolean verify(@Nullable String clientRegistrationLock) {
if (hasLockAndSalt() && Util.nonEmpty(clientRegistrationLock)) {
if (hasLockAndSalt() && StringUtils.isNotEmpty(clientRegistrationLock)) {
SaltedTokenHash credentials = new SaltedTokenHash(registrationLock.get(), registrationLockSalt.get());
return credentials.verify(clientRegistrationLock);
} else {

View File

@@ -1,30 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import java.security.MessageDigest;
import java.time.Duration;
import javax.annotation.Nullable;
import org.whispersystems.textsecuregcm.util.Util;
public record StoredVerificationCode(@Nullable String code,
long timestamp,
@Nullable String pushCode,
@Nullable byte[] sessionId) {
public static final Duration EXPIRATION = Duration.ofMinutes(10);
public boolean isValid(String theirCodeString) {
if (Util.isEmpty(code) || Util.isEmpty(theirCodeString)) {
return false;
}
byte[] ourCode = code.getBytes();
byte[] theirCode = theirCodeString.getBytes();
return MessageDigest.isEqual(ourCode, theirCode);
}
}

View File

@@ -5,30 +5,7 @@
package org.whispersystems.textsecuregcm.auth;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import java.util.List;
public class TurnToken {
@JsonProperty
private String username;
@JsonProperty
private String password;
@JsonProperty
private List<String> urls;
public TurnToken(String username, String password, List<String> urls) {
this.username = username;
this.password = password;
this.urls = urls;
}
@VisibleForTesting
List<String> getUrls() {
return urls;
}
public record TurnToken(String username, String password, List<String> urls) {
}

View File

@@ -23,6 +23,7 @@ import java.time.Instant;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public class TurnTokenGenerator {
@@ -39,9 +40,9 @@ public class TurnTokenGenerator {
this.turnSecret = turnSecret;
}
public TurnToken generate(final String e164) {
public TurnToken generate(final UUID aci) {
try {
final List<String> urls = urls(e164);
final List<String> urls = urls(aci);
final Mac mac = Mac.getInstance(ALGORITHM);
final long validUntilSeconds = Instant.now().plus(Duration.ofDays(1)).getEpochSecond();
final long user = Util.ensureNonNegativeInt(new SecureRandom().nextInt());
@@ -56,12 +57,12 @@ public class TurnTokenGenerator {
}
}
private List<String> urls(final String e164) {
private List<String> urls(final UUID aci) {
final DynamicTurnConfiguration turnConfig = dynamicConfigurationManager.getConfiguration().getTurnConfiguration();
// Check if number is enrolled to test out specific turn servers
final Optional<TurnUriConfiguration> enrolled = turnConfig.getUriConfigs().stream()
.filter(config -> config.getEnrolledNumbers().contains(e164))
.filter(config -> config.getEnrolledAcis().contains(aci))
.findFirst();
if (enrolled.isPresent()) {

View File

@@ -9,23 +9,21 @@ import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Optional;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class UnidentifiedAccessChecksum {
public static String generateFor(Optional<byte[]> unidentifiedAccessKey) {
public static byte[] generateFor(byte[] unidentifiedAccessKey) {
try {
if (!unidentifiedAccessKey.isPresent()|| unidentifiedAccessKey.get().length != 16) return null;
if (unidentifiedAccessKey.length != UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH) {
throw new IllegalArgumentException("Invalid UAK length: " + unidentifiedAccessKey.length);
}
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(unidentifiedAccessKey.get(), "HmacSHA256"));
mac.init(new SecretKeySpec(unidentifiedAccessKey, "HmacSHA256"));
return Base64.getEncoder().encodeToString(mac.doFinal(new byte[32]));
return mac.doFinal(new byte[32]);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -10,6 +10,8 @@ import java.security.MessageDigest;
public class UnidentifiedAccessUtil {
public static final int UNIDENTIFIED_ACCESS_KEY_LENGTH = 16;
private UnidentifiedAccessUtil() {
}

View File

@@ -30,5 +30,5 @@ public interface WebsocketRefreshRequirementProvider {
* @return a list of pairs of account UUID/device ID pairs identifying websockets that need to be refreshed as a
* result of the observed request
*/
List<Pair<UUID, Long>> handleRequestFinished(RequestEvent requestEvent);
List<Pair<UUID, Byte>> handleRequestFinished(RequestEvent requestEvent);
}

View File

@@ -7,5 +7,5 @@ package org.whispersystems.textsecuregcm.auth.grpc;
import java.util.UUID;
public record AuthenticatedDevice(UUID accountIdentifier, long deviceId) {
public record AuthenticatedDevice(UUID accountIdentifier, byte deviceId) {
}

View File

@@ -9,9 +9,7 @@ import io.grpc.Context;
import io.grpc.Status;
import java.util.UUID;
import javax.annotation.Nullable;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
import org.whispersystems.textsecuregcm.storage.Device;
/**
* Provides utility methods for working with authentication in the context of gRPC calls.
@@ -19,20 +17,20 @@ import reactor.util.function.Tuples;
public class AuthenticationUtil {
static final Context.Key<UUID> CONTEXT_AUTHENTICATED_ACCOUNT_IDENTIFIER_KEY = Context.key("authenticated-aci");
static final Context.Key<Long> CONTEXT_AUTHENTICATED_DEVICE_IDENTIFIER_KEY = Context.key("authenticated-device-id");
static final Context.Key<Byte> CONTEXT_AUTHENTICATED_DEVICE_IDENTIFIER_KEY = Context.key("authenticated-device-id");
/**
* Returns the account/device authenticated in the current gRPC context or throws an "unauthenticated" exception if
* no authenticated account/device is available.
*
* @return the account/device authenticated in the current gRPC context
* @return the account/device identifier authenticated in the current gRPC context
*
* @throws io.grpc.StatusRuntimeException with a status of {@code UNAUTHENTICATED} if no authenticated account/device
* could be retrieved from the current gRPC context
*/
public static AuthenticatedDevice requireAuthenticatedDevice() {
@Nullable final UUID accountIdentifier = CONTEXT_AUTHENTICATED_ACCOUNT_IDENTIFIER_KEY.get();
@Nullable final Long deviceId = CONTEXT_AUTHENTICATED_DEVICE_IDENTIFIER_KEY.get();
@Nullable final Byte deviceId = CONTEXT_AUTHENTICATED_DEVICE_IDENTIFIER_KEY.get();
if (accountIdentifier != null && deviceId != null) {
return new AuthenticatedDevice(accountIdentifier, deviceId);
@@ -40,4 +38,25 @@ public class AuthenticationUtil {
throw Status.UNAUTHENTICATED.asRuntimeException();
}
/**
* Returns the account/device authenticated in the current gRPC context or throws an "unauthenticated" exception if
* no authenticated account/device is available or "permission denied" if the authenticated device is not the primary
* device for the account.
*
* @return the account/device identifier authenticated in the current gRPC context
*
* @throws io.grpc.StatusRuntimeException with a status of {@code UNAUTHENTICATED} if no authenticated account/device
* could be retrieved from the current gRPC context or a status of {@code PERMISSION_DENIED} if the authenticated
* device is not the primary device for the authenticated account
*/
public static AuthenticatedDevice requireAuthenticatedPrimaryDevice() {
final AuthenticatedDevice authenticatedDevice = requireAuthenticatedDevice();
if (authenticatedDevice.deviceId() != Device.PRIMARY_ID) {
throw Status.PERMISSION_DENIED.asRuntimeException();
}
return authenticatedDevice;
}
}

View File

@@ -18,6 +18,7 @@ import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.BaseAccountAuthenticator;
import org.whispersystems.textsecuregcm.util.HeaderUtils;
/**
* A basic credential authentication interceptor enforces the presence of a valid username and password on every call.
@@ -39,7 +40,7 @@ public class BasicCredentialAuthenticationInterceptor implements ServerIntercept
@VisibleForTesting
static final Metadata.Key<String> BASIC_CREDENTIALS =
Metadata.Key.of("x-signal-basic-auth-credentials", Metadata.ASCII_STRING_MARSHALLER);
Metadata.Key.of("x-signal-auth", Metadata.ASCII_STRING_MARSHALLER);
private static final Metadata EMPTY_TRAILERS = new Metadata();
@@ -48,17 +49,20 @@ public class BasicCredentialAuthenticationInterceptor implements ServerIntercept
}
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(final ServerCall<ReqT, RespT> call,
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
final ServerCall<ReqT, RespT> call,
final Metadata headers,
final ServerCallHandler<ReqT, RespT> next) {
final String credentialString = headers.get(BASIC_CREDENTIALS);
final String authHeader = headers.get(BASIC_CREDENTIALS);
if (StringUtils.isNotBlank(credentialString)) {
try {
final BasicCredentials credentials = extractBasicCredentials(credentialString);
if (StringUtils.isNotBlank(authHeader)) {
final Optional<BasicCredentials> maybeCredentials = HeaderUtils.basicCredentialsFromAuthHeader(authHeader);
if (maybeCredentials.isEmpty()) {
call.close(Status.UNAUTHENTICATED.withDescription("Could not parse credentials"), EMPTY_TRAILERS);
} else {
final Optional<AuthenticatedAccount> maybeAuthenticatedAccount =
baseAccountAuthenticator.authenticate(credentials, false);
baseAccountAuthenticator.authenticate(maybeCredentials.get(), false);
if (maybeAuthenticatedAccount.isPresent()) {
final AuthenticatedAccount authenticatedAccount = maybeAuthenticatedAccount.get();
@@ -71,8 +75,6 @@ public class BasicCredentialAuthenticationInterceptor implements ServerIntercept
} else {
call.close(Status.UNAUTHENTICATED.withDescription("Credentials not accepted"), EMPTY_TRAILERS);
}
} catch (final IllegalArgumentException e) {
call.close(Status.UNAUTHENTICATED.withDescription("Could not parse credentials"), EMPTY_TRAILERS);
}
} else {
call.close(Status.UNAUTHENTICATED.withDescription("No credentials provided"), EMPTY_TRAILERS);
@@ -80,15 +82,4 @@ public class BasicCredentialAuthenticationInterceptor implements ServerIntercept
return new ServerCall.Listener<>() {};
}
@VisibleForTesting
static BasicCredentials extractBasicCredentials(final String credentials) {
if (credentials.indexOf(':') < 0) {
throw new IllegalArgumentException("Credentials do not include a username and password part");
}
final String[] pieces = credentials.split(":", 2);
return new BasicCredentials(pieces[0], pieces[1]);
}
}

View File

@@ -0,0 +1,165 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.backup;
import io.grpc.Status;
import java.security.MessageDigest;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequest;
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialResponse;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.util.Util;
/**
* Issues ZK backup auth credentials for authenticated accounts
* <p>
* Authenticated callers can create ZK credentials that contain a blinded backup-id, so that they can later use that
* backup id without the verifier learning that the id is associated with this account.
* <p>
* First use {@link #commitBackupId} to provide a blinded backup-id. This is stored in durable storage. Then the caller
* can use {@link #getBackupAuthCredentials} to retrieve credentials that can subsequently be used to make anonymously
* authenticated requests against their backup-id.
*/
public class BackupAuthManager {
private static final Duration MAX_REDEMPTION_DURATION = Duration.ofDays(7);
final static String BACKUP_EXPERIMENT_NAME = "backup";
final static String BACKUP_MEDIA_EXPERIMENT_NAME = "backupMedia";
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
private final GenericServerSecretParams serverSecretParams;
private final Clock clock;
private final RateLimiters rateLimiters;
private final AccountsManager accountsManager;
public BackupAuthManager(
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
final RateLimiters rateLimiters,
final AccountsManager accountsManager,
final GenericServerSecretParams serverSecretParams,
final Clock clock) {
this.dynamicConfigurationManager = dynamicConfigurationManager;
this.rateLimiters = rateLimiters;
this.accountsManager = accountsManager;
this.serverSecretParams = serverSecretParams;
this.clock = clock;
}
/**
* Store a credential request containing a blinded backup-id for future use.
*
* @param account The account using the backup-id
* @param backupAuthCredentialRequest A request containing the blinded backup-id
* @return A future that completes when the credentialRequest has been stored
* @throws RateLimitExceededException If too many backup-ids have been committed
*/
public CompletableFuture<Void> commitBackupId(final Account account,
final BackupAuthCredentialRequest backupAuthCredentialRequest) throws RateLimitExceededException {
if (receiptLevel(account).isEmpty()) {
throw Status.PERMISSION_DENIED.withDescription("Backups not allowed on account").asRuntimeException();
}
byte[] serializedRequest = backupAuthCredentialRequest.serialize();
byte[] existingRequest = account.getBackupCredentialRequest();
if (existingRequest != null && MessageDigest.isEqual(serializedRequest, existingRequest)) {
// No need to update or enforce rate limits, this is the credential that the user has already
// committed to.
return CompletableFuture.completedFuture(null);
}
rateLimiters.forDescriptor(RateLimiters.For.SET_BACKUP_ID).validate(account.getUuid());
return this.accountsManager
.updateAsync(account, acc -> acc.setBackupCredentialRequest(serializedRequest))
.thenRun(Util.NOOP);
}
public record Credential(BackupAuthCredentialResponse credential, Instant redemptionTime) {}
/**
* Create a credential for every day between redemptionStart and redemptionEnd
* <p>
* This uses a {@link BackupAuthCredentialRequest} previous stored via {@link this#commitBackupId} to generate the
* credentials.
*
* @param account The account to create the credentials for
* @param redemptionStart The day (must be truncated to a day boundary) the first credential should be valid
* @param redemptionEnd The day (must be truncated to a day boundary) the last credential should be valid
* @return Credentials and the day on which they may be redeemed
*/
public CompletableFuture<List<Credential>> getBackupAuthCredentials(
final Account account,
final Instant redemptionStart,
final Instant redemptionEnd) {
final long receiptLevel = receiptLevel(account).orElseThrow(
() -> Status.PERMISSION_DENIED.withDescription("Backups not allowed on account").asRuntimeException());
final Instant startOfDay = clock.instant().truncatedTo(ChronoUnit.DAYS);
if (redemptionStart.isAfter(redemptionEnd) ||
redemptionStart.isBefore(startOfDay) ||
redemptionEnd.isAfter(startOfDay.plus(MAX_REDEMPTION_DURATION)) ||
!redemptionStart.equals(redemptionStart.truncatedTo(ChronoUnit.DAYS)) ||
!redemptionEnd.equals(redemptionEnd.truncatedTo(ChronoUnit.DAYS))) {
throw Status.INVALID_ARGUMENT.withDescription("invalid redemption window").asRuntimeException();
}
// fetch the blinded backup-id the account should have previously committed to
final byte[] committedBytes = account.getBackupCredentialRequest();
if (committedBytes == null) {
throw Status.NOT_FOUND.withDescription("No blinded backup-id has been added to the account").asRuntimeException();
}
try {
// create a credential for every day in the requested period
final BackupAuthCredentialRequest credentialReq = new BackupAuthCredentialRequest(committedBytes);
return CompletableFuture.completedFuture(Stream
.iterate(redemptionStart, curr -> curr.plus(Duration.ofDays(1)))
.takeWhile(redemptionTime -> !redemptionTime.isAfter(redemptionEnd))
.map(redemption -> new Credential(
credentialReq.issueCredential(redemption, receiptLevel, serverSecretParams),
redemption))
.toList());
} catch (InvalidInputException e) {
throw Status.INTERNAL
.withDescription("Could not deserialize stored request credential")
.withCause(e)
.asRuntimeException();
}
}
private Optional<Long> receiptLevel(final Account account) {
if (inExperiment(BACKUP_MEDIA_EXPERIMENT_NAME, account)) {
return Optional.of(BackupTier.MEDIA.getReceiptLevel());
}
if (inExperiment(BACKUP_EXPERIMENT_NAME, account)) {
return Optional.of(BackupTier.MESSAGES.getReceiptLevel());
}
return Optional.empty();
}
private boolean inExperiment(final String experimentName, final Account account) {
return dynamicConfigurationManager.getConfiguration()
.getExperimentEnrollmentConfiguration(experimentName)
.map(config -> config.getEnrolledUuids().contains(account.getUuid()))
.orElse(false);
}
}

View File

@@ -0,0 +1,380 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.backup;
import io.grpc.Status;
import io.micrometer.core.instrument.Metrics;
import java.net.URI;
import java.time.Clock;
import java.util.Base64;
import java.util.HexFormat;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialPresentation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
public class BackupManager {
private static final Logger logger = LoggerFactory.getLogger(BackupManager.class);
static final String MESSAGE_BACKUP_NAME = "messageBackup";
private static final long MAX_TOTAL_BACKUP_MEDIA_BYTES = 1024L * 1024L * 1024L * 50L;
private static final long MAX_MEDIA_OBJECT_SIZE = 1024L * 1024L * 101L;
private static final String ZK_AUTHN_COUNTER_NAME = MetricsUtil.name(BackupManager.class, "authentication");
private static final String ZK_AUTHZ_FAILURE_COUNTER_NAME = MetricsUtil.name(BackupManager.class,
"authorizationFailure");
private static final String SUCCESS_TAG_NAME = "success";
private static final String FAILURE_REASON_TAG_NAME = "reason";
private final BackupsDb backupsDb;
private final GenericServerSecretParams serverSecretParams;
private final Cdn3BackupCredentialGenerator cdn3BackupCredentialGenerator;
private final RemoteStorageManager remoteStorageManager;
private final Map<Integer, String> attachmentCdnBaseUris;
private final Clock clock;
public BackupManager(
final BackupsDb backupsDb,
final GenericServerSecretParams serverSecretParams,
final Cdn3BackupCredentialGenerator cdn3BackupCredentialGenerator,
final RemoteStorageManager remoteStorageManager,
final Map<Integer, String> attachmentCdnBaseUris,
final Clock clock) {
this.backupsDb = backupsDb;
this.serverSecretParams = serverSecretParams;
this.cdn3BackupCredentialGenerator = cdn3BackupCredentialGenerator;
this.remoteStorageManager = remoteStorageManager;
this.clock = clock;
// strip trailing "/" for easier URI construction
this.attachmentCdnBaseUris = attachmentCdnBaseUris.entrySet().stream().collect(Collectors.toMap(
Map.Entry::getKey,
entry -> StringUtils.removeEnd(entry.getValue(), "/")
));
}
/**
* Set the public key for the backup-id.
* <p>
* Once set, calls {@link BackupManager#authenticateBackupUser} can succeed if the presentation is signed with the
* private key corresponding to this public key.
*
* @param presentation a ZK credential presentation that encodes the backupId
* @param signature the signature of the presentation
* @param publicKey the public key of a key-pair that the presentation must be signed with
*/
public CompletableFuture<Void> setPublicKey(
final BackupAuthCredentialPresentation presentation,
final byte[] signature,
final ECPublicKey publicKey) {
// Note: this is a special case where we can't validate the presentation signature against the stored public key
// because we are currently setting it. We check against the provided public key, but we must also verify that
// there isn't an existing, different stored public key for the backup-id (verified with a condition expression)
final BackupTier backupTier = verifySignatureAndCheckPresentation(presentation, signature, publicKey);
if (backupTier.compareTo(BackupTier.MESSAGES) < 0) {
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
throw Status.PERMISSION_DENIED
.withDescription("credential does not support setting public key")
.asRuntimeException();
}
return backupsDb.setPublicKey(presentation.getBackupId(), backupTier, publicKey)
.exceptionally(ExceptionUtils.exceptionallyHandler(PublicKeyConflictException.class, ex -> {
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
SUCCESS_TAG_NAME, String.valueOf(false),
FAILURE_REASON_TAG_NAME, "public_key_conflict")
.increment();
throw Status.UNAUTHENTICATED
.withDescription("public key does not match existing public key for the backup-id")
.asRuntimeException();
}));
}
/**
* Create a form that may be used to upload a backup file for the backupId encoded in the presentation.
* <p>
* If successful, this also updates the TTL of the backup.
*
* @param backupUser an already ZK authenticated backup user
* @return the upload form
*/
public CompletableFuture<MessageBackupUploadDescriptor> createMessageBackupUploadDescriptor(
final AuthenticatedBackupUser backupUser) {
final String encodedBackupId = encodeBackupIdForCdn(backupUser);
// this could race with concurrent updates, but the only effect would be last-writer-wins on the timestamp
return backupsDb
.addMessageBackup(backupUser)
.thenApply(result -> cdn3BackupCredentialGenerator.generateUpload(encodedBackupId, MESSAGE_BACKUP_NAME));
}
/**
* Update the last update timestamps for the backupId in the presentation
*
* @param backupUser an already ZK authenticated backup user
*/
public CompletableFuture<Void> ttlRefresh(final AuthenticatedBackupUser backupUser) {
if (backupUser.backupTier().compareTo(BackupTier.MESSAGES) < 0) {
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
throw Status.PERMISSION_DENIED
.withDescription("credential does not support ttl operation")
.asRuntimeException();
}
// update message backup TTL
return backupsDb.ttlRefresh(backupUser);
}
public record BackupInfo(int cdn, String backupSubdir, String messageBackupKey, Optional<Long> mediaUsedSpace) {}
/**
* Retrieve information about the existing backup
*
* @param backupUser an already ZK authenticated backup user
* @return Information about the existing backup
*/
public CompletableFuture<BackupInfo> backupInfo(final AuthenticatedBackupUser backupUser) {
if (backupUser.backupTier().compareTo(BackupTier.MESSAGES) < 0) {
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
throw Status.PERMISSION_DENIED.withDescription("credential does not support info operation")
.asRuntimeException();
}
return backupsDb.describeBackup(backupUser)
.thenApply(backupDescription -> new BackupInfo(
backupDescription.cdn(),
encodeBackupIdForCdn(backupUser),
MESSAGE_BACKUP_NAME,
backupDescription.mediaUsedSpace()));
}
/**
* Check if there is enough capacity to store the requested amount of media
*
* @param backupUser an already ZK authenticated backup user
* @param mediaLength the desired number of media bytes to store
* @return true if mediaLength bytes can be stored
*/
public CompletableFuture<Boolean> canStoreMedia(final AuthenticatedBackupUser backupUser, final long mediaLength) {
if (backupUser.backupTier().compareTo(BackupTier.MEDIA) < 0) {
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
throw Status.PERMISSION_DENIED
.withDescription("credential does not support storing media")
.asRuntimeException();
}
return backupsDb.describeBackup(backupUser)
.thenApply(info -> info.mediaUsedSpace()
.filter(usedSpace -> MAX_TOTAL_BACKUP_MEDIA_BYTES - usedSpace >= mediaLength)
.isPresent());
}
public record StorageDescriptor(int cdn, byte[] key) {}
/**
* Copy an encrypted object to the backup cdn, adding a layer of encryption
* <p>
* Implementation notes: <p> This method guarantees that any object that gets successfully copied to the backup cdn
* will also have an entry for the user in the database. <p>
* <p>
* However, the converse isn't true; there may be entries in the database that have not made it to the cdn. On list,
* these entries are checked against the cdn and removed.
*
* @return A stage that completes successfully with location of the twice-encrypted object on the backup cdn. The
* returned CompletionStage can be completed exceptionally with the following exceptions.
* <ul>
* <li> {@link InvalidLengthException} If the expectedSourceLength does not match the length of the sourceUri </li>
* <li> {@link SourceObjectNotFoundException} If the no object at sourceUri is found </li>
* <li> {@link java.io.IOException} If there was a generic IO issue </li>
* </ul>
*/
public CompletableFuture<StorageDescriptor> copyToBackup(
final AuthenticatedBackupUser backupUser,
final int sourceCdn,
final String sourceKey,
final int sourceLength,
final MediaEncryptionParameters encryptionParameters,
final byte[] destinationMediaId) {
if (backupUser.backupTier().compareTo(BackupTier.MEDIA) < 0) {
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
throw Status.PERMISSION_DENIED
.withDescription("credential does not support storing media")
.asRuntimeException();
}
if (sourceLength > MAX_MEDIA_OBJECT_SIZE) {
throw Status.INVALID_ARGUMENT
.withDescription("Invalid sourceObject size")
.asRuntimeException();
}
final MessageBackupUploadDescriptor dst = cdn3BackupCredentialGenerator.generateUpload(
encodeBackupIdForCdn(backupUser),
encodeForCdn(destinationMediaId));
return this.backupsDb
// Write the ddb updates before actually updating backing storage
.trackMedia(backupUser, destinationMediaId, sourceLength)
// copy the objects. On a failure, make a best-effort attempt to reverse the ddb transaction. If cleanup fails
// the client may be left with some cleanup to do if they don't eventually upload the media id.
.thenCompose(ignored -> remoteStorageManager
// actually perform the copy
.copy(attachmentReadUri(sourceCdn, sourceKey), sourceLength, encryptionParameters, dst)
// best effort: on failure, untrack the copied media
.exceptionallyCompose(copyError -> backupsDb.untrackMedia(backupUser, destinationMediaId, sourceLength)
.thenCompose(ignoredSuccess -> CompletableFuture.failedFuture(copyError))))
// indicates where the backup was stored
.thenApply(ignore -> new StorageDescriptor(dst.cdn(), destinationMediaId));
}
/**
* Construct the URI for an attachment with the specified key
*
* @param cdn where the attachment is located
* @param key the attachment key
* @return A {@link URI} where the attachment can be retrieved
*/
private URI attachmentReadUri(final int cdn, final String key) {
final String baseUri = attachmentCdnBaseUris.get(cdn);
if (baseUri == null) {
throw Status.INVALID_ARGUMENT.withDescription("Unknown cdn " + cdn).asRuntimeException();
}
return URI.create("%s/%s".formatted(baseUri, key));
}
/**
* Generate credentials that can be used to read from the backup CDN
*
* @param backupUser an already ZK authenticated backup user
* @return A map of headers to include with CDN requests
*/
public Map<String, String> generateReadAuth(final AuthenticatedBackupUser backupUser) {
if (backupUser.backupTier().compareTo(BackupTier.MESSAGES) < 0) {
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
throw Status.PERMISSION_DENIED
.withDescription("credential does not support read auth operation")
.asRuntimeException();
}
final String encodedBackupId = encodeBackupIdForCdn(backupUser);
return cdn3BackupCredentialGenerator.readHeaders(encodedBackupId);
}
/**
* Authenticate the ZK anonymous backup credential's presentation
* <p>
* This validates:
* <li> The presentation was for a credential issued by the server </li>
* <li> The credential is in its redemption window </li>
* <li> The backup-id matches a previously committed blinded backup-id and server issued receipt level </li>
* <li> The signature of the credential matches an existing publicKey associated with this backup-id </li>
*
* @param presentation A {@link BackupAuthCredentialPresentation}
* @param signature An XEd25519 signature of the presentation bytes
* @return On authentication success, the authenticated backup-id and backup-tier encoded in the presentation
*/
public CompletableFuture<AuthenticatedBackupUser> authenticateBackupUser(
final BackupAuthCredentialPresentation presentation,
final byte[] signature) {
return backupsDb
.retrievePublicKey(presentation.getBackupId())
.thenApply(optionalPublicKey -> {
final byte[] publicKeyBytes = optionalPublicKey
.orElseThrow(() -> {
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
SUCCESS_TAG_NAME, String.valueOf(false),
FAILURE_REASON_TAG_NAME, "missing_public_key")
.increment();
return Status.NOT_FOUND.withDescription("Backup not found").asRuntimeException();
});
try {
final ECPublicKey publicKey = new ECPublicKey(publicKeyBytes);
return new AuthenticatedBackupUser(
presentation.getBackupId(),
verifySignatureAndCheckPresentation(presentation, signature, publicKey));
} catch (InvalidKeyException e) {
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
SUCCESS_TAG_NAME, String.valueOf(false),
FAILURE_REASON_TAG_NAME, "invalid_public_key")
.increment();
logger.error("Invalid publicKey for backupId hash {}",
HexFormat.of().formatHex(BackupsDb.hashedBackupId(presentation.getBackupId())), e);
throw Status.INTERNAL
.withCause(e)
.withDescription("Could not deserialize stored public key")
.asRuntimeException();
}
})
.thenApply(result -> {
Metrics.counter(ZK_AUTHN_COUNTER_NAME, SUCCESS_TAG_NAME, String.valueOf(true)).increment();
return result;
});
}
/**
* Verify the presentation and return the extracted backup tier
*
* @param presentation A ZK credential presentation that encodes the backupId and the receipt level of the requester
* @return The backup tier this presentation supports
*/
private BackupTier verifySignatureAndCheckPresentation(
final BackupAuthCredentialPresentation presentation,
final byte[] signature,
final ECPublicKey publicKey) {
if (!publicKey.verifySignature(presentation.serialize(), signature)) {
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
SUCCESS_TAG_NAME, String.valueOf(false),
FAILURE_REASON_TAG_NAME, "signature_validation")
.increment();
throw Status.UNAUTHENTICATED
.withDescription("backup auth credential presentation signature verification failed")
.asRuntimeException();
}
try {
presentation.verify(clock.instant(), serverSecretParams);
} catch (VerificationFailedException e) {
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
SUCCESS_TAG_NAME, String.valueOf(false),
FAILURE_REASON_TAG_NAME, "presentation_verification")
.increment();
throw Status.UNAUTHENTICATED
.withDescription("backup auth credential presentation verification failed")
.withCause(e)
.asRuntimeException();
}
return BackupTier
.fromReceiptLevel(presentation.getReceiptLevel())
.orElseThrow(() -> {
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
SUCCESS_TAG_NAME, String.valueOf(false),
FAILURE_REASON_TAG_NAME, "invalid_receipt_level")
.increment();
return Status.PERMISSION_DENIED.withDescription("invalid receipt level").asRuntimeException();
});
}
private static String encodeBackupIdForCdn(final AuthenticatedBackupUser backupUser) {
return encodeForCdn(BackupsDb.hashedBackupId(backupUser.backupId()));
}
private static String encodeForCdn(final byte[] bytes) {
return Base64.getUrlEncoder().encodeToString(bytes);
}
}

View File

@@ -0,0 +1,103 @@
package org.whispersystems.textsecuregcm.backup;
import java.net.http.HttpRequest;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.concurrent.Flow;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import org.reactivestreams.FlowAdapters;
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public class BackupMediaEncrypter {
private final Cipher cipher;
private final Mac mac;
public BackupMediaEncrypter(final MediaEncryptionParameters encryptionParameters) {
cipher = initializeCipher(encryptionParameters);
mac = initializeMac(encryptionParameters);
}
public int outputSize(final int inputSize) {
return cipher.getIV().length + cipher.getOutputSize(inputSize) + mac.getMacLength();
}
/**
* Perform streaming encryption
*
* @param sourceBody A source of ByteBuffers, typically from an asynchronous HttpResponse
* @return A publisher that returns IV + AES/CBC/PKCS5Padding encrypted source + HMAC(IV + encrypted source) suitable
* to write with an asynchronous HttpRequest
*/
public Flow.Publisher<ByteBuffer> encryptBody(Flow.Publisher<List<ByteBuffer>> sourceBody) {
// Write IV, encrypted payload, mac
final Flux<ByteBuffer> encryptedBody = Flux.concat(
Mono.fromSupplier(() -> {
mac.update(cipher.getIV());
return ByteBuffer.wrap(cipher.getIV());
}),
Flux.from(FlowAdapters.toPublisher(sourceBody))
.flatMap(buffers -> Flux.fromIterable(buffers))
.concatMap(byteBuffer -> {
final byte[] copy = new byte[byteBuffer.remaining()];
byteBuffer.get(copy);
final byte[] res = cipher.update(copy);
if (res == null) {
return Mono.empty();
} else {
mac.update(res);
return Mono.just(ByteBuffer.wrap(res));
}
}),
Mono.fromSupplier(() -> {
try {
final byte[] finalBytes = cipher.doFinal();
mac.update(finalBytes);
return ByteBuffer.wrap(finalBytes);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw ExceptionUtils.wrap(e);
}
}),
Mono.fromSupplier(() -> ByteBuffer.wrap(mac.doFinal())));
return FlowAdapters.toFlowPublisher(encryptedBody);
}
private static Mac initializeMac(final MediaEncryptionParameters encryptionParameters) {
try {
final Mac mac = Mac.getInstance("HmacSHA256");
mac.init(encryptionParameters.hmacSHA256Key());
return mac;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException(e);
}
}
private static Cipher initializeCipher(final MediaEncryptionParameters encryptionParameters) {
try {
final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(
Cipher.ENCRYPT_MODE,
encryptionParameters.aesEncryptionKey(),
encryptionParameters.iv());
return cipher;
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new AssertionError(e);
} catch (InvalidAlgorithmParameterException | InvalidKeyException e) {
throw new IllegalArgumentException(e);
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.backup;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
public enum BackupTier {
NONE(0),
MESSAGES(10),
MEDIA(20);
private static Map<Long, BackupTier> LOOKUP = Arrays.stream(BackupTier.values())
.collect(Collectors.toMap(BackupTier::getReceiptLevel, Function.identity()));
private long receiptLevel;
private BackupTier(long receiptLevel) {
this.receiptLevel = receiptLevel;
}
long getReceiptLevel() {
return receiptLevel;
}
static Optional<BackupTier> fromReceiptLevel(long receiptLevel) {
return Optional.ofNullable(LOOKUP.get(receiptLevel));
}
}

View File

@@ -0,0 +1,489 @@
package org.whispersystems.textsecuregcm.backup;
import io.grpc.Status;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Clock;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
import org.whispersystems.textsecuregcm.util.Util;
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.CancellationReason;
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
import software.amazon.awssdk.services.dynamodb.model.Delete;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.Put;
import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure;
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem;
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest;
import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException;
import software.amazon.awssdk.services.dynamodb.model.Update;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
/**
* Tracks backup metadata in a persistent store.
*
* It's assumed that the caller has already validated that the backupUser being operated on has valid credentials and
* possesses the appropriate {@link BackupTier} to perform the current operation.
*/
public class BackupsDb {
private static final Logger logger = LoggerFactory.getLogger(BackupsDb.class);
static final int BACKUP_CDN = 3;
private final DynamoDbAsyncClient dynamoClient;
private final String backupTableName;
private final String backupMediaTableName;
private final Clock clock;
// The backups table
// B: 16 bytes that identifies the backup
public static final String KEY_BACKUP_ID_HASH = "U";
// N: Time in seconds since epoch of the last backup refresh. This timestamp must be periodically updated to avoid
// garbage collection of archive objects.
public static final String ATTR_LAST_REFRESH = "R";
// N: Time in seconds since epoch of the last backup media refresh. This timestamp can only be updated if the client
// has BackupTier.MEDIA, and must be periodically updated to avoid garbage collection of media objects.
public static final String ATTR_LAST_MEDIA_REFRESH = "MR";
// B: A 32 byte public key that should be used to sign the presentation used to authenticate requests against the
// backup-id
public static final String ATTR_PUBLIC_KEY = "P";
// N: Bytes consumed by this backup
public static final String ATTR_MEDIA_BYTES_USED = "MB";
// N: Number of media objects in the backup
public static final String ATTR_MEDIA_COUNT = "MC";
// N: The cdn number where the message backup is stored
public static final String ATTR_CDN = "CDN";
// The stored media table (hashedBackupId, mediaId, cdn, objectLength)
// B: 15-byte mediaId
public static final String KEY_MEDIA_ID = "M";
// N: The length of the encrypted media object
public static final String ATTR_LENGTH = "L";
public BackupsDb(
final DynamoDbAsyncClient dynamoClient,
final String backupTableName,
final String backupMediaTableName,
final Clock clock) {
this.dynamoClient = dynamoClient;
this.backupTableName = backupTableName;
this.backupMediaTableName = backupMediaTableName;
this.clock = clock;
}
/**
* Set the public key associated with a backupId.
*
* @param authenticatedBackupId The backup-id bytes that should be associated with the provided public key
* @param authenticatedBackupTier The backup tier
* @param publicKey The public key to associate with the backup id
* @return A stage that completes when the public key has been set. If the backup-id already has a set public key that
* does not match, the stage will be completed exceptionally with a {@link PublicKeyConflictException}
*/
CompletableFuture<Void> setPublicKey(
final byte[] authenticatedBackupId,
final BackupTier authenticatedBackupTier,
final ECPublicKey publicKey) {
final byte[] hashedBackupId = hashedBackupId(authenticatedBackupId);
return dynamoClient.updateItem(new UpdateBuilder(backupTableName, authenticatedBackupTier, hashedBackupId)
.addSetExpression("#publicKey = :publicKey",
Map.entry("#publicKey", ATTR_PUBLIC_KEY),
Map.entry(":publicKey", AttributeValues.b(publicKey.serialize())))
.setRefreshTimes(clock)
.withConditionExpression("attribute_not_exists(#publicKey) OR #publicKey = :publicKey")
.updateItemBuilder()
.build())
.exceptionally(throwable -> {
// There was already a row for this backup-id and it contained a different publicKey
if (ExceptionUtils.unwrap(throwable) instanceof ConditionalCheckFailedException) {
throw ExceptionUtils.wrap(new PublicKeyConflictException());
}
throw ExceptionUtils.wrap(throwable);
})
.thenRun(Util.NOOP);
}
CompletableFuture<Optional<byte[]>> retrievePublicKey(byte[] backupId) {
final byte[] hashedBackupId = hashedBackupId(backupId);
return dynamoClient.getItem(GetItemRequest.builder()
.tableName(backupTableName)
.key(Map.of(KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId)))
.consistentRead(true)
.projectionExpression("#publicKey")
.expressionAttributeNames(Map.of("#publicKey", ATTR_PUBLIC_KEY))
.build())
.thenApply(response ->
AttributeValues.get(response.item(), ATTR_PUBLIC_KEY)
.map(AttributeValue::b)
.map(SdkBytes::asByteArray));
}
/**
* Add media to the backup media table and update the quota in the backup table
*
* @param backupUser The
* @param mediaId The mediaId to add
* @param mediaLength The length of the media before encryption (the length of the source media)
* @return A stage that completes successfully once the tables are updated. If the media with the provided id has
* previously been tracked with a different length, the stage will complete exceptionally with an
* {@link InvalidLengthException}
*/
CompletableFuture<Void> trackMedia(
final AuthenticatedBackupUser backupUser,
final byte[] mediaId,
final int mediaLength) {
final byte[] hashedBackupId = hashedBackupId(backupUser);
return dynamoClient
.transactWriteItems(TransactWriteItemsRequest.builder().transactItems(
// Add the media to the media table
TransactWriteItem.builder().put(Put.builder()
.tableName(backupMediaTableName)
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.item(Map.of(
KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId),
KEY_MEDIA_ID, AttributeValues.b(mediaId),
ATTR_CDN, AttributeValues.n(BACKUP_CDN),
ATTR_LENGTH, AttributeValues.n(mediaLength)))
.conditionExpression("attribute_not_exists(#mediaId)")
.expressionAttributeNames(Map.of("#mediaId", KEY_MEDIA_ID))
.build()).build(),
// Update the media quota and TTL
TransactWriteItem.builder().update(
UpdateBuilder.forUser(backupTableName, backupUser)
.setRefreshTimes(clock)
.incrementMediaBytes(mediaLength)
.incrementMediaCount(1)
.transactItemBuilder()
.build()).build()).build())
.exceptionally(throwable -> {
if (ExceptionUtils.unwrap(throwable) instanceof TransactionCanceledException txCancelled) {
final long oldItemLength = conditionCheckFailed(txCancelled, 0)
.flatMap(item -> Optional.ofNullable(item.get(ATTR_LENGTH)))
.map(attr -> Long.parseLong(attr.n()))
.orElseThrow(() -> ExceptionUtils.wrap(throwable));
if (oldItemLength != mediaLength) {
throw new CompletionException(
new InvalidLengthException("Previously tried to copy media with a different length. "
+ "Provided " + mediaLength + " was " + oldItemLength));
}
// The client already "paid" for this media, can let them through
return null;
} else {
// rethrow original exception
throw ExceptionUtils.wrap(throwable);
}
})
.thenRun(Util.NOOP);
}
/**
* Remove media from backup media table and update the quota in the backup table
*
* @param backupUser The backup user
* @param mediaId The mediaId to add
* @param mediaLength The length of the media before encryption (the length of the source media)
* @return A stage that completes successfully once the tables are updated
*/
CompletableFuture<Void> untrackMedia(
final AuthenticatedBackupUser backupUser,
final byte[] mediaId,
final int mediaLength) {
final byte[] hashedBackupId = hashedBackupId(backupUser);
return dynamoClient.transactWriteItems(TransactWriteItemsRequest.builder().transactItems(
TransactWriteItem.builder().delete(Delete.builder()
.tableName(backupMediaTableName)
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.key(Map.of(
KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId),
KEY_MEDIA_ID, AttributeValues.b(mediaId)
))
.conditionExpression("#length = :length")
.expressionAttributeNames(Map.of("#length", ATTR_LENGTH))
.expressionAttributeValues(Map.of(":length", AttributeValues.n(mediaLength)))
.build()).build(),
// Don't update TTLs, since we're just cleaning up media
TransactWriteItem.builder().update(UpdateBuilder.forUser(backupTableName, backupUser)
.incrementMediaBytes(-mediaLength)
.incrementMediaCount(-1)
.transactItemBuilder().build()).build()).build())
.exceptionally(error -> {
logger.warn("failed cleanup after failed copy operation", error);
return null;
})
.thenRun(Util.NOOP);
}
/**
* Update the last update timestamps for the backupId in the presentation
*
* @param backupUser an already authorized backup user
*/
CompletableFuture<Void> ttlRefresh(final AuthenticatedBackupUser backupUser) {
// update message backup TTL
return dynamoClient.updateItem(UpdateBuilder.forUser(backupTableName, backupUser)
.setRefreshTimes(clock)
.updateItemBuilder()
.build())
.thenRun(Util.NOOP);
}
/**
* Track that a backup will be stored for the user
* @param backupUser an already authorized backup user
*/
CompletableFuture<Void> addMessageBackup(final AuthenticatedBackupUser backupUser) {
// this could race with concurrent updates, but the only effect would be last-writer-wins on the timestamp
return dynamoClient.updateItem(
UpdateBuilder.forUser(backupTableName, backupUser)
.setRefreshTimes(clock)
.setCdn(BACKUP_CDN)
.updateItemBuilder()
.build())
.thenRun(Util.NOOP);
}
record BackupDescription(int cdn, Optional<Long> mediaUsedSpace) {}
/**
* Retrieve information about the backup
*
* @param backupUser an already authorized backup user
* @return A {@link BackupDescription} containing the cdn of the message backup and the total number of media space
* bytes used by the backup user.
*/
CompletableFuture<BackupDescription> describeBackup(final AuthenticatedBackupUser backupUser) {
return dynamoClient.getItem(GetItemRequest.builder()
.tableName(backupTableName)
.key(Map.of(KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId(backupUser))))
.projectionExpression("#cdn,#bytesUsed")
.expressionAttributeNames(Map.of("#cdn", ATTR_CDN, "#bytesUsed", ATTR_MEDIA_BYTES_USED))
.consistentRead(true)
.build())
.thenApply(response -> {
if (!response.hasItem()) {
throw Status.NOT_FOUND.withDescription("Backup not found").asRuntimeException();
}
final int cdn = AttributeValues.get(response.item(), ATTR_CDN)
.map(AttributeValue::n)
.map(Integer::parseInt)
.orElseThrow(() -> Status.NOT_FOUND.withDescription("Stored backup not found").asRuntimeException());
final Optional<Long> mediaUsed = AttributeValues.get(response.item(), ATTR_MEDIA_BYTES_USED)
.map(AttributeValue::n)
.map(Long::parseLong);
return new BackupDescription(cdn, mediaUsed);
});
}
/**
* Build ddb update statements for the backups table
*/
private static class UpdateBuilder {
private final List<String> setStatements = new ArrayList<>();
private final Map<String, AttributeValue> attrValues = new HashMap<>();
private final Map<String, String> attrNames = new HashMap<>();
private final String tableName;
private final BackupTier backupTier;
private final byte[] hashedBackupId;
private String conditionExpression = null;
static UpdateBuilder forUser(String tableName, AuthenticatedBackupUser backupUser) {
return new UpdateBuilder(tableName, backupUser.backupTier(), hashedBackupId(backupUser));
}
UpdateBuilder(String tableName, BackupTier backupTier, byte[] hashedBackupId) {
this.tableName = tableName;
this.backupTier = backupTier;
this.hashedBackupId = hashedBackupId;
}
private void addAttrValue(Map.Entry<String, AttributeValue> attrValue) {
final AttributeValue old = attrValues.put(attrValue.getKey(), attrValue.getValue());
if (old != null && !old.equals(attrValue.getValue())) {
throw new IllegalArgumentException("duplicate attrValue key used for different values");
}
}
private void addAttrName(Map.Entry<String, String> attrName) {
final String oldName = attrNames.put(attrName.getKey(), attrName.getValue());
if (oldName != null && !oldName.equals(attrName.getValue())) {
throw new IllegalArgumentException("duplicate attrName key used for different attribute names");
}
}
private void addAttrs(final Map.Entry<String, String> attrName, final Map.Entry<String, AttributeValue> attrValue) {
addAttrName(attrName);
addAttrValue(attrValue);
}
UpdateBuilder addSetExpression(
final String update,
final Map.Entry<String, String> attrName,
final Map.Entry<String, AttributeValue> attrValue) {
setStatements.add(update);
addAttrs(attrName, attrValue);
return this;
}
UpdateBuilder addSetExpression(final String update) {
setStatements.add(update);
return this;
}
UpdateBuilder withConditionExpression(final String conditionExpression) {
this.conditionExpression = conditionExpression;
return this;
}
UpdateBuilder withConditionExpression(
final String conditionExpression,
final Map.Entry<String, String> attrName,
final Map.Entry<String, AttributeValue> attrValue) {
this.addAttrs(attrName, attrValue);
this.conditionExpression = conditionExpression;
return this;
}
UpdateBuilder setCdn(final int cdn) {
return addSetExpression(
"#cdn = :cdn",
Map.entry("#cdn", ATTR_CDN),
Map.entry(":cdn", AttributeValues.n(cdn)));
}
UpdateBuilder incrementMediaCount(long delta) {
addAttrName(Map.entry("#mediaCount", ATTR_MEDIA_COUNT));
addAttrValue(Map.entry(":zero", AttributeValues.n(0)));
addAttrValue(Map.entry(":mediaCountDelta", AttributeValues.n(delta)));
addSetExpression("#mediaCount = if_not_exists(#mediaCount, :zero) + :mediaCountDelta");
return this;
}
UpdateBuilder incrementMediaBytes(long delta) {
addAttrName(Map.entry("#mediaBytes", ATTR_MEDIA_BYTES_USED));
addAttrValue(Map.entry(":zero", AttributeValues.n(0)));
addAttrValue(Map.entry(":mediaBytesDelta", AttributeValues.n(delta)));
addSetExpression("#mediaBytes = if_not_exists(#mediaBytes, :zero) + :mediaBytesDelta");
return this;
}
/**
* Set the lastRefresh time as part of the update
* <p>
* This always updates lastRefreshTime, and updates lastMediaRefreshTime if the backup user has the appropriate
* tier
*/
UpdateBuilder setRefreshTimes(final Clock clock) {
final long refreshTimeSecs = clock.instant().getEpochSecond();
addSetExpression("#lastRefreshTime = :lastRefreshTime",
Map.entry("#lastRefreshTime", ATTR_LAST_REFRESH),
Map.entry(":lastRefreshTime", AttributeValues.n(refreshTimeSecs)));
if (backupTier.compareTo(BackupTier.MEDIA) >= 0) {
// update the media time if we have the appropriate tier
addSetExpression("#lastMediaRefreshTime = :lastMediaRefreshTime",
Map.entry("#lastMediaRefreshTime", ATTR_LAST_MEDIA_REFRESH),
Map.entry(":lastMediaRefreshTime", AttributeValues.n(refreshTimeSecs)));
}
return this;
}
/**
* Prepare a non-transactional update
*
* @return An {@link UpdateItemRequest#builder()} that can be used with updateItem
*/
UpdateItemRequest.Builder updateItemBuilder() {
final UpdateItemRequest.Builder bldr = UpdateItemRequest.builder()
.tableName(tableName)
.key(Map.of(KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId)))
.updateExpression("SET %s".formatted(String.join(",", setStatements)))
.expressionAttributeNames(attrNames)
.expressionAttributeValues(attrValues);
if (this.conditionExpression != null) {
bldr.conditionExpression(conditionExpression);
}
return bldr;
}
/**
* Prepare a transactional update
*
* @return An {@link Update#builder()} that can be used with transactItem
*/
Update.Builder transactItemBuilder() {
final Update.Builder bldr = Update.builder()
.tableName(tableName)
.key(Map.of(KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId)))
.updateExpression("SET %s".formatted(String.join(",", setStatements)))
.expressionAttributeNames(attrNames)
.expressionAttributeValues(attrValues);
if (this.conditionExpression != null) {
bldr.conditionExpression(conditionExpression);
}
return bldr;
}
}
private static byte[] hashedBackupId(final AuthenticatedBackupUser backupId) {
return hashedBackupId(backupId.backupId());
}
static byte[] hashedBackupId(final byte[] backupId) {
try {
return Arrays.copyOf(MessageDigest.getInstance("SHA-256").digest(backupId), 16);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
/**
* Check if a DynamoDb error indicates a condition check failed error, and return the value of the item failed to
* update.
*
* @param e The error returned by {@link DynamoDbAsyncClient#transactWriteItems} attempt
* @param itemIndex The index of the item in the transaction that had a condition expression
* @return The remote value of the item that failed to update, or empty if the error was not a condition check failure
*/
private static Optional<Map<String, AttributeValue>> conditionCheckFailed(TransactionCanceledException e,
int itemIndex) {
if (!e.hasCancellationReasons()) {
return Optional.empty();
}
if (e.cancellationReasons().size() < itemIndex + 1) {
return Optional.empty();
}
final CancellationReason reason = e.cancellationReasons().get(itemIndex);
if (!"ConditionalCheckFailed".equals(reason.code()) || !reason.hasItem()) {
return Optional.empty();
}
return Optional.of(reason.item());
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.backup;
import org.apache.http.HttpHeaders;
import org.whispersystems.textsecuregcm.attachments.TusConfiguration;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
import org.whispersystems.textsecuregcm.util.HeaderUtils;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.util.Base64;
import java.util.Map;
public class Cdn3BackupCredentialGenerator {
public static final String CDN_PATH = "backups";
public static final int BACKUP_CDN = 3;
private static String READ_PERMISSION = "read";
private static String WRITE_PERMISSION = "write";
private static String PERMISSION_SEPARATOR = "$";
// Write entities will be of the form 'write$backups/<string>
private static final String WRITE_ENTITY_PREFIX = String.format("%s%s%s/", WRITE_PERMISSION, PERMISSION_SEPARATOR,
CDN_PATH);
// Read entities will be of the form 'read$backups/<string>
private static final String READ_ENTITY_PREFIX = String.format("%s%s%s/", READ_PERMISSION, PERMISSION_SEPARATOR,
CDN_PATH);
private final ExternalServiceCredentialsGenerator credentialsGenerator;
private final String tusUri;
public Cdn3BackupCredentialGenerator(final TusConfiguration cfg) {
this.tusUri = cfg.uploadUri();
this.credentialsGenerator = credentialsGenerator(Clock.systemUTC(), cfg);
}
private static ExternalServiceCredentialsGenerator credentialsGenerator(final Clock clock,
final TusConfiguration cfg) {
return ExternalServiceCredentialsGenerator
.builder(cfg.userAuthenticationTokenSharedSecret())
.prependUsername(false)
.withClock(clock)
.build();
}
public MessageBackupUploadDescriptor generateUpload(final String hashedBackupId, final String objectName) {
if (hashedBackupId.isBlank() || objectName.isBlank()) {
throw new IllegalArgumentException("Upload descriptors must have non-empty keys");
}
final String key = "%s/%s".formatted(hashedBackupId, objectName);
final String entity = WRITE_ENTITY_PREFIX + key;
final ExternalServiceCredentials credentials = credentialsGenerator.generateFor(entity);
final String b64Key = Base64.getEncoder().encodeToString(key.getBytes(StandardCharsets.UTF_8));
final Map<String, String> headers = Map.of(
HttpHeaders.AUTHORIZATION, HeaderUtils.basicAuthHeader(credentials),
"Upload-Metadata", String.format("filename %s", b64Key));
return new MessageBackupUploadDescriptor(
BACKUP_CDN,
key,
headers,
tusUri + "/" + CDN_PATH);
}
public Map<String, String> readHeaders(final String hashedBackupId) {
if (hashedBackupId.isBlank()) {
throw new IllegalArgumentException("Backup subdir name must be non-empty");
}
final ExternalServiceCredentials credentials = credentialsGenerator.generateFor(
READ_ENTITY_PREFIX + hashedBackupId);
return Map.of(HttpHeaders.AUTHORIZATION, HeaderUtils.basicAuthHeader(credentials));
}
}

View File

@@ -0,0 +1,102 @@
package org.whispersystems.textsecuregcm.backup;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.cert.CertificateException;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.stream.Stream;
import javax.ws.rs.core.Response;
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
import org.whispersystems.textsecuregcm.configuration.RetryConfiguration;
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
public class Cdn3RemoteStorageManager implements RemoteStorageManager {
private final FaultTolerantHttpClient httpClient;
public Cdn3RemoteStorageManager(
final ScheduledExecutorService retryExecutor,
final CircuitBreakerConfiguration circuitBreakerConfiguration,
final RetryConfiguration retryConfiguration,
final List<String> caCertificates) throws CertificateException {
this.httpClient = FaultTolerantHttpClient.newBuilder()
.withName("cdn3-remote-storage")
.withCircuitBreaker(circuitBreakerConfiguration)
.withExecutor(Executors.newCachedThreadPool())
.withRetryExecutor(retryExecutor)
.withRetry(retryConfiguration)
.withConnectTimeout(Duration.ofSeconds(10))
.withVersion(HttpClient.Version.HTTP_2)
.withTrustedServerCertificates(caCertificates.toArray(new String[0]))
.build();
}
@Override
public int cdnNumber() {
return 3;
}
@Override
public CompletionStage<Void> copy(
final URI sourceUri,
final int expectedSourceLength,
final MediaEncryptionParameters encryptionParameters,
final MessageBackupUploadDescriptor uploadDescriptor) {
if (uploadDescriptor.cdn() != cdnNumber()) {
throw new IllegalArgumentException("Cdn3RemoteStorageManager can only copy to cdn3");
}
final BackupMediaEncrypter encrypter = new BackupMediaEncrypter(encryptionParameters);
final HttpRequest request = HttpRequest.newBuilder().GET().uri(sourceUri).build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofPublisher()).thenCompose(response -> {
if (response.statusCode() == Response.Status.NOT_FOUND.getStatusCode()) {
throw new CompletionException(new SourceObjectNotFoundException());
} else if (response.statusCode() != Response.Status.OK.getStatusCode()) {
throw new CompletionException(new IOException("error reading from source: " + response.statusCode()));
}
final int actualSourceLength = Math.toIntExact(response.headers().firstValueAsLong("Content-Length")
.orElseThrow(() -> new CompletionException(new IOException("upstream missing Content-Length"))));
if (actualSourceLength != expectedSourceLength) {
throw new CompletionException(
new InvalidLengthException("Provided sourceLength " + expectedSourceLength + " was " + actualSourceLength));
}
final int expectedEncryptedLength = encrypter.outputSize(actualSourceLength);
final HttpRequest.BodyPublisher encryptedBody = HttpRequest.BodyPublishers.fromPublisher(
encrypter.encryptBody(response.body()), expectedEncryptedLength);
final String[] headers = Stream.concat(
uploadDescriptor.headers().entrySet()
.stream()
.flatMap(e -> Stream.of(e.getKey(), e.getValue())),
Stream.of("Upload-Length", Integer.toString(expectedEncryptedLength), "Tus-Resumable", "1.0.0"))
.toArray(String[]::new);
final HttpRequest put = HttpRequest.newBuilder()
.uri(URI.create(uploadDescriptor.signedUploadLocation()))
.headers(headers)
.POST(encryptedBody)
.build();
return httpClient.sendAsync(put, HttpResponse.BodyHandlers.discarding());
})
.thenAccept(response -> {
if (response.statusCode() != Response.Status.CREATED.getStatusCode() &&
response.statusCode() != Response.Status.OK.getStatusCode()) {
throw new CompletionException(new IOException("Failed to copy object: " + response.statusCode()));
}
});
}
}

View File

@@ -0,0 +1,10 @@
package org.whispersystems.textsecuregcm.backup;
import java.io.IOException;
public class InvalidLengthException extends IOException {
public InvalidLengthException(String s) {
super(s);
}
}

View File

@@ -0,0 +1,17 @@
package org.whispersystems.textsecuregcm.backup;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public record MediaEncryptionParameters(
SecretKeySpec aesEncryptionKey,
SecretKeySpec hmacSHA256Key,
IvParameterSpec iv) {
public MediaEncryptionParameters(byte[] encryptionKey, byte[] macKey, byte[] iv) {
this(
new SecretKeySpec(encryptionKey, "AES"),
new SecretKeySpec(macKey, "HmacSHA256"),
new IvParameterSpec(iv));
}
}

View File

@@ -0,0 +1,14 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.backup;
import java.util.Map;
public record MessageBackupUploadDescriptor(
int cdn,
String key,
Map<String, String> headers,
String signedUploadLocation) {}

View File

@@ -0,0 +1,6 @@
package org.whispersystems.textsecuregcm.backup;
import java.io.IOException;
public class PublicKeyConflictException extends IOException {
}

View File

@@ -0,0 +1,38 @@
package org.whispersystems.textsecuregcm.backup;
import java.net.URI;
import java.util.concurrent.CompletionStage;
/**
* Handles management operations over a external cdn storage system.
*/
public interface RemoteStorageManager {
/**
* @return The cdn number that this RemoteStorageManager manages
*/
int cdnNumber();
/**
* Copy and the object from a remote source into the backup, adding an additional layer of encryption
*
* @param sourceUri The location of the object to copy
* @param expectedSourceLength The length of the source object, should match the content-length of the object returned
* from the sourceUri.
* @param encryptionParameters The encryption keys that should be used to apply an additional layer of encryption to
* the object
* @param uploadDescriptor The destination, which must be in the cdn returned by {@link #cdnNumber()}
* @return A stage that completes successfully when the source has been successfully re-encrypted and copied into
* uploadDescriptor. The returned CompletionStage can be completed exceptionally with the following exceptions.
* <ul>
* <li> {@link InvalidLengthException} If the expectedSourceLength does not match the length of the sourceUri </li>
* <li> {@link SourceObjectNotFoundException} If the no object at sourceUri is found </li>
* <li> {@link java.io.IOException} If there was a generic IO issue </li>
* </ul>
*/
CompletionStage<Void> copy(
URI sourceUri,
int expectedSourceLength,
MediaEncryptionParameters encryptionParameters,
MessageBackupUploadDescriptor uploadDescriptor);
}

View File

@@ -0,0 +1,11 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.backup;
import java.io.IOException;
public class SourceObjectNotFoundException extends IOException {
}

View File

@@ -29,9 +29,15 @@ public class CaptchaChecker {
@VisibleForTesting
static final String SEPARATOR = ".";
private static final String SHORT_SUFFIX = "-short";
private final ShortCodeExpander shortCodeExpander;
private final Map<String, CaptchaClient> captchaClientMap;
public CaptchaChecker(final List<CaptchaClient> captchaClients) {
public CaptchaChecker(
final ShortCodeExpander shortCodeRetriever,
final List<CaptchaClient> captchaClients) {
this.shortCodeExpander = shortCodeRetriever;
this.captchaClientMap = captchaClients.stream()
.collect(Collectors.toMap(CaptchaClient::scheme, Function.identity()));
}
@@ -63,9 +69,17 @@ public class CaptchaChecker {
final String prefix = parts[0];
final String siteKey = parts[1].toLowerCase(Locale.ROOT).strip();
final String action = parts[2];
final String token = parts[3];
String token = parts[3];
final CaptchaClient client = this.captchaClientMap.get(prefix);
String provider = prefix;
if (prefix.endsWith(SHORT_SUFFIX)) {
// This is a "short" solution that points to the actual solution. We need to fetch the
// full solution before proceeding
provider = prefix.substring(0, prefix.length() - SHORT_SUFFIX.length());
token = shortCodeExpander.retrieve(token).orElseThrow(() -> new BadRequestException("invalid shortcode"));
}
final CaptchaClient client = this.captchaClientMap.get(provider);
if (client == null) {
throw new BadRequestException("invalid captcha scheme");
}
@@ -92,7 +106,7 @@ public class CaptchaChecker {
Metrics.counter(ASSESSMENTS_COUNTER_NAME,
"action", action,
"score", result.getScoreString(),
"provider", prefix)
"provider", provider)
.increment();
return result;
}

View File

@@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.captcha;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
import com.google.common.annotations.VisibleForTesting;
import io.micrometer.core.instrument.Metrics;
import java.io.IOException;
import java.math.BigDecimal;
@@ -16,15 +17,25 @@ import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import javax.ws.rs.core.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
import org.whispersystems.textsecuregcm.configuration.RetryConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicCaptchaConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
import org.whispersystems.textsecuregcm.util.SystemMapper;
public class HCaptchaClient implements CaptchaClient {
@@ -34,16 +45,36 @@ public class HCaptchaClient implements CaptchaClient {
private static final String ASSESSMENT_REASON_COUNTER_NAME = name(HCaptchaClient.class, "assessmentReason");
private static final String INVALID_REASON_COUNTER_NAME = name(HCaptchaClient.class, "invalidReason");
private final String apiKey;
private final HttpClient client;
private final FaultTolerantHttpClient client;
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
@VisibleForTesting
HCaptchaClient(final String apiKey,
final FaultTolerantHttpClient faultTolerantHttpClient,
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
this.apiKey = apiKey;
this.client = faultTolerantHttpClient;
this.dynamicConfigurationManager = dynamicConfigurationManager;
}
public HCaptchaClient(
final String apiKey,
final HttpClient client,
final ScheduledExecutorService retryExecutor,
final CircuitBreakerConfiguration circuitBreakerConfiguration,
final RetryConfiguration retryConfiguration,
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
this.apiKey = apiKey;
this.client = client;
this.dynamicConfigurationManager = dynamicConfigurationManager;
this(apiKey,
FaultTolerantHttpClient.newBuilder()
.withName("hcaptcha")
.withCircuitBreaker(circuitBreakerConfiguration)
.withExecutor(Executors.newCachedThreadPool())
.withRetryExecutor(retryExecutor)
.withRetry(retryConfiguration)
.withRetryOnException(ex -> ex instanceof IOException)
.withConnectTimeout(Duration.ofSeconds(10))
.withVersion(HttpClient.Version.HTTP_2)
.build(),
dynamicConfigurationManager);
}
@Override
@@ -82,11 +113,12 @@ public class HCaptchaClient implements CaptchaClient {
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response;
final HttpResponse<String> response;
try {
response = this.client.send(request, HttpResponse.BodyHandlers.ofString());
} catch (InterruptedException e) {
throw new IOException(e);
response = this.client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join();
} catch (CompletionException e) {
logger.warn("failed to make http request to hCaptcha: {}", e.getMessage());
throw new IOException(ExceptionUtils.unwrap(e));
}
if (response.statusCode() != Response.Status.OK.getStatusCode()) {

View File

@@ -58,50 +58,4 @@ public class RegistrationCaptchaManager {
? Optional.of(captchaChecker.verify(Action.REGISTRATION, captcha.get(), sourceHost))
: Optional.empty();
}
public boolean requiresCaptcha(final String number, final String forwardedFor, String sourceHost,
final boolean pushChallengeMatch) {
if (testDevices.contains(number)) {
return false;
}
if (!pushChallengeMatch) {
return true;
}
final String countryCode = Util.getCountryCode(number);
final String region = Util.getRegion(number);
DynamicCaptchaConfiguration captchaConfig = dynamicConfigurationManager.getConfiguration()
.getCaptchaConfiguration();
boolean countryFiltered = captchaConfig.getSignupCountryCodes().contains(countryCode) ||
captchaConfig.getSignupRegions().contains(region);
try {
rateLimiters.getSmsVoiceIpLimiter().validate(sourceHost);
} catch (RateLimitExceededException e) {
logger.info("Rate limit exceeded: {}, {} ({})", number, sourceHost, forwardedFor);
rateLimitedHostMeter.mark();
return true;
}
try {
rateLimiters.getSmsVoicePrefixLimiter().validate(Util.getNumberPrefix(number));
} catch (RateLimitExceededException e) {
logger.info("Prefix rate limit exceeded: {}, {} ({})", number, sourceHost, forwardedFor);
rateLimitedPrefixMeter.mark();
return true;
}
if (countryFiltered) {
countryFilteredHostMeter.mark();
return true;
}
return false;
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.captcha;
import io.micrometer.core.instrument.Metrics;
import org.apache.http.HttpStatus;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Optional;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
public class ShortCodeExpander {
private static final String EXPAND_COUNTER_NAME = name(ShortCodeExpander.class, "expand");
private final HttpClient client;
private final URI shortenerHost;
public ShortCodeExpander(final HttpClient client, final String shortenerHost) {
this.client = client;
this.shortenerHost = URI.create(shortenerHost);
}
public Optional<String> retrieve(final String shortCode) throws IOException {
final URI uri = shortenerHost.resolve(shortCode);
final HttpRequest request = HttpRequest.newBuilder().uri(uri).GET().build();
try {
final HttpResponse<String> response = this.client.send(request, HttpResponse.BodyHandlers.ofString());
Metrics.counter(EXPAND_COUNTER_NAME, "responseCode", Integer.toString(response.statusCode())).increment();
return switch (response.statusCode()) {
case HttpStatus.SC_OK -> Optional.of(response.body());
case HttpStatus.SC_NOT_FOUND -> Optional.empty();
default -> throw new IOException("Failed to look up shortcode");
};
} catch (InterruptedException e) {
throw new IOException(e);
}
}
}

View File

@@ -1,18 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
public class AccountDatabaseCrawlerConfiguration {
@JsonProperty
private int chunkSize = 1000;
public int getChunkSize() {
return chunkSize;
}
}

View File

@@ -10,22 +10,19 @@ public class AccountsTableConfiguration extends Table {
private final String phoneNumberTableName;
private final String phoneNumberIdentifierTableName;
private final String usernamesTableName;
private final int scanPageSize;
@JsonCreator
public AccountsTableConfiguration(
@JsonProperty("tableName") final String tableName,
@JsonProperty("phoneNumberTableName") final String phoneNumberTableName,
@JsonProperty("phoneNumberIdentifierTableName") final String phoneNumberIdentifierTableName,
@JsonProperty("usernamesTableName") final String usernamesTableName,
@JsonProperty("scanPageSize") final int scanPageSize) {
@JsonProperty("usernamesTableName") final String usernamesTableName) {
super(tableName);
this.phoneNumberTableName = phoneNumberTableName;
this.phoneNumberIdentifierTableName = phoneNumberIdentifierTableName;
this.usernamesTableName = usernamesTableName;
this.scanPageSize = scanPageSize;
}
@NotBlank
@@ -42,8 +39,4 @@ public class AccountsTableConfiguration extends Table {
public String getUsernamesTableName() {
return usernamesTableName;
}
public int getScanPageSize() {
return scanPageSize;
}
}

View File

@@ -1,16 +0,0 @@
/*
* Copyright 2022 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
public record AdminEventLoggingConfiguration(
@NotBlank String credentials,
@NotBlank String secondaryCredentials,
@NotEmpty String projectId,
@NotEmpty String logName) {
}

View File

@@ -9,8 +9,8 @@ import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
public record ApnConfiguration(@NotBlank String teamId,
@NotBlank String keyId,
public record ApnConfiguration(@NotNull SecretString teamId,
@NotNull SecretString keyId,
@NotNull SecretString signingKey,
@NotBlank String bundleId,
boolean sandbox) {

View File

@@ -12,6 +12,7 @@ import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod;
/**
* @param merchantId the Braintree merchant ID
@@ -27,7 +28,7 @@ public record BraintreeConfiguration(@NotBlank String merchantId,
@NotBlank String publicKey,
@NotNull SecretString privateKey,
@NotBlank String environment,
@NotEmpty Set<@NotBlank String> supportedCurrencies,
@Valid @NotEmpty Map<PaymentMethod, Set<@NotBlank String>> supportedCurrenciesByPaymentMethod,
@NotBlank String graphqlUrl,
@NotEmpty Map<String, String> merchantAccounts,
@NotNull

View File

@@ -0,0 +1,53 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Configuration used to interact with a cdn via HTTP
*/
public class ClientCdnConfiguration {
/**
* Map from cdn number to the base url for attachments.
* <p>
* For example, if an attachment with the id 'abc' can be retrieved from cdn 2 at https://example.org/attachments/abc,
* the attachment url for 2 should https://example.org/attachments
*/
@JsonProperty
@NotNull
Map<Integer, @NotBlank String> attachmentUrls;
@JsonProperty
@NotNull
@NotEmpty List<@NotBlank String> caCertificates = new ArrayList<>();
@JsonProperty
@NotNull
CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration();
@JsonProperty
@NotNull
RetryConfiguration retry = new RetryConfiguration();
public List<String> getCaCertificates() {
return caCertificates;
}
public CircuitBreakerConfiguration getCircuitBreaker() {
return circuitBreaker;
}
public RetryConfiguration getRetry() {
return retry;
}
public Map<Integer, String> getAttachmentUrls() {
return attachmentUrls;
}
}

View File

@@ -0,0 +1,12 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import javax.validation.constraints.NotNull;
import java.time.Duration;
public record ClientReleaseConfiguration(@NotNull Duration refreshInterval) {
}

View File

@@ -0,0 +1,12 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import javax.validation.constraints.NotNull;
public record CommandStopListenerConfiguration(@NotNull String path) {
}

View File

@@ -1,24 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotNull;
import io.dropwizard.db.DataSourceFactory;
public class DatabaseConfiguration extends DataSourceFactory {
@NotNull
@JsonProperty
private CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration();
public CircuitBreakerConfiguration getCircuitBreakerConfiguration() {
return circuitBreaker;
}
}

View File

@@ -6,18 +6,13 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.micrometer.datadog.DatadogConfig;
import io.micrometer.statsd.StatsdConfig;
import io.micrometer.statsd.StatsdFlavor;
import java.time.Duration;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
public class DatadogConfiguration implements DatadogConfig {
@JsonProperty
@NotNull
private SecretString apiKey;
public class DogstatsdConfiguration implements StatsdConfig {
@JsonProperty
@NotNull
@@ -27,15 +22,6 @@ public class DatadogConfiguration implements DatadogConfig {
@NotBlank
private String environment;
@JsonProperty
@Min(1)
private int batchSize = 5_000;
@Override
public String apiKey() {
return apiKey.value();
}
@Override
public Duration step() {
return step;
@@ -46,17 +32,13 @@ public class DatadogConfiguration implements DatadogConfig {
}
@Override
public int batchSize() {
return batchSize;
}
@Override
public String hostTag() {
return "host";
public StatsdFlavor flavor() {
return StatsdFlavor.DATADOG;
}
@Override
public String get(final String key) {
// We have no Micrometer key/value pairs to report, so always return `null`
return null;
}
}

View File

@@ -5,37 +5,27 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.Duration;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
public class DynamoDbClientConfiguration {
public record DynamoDbClientConfiguration(@NotBlank String region,
@NotNull Duration clientExecutionTimeout,
@NotNull Duration clientRequestTimeout,
@Positive int maxConnections) {
private final String region;
private final Duration clientExecutionTimeout;
private final Duration clientRequestTimeout;
public DynamoDbClientConfiguration {
if (clientExecutionTimeout == null) {
clientExecutionTimeout = Duration.ofSeconds(30);
}
@JsonCreator
public DynamoDbClientConfiguration(
@JsonProperty("region") final String region,
@JsonProperty("clientExcecutionTimeout") final Duration clientExecutionTimeout,
@JsonProperty("clientRequestTimeout") final Duration clientRequestTimeout) {
this.region = region;
this.clientExecutionTimeout = clientExecutionTimeout != null ? clientExecutionTimeout : Duration.ofSeconds(30);
this.clientRequestTimeout = clientRequestTimeout != null ? clientRequestTimeout : Duration.ofSeconds(10);
}
if (clientRequestTimeout == null) {
clientRequestTimeout = Duration.ofSeconds(10);
}
@NotEmpty
public String getRegion() {
return region;
}
public Duration getClientExecutionTimeout() {
return clientExecutionTimeout;
}
public Duration getClientRequestTimeout() {
return clientRequestTimeout;
if (maxConnections == 0) {
maxConnections = 50;
}
}
}

View File

@@ -47,6 +47,10 @@ public class DynamoDbTables {
}
private final AccountsTableConfiguration accounts;
private final Table backups;
private final Table backupMedia;
private final Table clientReleases;
private final Table deletedAccounts;
private final Table deletedAccountsLock;
private final IssuedReceiptsTableConfiguration issuedReceipts;
@@ -55,8 +59,7 @@ public class DynamoDbTables {
private final Table kemKeys;
private final Table kemLastResortKeys;
private final TableWithExpiration messages;
private final Table pendingAccounts;
private final Table pendingDevices;
private final TableWithExpiration onetimeDonations;
private final Table phoneNumberIdentifiers;
private final Table profiles;
private final Table pushChallenge;
@@ -69,6 +72,9 @@ public class DynamoDbTables {
public DynamoDbTables(
@JsonProperty("accounts") final AccountsTableConfiguration accounts,
@JsonProperty("backups") final Table backups,
@JsonProperty("backupMedia") final Table backupMedia,
@JsonProperty("clientReleases") final Table clientReleases,
@JsonProperty("deletedAccounts") final Table deletedAccounts,
@JsonProperty("deletedAccountsLock") final Table deletedAccountsLock,
@JsonProperty("issuedReceipts") final IssuedReceiptsTableConfiguration issuedReceipts,
@@ -77,8 +83,7 @@ public class DynamoDbTables {
@JsonProperty("pqKeys") final Table kemKeys,
@JsonProperty("pqLastResortKeys") final Table kemLastResortKeys,
@JsonProperty("messages") final TableWithExpiration messages,
@JsonProperty("pendingAccounts") final Table pendingAccounts,
@JsonProperty("pendingDevices") final Table pendingDevices,
@JsonProperty("onetimeDonations") final TableWithExpiration onetimeDonations,
@JsonProperty("phoneNumberIdentifiers") final Table phoneNumberIdentifiers,
@JsonProperty("profiles") final Table profiles,
@JsonProperty("pushChallenge") final Table pushChallenge,
@@ -90,6 +95,9 @@ public class DynamoDbTables {
@JsonProperty("verificationSessions") final Table verificationSessions) {
this.accounts = accounts;
this.backups = backups;
this.backupMedia = backupMedia;
this.clientReleases = clientReleases;
this.deletedAccounts = deletedAccounts;
this.deletedAccountsLock = deletedAccountsLock;
this.issuedReceipts = issuedReceipts;
@@ -98,8 +106,7 @@ public class DynamoDbTables {
this.kemKeys = kemKeys;
this.kemLastResortKeys = kemLastResortKeys;
this.messages = messages;
this.pendingAccounts = pendingAccounts;
this.pendingDevices = pendingDevices;
this.onetimeDonations = onetimeDonations;
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
this.profiles = profiles;
this.pushChallenge = pushChallenge;
@@ -117,6 +124,24 @@ public class DynamoDbTables {
return accounts;
}
@NotNull
@Valid
public Table getBackups() {
return backups;
}
@NotNull
@Valid
public Table getBackupMedia() {
return backupMedia;
}
@NotNull
@Valid
public Table getClientReleases() {
return clientReleases;
}
@NotNull
@Valid
public Table getDeletedAccounts() {
@@ -167,14 +192,8 @@ public class DynamoDbTables {
@NotNull
@Valid
public Table getPendingAccounts() {
return pendingAccounts;
}
@NotNull
@Valid
public Table getPendingDevices() {
return pendingDevices;
public TableWithExpiration getOnetimeDonations() {
return onetimeDonations;
}
@NotNull

View File

@@ -5,11 +5,11 @@
package org.whispersystems.textsecuregcm.configuration;
import io.dropwizard.util.Strings;
import io.dropwizard.validation.ValidationMethod;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import org.apache.commons.lang3.StringUtils;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
public record GcpAttachmentsConfiguration(@NotBlank String domain,
@@ -20,6 +20,6 @@ public record GcpAttachmentsConfiguration(@NotBlank String domain,
@SuppressWarnings("unused")
@ValidationMethod(message = "pathPrefix must be empty or start with /")
public boolean isPathPrefixValid() {
return Strings.isNullOrEmpty(pathPrefix) || pathPrefix.startsWith("/");
return StringUtils.isEmpty(pathPrefix) || pathPrefix.startsWith("/");
}
}

View File

@@ -1,27 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
public class GraphiteConfiguration {
@JsonProperty
private String host;
@JsonProperty
private int port;
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public boolean isEnabled() {
return host != null && port != 0;
}
}

View File

@@ -5,8 +5,35 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
public record HCaptchaConfiguration(@NotNull SecretString apiKey) {
public class HCaptchaConfiguration {
@JsonProperty
@NotNull
SecretString apiKey;
@JsonProperty
@NotNull
CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration();
@JsonProperty
@NotNull
RetryConfiguration retry = new RetryConfiguration();
public SecretString getApiKey() {
return apiKey;
}
public CircuitBreakerConfiguration getCircuitBreaker() {
return circuitBreaker;
}
public RetryConfiguration getRetry() {
return retry;
}
}

View File

@@ -0,0 +1,11 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
public record LinkDeviceSecretConfiguration(SecretBytes secret) {
}

View File

@@ -0,0 +1,11 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import javax.validation.constraints.NotNull;
import java.time.Duration;
public record MessageByteLimitCardinalityEstimatorConfiguration(@NotNull Duration period) {}

View File

@@ -5,6 +5,7 @@
package org.whispersystems.textsecuregcm.configuration;
import java.math.BigDecimal;
import java.time.Duration;
import java.util.Map;
import javax.validation.Valid;
@@ -18,7 +19,8 @@ import javax.validation.constraints.Positive;
*/
public record OneTimeDonationConfiguration(@Valid ExpiringLevelConfiguration boost,
@Valid ExpiringLevelConfiguration gift,
Map<String, @Valid OneTimeDonationCurrencyConfiguration> currencies) {
Map<String, @Valid OneTimeDonationCurrencyConfiguration> currencies,
BigDecimal sepaMaximumEuros) {
/**
* @param badge the numeric donation level ID

View File

@@ -7,7 +7,6 @@ package org.whispersystems.textsecuregcm.configuration;
import javax.validation.constraints.NotEmpty;
public record RecaptchaConfiguration(@NotEmpty String projectPath, @NotEmpty String credentialConfigurationJson,
@NotEmpty String secondaryCredentialConfigurationJson) {
public record RecaptchaConfiguration(@NotEmpty String projectPath, @NotEmpty String credentialConfigurationJson) {
}

View File

@@ -5,7 +5,6 @@ import javax.validation.constraints.NotBlank;
public record RegistrationServiceConfiguration(@NotBlank String host,
int port,
@NotBlank String credentialConfigurationJson,
@NotBlank String secondaryCredentialConfigurationJson,
@NotBlank String identityTokenAudience,
@NotBlank String registrationCaCertificate) {
}

View File

@@ -5,15 +5,9 @@
package org.whispersystems.textsecuregcm.configuration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
public record RemoteConfigConfiguration(@NotNull Set<String> authorizedUsers,
@NotNull String requiredHostedDomain,
@NotNull @NotEmpty List<String> audiences,
@NotNull Map<String, String> globalConfig) {
public record RemoteConfigConfiguration(@NotNull Map<String, String> globalConfig) {
}

View File

@@ -1,70 +0,0 @@
/*
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
public class SecureBackupServiceConfiguration {
@NotNull
@JsonProperty
private SecretBytes userAuthenticationTokenSharedSecret;
@NotBlank
@JsonProperty
private String uri;
@NotEmpty
@JsonProperty
private List<@NotBlank String> backupCaCertificates;
@NotNull
@Valid
@JsonProperty
private CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration();
@NotNull
@Valid
@JsonProperty
private RetryConfiguration retry = new RetryConfiguration();
public SecretBytes userAuthenticationTokenSharedSecret() {
return userAuthenticationTokenSharedSecret;
}
@VisibleForTesting
public void setUri(final String uri) {
this.uri = uri;
}
public String getUri() {
return uri;
}
@VisibleForTesting
public void setBackupCaCertificates(final List<String> backupCaCertificates) {
this.backupCaCertificates = backupCaCertificates;
}
public List<String> getBackupCaCertificates() {
return backupCaCertificates;
}
public CircuitBreakerConfiguration getCircuitBreakerConfiguration() {
return circuitBreaker;
}
public RetryConfiguration getRetryConfiguration() {
return retry;
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
import org.whispersystems.textsecuregcm.util.ExactlySize;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
public record SecureValueRecovery3Configuration(
@NotBlank String uri,
@ExactlySize(32) SecretBytes userAuthenticationTokenSharedSecret,
@ExactlySize(32) SecretBytes userIdTokenSharedSecret,
@NotEmpty List<@NotBlank String> svrCaCertificates,
@NotNull @Valid CircuitBreakerConfiguration circuitBreaker,
@NotNull @Valid RetryConfiguration retry) {
public SecureValueRecovery3Configuration {
if (circuitBreaker == null) {
circuitBreaker = new CircuitBreakerConfiguration();
}
if (retry == null) {
retry = new RetryConfiguration();
}
}
}

View File

@@ -0,0 +1,9 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
public record ShortCodeExpanderConfiguration(String baseUrl) {
}

View File

@@ -1,43 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import javax.validation.constraints.NotEmpty;
public class SqsConfiguration {
@NotEmpty
@JsonProperty
private String accessKey;
@NotEmpty
@JsonProperty
private String accessSecret;
@NotEmpty
@JsonProperty
private List<String> queueUrls;
@NotEmpty
@JsonProperty
private String region = "us-east-1";
public String getAccessKey() {
return accessKey;
}
public String getAccessSecret() {
return accessSecret;
}
public List<String> getQueueUrls() {
return queueUrls;
}
public String getRegion() {
return region;
}
}

View File

@@ -5,15 +5,18 @@
package org.whispersystems.textsecuregcm.configuration;
import java.util.Map;
import java.util.Set;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod;
public record StripeConfiguration(@NotNull SecretString apiKey,
@NotNull SecretBytes idempotencyKeyGenerator,
@NotBlank String boostDescription,
@NotEmpty Set<@NotBlank String> supportedCurrencies) {
@Valid @NotEmpty Map<PaymentMethod, Set<@NotBlank String>> supportedCurrenciesByPaymentMethod) {
}

View File

@@ -20,13 +20,16 @@ import javax.validation.constraints.NotNull;
public class SubscriptionConfiguration {
private final Duration badgeGracePeriod;
private final Duration badgeExpiration;
private final Map<Long, SubscriptionLevelConfiguration> levels;
@JsonCreator
public SubscriptionConfiguration(
@JsonProperty("badgeGracePeriod") @Valid Duration badgeGracePeriod,
@JsonProperty("badgeExpiration") @Valid Duration badgeExpiration,
@JsonProperty("levels") @Valid Map<@NotNull @Min(1) Long, @NotNull @Valid SubscriptionLevelConfiguration> levels) {
this.badgeGracePeriod = badgeGracePeriod;
this.badgeExpiration = badgeExpiration;
this.levels = levels;
}
@@ -34,6 +37,11 @@ public class SubscriptionConfiguration {
return badgeGracePeriod;
}
// This is the badge expiration time starting from when a payment successfully completes
public Duration getBadgeExpiration() {
return badgeExpiration;
}
public Map<Long, SubscriptionLevelConfiguration> getLevels() {
return levels;
}

View File

@@ -0,0 +1,12 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
import javax.validation.constraints.NotNull;
public record TlsKeyStoreConfiguration(@NotNull SecretString password) {
}

View File

@@ -6,6 +6,7 @@ import javax.validation.constraints.NotNull;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public class TurnUriConfiguration {
@JsonProperty
@@ -22,7 +23,8 @@ public class TurnUriConfiguration {
/**
* Enrolled numbers will always get this uri list
*/
private Set<String> enrolledNumbers = Collections.emptySet();
@JsonProperty
private Set<UUID> enrolledAcis = Collections.emptySet();
public List<String> getUris() {
return uris;
@@ -32,7 +34,7 @@ public class TurnUriConfiguration {
return weight;
}
public Set<String> getEnrolledNumbers() {
return Collections.unmodifiableSet(enrolledNumbers);
public Set<UUID> getEnrolledAcis() {
return Collections.unmodifiableSet(enrolledAcis);
}
}

View File

@@ -43,36 +43,10 @@ public class DynamicCaptchaConfiguration {
@NotNull
private Map<Action, BigDecimal> scoreFloorByAction = Collections.emptyMap();
@JsonProperty
@NotNull
private Set<String> signupCountryCodes = Collections.emptySet();
@JsonProperty
@NotNull
private Set<String> signupRegions = Collections.emptySet();
public BigDecimal getScoreFloor() {
return scoreFloor;
}
public Set<String> getSignupCountryCodes() {
return signupCountryCodes;
}
@VisibleForTesting
public void setSignupCountryCodes(Set<String> numbers) {
this.signupCountryCodes = numbers;
}
@VisibleForTesting
public void setSignupRegions(final Set<String> signupRegions) {
this.signupRegions = signupRegions;
}
public Set<String> getSignupRegions() {
return signupRegions;
}
public boolean isAllowHCaptcha() {
return allowHCaptcha;
}

View File

@@ -39,10 +39,6 @@ public class DynamicConfiguration {
@Valid
private DynamicCaptchaConfiguration captcha = new DynamicCaptchaConfiguration();
@JsonProperty
@Valid
private DynamicPushLatencyConfiguration pushLatency = new DynamicPushLatencyConfiguration(Collections.emptyMap());
@JsonProperty
@Valid
private DynamicTurnConfiguration turn = new DynamicTurnConfiguration();
@@ -61,11 +57,12 @@ public class DynamicConfiguration {
@JsonProperty
@Valid
DynamicDeliveryLatencyConfiguration deliveryLatency = new DynamicDeliveryLatencyConfiguration(Collections.emptyMap());
DynamicInboundMessageByteLimitConfiguration inboundMessageByteLimit = new DynamicInboundMessageByteLimitConfiguration(true);
@JsonProperty
@Valid
DynamicInboundMessageByteLimitConfiguration inboundMessageByteLimit = new DynamicInboundMessageByteLimitConfiguration(true);
DynamicRegistrationConfiguration registrationConfiguration = new DynamicRegistrationConfiguration(false);
public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(
final String experimentName) {
@@ -93,10 +90,6 @@ public class DynamicConfiguration {
return captcha;
}
public DynamicPushLatencyConfiguration getPushLatencyConfiguration() {
return pushLatency;
}
public DynamicTurnConfiguration getTurnConfiguration() {
return turn;
}
@@ -113,11 +106,11 @@ public class DynamicConfiguration {
return ecPreKeyMigration;
}
public DynamicDeliveryLatencyConfiguration getDeliveryLatencyConfiguration() {
return deliveryLatency;
}
public DynamicInboundMessageByteLimitConfiguration getInboundMessageByteLimitConfiguration() {
return inboundMessageByteLimit;
}
public DynamicRegistrationConfiguration getRegistrationConfiguration() {
return registrationConfiguration;
}
}

View File

@@ -1,14 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration.dynamic;
import com.vdurmont.semver4j.Semver;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
import java.util.Map;
import java.util.Set;
public record DynamicDeliveryLatencyConfiguration(Map<ClientPlatform, Set<Semver>> instrumentedVersions) {
}

View File

@@ -1,16 +0,0 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration.dynamic;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.vdurmont.semver4j.Semver;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
import java.util.Map;
import java.util.Set;
public record DynamicPushLatencyConfiguration(Map<ClientPlatform, Set<Semver>> instrumentedVersions) {
}

View File

@@ -0,0 +1,7 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration.dynamic;
public record DynamicRegistrationConfiguration(boolean squashDeclinedAttemptErrors) {}

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