Compare commits

...

785 Commits

Author SHA1 Message Date
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
Jon Chambers
f5c57e5741 Make ContestedOptimisticLockException extend NoStackTraceRuntimeException 2023-07-20 11:15:08 -04:00
Jon Chambers
5627209fdd Add a gRPC service for working with pre-keys 2023-07-20 11:10:26 -04:00
Jonathan Klabunde Tomer
0188d314ce minor username api refinements 2023-07-19 15:12:47 -07:00
Jonathan Klabunde Tomer
67343f6bdc accept encrypted username with confirm-username-hash requests 2023-07-19 10:54:11 -07:00
Katherine Yen
ade2e9c6cf Define asynchronous ProfilesManager operations 2023-07-19 10:43:58 -07:00
Sergey Skrobotov
352e1b2249 test classes moved to same packages with components they test 2023-07-17 13:34:58 -07:00
Jon Chambers
b8d8d349f4 Control inbound message byte limits with a dynamic configuration flag 2023-07-14 16:25:33 -04:00
Jon Chambers
e87468fbe0 Add a rate limit for inbound message bytes for a given account 2023-07-14 16:25:33 -04:00
Jon Chambers
e38a713ccc Support sub-millisecond permit regeneration durations in rate limiters 2023-07-14 16:25:33 -04:00
Jon Chambers
82ed783a2d Introduce async account updaters 2023-07-14 16:25:19 -04:00
Jon Chambers
d17c7aaba6 Add support for clearing accounts from Redis asynchronously 2023-07-14 16:25:19 -04:00
Katherine Yen
8c93368b20 Update to the latest version of the spam filter 2023-07-13 12:43:07 -07:00
Jon Chambers
41f61c66a3 Add public methods for fetching accounts asynchronously 2023-07-13 13:53:29 -04:00
Jon Chambers
1b7a20619e Add tools for testing asynchronous Redis operations 2023-07-13 13:53:29 -04:00
Jon Chambers
7d19e58953 Add parallel pathways for getting accounts asyncronously to Accounts 2023-07-13 13:53:29 -04:00
Jon Chambers
1605676509 Store signed EC pre-keys in a dedicated table when setting signed pre-keys individually 2023-07-12 14:58:10 -04:00
Jon Chambers
a0d6146ff5 Make key deletion operations asynchronous 2023-07-12 14:58:10 -04:00
Jon Chambers
f709b00be3 Make KeysManager storage/retrieval operations asynchronous 2023-07-12 14:58:10 -04:00
Jonathan Klabunde Tomer
5847300290 Revert "Allow use of the token returned with spam challenges as auth for the challenge verification request" 2023-07-12 11:45:02 -07:00
Jonathan Klabunde Tomer
9aaac0eefd don't require all devices to support PNI for PNIHW 2023-07-12 10:14:16 -07:00
Jon Chambers
c5ae9913fe Update to the latest version of the spam filter 2023-07-11 13:48:07 -04:00
Jon Chambers
fc2ad20c63 Update to the latest version of the spam filter 2023-07-11 13:36:12 -04:00
Jon Chambers
6db97f5541 Standardize client tag version handling; add client version tags to delivery latency metrics 2023-07-11 13:35:29 -04:00
Jon Chambers
adf6c751ee Use an explicit-allow model for tagging client versions in metrics 2023-07-11 13:35:29 -04:00
Jon Chambers
c315b34395 Update formatting in UserAgentTagUtil 2023-07-11 13:35:29 -04:00
Jon Chambers
f592201e4c Limit attachment controller tags to UA platform (instead of platform and version) 2023-07-11 13:35:29 -04:00
Jon Chambers
8bf5ee45ed Filter out command tags from Lettuce metrics and prepend a "chat." prefix to Lettuce metric names 2023-07-11 13:35:03 -04:00
Jon Chambers
25f759dd07 Drop ActiveUserTally 2023-07-11 13:34:36 -04:00
Jonathan Klabunde Tomer
e5f4c17148 update openapi docs for several endpoints, notably those with PQXDH changes
Co-authored-by: Katherine Yen <katherine@signal.org>
2023-07-06 15:45:33 -07:00
Jonathan Klabunde Tomer
098b177bd3 Allow use of the token returned with spam challenges as auth for the challenge verification request 2023-07-06 15:25:19 -07:00
Jon Chambers
ef1a8fc50f Use PascalCase RPC names for the registration service 2023-07-06 17:12:37 -04:00
Jon Chambers
76f2e93a2c Reduce concurrency limit for pre-key migration task 2023-07-06 16:45:03 -04:00
Jon Chambers
25ea1df299 Limit concurrency when writing signed EC pre-keys 2023-07-06 15:51:12 -04:00
Chris Eager
5ced86af1d Set consistentRead=true for registration recovery password lookup
This avoids a race condition (in integration test situations) where a lookup could return no results
2023-07-06 15:47:16 -04:00
Jon Chambers
62e02a49df Log errors from single-shot account crawlers rather than printing them to stderr 2023-07-06 15:46:28 -04:00
Jon Chambers
540550d72a Handle exceptions thrown when checking pre-key signatures 2023-07-06 15:46:11 -04:00
Jon Chambers
8cb83fb6e4 Switch to temporary registration endpoints to facilitate a change from snake_case to PascalCase 2023-07-06 15:46:00 -04:00
Jon Chambers
56db925f0e Update to the latest version of the spam filter 2023-07-06 10:33:58 -04:00
Jon Chambers
2c0fc8fe3e Remove legacy methods from RegistrationServiceClient 2023-07-06 10:32:58 -04:00
Jon Chambers
08c7baafac Remove legacy registration endpoints from AccountController 2023-07-06 10:32:58 -04:00
Jon Chambers
8edb450d73 Parallelize single-shot account crawlers 2023-07-06 10:15:16 -04:00
ravi-signal
fedeef4da5 Add an optional parameter to require atomic account creation
By default, if a registration request has no optional fields for atomic
account creation set, the request will proceed non-atomically. If a
client sets the `atomic` field, now such a request would be rejected.
2023-07-05 11:24:11 -05:00
Jon Chambers
b593d49399 Control signed pre-key deletion via a dynamic configuration flag to facilitate migration 2023-07-05 12:17:17 -04:00
Chris Eager
4a91fc3c3d Set daemon=true for pubsub topology change event thread 2023-07-05 11:15:12 -05:00
Chris Eager
bb9605d7c3 Use RedisClient#setDefaultTimeout for a non-clustered client 2023-07-05 11:09:28 -05:00
Jon Chambers
1049326a70 Turn on Lettuce latency metrics 2023-06-30 18:20:43 -04:00
Chris Eager
457ecf145f Add test for Redis timeouts 2023-06-30 12:55:37 -05:00
Chris Eager
463dd9d7d8 Update to Lettuce 6.2.4 2023-06-30 12:55:37 -05:00
Chris Eager
bdcd055aaf Configure Redis timeouts using TimeoutOptions and RediURI 2023-06-30 12:55:37 -05:00
Jon Chambers
30ae2037e8 Correct order of constructor arguments for KeysManager 2023-06-30 12:05:16 -04:00
Jon Chambers
ce4fdbfb3c Untangle metric names for RepeatedUseSignedPreKeyStore subclasses 2023-06-30 10:33:24 -04:00
Jon Chambers
2d154eb0cf Add a command to copy signed pre-keys from Account records to their own table 2023-06-30 10:33:24 -04:00
Jon Chambers
a3e82dfae8 Add a temporary method for storing signed EC pre-keys if and only if another key has not already been stored 2023-06-30 10:33:24 -04:00
Jon Chambers
97a7469432 Measure signed EC pre-key agreement 2023-06-30 10:33:24 -04:00
Jon Chambers
1a1defb055 Store signed EC pre-keys in a dedicated table 2023-06-30 10:33:24 -04:00
Jon Chambers
93c78b6e40 Introduce RepeatedUseECSignedPreKeyStore 2023-06-30 10:33:24 -04:00
Chris Eager
b852d6681d FaultTolerantHttpClient: used managed ScheduledExecutorService for retries 2023-06-30 10:24:18 -04:00
Chris Eager
8e48ac4ede Add messagesCache and clientPresenceManager to managed command dependencies 2023-06-30 10:24:18 -04:00
Ehren Kret
859f646c55 Correct timestamp resolution to intended integer value 2023-06-29 16:05:59 -05:00
Chris Eager
fb39b2edaf Improve two @Disabled flaky tests 2023-06-29 14:56:41 -05:00
Chris Eager
d7bf815bd5 Update to the latest version of the spam filter 2023-06-28 14:30:15 -05:00
Chris Eager
c93af9e31e Remove MessagePersister from WhisperServerService environment
Persistence is now exclusively done by a separate command.
2023-06-28 14:17:49 -05:00
Chris Eager
b81a0e99d4 Always have 0 ApnPushNotificationScheduler worker threads in front-end service 2023-06-28 14:17:23 -05:00
Chris Eager
f8fefe2e5e Remove AccountCrawler (and doPeriodicWork) from WhisperServerService 2023-06-28 14:16:07 -05:00
Jon Chambers
f26bc70b59 Add a basic, prototype authentication interceptor for gRPC services 2023-06-27 10:21:11 -04:00
Jon Chambers
b5fd131aba Add an abstract base class for single-shot account crawls 2023-06-27 10:18:35 -04:00
Jon Chambers
06997e19e0 Add a method for iterating across all accounts 2023-06-27 10:18:35 -04:00
Jon Chambers
97710540c0 Use Timer.Sample throughout Experiment 2023-06-27 10:18:20 -04:00
Jon Chambers
c78c109577 Drop a disused endpoint for fetching the caller's own signed pre-key 2023-06-27 10:16:39 -04:00
Jonathan Klabunde Tomer
8d995e456e initial grpc service code in chat 2023-06-26 17:10:13 -07:00
Ehren Kret
cc3cab9c88 Add server time to remote config fetch
Enable clients to very roughly adjust some actions for clock skew by
providing current server time in the remote config fetch.
2023-06-21 17:11:35 -05:00
Jon Chambers
0122b410be Include push notification urgency in push latency metrics 2023-06-21 15:10:26 -04:00
Jon Chambers
2ddd2b9476 Convert PushRecord to a record and make PushType non-optional 2023-06-21 15:10:26 -04:00
Jon Chambers
a768498250 Record general message delivery latency 2023-06-21 15:10:14 -04:00
Sergey Skrobotov
a45aadae16 Cleaning up references to the legacy format from the rate limiters lua script 2023-06-21 15:09:46 -04:00
Sergey Skrobotov
25802432c2 adding a property to skip uploading to s3 during deploy task 2023-06-21 15:09:18 -04:00
Chris Eager
98578b18aa Update to dynamodb-lock-client 1.2.0 2023-06-21 15:08:40 -04:00
Chris Eager
6d81f69785 Start and stop all lifecycle-managed objects in CrawlAccountsCommand 2023-06-17 10:17:46 -05:00
Chris Eager
7dce183170 Add worker thread pool to PushFeedbackProcessor 2023-06-16 11:36:28 -05:00
Chris Eager
f1962a03ef Parameterize worker thread count in CrawlAccountsCommand 2023-06-16 11:36:28 -05:00
Jon Chambers
cb26bfd807 Update to the latest version of the spam filter 2023-06-15 13:12:54 -04:00
Chris Eager
befd336372 Remove static Remote Config auth tokens 2023-06-15 12:11:20 -05:00
Chris Eager
8501e61eb1 Set maxThreads = minThreads on command thread pools 2023-06-15 12:11:10 -05:00
Jon Chambers
ae489e5a52 Log account ages when identity keys change 2023-06-15 13:10:35 -04:00
Chris Eager
13afdbda97 Report system resource metrics from background tasks 2023-06-14 16:48:23 -05:00
Jon Chambers
9cfd88a23f Move turn secret to static configuration 2023-06-14 10:47:17 -04:00
Jon Chambers
13456bad3a Update date math with JSR310 types 2023-06-14 10:47:17 -04:00
Jon Chambers
45be85c5ef Update formatting and resolve warnings/suggestions 2023-06-14 10:47:17 -04:00
Jonathan Klabunde Tomer
861dc0d021 reject message sends that have the same device more than once 2023-06-13 09:49:50 -07:00
Chris Eager
128d709c99 Additional counters and timers for WebSocket connections 2023-06-13 11:46:15 -05:00
Jon Chambers
e8f01be8ef Inject version bytes if missing from existing EC pre-keys 2023-06-09 11:41:51 -04:00
Jon Chambers
7f1ee015d1 Treat blank strings as null pre-keys 2023-06-09 10:39:16 -04:00
Jon Chambers
17aa5d8e74 Use strongly-typed pre-keys 2023-06-09 10:08:49 -04:00
Jon Chambers
b27334b0ff Treat blank strings as null identity keys 2023-06-09 10:08:18 -04:00
Jon Chambers
7fc6b1e802 Count invalid pre-keys 2023-06-09 09:25:31 -04:00
Jon Chambers
25b7c8f802 Update to libsignal-server 0.26.0 2023-06-09 09:25:31 -04:00
Jon Chambers
8ec6a24a2d Fix a metric name/tag set 2023-06-08 12:34:27 -04:00
Jon Chambers
234707169e Represent identity keys as IdentityKey instances 2023-06-08 11:36:58 -04:00
Jon Chambers
1c8443210a Check for missing version bytes in invalid identity keys 2023-06-08 09:56:21 -04:00
g1a55er
aaf43a592f Replace reserved "notification" key with "newMessageAlert" 2023-06-08 09:53:31 -04:00
Jon Chambers
2b08742c0a Create separate key stores for different kinds of pre-keys 2023-06-06 17:08:26 -04:00
Jon Chambers
cac04146de Identify specific cases with invalid identity keys 2023-06-06 17:08:01 -04:00
ravi-signal
2b266c7beb Validate registration ids for new accounts 2023-06-06 11:08:54 -04:00
Jonathan Klabunde Tomer
099932ae68 ApnPushNotifcationScheduler: always run worker thread at least once 2023-06-06 11:04:44 -04:00
Jon Chambers
8579babde6 Count instances where an account's identity key could not be interpreted as an IdentityKey 2023-06-06 11:01:25 -04:00
Jon Chambers
9c93d379a8 Fix a sneaky merge conflict 2023-06-05 12:38:35 -04:00
Jon Chambers
085c7a67c8 Refactor account locks/deleted account manager 2023-06-05 12:30:44 -04:00
Sergey Skrobotov
e6917d8427 minor cleanup, docs, and integration tests for username API 2023-06-02 10:35:07 -07:00
Sergey Skrobotov
47cc7fd615 username links API 2023-06-02 10:26:14 -07:00
Jonathan Klabunde Tomer
ecd207f0a1 Check structural validity of prekeys at upload time 2023-05-31 14:29:39 -07:00
Chris Eager
0ab66f2f14 Add aws-java-sdk-sts to dependencies 2023-05-31 14:57:48 -05:00
Chris Eager
d1e38737ce Support ID token at PUT /v1/config and DELETE /v1/config 2023-05-30 10:28:28 -05:00
Chris Eager
f17de58a71 Change ScheduledApnPushNotificationSenderServiceCommand to extend ServerCommand 2023-05-30 10:14:33 -05:00
Chris Eager
dd552e8e8f Change MessagePersisterServiceCommand to extend ServerCommand 2023-05-30 10:14:33 -05:00
Chris Eager
18480e9d18 Move metrics registry environment.manage() to utility 2023-05-30 10:14:33 -05:00
Chris Eager
7ffccd9c3a Initialize metrics in ScheduledApnPushNotificationSenderServiceCommand 2023-05-26 16:41:17 -05:00
Chris Eager
0edd99e9cf Initialize metrics in MessagePersisterServiceCommand 2023-05-26 16:41:17 -05:00
Chris Eager
defdc14d5e Initialize metrics in CrawlAccountsCommand 2023-05-26 16:41:17 -05:00
Chris Eager
5dcf8edd38 Factor metrics registry intialization to a utility 2023-05-26 16:41:17 -05:00
Jon Chambers
a320766bb6 Update to the latest version of the spam filter 2023-05-26 14:22:52 -04:00
Jon Chambers
91805caa9a Finalize rate limit unit inversion 2023-05-26 14:17:30 -04:00
Jon Chambers
48d39dccbd Fix rate limit division errors 2023-05-26 13:13:02 -04:00
Jon Chambers
fc9e1f59a5 Update to the latest version of the spam filter 2023-05-26 12:46:36 -04:00
Chris Eager
e7bc8bd6b9 Consistently use AWS credentials providers from WhisperServerService 2023-05-26 12:45:38 -04:00
Jon Chambers
23337d7992 Update to the latest version of the spam filter 2023-05-26 11:43:16 -04:00
Jon Chambers
f513dc0398 Invert rate limit units in default configurations 2023-05-26 11:37:06 -04:00
Jon Chambers
184969336e Allow RateLimiterConfig to accept either a leak rate per minute or a permit regeneration duration 2023-05-26 11:37:06 -04:00
Chris Eager
1534f1aa6a Add web identity token AWS SDK credentials provider 2023-05-26 11:07:41 -04:00
Chris Eager
cd8f74e60b Add support for environment-dependent secondary OAuth2 credentials JSON 2023-05-26 11:07:30 -04:00
Jon Chambers
d832eaa759 Represent identity keys as byte arrays 2023-05-26 10:12:22 -04:00
Jon Chambers
796863341d Revert "Count identity keys that are present, but can't be parsed as base64"
This reverts commit 024dd02628a7d989424273501528b52fe18c3ee9.
2023-05-26 10:12:22 -04:00
Jon Chambers
217b68a1e0 Represent pre-key public keys and signatures as byte arrays in DAOs 2023-05-26 09:58:38 -04:00
Jon Chambers
4a8ad3103c Actually write pre-keys as byte arrays 2023-05-26 08:23:54 -04:00
Jon Chambers
a5f853c67a Change inactive account age threshold from 365 to 180 days 2023-05-26 08:23:19 -04:00
Jon Chambers
70b54e227e Count the prevalence of keys stored as strings or as bytes 2023-05-25 10:04:38 -05:00
Jonathan Klabunde Tomer
1ab6bff54e add @Produces annotations to a few methods in DeviceController 2023-05-25 09:57:06 -05:00
Chris Eager
c2317e8493 Start the dynamic configuration manager in dependent commands 2023-05-25 09:52:01 -05:00
Jon Chambers
b034a088b1 Add support for "atomic" device linking/activation 2023-05-19 16:13:37 -04:00
Jon Chambers
ae7cb8036e Factor DeviceActivationRequest out into its own record 2023-05-19 16:13:37 -04:00
Jon Chambers
1a5327aece Update to the latest version of the spam filter 2023-05-19 15:59:09 -04:00
Jon Chambers
8ce2b04fe4 Discard test device codes 2023-05-19 15:57:14 -04:00
Chris Eager
a3c37aed47 Remove obsolete field from SecureValueRecovery2Configuration 2023-05-19 15:57:01 -04:00
Jon Chambers
fa8f19fd43 Group atomic account creation operations 2023-05-19 15:56:45 -04:00
Jon Chambers
c9a9409b9a Count identity keys that are present, but can't be parsed as base64 2023-05-19 15:56:27 -04:00
Jon Chambers
d3e0ba6d44 Prepare to read pre-keys stored as byte arrays 2023-05-19 15:56:13 -04:00
Jon Chambers
300ac16cf1 Handle "transport not allowed" responses from the registration service 2023-05-19 15:55:53 -04:00
Chris Eager
3e53884979 Add MessagePersisterServiceCommand 2023-05-18 15:37:54 -05:00
Chris Eager
859fbe9ab1 Update to the latest version of the spam filter 2023-05-18 11:44:38 -05:00
Chris Eager
6043c1a4e8 Add ScheduledApnPushNotificationSenderServiceCommand 2023-05-18 11:44:01 -05:00
Chris Eager
0d9fd043a4 Add container image build using Jib 2023-05-18 11:43:29 -05:00
Chris Eager
f06eaf13d1 Send 1009 for too-large message frames 2023-05-18 11:42:22 -05:00
Jon Chambers
66a619a378 Allow for atomic account creation and activation 2023-05-18 09:51:13 -04:00
Jon Chambers
fb1b1e1c04 Update libsignal-server to 0.24.0 2023-05-18 09:51:13 -04:00
Katherine Yen
9450f88c8c Add annotation to catch empty request body 2023-05-17 14:28:41 -07:00
Sergey Skrobotov
0706171264 Update to the latest version of the spam filter 2023-05-17 11:43:17 -07:00
Sergey Skrobotov
287e2fa89a Moving secret values out of the main configuration file 2023-05-17 11:25:59 -07:00
Chris Eager
8d1c26d07d Add CrawlAccountsCommand 2023-05-17 12:22:49 -05:00
Jonathan Klabunde Tomer
caae27c44c PQXDH endpoints for chat server 2023-05-16 14:34:33 -07:00
Katherine Yen
34d77e73ff Fix integer division in call link ratelimit leak rate 2023-05-16 14:34:06 -07:00
Chris Eager
0889741f34 Update GitHub Actions versions 2023-05-12 12:53:47 -05:00
Ravi Khadiwala
8c42199baf Add svr2 credentials to RegistrationLockFailure responses
Add an svr2 credential to 423 responses for:
  - PUT v2/accounts/number
  - POST v1/registration

Also add some openapi annotations to those endpoints
2023-05-12 11:02:32 -05:00
Katherine Yen
7395b5760a Remove unused call link config 2023-05-12 11:01:42 -05:00
Jon Chambers
c8f97ed065 Update to the latest version of the spam filter 2023-05-10 15:29:10 -04:00
Jon Chambers
d2baa8b8fb Stop sending API keys to the registration service 2023-05-10 15:28:12 -04:00
Jon Chambers
1beee5fd04 Update to the latest version of the spam filter 2023-05-10 15:01:37 -04:00
Chris Eager
281b91a59a Remove obsolete ContactDiscoveryWriter 2023-05-10 14:01:09 -05:00
Jon Chambers
2be2b4ff23 Authenticate with the registration service using OIDC identity tokens in addition to shared API keys 2023-05-10 14:59:07 -04:00
Jon Chambers
a83fd1d3fe Include request method as a request counter dimension 2023-05-09 15:17:46 -05:00
Jon Chambers
cb72e4f426 Simplify request counter 2023-05-09 15:17:46 -05:00
Chris Eager
3214852a41 Fix 401 on /v1/keepalive 2023-05-09 15:08:03 -05:00
Jon Chambers
1057bd7e1f Resolve warnings/suggestions throughout ProfileControllerTest 2023-05-09 10:32:32 -04:00
Jonathan Klabunde Tomer
33903553ab reinstate per-{path,status,platform,is-websocket} request counters 2023-05-09 09:49:20 -04:00
Katherine Yen
c309afc04b Displace client presence when existing account reregisters 2023-05-05 11:31:18 -07:00
Erik Osheim
f6c4ba898b Update to the latest version of the spam filter 2023-05-05 11:22:29 -04:00
Katherine Yen
7ba86b40aa Create call link credential endpoint 2023-05-04 14:33:45 -07:00
Katherine Yen
b2b0aee4b7 Call link auth credential 2023-05-04 14:17:01 -07:00
Jon Chambers
919cc7e5eb Update libsignal to 0.23 2023-05-04 14:10:51 -07:00
Jonathan Klabunde Tomer
e38911b2c5 Always check prekey signatures when new prekeys are uploaded 2023-05-04 11:31:45 -07:00
Chris Eager
bc68b67cdf account crawler: remove obsolete accelerated mode 2023-05-04 11:27:16 -05:00
Chris Eager
42a9f1b3e4 account crawler: remove set*Dynamo methods 2023-05-04 11:27:16 -05:00
ravi-signal
08333d5989 Implement /v2/backup/auth/check 2023-05-04 11:23:33 -05:00
Ravi Khadiwala
0e0c0c5dfe return 400 instead of 503 for bad verification session-id 2023-05-04 09:22:51 -07:00
Ravi Khadiwala
59ebe65643 Add counter to /v2/attachments 2023-05-04 09:22:18 -07:00
Chris Eager
4fd2422e4d Catch and close() after UninitializedMessageException in websocket messages 2023-05-03 13:36:29 -05:00
Chris Eager
6181d439f6 Update to the latest version of the spam filter 2023-05-03 13:35:35 -05:00
Chris Eager
57b6c10dd1 Remove obsolete dynamic configuration 2023-05-03 13:20:44 -05:00
Jon Chambers
3ee5ac4514 Fix a late-breaking merge conflict 2023-05-02 16:12:26 -04:00
Jonathan Klabunde Tomer
be176f98ad metric for take-prekey yielding an empty result 2023-05-02 13:03:49 -07:00
Jon Chambers
12b58a31a1 Retire integration with legacy contact discovery system 2023-05-02 15:57:03 -04:00
Jon Chambers
8d468d17e3 Add a temporary counter for profile key credential types 2023-05-02 15:56:19 -04:00
Erik Osheim
30df4c3d29 Update to the latest version of the spam filter 2023-05-02 10:37:22 -05:00
Brenden Stahle
5122a1c466 Change the copyright date from 2022 to 2023. 2023-05-02 10:31:04 -05:00
Chris Eager
e135d50d82 Add counter for ContactDiscoverWriter updates 2023-05-01 13:42:14 -05:00
Chris Eager
487b5edc75 Handle potentially null payment method when canceling subscription 2023-05-01 13:42:05 -05:00
Jonathan Klabunde Tomer
47ad5779ad new /v2/accounts endpoint to distribute PNI key material without changing phone number 2023-04-21 12:20:57 -07:00
Katherine Yen
4fb89360ce Allow registration via recovery password for reglock enabled accounts 2023-04-20 09:21:04 -07:00
Jon Chambers
6dfdbeb7bb Check for no-op APNs token changes 2023-04-19 17:01:01 -04:00
Jon Chambers
d0ccbd5526 Simplify a check for no-op FCM token changes 2023-04-19 17:01:01 -04:00
Jon Chambers
031ee57371 Convert "set push token" request objects to records 2023-04-19 17:01:01 -04:00
Jon Chambers
2043678739 Remove the removeSignalingKey API endpoint 2023-04-19 17:00:47 -04:00
Jon Chambers
dd27e3b0c8 Convert attachment descriptors to records 2023-04-19 17:00:34 -04:00
Jon Chambers
1083d8bde0 Remove the legacy group credential endpoint 2023-04-19 17:00:14 -04:00
Jon Chambers
d1eb247d8c Clarify the purpose of an addListener method 2023-04-18 12:04:54 -04:00
Jon Chambers
fd5e9ea016 Drop the old (and now unused!) redis-dispatch module 2023-04-18 12:04:54 -04:00
Jon Chambers
11829d1f9f Refactor provisioning plumbing to use Lettuce 2023-04-18 12:04:54 -04:00
Ehren Kret
ae70d1113c use same protoc version as library dependency 2023-04-17 14:41:55 -05:00
Katherine Yen
c485d317fb Mock apnPushNotificationScheduler 2023-04-17 10:55:15 -07:00
Katherine Yen
350682b83a Lock account and send notification when someone passes phone verification but fails reglock 2023-04-17 10:30:36 -07:00
ravi-signal
0fe6485038 Add a configuration to make rate limiters fail open 2023-04-14 13:08:14 -05:00
Sergey Skrobotov
a553093046 integration tests initial setup 2023-04-13 11:12:34 -07:00
Erik Osheim
af0d5adcdc Update to the latest version of the spam filter 2023-04-11 16:40:03 -04:00
Katherine Yen
61af1ba029 Clean up prohibited username references 2023-04-10 15:21:02 -07:00
ravi-signal
8847cb92ac Don't block when scheduling background apns pushes 2023-04-10 13:51:36 -05:00
Erik Osheim
5242514874 Update to the latest version of the spam filter 2023-04-07 17:13:48 -04:00
Chris Eager
33a6577b6e Decrease message delivery executor thread count to 20 2023-04-07 10:56:23 -05:00
Chris Eager
23d5006f70 Add prefix to executor metric names 2023-04-05 09:51:53 -05:00
Chris Eager
2697872bdd Use Apache StringUtils#join 2023-04-05 09:51:30 -05:00
Ravi Khadiwala
7b331edcde Separate username and signature truncation fields 2023-04-05 09:51:00 -05:00
Katherine Yen
e4da59c236 Generic credential auth endpoint for call links 2023-04-04 10:28:35 -07:00
Jonathan Klabunde Tomer
48ebafa4e0 DynamoDBExtension refactor and helpers for our schema (#1327)
There's a lot of boilerplate involved in setting up a DynamoDBExtension, and some tests were creating several extensions
rather than one with several tables, which is probably slower than it has to be.

This change adds a new DynamoDbExtensionSchema class in which we can define the Dynamo schema for tests, and refactors
DynamoDbExtension to make it easy to instantiate a single extension with all the tables one wants (and no more, both to
minimize test startup time and to ensure we explicitly test our dependencies and lack thereof).

Tests requiring a DynamoDbExtension with a table schema that's not part of the normal Signal schema can instantiate a
DynamoDbExtension.RawSchema instead.

Test timings are unaffected, at least on my machine. Before:
```[INFO] service ............................................ SUCCESS [01:18 min]```

After:
```[INFO] service ............................................ SUCCESS [01:18 min]```

Co-authored-by: Jonathan Klabunde Tomer <jkt@viola.signal.org>
2023-04-03 13:08:43 -07:00
Erik Osheim
f5726f63bd Update to the latest version of the spam filter 2023-04-03 14:34:13 -04:00
Jonathan Klabunde Tomer
391b070cff KeysController: return correct number of unsigned prekeys
When GET /v2/keys was orignally added in b263f47, prekeys were stored in
Postgres, with a user's unsigned and signed keys together in the same table.
Therefore GET /v2/keys subtracted one from the count returned by storage.

In d4d9403, we changed to a different storage schema, with unsigned prekeys in
one DynamoDB table and unsigned prekeys in the accounts Dynamo table.
Unfortunately, GET /v2/keys was not changed to stop subtracting one from the
count of prekeys in the keys table at the same time. This commit fixes that.
2023-04-03 14:32:45 -04:00
gram-signal
781cd0ca3f Truncate SVR2 IDs to 16 bytes rather than 10. 2023-03-30 17:19:18 -06:00
Erik Osheim
84355963f9 Update to the latest version of the spam filter 2023-03-29 16:51:48 -04:00
Chris Eager
3ccfeb490b Add retry after exceptions during a cluster topology change event callback 2023-03-29 11:41:19 -05:00
Chris Eager
0cc84131de Add enabled to SVR2 configuration 2023-03-29 11:40:21 -05:00
Chris Eager
4fa08fb189 Add secure value recovery 2 to AccountsManager#delete() 2023-03-29 11:40:21 -05:00
Chris Eager
2a551d1d41 Add SecureValueRecovery2Client 2023-03-29 11:40:21 -05:00
Chris Eager
391aa9c518 Wrap runtime exceptions during WebSocket auth into AuthenticationException 2023-03-29 10:08:55 -05:00
Erik Osheim
39d9fd0317 Update to the latest version of the spam filter 2023-03-28 11:20:18 -04:00
Chris Eager
18b1fcd724 Update to the latest version of the spam filter 2023-03-22 13:08:58 -05:00
Chris Eager
f5c62a3d85 Migrate from bounded elastic to dedicated executor for message delivery 2023-03-22 12:57:44 -05:00
Chris Eager
6075d5137b Add /v2/accounts/data_report 2023-03-22 12:57:21 -05:00
ravi-signal
890293e429 change v1/challenge response for invalid captcha 2023-03-21 17:38:30 -05:00
Ravi Khadiwala
05b43a878b Register unlink device command 2023-03-21 17:35:57 -05:00
Chris Eager
fe9c3982a1 Remove prepended username from /v2/backup/auth response 2023-03-21 17:35:42 -05:00
Ravi Khadiwala
82baa892f7 Update to the latest version of spam filter 2023-03-21 17:34:58 -05:00
Ravi Khadiwala
ee53260d72 Add filter-provided captcha score thresholds 2023-03-21 17:34:58 -05:00
Ravi Khadiwala
a8eb27940d Add per-action captcha site-key configuration
- reject captcha requests without valid actions
- require specific site keys for each action
2023-03-21 17:34:58 -05:00
Erik Osheim
fd8918eaff Update to the latest version of the spam filter 2023-03-21 15:47:55 -04:00
Katherine Yen
a3a7d7108b Change reglock expiration check to be > 0 instead of >= 0 2023-03-21 12:46:35 -07:00
Jon Chambers
cd27fe0409 Update to the latest version of the spam filter 2023-03-20 15:28:01 -04:00
Jon Chambers
35606a9afd Send "account already exists" flag when creating registration sessions 2023-03-20 15:18:55 -04:00
Jon Chambers
2052e62c01 Use a purpose-specific method when checking verification codes via the legacy registration API 2023-03-20 15:18:38 -04:00
Erik Osheim
8ccab5c1e0 Update to the latest version of the spam filter 2023-03-17 16:41:48 -04:00
Chris Eager
292f69256e Refactor WebSocket message sending error and completion to subscriber from “doOn…” 2023-03-17 12:42:57 -05:00
ravi-signal
fbdcb942e8 Add unlink user command 2023-03-16 11:17:36 -05:00
Sergey Skrobotov
c14ef7e6cf migrate token bucket redis record format from json to hash: phase 2 2023-03-16 09:15:22 -07:00
Jon Chambers
a04fe133b6 Fix a typo in a method name 2023-03-15 16:01:14 -07:00
Sergey Skrobotov
483e444174 migrate token bucket redis record format from json to hash: phase 1 2023-03-15 16:01:06 -07:00
Sergey Skrobotov
ebf8aa7b15 fixing embedded redis based tests 2023-03-15 13:56:40 -07:00
Katherine Yen
7c52be2ac1 Bump old registration default ratelimiter to match Bravo 2023-03-15 09:44:02 -07:00
Sergey Skrobotov
203a49975c artifact is now available in maven central 2023-03-14 12:02:16 -07:00
Sergey Skrobotov
7d45838a1e reordering maven repositories 2023-03-13 22:22:25 -07:00
Katherine Yen
2683f1c6e7 Encode username hash to base64 string without padding 2023-03-13 15:35:27 -07:00
Sergey Skrobotov
d13413aff2 Update to the latest version of the spam filter 2023-03-13 15:04:51 -07:00
Sergey Skrobotov
4c85e7ba66 Moving RateLimiter logic to Redis Lua and adding async API 2023-03-13 14:50:26 -07:00
Katherine Yen
46fef4082c Add metrics for registration lock flow 2023-03-09 09:07:21 -08:00
Ravi Khadiwala
c06313dd2e Drop tagging for legacy user agents 2023-03-09 10:43:45 -06:00
Ravi Khadiwala
59bc2c5535 Add by-action captcha score config
Enable setting different captcha score thresholds for different captcha
actions via configuration
2023-03-09 10:43:16 -06:00
Chris Eager
437bc1358b Use server timestamp for queue score 2023-03-06 11:31:11 -06:00
Katherine Yen
99e651e902 Update to the latest version of the spam filter 2023-03-03 14:10:56 -08:00
Chris Eager
757ce42a35 Update s3-upload-maven-plugin to 2.0.1 2023-03-03 13:17:28 -06:00
Chris Eager
179f3df847 Allow DisabledPermittedAuthenticatedAccount at /v1/accounts/me 2023-03-03 13:17:17 -06:00
Chris Eager
8a889516b0 Improve LoggingUnhandledExceptionMapper combination with CompletionExceptionMapper 2023-03-03 13:17:07 -06:00
Jon Chambers
7de5c0a27d Keep counts of open websockets by client platform 2023-03-03 13:16:24 -06:00
Chris Eager
71d234e1e4 Update default rate limiter config 2023-03-02 10:27:07 -06:00
Chris Eager
b5fb33e21e Remove unused metrics 2023-03-02 10:14:58 -06:00
Sergey Skrobotov
2be22c2a8e Updating documentation github action to handle no changes case 2023-02-28 14:48:09 -08:00
Chris Eager
db198237f3 Expand try-finally scope of deleted accounts reconciliation lock 2023-02-28 12:42:18 -06:00
Chris Eager
d0ccae129a Remove obsolete metric 2023-02-27 16:33:34 -06:00
Chris Eager
ecbef9c6ee Add micrometer metrics to RateLimiter 2023-02-27 16:33:27 -06:00
Chris Eager
ef2cc6620e Add @Produces annotation for validation error response 2023-02-27 16:33:18 -06:00
ravi-signal
b8f363b187 Add documentation to challenge controller 2023-02-24 17:41:15 -06:00
Sergey Skrobotov
c3f4956ead OpenAPI support 2023-02-24 13:03:30 -08:00
Chris Eager
047f4a1c00 Update metric name 2023-02-24 13:07:07 -06:00
Sergey Skrobotov
41c0fe9ffa Adding a uniform configuration for all json/yaml mapper use cases: part 2 2023-02-24 09:28:55 -08:00
Sergey Skrobotov
6edb0d49e9 Adding a uniform configuration for all json/yaml mapper use cases: bugfix 2023-02-23 20:01:32 -08:00
Sergey Skrobotov
a5e3b81a50 Update to the latest version of the spam filter 2023-02-23 17:12:12 -08:00
Sergey Skrobotov
b9b4e3fdd8 Adding a uniform configuration for all json/yaml mapper use cases: part 1 2023-02-23 16:38:48 -08:00
Jon Chambers
6ee9c6ad46 Remove deprecated registration service response fields 2023-02-23 12:41:56 -08:00
Sergey Skrobotov
6d6556eee5 Update to the latest version of the spam filter 2023-02-23 11:04:14 -08:00
Sergey Skrobotov
7529c35013 Rate limiters code refactored 2023-02-23 10:49:06 -08:00
erik-signal
378b32d44d Add missing token field to OutgoingMessageEntity 2023-02-23 11:18:07 -05:00
Chris Eager
e1fcd3e3f6 Remove Lettuce command latency recorder 2023-02-23 10:17:31 -06:00
Chris Eager
d7ad8dd448 Add micrometer timer to FaultTolerantPubSubConnection 2023-02-23 10:17:24 -06:00
Chris Eager
859f2302a9 Remove unused metrics 2023-02-23 10:17:24 -06:00
Chris Eager
a6d11789e9 Add ClosedChannelException to expected errors 2023-02-23 10:17:16 -06:00
Chris Eager
43f83076fa Update to reactor 3.5.3 2023-02-23 10:16:57 -06:00
erik-signal
71c0fc8d4a Improve metrics around spam report tokens. 2023-02-22 15:43:44 -05:00
Chris Eager
d2f723de12 Update to the latest version of the spam filter 2023-02-22 14:33:29 -06:00
Chris Eager
1f4f926ce6 Add platform tag to subscription receipt metrics 2023-02-22 14:31:30 -06:00
Chris Eager
35286f838e Add /v1/verification 2023-02-22 14:27:05 -06:00
Jon Chambers
e1ea3795bb Reuse registration sessions if possible when requesting pre-auth codes 2023-02-22 12:45:26 -05:00
erik-signal
95237a22a9 Relax validation to allow null reporting tokens. 2023-02-22 11:06:51 -05:00
Katherine Yen
11c93c5f53 Keep username hash during reregistration 2023-02-21 09:07:30 -08:00
Jon Chambers
b59b8621c5 Add reporter platform as a reported message dimension 2023-02-17 16:44:13 -05:00
Chris Eager
44c61d9a58 Allow updates if the profile already has a payment address 2023-02-17 16:44:01 -05:00
Ehren Kret
63a17bc14b add support for running tests from aarch64 2023-02-16 09:57:34 -06:00
Jon Chambers
f4f93bb24d Update to the latest version of the spam filter 2023-02-14 12:36:34 -05:00
Jon Chambers
7561622bc8 Log cases where we fall back to a no-op spam-reporting token provider 2023-02-14 12:35:56 -05:00
Jon Chambers
b041566aba Simplify construction of spam reporting token providers 2023-02-14 12:35:56 -05:00
Jon Chambers
cb72158abc Add the presence of spam reporting tokens as a dimension 2023-02-14 12:35:21 -05:00
Jon Chambers
5c432d094f Fix a typo in a metric name 2023-02-14 12:34:48 -05:00
Chris Eager
24ac48b3b1 Update counter name 2023-02-10 14:54:02 -06:00
Katherine Yen
c03060fe3c Phone number discoverability update endpoint 2023-02-10 11:52:51 -08:00
Chris Eager
3ebd5141ae Update to the latest version of the spam filter 2023-02-10 12:15:10 -06:00
Chris Eager
c16006dc4b Add PUT /v2/account/number 2023-02-10 12:09:03 -06:00
Sergey Skrobotov
8fc465b3e8 removing redundant logic in new registration flow 2023-02-09 09:06:48 -08:00
Chris Eager
ce689bdff3 Use DisabledPermittedAuthenticatedAccount at DELETE /v1/accounts/me 2023-02-09 09:05:29 -08:00
Chris Eager
e23386ddc7 Remove unused JUnit extension from test 2023-02-09 09:05:11 -08:00
Jon Chambers
0f17d63774 Add tests for ProvisioningController 2023-02-09 09:04:52 -08:00
Katherine Yen
4fc3949367 Add zkproof validation in username flow 2023-02-09 09:02:53 -08:00
Katherine Yen
e19c04377b Update to the latest version of the spam filter 2023-02-09 09:00:38 -08:00
Sergey Skrobotov
7c3f429c56 Update E164 constraint message 2023-02-08 13:22:00 -08:00
Sergey Skrobotov
7558489ad0 Registration Recovery Password support in /v1/registration 2023-02-08 13:20:23 -08:00
Katherine Yen
4a3880b5ae usernameHashes on reserve request can't be null 2023-02-07 08:44:04 -08:00
Chris Eager
ca7a4abd30 Update to the latest version of the spam filter 2023-02-06 16:40:09 -06:00
Chris Eager
a4a45de161 Add /v1/registration 2023-02-06 16:11:59 -06:00
Chris Eager
358a286523 Use java.util Hex and Base64 codecs 2023-02-06 12:16:59 -06:00
Sergey Skrobotov
3bbab0027b Update to the latest version of the spam filter 2023-02-03 16:39:34 -08:00
Sergey Skrobotov
8afe917a6c Registration recovery passwords store and manager 2023-02-03 16:33:03 -08:00
Erik Osheim
f5fec5e6bb Update to the latest version of the spam filter 2023-02-03 16:24:35 -05:00
Erik Osheim
0b81743683 Update to the latest version of the spam filter 2023-02-02 18:06:43 -05:00
Erik Osheim
9f715c3224 Update to the latest version of the spam filter 2023-02-02 18:05:02 -05:00
Katherine Yen
24f515ccb4 Revert "Revert "Stored hashed username"" 2023-02-02 11:20:44 -08:00
Erik Osheim
fd531242c9 Update to the latest version of the spam filter 2023-02-02 12:20:45 -05:00
Erik Osheim
3855bd257d Update to the latest version of the spam filter 2023-02-01 17:41:58 -05:00
Katherine Yen
c98b54ff15 Revert "Stored hashed username" 2023-02-01 14:31:44 -08:00
Katherine Yen
d93d50d038 Stored hashed username 2023-02-01 12:08:25 -08:00
Jon Chambers
448365c7a0 Preserve legacy registration API error handling 2023-01-31 15:45:23 -05:00
Sergey Skrobotov
515a863195 Update .gitmodules 2023-01-30 15:45:41 -08:00
Sergey Skrobotov
8d0e23bde1 AuthenticationCredentials name changed to SaltedTokenHash 2023-01-30 15:45:24 -08:00
Sergey Skrobotov
dc8f62a4ad /v1/backup/auth/check endpoint added 2023-01-30 15:39:42 -08:00
Jon Chambers
896e65545e Update to the latest version of the spam filter 2023-01-30 16:30:14 -05:00
Jon Chambers
cd4a4b1dcf Retire VoiceVerificationController 2023-01-30 16:28:14 -05:00
Jon Chambers
38a0737afb Retire ReportSpamTokenHandler interface in favor of ReportedMessageListener 2023-01-30 16:27:54 -05:00
Jon Chambers
4a2768b81d Add spam report token support to ReportedMessageListener 2023-01-30 16:27:54 -05:00
Jon Chambers
00e08b8402 Simplify parsing/validation of spam report tokens 2023-01-30 16:27:54 -05:00
Erik Osheim
48e8584e13 Update to current version of the spam-filter. 2023-01-27 11:41:27 -05:00
erik-signal
a89e30fe75 Clarify naming around spam filtering. 2023-01-27 11:40:33 -05:00
gram-signal
a01fcdad28 Add in controller for SVR2 auth. 2023-01-27 09:15:52 -07:00
Chris Eager
2a99529921 Remove old badge strings 2023-01-26 09:23:11 -06:00
Sergey Skrobotov
c934405a3e fixing config field names 2023-01-25 17:28:03 -08:00
Sergey Skrobotov
b8d922fcb7 Update to latest version of the spam module 2023-01-25 15:41:54 -08:00
Sergey Skrobotov
eb499833c6 refactoring of ExternalServiceCredentialGenerator 2023-01-25 15:20:28 -08:00
Chris Eager
dd98f7f043 Support changing just the currency of an existing subscription 2023-01-25 15:14:17 -06:00
Chris Eager
e8978ef91c Add tests for SubscriptionController#setSubscriptionLevel 2023-01-25 15:14:17 -06:00
Chris Eager
669ff1cadf DynamoDB Local Release Repository: snapshots.enabled = false 2023-01-25 15:12:02 -06:00
Jon Chambers
4ce85fdb19 Treat "check code" exceptions as false for legacy API compatibility 2023-01-25 14:39:29 -05:00
Jon Chambers
035ddc4834 Fix a mistake where we're looking for verification codes in place that hasn't been deployed yet (but will be soon!) 2023-01-25 11:43:06 -05:00
Chris Eager
c2f40b8503 Remove duplicate code 2023-01-25 11:09:23 -05:00
Jon Chambers
cf738a1c14 Look for registration service errors in response bodies in addition to status responses 2023-01-25 10:49:36 -05:00
erik-signal
52d40c2321 Add metrics for spam report tokens received. 2023-01-24 10:25:40 -05:00
Erik Osheim
cbf12d6b46 Update to latest version of the spam module 2023-01-19 11:20:08 -05:00
erik-signal
ab26a65b6a Introduce spam report tokens 2023-01-19 11:13:43 -05:00
erik-signal
ee5aaf5383 Ignore files created by emacs / lsp. 2023-01-18 15:44:29 -05:00
Jon Chambers
1c1714b2c2 Clarify a counter name 2023-01-17 17:13:06 -05:00
Jon Chambers
accb017ec5 Use a longer expiration window for quantile calculation 2023-01-17 17:13:06 -05:00
Chris Eager
304782d583 Use processor from SubscriptionProcessorManager for issued receipts 2023-01-17 16:12:03 -06:00
Chris Eager
f361f436d8 Support PayPal for recurring donations 2023-01-17 12:20:17 -06:00
Chris Eager
a34b5a6122 grpc, guava: use version from google cloud libraries-bom 2023-01-17 11:20:46 -06:00
Chris Eager
f75ea18ccb Add test for GoogleCloudAdminEventLogger 2023-01-17 11:20:46 -06:00
Dimitris Apostolou
9a06c40a28 Fix typos 2023-01-13 16:05:06 -06:00
Chris Eager
e6ab97dc5a Update enabled-required authenticator metrics 2023-01-13 14:05:56 -06:00
Chris Eager
ba73f757e2 Update google libraries-bom to 26.1.3, firebase-admin to 9.1.1 2023-01-13 12:22:55 -06:00
Chris Eager
30f131096d Update AWS SDK v1 to 1.12.376 2023-01-13 12:17:39 -06:00
Chris Eager
b8ce922f92 Update logstash-logback-encoder to 7.2 2023-01-13 12:17:39 -06:00
Chris Eager
11b62345e1 Update mockito to 4.11.0 2023-01-13 12:17:39 -06:00
Chris Eager
77289ecb51 Update micrometer to 1.10.3 2023-01-13 12:17:39 -06:00
Chris Eager
dfb0b68997 Update DynamoDBLocal to 1.20.0 2023-01-13 12:17:39 -06:00
Chris Eager
d545f60fc4 Update wiremock to 2.35.0 2023-01-13 12:17:39 -06:00
Chris Eager
5cda6e9d84 Update pushy to 0.15.2 2023-01-13 12:17:39 -06:00
Chris Eager
7caba89210 Update AWS SDK v2 to 2.19.8 2023-01-13 12:17:39 -06:00
Chris Eager
b8967b75c6 Update dropwizard to 2.0.34 2023-01-13 12:17:39 -06:00
Chris Eager
74d9849472 Update badge strings 2023-01-13 12:08:05 -06:00
Fedor Indutny
96b753cfd0 Add an extra kb to max sticker size 2023-01-13 12:07:45 -06:00
Jon Chambers
5a89e66fc0 Convert AccountIdentityResponse to a record 2023-01-13 12:36:17 -05:00
Jon Chambers
b4a143b9de Convert RegistrationLockFailure to a record 2023-01-13 12:36:02 -05:00
Jon Chambers
050035dd52 Convert ExternalServiceCredentials to a record 2023-01-13 12:36:02 -05:00
Jon Chambers
7018062606 Explicitly create registration sessions 2023-01-09 15:27:07 -05:00
Jon Chambers
9e1485de0a Assume stored verification codes will always have a session ID instead of a verification code 2023-01-09 15:27:07 -05:00
Jon Chambers
4e358b891f Retire StoredVerificationCode#twilioVerificationSid 2023-01-09 15:27:07 -05:00
Ehren Kret
4044a9df30 stop warning about lack of syntax specification during proto generation 2023-01-09 12:20:07 -06:00
Ehren Kret
5a7b675001 import cleanup on controllers package 2023-01-09 12:20:07 -06:00
Ehren Kret
3be4e4bc57 remove unused exception type 2023-01-09 12:20:07 -06:00
Chris Eager
5de51919bb Remove Subscriptions.PCI 2023-01-05 12:02:34 -06:00
Chris Eager
b02b00818b Remove Subscriptions.PCI attribute 2023-01-04 11:31:46 -06:00
Chris Eager
010f88a2ad Remove Subscriptions.C attribute 2023-01-04 11:31:46 -06:00
Jon Chambers
60edf4835f Add a pni capability to UserCapabilities 2022-12-21 16:26:07 -05:00
Jon Chambers
a60450d931 Convert UserCapabilities to a record 2022-12-21 16:26:07 -05:00
erik-signal
d138fa45df Handle edge cases of Math.abs on integers. 2022-12-20 12:25:04 -05:00
Katherine Yen
2c2c497c12 Define reregistrationIdleDays DistributionSummary with custom expiry 2022-12-20 09:21:24 -08:00
Katherine Yen
cb5d3840d9 Add paymentActivation capability 2022-12-20 09:20:42 -08:00
Fedor Indutny
9aceaa7a4d Introduce ArtController 2022-12-19 11:58:16 -08:00
Katherine Yen
636c8ba384 Add metric for distribution of account idle time at reregistration 2022-12-16 13:50:29 -08:00
Ravi Khadiwala
ac78eb1425 Update to the latest version of the abusive message filter 2022-12-16 11:28:30 -06:00
Ravi Khadiwala
65ad3fe623 Add hCaptcha support 2022-12-16 11:28:30 -06:00
Sergey Skrobotov
dcec90fc52 Update to the latest version of the abusive message filter 2022-12-13 13:30:47 -08:00
Chris Eager
24ac32e6e6 Add PayPalExperienceProfileInput.userAction 2022-12-13 10:03:58 -06:00
Katherine Yen
26f5ffdde3 Enable case-sensitive usernames 2022-12-13 07:59:37 -08:00
Jon Chambers
a883426402 Simplify account cleaner 2022-12-06 16:21:25 -06:00
Chris Eager
2f21e930e2 Add minimum one-time donation amont to validation error map 2022-12-06 16:21:15 -06:00
Chris Eager
5fb158635c Use existing WebApplicationException entity, if available 2022-12-06 16:21:15 -06:00
Chris Eager
6f844f9ebb Update to the latest version of the abusive message filter 2022-12-06 16:20:17 -06:00
Sergey Skrobotov
d88e358016 Update to the latest version of the abusive message filter 2022-12-05 10:07:40 -08:00
Sergey Skrobotov
9cf2635528 some accounts classes refactorings 2022-12-05 09:30:40 -08:00
Chris Eager
d0e7579f13 Revert transaction descriptor 2022-12-01 18:52:45 -06:00
Chris Eager
cda82b0ea0 Update kotlin + Apollo 2022-12-01 18:11:35 -06:00
Chris Eager
2ecbb18fe5 Add support for one-time PayPal donations 2022-12-01 18:11:35 -06:00
Chris Eager
d40d2389a9 Update to Maven 3.8.6 2022-12-01 18:09:38 -06:00
Chris Eager
df8fb5cab7 Move messages cache stale discard to a separate scheduler 2022-12-01 18:09:28 -06:00
katherine-signal
99ad211c01 Enforce minimum amount by currency for one time donations 2022-11-28 11:44:59 -08:00
katherine-signal
fb4ed20ff5 Remove groups v2 capability
* wip removing groups v2 capabilities

* comments

* finish removing groups v2 references

* hardcode gv1migration flag on user capability, remove other references
2022-11-21 09:31:47 -08:00
Jon Chambers
cb50b44d8f Allow the account cleaner to operate on multiple accounts in parallel 2022-11-18 11:15:00 -05:00
Jon Chambers
ae57853ec4 Simplify deletion reason reporting 2022-11-18 11:15:00 -05:00
Jon Chambers
2881c0fd7e Allow the account cleaner to act on all accounts in a crawled chunk 2022-11-18 11:15:00 -05:00
Chris Eager
483fb0968b Use badge name in level configuration for one-time donations 2022-11-18 11:05:23 -05:00
Jon Chambers
4d37418c15 Update to the latest version of the abusive message filter 2022-11-18 10:55:15 -05:00
Jon Chambers
e8ee4b50ff Retire the legacy "abusive hosts" system in favor of newer tools 2022-11-18 10:54:25 -05:00
Chris Eager
4f8aa2eee2 Mark flaky test @Disabled 2022-11-17 13:23:42 -06:00
Chris Eager
397d3cb45a Add consolidated subscription configuration API 2022-11-16 12:27:00 -06:00
Chris Eager
e883d727fb Note deprecation of localized string 2022-11-16 12:09:00 -06:00
Chris Eager
986545a140 Set error_if_incomplete for subscription payment behavior 2022-11-16 12:08:21 -06:00
Sergey Skrobotov
836307b0c7 adding a metric for ipv4/ipv6 requests count 2022-11-15 11:17:01 -08:00
Sergey Skrobotov
b5a75d3079 Update to the latest version of the abusive message filter 2022-11-15 11:16:55 -08:00
Sergey Skrobotov
c32067759c refactoring: use constants for header names 2022-11-15 11:16:49 -08:00
Chris Eager
7fb7abb593 Update to micrometer 1.10.0 2022-11-15 11:16:41 -08:00
Erik Osheim
0d50b58c60 Update to the latest version of the abusive message filter 2022-11-11 17:09:24 -05:00
Chris Eager
bdf4e24266 Update to the latest version of the abusive message filter 2022-11-11 13:54:19 -06:00
Chris Eager
f41bdf1acb Make MessagesController#getPendingMessages fully async 2022-11-11 13:19:57 -06:00
Chris Eager
77d691df59 Always use reactived message processing in WebSocketConnection 2022-11-11 13:14:39 -06:00
Chris Eager
12300761ab Update reactor-bom to 2020.0.24 2022-11-11 13:14:26 -06:00
Chris Eager
25efcbda81 Update lettuce to 6.2.1.RELEASE 2022-11-11 13:14:26 -06:00
Jon Chambers
a01f96e0e4 Temporarily disable account freezing on contention 2022-11-10 18:53:58 -05:00
erik-signal
1d1e3ba79d Add metric to track newly-locked accounts. 2022-11-10 12:55:08 -05:00
Jon Chambers
2c9c50711f Avoid reading from a stale Account after a contested reglock event 2022-11-10 12:41:50 -05:00
Jon Chambers
d3f0ab8c6d Introduce an alternative exchange rate data provider 2022-11-10 10:25:06 -05:00
erik-signal
80a3a8a43c Lock account when number owner lacks registration lock. 2022-11-09 14:03:09 -05:00
Chris Eager
e6e6eb323d Update metric name 2022-11-08 11:15:42 -06:00
Chris Eager
681a5bafb4 Update MessagesManager#getMessagesForDevice
- add `subscribeOn()`
- use `CompletableFuture` for consistency
2022-11-08 09:38:52 -06:00
Chris Eager
5bec89ecc8 Measure individual message timeouts 2022-11-08 09:37:37 -06:00
Chris Eager
69ed0edb74 Revert "Add more detailed queue processing rate metrics"
This reverts commit bbbab4b8a4.
2022-11-08 09:35:39 -06:00
Chris Eager
ad5925908e Change dispatch queues to LinkedBlockingQueues 2022-11-04 11:08:17 -05:00
Chris Eager
d186245c5c Move all receipt sending work to executor 2022-11-04 11:08:06 -05:00
Chris Eager
bbbab4b8a4 Add more detailed queue processing rate metrics 2022-11-04 11:06:38 -05:00
Chris Eager
f83080eb8d Update metric name 2022-11-03 14:50:20 -05:00
Chris Eager
e0178fa0ea Move additional handling of MessagesManager#delete to executor 2022-11-03 13:02:25 -05:00
Chris Eager
c6a79ca176 Enable metrics on messages fluxes 2022-11-03 13:02:25 -05:00
Chris Eager
6426e6cc49 Enable reactor Schedulers metrics 2022-11-03 13:02:25 -05:00
Chris Eager
b13cb098ce lettuce: set publishOnScheduler to true 2022-11-03 13:02:25 -05:00
Jon Chambers
afda5ca98f Add a test for checking push challenge tokens 2022-11-03 11:14:59 -05:00
Chris Eager
eb57d87513 Remove message listener key only after successfully unsubscribing 2022-11-03 11:09:11 -05:00
Chris Eager
fbf6b9826e tests: only call SQLite.setLibraryPath once 2022-11-03 11:08:43 -05:00
Chris Eager
a01b29a6bd set off_session=true for subscription updates 2022-11-02 14:34:26 -05:00
Chris Eager
102992b095 Set off_session=true when creating subscriptions 2022-11-02 11:30:29 -05:00
Chris Eager
bd69905f2e Remove obsolete donation endpoint 2022-11-02 11:29:03 -05:00
Chris Eager
ce5a4bd94a Update wiremock to 2.34.0 2022-11-02 11:24:54 -05:00
Chris Eager
f65a613815 Update jackson to 2.13.4 2022-11-02 11:24:54 -05:00
sergey-signal
d87c8468bd Update to the latest version of the abusive message filter (#1138) 2022-11-02 09:23:38 -07:00
Chris Eager
aa829af43b Handle expected case of empty flux in message deletion 2022-10-31 12:29:25 -05:00
Chris Eager
c10fda8363 Use reactive streams for WebSocket message queue
Initially, uses `ExperimentEnrollmentManager` to do a safe rollout.
2022-10-31 10:35:37 -05:00
Jon Chambers
4252284405 Update to the latest version of the abusive message filter 2022-10-28 10:50:49 -04:00
Jon Chambers
74d65b37a8 Discard old Twilio machinery and rely entirely on the stand-alone registration service 2022-10-28 10:40:37 -04:00
sergey-signal
78f95e4859 Update to the latest version of the abusive message filter (#1132) 2022-10-27 14:01:16 -07:00
Jon Chambers
91626dea45 Count accounts rather than devices that are stories-capable 2022-10-25 16:36:05 -04:00
sergey-signal
5868d9969a minor changes to utility classes (#1127) 2022-10-25 08:48:56 -07:00
erik-signal
90490c9c84 Clean up the TestClock code a bit more. 2022-10-21 15:27:15 -04:00
Chris Eager
8ea794baef Add additional handling for nullable field in recurring donation record 2022-10-21 12:56:39 -05:00
Chris Eager
70a6c3e8e5 Update to libsignal-server 0.21.1 2022-10-21 12:54:18 -05:00
Jon Chambers
4813803c49 Add .java-version to .gitignore 2022-10-21 12:40:11 -04:00
erik-signal
fe60cf003f Clean up testing with clocks. 2022-10-21 12:39:47 -04:00
erik-signal
0c357bc340 Add metrics tracking story capability adoption. 2022-10-20 12:25:03 -04:00
Chris Eager
b711288faa Run GitHub Action in a container 2022-10-18 16:59:35 -05:00
Jon Chambers
44a5d86641 Revert "Update to libsignal-server 0.21.0"
This reverts commit cccccb4dd6.
2022-10-18 11:44:50 -04:00
Jon Chambers
e7048aa9cf Allow the reconciliation client to trust multiple CA certificates to facilitate certificate rotation 2022-10-18 11:17:47 -04:00
Jon Chambers
0120a85c39 Allow HTTP clients to trust multiple certificates to support certificate rollover 2022-10-18 11:17:47 -04:00
Jon Chambers
a41d047f58 Retire CertificateExpirationGauge in favor of other expiration monitoring tools 2022-10-18 11:17:47 -04:00
Chris Eager
cccccb4dd6 Update to libsignal-server 0.21.0 2022-10-18 11:17:29 -04:00
Jon Chambers
0a64e31625 Check verification codes for changing phone numbers against the stand-alone registration service when possible 2022-10-18 11:17:15 -04:00
Jon Chambers
3c6c6c3706 Use the gRPC BOM instead of calling out dependencies individually 2022-10-18 11:16:56 -04:00
Jon Chambers
8088b58b3b Clarify default value for includeE164 2022-10-18 11:16:06 -04:00
erik-signal
a7d5d51fb4 Improve testing of MultiRecipientMessageProvider 2022-10-17 16:50:39 -04:00
Chris Eager
378d7987a8 device capabilities: prevent stories downgrade 2022-10-17 15:25:13 -04:00
erik-signal
3e0baf82a4 Filter unknown UUIDs for /multi_recipient&story=true. 2022-10-13 15:33:51 -04:00
Chris Eager
7a2683a06b Remove /.tx/config from .gitignore 2022-10-11 15:04:50 -05:00
erik-signal
17a3c90286 Add "urgent" query parameter to /v1/messages/multi_recipient endpoint. 2022-10-11 11:10:11 -04:00
Chris Eager
6341770768 Update SubscriptionManager to store processor+customerId in a single attribute and a map
- add `type` query parameter to `/v1/subscription/{subscriberId}/create_payment_method`
2022-10-07 14:26:17 -05:00
Jon Chambers
308437ec93 Resolve gRPC/Netty version conflicts 2022-10-06 16:23:47 -04:00
Jon Chambers
d3d4916d6c Update to the latest version of the abusive message filter 2022-10-06 15:43:37 -04:00
Jon Chambers
d2fa00f0c6 Add experiment to test standalone registration service 2022-10-06 15:42:53 -04:00
erik-signal
d6c9652a70 Fix internal server error when sending stories to unknown recipient. 2022-10-06 13:53:57 -04:00
Jon Chambers
0d20b73e76 Update to the latest version of the abusive message filter 2022-10-05 15:20:49 -04:00
Jon Chambers
3c655cdd5a Migrate to "regionCode" instead of "region" to avoid tag name conflicts 2022-10-05 15:15:46 -04:00
Jon Chambers
fc5cd3a9ca Update to protobuf-java 3.21.7 2022-10-05 15:15:34 -04:00
Jon Chambers
83ab926f96 Add a dimension for story messages 2022-10-05 15:15:22 -04:00
erik-signal
56e54e0724 Update to the latest version of the abusive message filter 2022-10-05 13:19:47 -04:00
erik-signal
544e4fb89a Adjust routing for stories. 2022-10-05 12:20:42 -04:00
erik-signal
966c3a8f47 Add routing for stories. 2022-10-05 10:44:50 -04:00
Ravi Khadiwala
c2ab72c77e Update to the latest version of the abusive message filter 2022-09-30 12:57:21 -05:00
Ravi Khadiwala
4468ee3142 Update to the latest version of the abusive message filter 2022-09-30 12:10:02 -05:00
Ravi Khadiwala
c82c2c0ba4 Add country tag to twilio failures 2022-09-30 12:03:46 -05:00
Ravi Khadiwala
6e595a0959 add an optionals utility and fix push challenge metric 2022-09-30 12:02:47 -05:00
Ravi Khadiwala
a79d709039 Return 403 when a push challenge is incorrect 2022-09-30 12:02:47 -05:00
Ravi Khadiwala
538a07542e Update to the latest version of the abusive message filter 2022-09-22 11:20:48 -05:00
Ravi Khadiwala
07ed765250 Update abusive message filter and filter account creates 2022-09-20 14:52:18 -05:00
Ravi Khadiwala
2e497b5834 Fix operator order in metric calculation 2022-09-15 14:04:18 -05:00
Ravi Khadiwala
61b3cecd17 Fix missing increment on recaptcha counter 2022-09-14 17:07:26 -05:00
Ravi Khadiwala
a4a666bb80 Add metrics for recaptcha reasons 2022-09-14 16:00:11 -05:00
Ravi Khadiwala
c14621a09f Add metrics for captcha scores 2022-09-14 16:00:11 -05:00
Ravi Khadiwala
d0a8899daf Change discriminator seperator and default width 2022-09-14 15:53:15 -05:00
Chris Eager
65dbcb3e5f Remove duplicate bom from dependencyManagement 2022-09-12 16:54:31 -05:00
Chris Eager
7f725b67c4 Update to the latest version of the abusive message filter 2022-09-12 11:24:37 -05:00
Chris Eager
e25252dc69 Remove unused exception 2022-09-12 11:19:15 -05:00
Chris Eager
8b65c11e1e Update batch check entities from two optional fields to a single field 2022-09-12 11:19:01 -05:00
Chris Eager
320c5eac53 Add support for PNIs at v1/profile/identity_check/batch 2022-09-09 10:55:34 -05:00
Ehren Kret
8199e0d2d5 Set resource field on log entry 2022-09-07 19:37:26 -05:00
Ehren Kret
53387f5a0c Register polymorphic serialization 2022-09-07 19:37:26 -05:00
Ehren Kret
7d171a79d7 Remove redundant @NotNull annotation 2022-09-07 19:37:26 -05:00
Ehren Kret
3b99bb9e78 Log remote config delete events 2022-09-07 19:37:26 -05:00
Ehren Kret
132f026c75 Improve readability of event code 2022-09-07 19:37:26 -05:00
Ehren Kret
abd0f9630c Create GCP Logging implementation of AdminEventLogger 2022-09-07 19:37:26 -05:00
Ehren Kret
a4508ec84f Add new event logging module 2022-09-07 19:37:26 -05:00
Ehren Kret
6119b6ab89 Upgrade java-uuid-generator dependency 2022-09-07 19:37:26 -05:00
Ehren Kret
307ac47ce0 Update DynamoDBLocal dependency version 2022-09-07 19:37:26 -05:00
Ravi Khadiwala
4032ddd4fd Add reserve/confirm for usernames 2022-09-07 11:49:49 -05:00
Chris Eager
98c8dc05f1 Update to the latest version of the abusive message filter 2022-09-07 11:49:01 -05:00
Chris Eager
4c677ec2da Remove deprecated /v1/attachments 2022-09-07 11:48:16 -05:00
Chris Eager
c05692e417 Update deprecated CircuitBreakerConfig usage 2022-09-07 11:47:15 -05:00
Chris Eager
1e7aa89664 Update resilience4j to 1.7.0 2022-09-07 11:47:15 -05:00
gram-signal
ae1edf3c5c Remove experiment associated with auth1->auth2 rollout. 2022-08-31 12:10:46 -06:00
gram-signal
b17f41c3e8 Check if dashes work in dynamic configuration keys. 2022-08-29 15:51:37 -06:00
gram-signal
08db4ba54b Update authentication to use HKDF_SHA256. 2022-08-29 14:20:47 -06:00
gram-signal
cb6cc39679 Ignore null identity key. 2022-08-29 13:26:49 -06:00
Jon Chambers
b6bf6c994c Remove a spurious @Nullable annotation 2022-08-26 15:22:23 -04:00
Jon Chambers
3bb4709563 Add CLDR region as a dimension 2022-08-26 12:41:51 -04:00
Jon Chambers
b280c768a4 Allow signup captchas to target CLDR two-letter region codes 2022-08-26 12:41:51 -04:00
Chris Eager
d23e89fb9c Update micrometer to 1.9.3 2022-08-25 13:46:36 -07:00
Chris Eager
3a27bd0318 Update test dependencies 2022-08-25 13:40:46 -07:00
Chris Eager
616513edaf Remove unused jdbi dependency 2022-08-25 13:40:46 -07:00
Chris Eager
09a51020e9 Update stripe-java to 21.2.0 2022-08-25 13:40:46 -07:00
Chris Eager
cb8cb94d1a Update aws java v1 SDK to 1.12.287 2022-08-25 13:40:46 -07:00
Chris Eager
2440dc0089 Update netty to 4.1.79.Final 2022-08-25 13:40:46 -07:00
Chris Eager
2336eef333 Update aws java v2 SDK to 2.17.258 2022-08-25 13:40:46 -07:00
Chris Eager
a0e948627c Update jackson to 2.13.3 2022-08-25 13:40:46 -07:00
Chris Eager
88159af588 Update dropwizard to 2.0.32 2022-08-25 13:40:46 -07:00
Chris Eager
38b77bb550 Update libphonenumber to 8.12.54 2022-08-25 13:40:32 -07:00
Jon Chambers
e72d1d0b6f Stop reading attribute-based messages from the messages table 2022-08-22 13:37:39 -07:00
Ravi Khadiwala
1891622e69 Zero-pad discriminators less than initial width 2022-08-22 13:36:38 -07:00
Chris Eager
628a112b38 Include country code for verify failure 2022-08-19 12:21:05 -07:00
Jon Chambers
50f5d760c9 Use existing tagging tools for keepalive counters 2022-08-16 13:16:19 -07:00
Jon Chambers
7292a88ea3 Record table performance metrics around reported messages 2022-08-16 13:15:30 -07:00
Jon Chambers
07cb3ab576 Add a "sealed sender" dimension to the sent message counter 2022-08-16 13:11:12 -07:00
Chris Eager
27b749abbd Filter expired items from Dynamo 2022-08-16 13:09:47 -07:00
Chris Eager
27f67a077c Add metrics for report-verification-succeeded response 2022-08-16 13:08:16 -07:00
Ravi Khadiwala
393e15815b Rename secondary account key namespace for usernames 2022-08-15 10:51:52 -05:00
Ravi Khadiwala
a7f1cd25b9 Remove UAK normalization code
All accounts now have UAKs in top-level attributes
2022-08-15 10:47:52 -05:00
Ravi Khadiwala
953cd2ae0c Revert "Delete any leftover usernames in the accounts db"
This reverts commit a44c18e9b7.

Old username cleanup is finished.
2022-08-15 10:45:38 -05:00
ravi-signal
a84a7dbc3d Add support for generating discriminators
- adds `PUT accounts/username` endpoint
- adds `GET accounts/username/{username}` to lookup aci by username
- deletes `PUT accounts/username/{username}`, `GET profile/username/{username}`
- adds randomized discriminator generation
2022-08-15 10:44:36 -05:00
Chris Eager
24d01f1ab2 Revert "device capabilities: prevent stories downgrade"
This reverts commit 1c67233eb0.
2022-08-12 14:21:27 -05:00
Chris Eager
06eb890761 Improve e164 normalization check by re-parsing without country code 2022-08-12 10:52:55 -07:00
Chris Eager
6d0345d327 Clean up Util 2022-08-12 10:52:55 -07:00
Chris Eager
1c67233eb0 device capabilities: prevent stories downgrade 2022-08-12 10:51:16 -07:00
Jon Chambers
b4281c5a70 Send non-urgent push notifications with lower priority 2022-08-12 11:06:31 -04:00
Jon Chambers
5f6b66dad6 Add support for scheduling background push notifications 2022-08-12 10:57:59 -04:00
Jon Chambers
c2be0af9d9 Refactor ApnPushNotificationSchedulerTest to use a Clock 2022-08-12 10:57:59 -04:00
Jon Chambers
c111e9a35a Update to the latest version of the abusive message filter 2022-08-12 10:50:53 -04:00
Jon Chambers
a53a85d788 Refactor scheduled APNs notifications in preparation for future development 2022-08-12 10:47:49 -04:00
851 changed files with 94514 additions and 30032 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
@@ -1135,7 +1135,7 @@ ij_kotlin_field_annotation_wrap = split_into_lines
ij_kotlin_finally_on_new_line = false
ij_kotlin_if_rparen_on_new_line = false
ij_kotlin_import_nested_classes = false
ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^
ij_kotlin_imports_layout = *
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
ij_kotlin_keep_blank_lines_before_right_brace = 2
ij_kotlin_keep_blank_lines_in_code = 2
@@ -1151,9 +1151,9 @@ ij_kotlin_method_call_chain_wrap = off
ij_kotlin_method_parameters_new_line_after_left_paren = false
ij_kotlin_method_parameters_right_paren_on_new_line = false
ij_kotlin_method_parameters_wrap = off
ij_kotlin_name_count_to_use_star_import = 5
ij_kotlin_name_count_to_use_star_import_for_members = 3
ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.**
ij_kotlin_name_count_to_use_star_import = 999
ij_kotlin_name_count_to_use_star_import_for_members = 999
ij_kotlin_packages_to_use_import_on_demand =
ij_kotlin_parameter_annotation_wrap = off
ij_kotlin_space_after_comma = true
ij_kotlin_space_after_extend_colon = true

33
.github/workflows/documentation.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Update Documentation
on:
push:
branches:
- main
jobs:
build:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0
with:
distribution: 'temurin'
java-version: '17'
cache: 'maven'
- name: Compile and Build OpenAPI file
run: ./mvnw compile
- name: Update Documentation
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
cp -r api-doc/target/openapi/signal-server-openapi.yaml /tmp/
git config user.email "github@signal.org"
git config user.name "Documentation Updater"
git fetch origin gh-pages
git checkout gh-pages
cp /tmp/signal-server-openapi.yaml .
git diff --quiet || git commit -a -m "Updating documentation"
git push origin gh-pages -q

30
.github/workflows/integration-tests.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Integration Tests
on: [workflow_dispatch]
jobs:
build:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0
with:
distribution: 'temurin'
java-version: '17'
cache: 'maven'
- uses: aws-actions/configure-aws-credentials@v2
name: Configure AWS credentials from Test account
with:
role-to-assume: ${{ vars.AWS_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
- name: Fetch integration utils library
run: |
mkdir -p integration-tests/.libs
mkdir -p integration-tests/src/main/resources
wget -O integration-tests/.libs/software.amazon.awssdk-sso.jar https://repo1.maven.org/maven2/software/amazon/awssdk/sso/2.19.8/sso-2.19.8.jar
aws s3 cp "s3://${{ vars.INTEGRATION_TESTS_BUCKET }}/config-latest.yml" integration-tests/src/main/resources/config.yml
- name: Run and verify integration tests
run: ./mvnw clean compile test-compile failsafe:integration-test failsafe:verify

View File

@@ -1,18 +1,26 @@
name: Service CI
on: [push]
on:
push:
branches-ignore:
- gh-pages
jobs:
build:
runs-on: ubuntu-latest
container: ubuntu:22.04
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- name: Set up JDK 17
uses: actions/setup-java@3bc31aaf88e8fc94dc1e632d48af61be5ca8721c
uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0
with:
distribution: 'temurin'
java-version: 17
cache: 'maven'
env:
# work around an issue with actions/runner setting an incorrect HOME in containers, which breaks maven caching
# https://github.com/actions/setup-java/issues/356
HOME: /root
- name: Build with Maven
run: mvn -e -B verify
run: ./mvnw -e -B verify

5
.gitignore vendored
View File

@@ -16,6 +16,7 @@ config/deploy.properties
/service/config/testing.yml
/service/config/deploy.properties
/service/dependency-reduced-pom.xml
.java-version
.opsmanage
put.sh
deployer-staging.properties
@@ -25,4 +26,6 @@ deployer.log
!/service/src/main/resources/org/signal/badges/Badges_en.properties
/service/src/main/resources/org/signal/subscriptions/Subscriptions_*.properties
!/service/src/main/resources/org/signal/subscriptions/Subscriptions_en.properties
/.tx/config
.project
.classpath
.settings

8
.gitmodules vendored
View File

@@ -1,11 +1,11 @@
# Note that the implementation of the abusive message filter is private; internal
# Note that the implementation of the spam filter is private; internal
# developers will need to override this URL with:
#
# ```
# git config submodule.abusive-message-filter.url PRIVATE_URL
# git config submodule.spam-filter.url PRIVATE_URL
# ```
#
# External developers may safely ignore this submodule.
[submodule "abusive-message-filter"]
path = abusive-message-filter
[submodule "spam-filter"]
path = spam-filter
url = REDACTED

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

@@ -5,14 +5,16 @@
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
#
# https://www.apache.org/licenses/LICENSE-2.0
#
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# 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.5/apache-maven-3.8.5-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.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

@@ -296,7 +296,7 @@ commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
procedures, authorization keysManager, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object

View File

@@ -21,6 +21,6 @@ The form and manner of this distribution makes it eligible for export under the
License
---------------------
Copyright 2013-2022 Signal Messenger, LLC
Copyright 2013-2023 Signal Messenger, LLC
Licensed under the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html

53
api-doc/pom.xml Normal file
View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>api-doc</artifactId>
<dependencies>
<dependency>
<groupId>org.whispersystems.textsecure</groupId>
<artifactId>service</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-maven-plugin</artifactId>
<version>${swagger.version}</version>
<configuration>
<outputFileName>signal-server-openapi</outputFileName>
<outputPath>${project.build.directory}/openapi</outputPath>
<outputFormat>YAML</outputFormat>
<configurationFilePath>${project.basedir}/src/main/resources/openapi/openapi-configuration.yaml
</configurationFilePath>
</configuration>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>resolve</goal>
</goals>
</execution>
</executions>
</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

@@ -0,0 +1,110 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.openapi;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.SimpleType;
import io.dropwizard.auth.Auth;
import io.swagger.v3.jaxrs2.ResolvedParameter;
import io.swagger.v3.jaxrs2.ext.AbstractOpenAPIExtension;
import io.swagger.v3.jaxrs2.ext.OpenAPIExtension;
import io.swagger.v3.oas.models.Components;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
import javax.ws.rs.Consumes;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
/**
* One of the extension mechanisms of Swagger Core library (OpenAPI processor) is via custom implementations
* of the {@link AbstractOpenAPIExtension} class.
* <p/>
* The purpose of this extension is to customize certain aspects of the OpenAPI model generation on a lower level.
* This extension works in coordination with {@link OpenApiReader} that has access to the model on a higher level.
* <p/>
* The extension is enabled by being listed in {@code META-INF/services/io.swagger.v3.jaxrs2.ext.OpenAPIExtension} file.
* @see ServiceLoader
* @see OpenApiReader
* @see <a href="https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Extensions">Swagger 2.X Extensions</a>
*/
public class OpenApiExtension extends AbstractOpenAPIExtension {
public static final ResolvedParameter AUTHENTICATED_ACCOUNT = new ResolvedParameter();
public static final ResolvedParameter OPTIONAL_AUTHENTICATED_ACCOUNT = new ResolvedParameter();
public static final ResolvedParameter DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT = new ResolvedParameter();
public static final ResolvedParameter OPTIONAL_DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT = new ResolvedParameter();
/**
* When parsing endpoint methods, Swagger will treat the first parameter not annotated as header/path/query param
* as a request body (and will ignore other not annotated parameters). In our case, this behavior conflicts with
* the {@code @Auth}-annotated parameters. Here we're checking if parameters are known to be anything other than
* a body and return an appropriate {@link ResolvedParameter} representation.
*/
@Override
public ResolvedParameter extractParameters(
final List<Annotation> annotations,
final Type type,
final Set<Type> typesToSkip,
final Components components,
final Consumes classConsumes,
final Consumes methodConsumes,
final boolean includeRequestBody,
final JsonView jsonViewAnnotation,
final Iterator<OpenAPIExtension> chain) {
if (annotations.stream().anyMatch(a -> a.annotationType().equals(Auth.class))) {
// this is the case of authenticated endpoint,
if (type instanceof SimpleType simpleType
&& simpleType.getRawClass().equals(AuthenticatedAccount.class)) {
return AUTHENTICATED_ACCOUNT;
}
if (type instanceof SimpleType simpleType
&& simpleType.getRawClass().equals(DisabledPermittedAuthenticatedAccount.class)) {
return DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT;
}
if (type instanceof SimpleType simpleType
&& isOptionalOfType(simpleType, AuthenticatedAccount.class)) {
return OPTIONAL_AUTHENTICATED_ACCOUNT;
}
if (type instanceof SimpleType simpleType
&& isOptionalOfType(simpleType, DisabledPermittedAuthenticatedAccount.class)) {
return OPTIONAL_DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT;
}
}
return super.extractParameters(
annotations,
type,
typesToSkip,
components,
classConsumes,
methodConsumes,
includeRequestBody,
jsonViewAnnotation,
chain);
}
private static boolean isOptionalOfType(final SimpleType simpleType, final Class<?> expectedType) {
if (!simpleType.getRawClass().equals(Optional.class)) {
return false;
}
final List<JavaType> typeParameters = simpleType.getBindings().getTypeParameters();
if (typeParameters.isEmpty()) {
return false;
}
return typeParameters.get(0) instanceof SimpleType optionalParameterType
&& optionalParameterType.getRawClass().equals(expectedType);
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.openapi;
import static com.google.common.base.MoreObjects.firstNonNull;
import static org.signal.openapi.OpenApiExtension.AUTHENTICATED_ACCOUNT;
import static org.signal.openapi.OpenApiExtension.DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT;
import static org.signal.openapi.OpenApiExtension.OPTIONAL_AUTHENTICATED_ACCOUNT;
import static org.signal.openapi.OpenApiExtension.OPTIONAL_DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT;
import com.fasterxml.jackson.annotation.JsonView;
import com.google.common.collect.ImmutableList;
import io.swagger.v3.jaxrs2.Reader;
import io.swagger.v3.jaxrs2.ResolvedParameter;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.List;
import javax.ws.rs.Consumes;
/**
* One of the extension mechanisms of Swagger Core library (OpenAPI processor) is via custom implementations
* of the {@link Reader} class.
* <p/>
* The purpose of this extension is to customize certain aspects of the OpenAPI model generation on a higher level.
* This extension works in coordination with {@link OpenApiExtension} that has access to the model on a lower level.
* <p/>
* The extension is enabled by being listed in {@code resources/openapi/openapi-configuration.yaml} file.
* @see OpenApiExtension
* @see <a href="https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Extensions">Swagger 2.X Extensions</a>
*/
public class OpenApiReader extends Reader {
private static final String AUTHENTICATED_ACCOUNT_AUTH_SCHEMA = "authenticatedAccount";
/**
* Overriding this method allows converting a resolved parameter into other operation entities,
* in this case, into security requirements.
*/
@Override
protected ResolvedParameter getParameters(
final Type type,
final List<Annotation> annotations,
final Operation operation,
final Consumes classConsumes,
final Consumes methodConsumes,
final JsonView jsonViewAnnotation) {
final ResolvedParameter resolved = super.getParameters(
type, annotations, operation, classConsumes, methodConsumes, jsonViewAnnotation);
if (resolved == AUTHENTICATED_ACCOUNT || resolved == DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT) {
operation.setSecurity(ImmutableList.<SecurityRequirement>builder()
.addAll(firstNonNull(operation.getSecurity(), Collections.emptyList()))
.add(new SecurityRequirement().addList(AUTHENTICATED_ACCOUNT_AUTH_SCHEMA))
.build());
}
if (resolved == OPTIONAL_AUTHENTICATED_ACCOUNT || resolved == OPTIONAL_DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT) {
operation.setSecurity(ImmutableList.<SecurityRequirement>builder()
.addAll(firstNonNull(operation.getSecurity(), Collections.emptyList()))
.add(new SecurityRequirement().addList(AUTHENTICATED_ACCOUNT_AUTH_SCHEMA))
.add(new SecurityRequirement())
.build());
}
return resolved;
}
}

View File

@@ -0,0 +1 @@
org.signal.openapi.OpenApiExtension

View File

@@ -0,0 +1,25 @@
resourcePackages:
- org.whispersystems.textsecuregcm
prettyPrint: true
cacheTTL: 0
readerClass: org.signal.openapi.OpenApiReader
openAPI:
info:
title: Signal Server API
license:
name: AGPL-3.0-only
url: https://www.gnu.org/licenses/agpl-3.0.txt
servers:
- url: https://chat.signal.org
description: Production service
- url: https://chat.staging.signal.org
description: Staging service
components:
securitySchemes:
authenticatedAccount:
type: http
scheme: basic
description: |
Account authentication is based on Basic authentication schema,
where `username` has a format of `<user_id>[.<device_id>]`. If `device_id` is not specified,
user's `main` device is assumed.

94
event-logger/pom.xml Normal file
View File

@@ -0,0 +1,94 @@
<?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

@@ -0,0 +1,40 @@
/*
* 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

@@ -0,0 +1,41 @@
/*
* 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

@@ -0,0 +1,35 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.event
import com.google.cloud.logging.Logging
import com.google.cloud.logging.LoggingOptions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
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)
}
@Test
fun testGetService() {
assertDoesNotThrow {
// This is a canary for version conflicts between the cloud logging library and protobuf-java
LoggingOptions.newBuilder()
.setProjectId("test")
.build()
.getService()
}
}
}

2
integration-tests/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.libs
src/main/resources/config.yml

62
integration-tests/pom.xml Normal file
View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>integration-tests</artifactId>
<dependencies>
<dependency>
<groupId>org.whispersystems.textsecure</groupId>
<artifactId>service</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>dynamodb</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<excludes>
<exclude>**</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<additionalClasspathElements>
<additionalClasspathElement>${project.basedir}/.libs/software.amazon.awssdk-sso.jar</additionalClasspathElement>
</additionalClasspathElements>
<includes>
<include>**/*.java</include>
</includes>
</configuration>
</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

@@ -0,0 +1,102 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.integration;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.util.Base64;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
public final class Codecs {
private Codecs() {
// utility class
}
@FunctionalInterface
public interface CheckedFunction<T, R> {
R apply(T t) throws Exception;
}
public static class Base64BasedSerializer<T> extends JsonSerializer<T> {
private final CheckedFunction<T, byte[]> mapper;
public Base64BasedSerializer(final CheckedFunction<T, byte[]> mapper) {
this.mapper = mapper;
}
@Override
public void serialize(final T value, final JsonGenerator gen, final SerializerProvider serializers) throws IOException {
try {
gen.writeString(Base64.getEncoder().withoutPadding().encodeToString(mapper.apply(value)));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public static class Base64BasedDeserializer<T> extends JsonDeserializer<T> {
private final CheckedFunction<byte[], T> mapper;
public Base64BasedDeserializer(final CheckedFunction<byte[], T> mapper) {
this.mapper = mapper;
}
@Override
public T deserialize(final JsonParser p, final DeserializationContext ctxt) throws IOException {
try {
return mapper.apply(Base64.getDecoder().decode(p.getValueAsString()));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public static class ByteArraySerializer extends Base64BasedSerializer<byte[]> {
public ByteArraySerializer() {
super(bytes -> bytes);
}
}
public static class ByteArrayDeserializer extends Base64BasedDeserializer<byte[]> {
public ByteArrayDeserializer() {
super(bytes -> bytes);
}
}
public static class ECPublicKeySerializer extends Base64BasedSerializer<ECPublicKey> {
public ECPublicKeySerializer() {
super(ECPublicKey::serialize);
}
}
public static class ECPublicKeyDeserializer extends Base64BasedDeserializer<ECPublicKey> {
public ECPublicKeyDeserializer() {
super(bytes -> Curve.decodePoint(bytes, 0));
}
}
public static class IdentityKeySerializer extends Base64BasedSerializer<IdentityKey> {
public IdentityKeySerializer() {
super(IdentityKey::serialize);
}
}
public static class IdentityKeyDeserializer extends Base64BasedDeserializer<IdentityKey> {
public IdentityKeyDeserializer() {
super(bytes -> new IdentityKey(bytes, 0));
}
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.integration;
import java.time.Clock;
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import org.signal.integration.config.Config;
import org.whispersystems.textsecuregcm.registration.VerificationSession;
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswords;
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
import org.whispersystems.textsecuregcm.storage.VerificationSessionManager;
import org.whispersystems.textsecuregcm.storage.VerificationSessions;
import org.whispersystems.textsecuregcm.util.DynamoDbFromConfig;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
public class IntegrationTools {
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
private final VerificationSessionManager verificationSessionManager;
public static IntegrationTools create(final Config config) {
final AwsCredentialsProvider credentialsProvider = DefaultCredentialsProvider.builder().build();
final DynamoDbAsyncClient dynamoDbAsyncClient = DynamoDbFromConfig.asyncClient(
config.dynamoDbClientConfiguration(),
credentialsProvider);
final DynamoDbClient dynamoDbClient = DynamoDbFromConfig.client(
config.dynamoDbClientConfiguration(),
credentialsProvider);
final RegistrationRecoveryPasswords registrationRecoveryPasswords = new RegistrationRecoveryPasswords(
config.dynamoDbTables().registrationRecovery(), Duration.ofDays(1), dynamoDbClient, dynamoDbAsyncClient);
final VerificationSessions verificationSessions = new VerificationSessions(
dynamoDbAsyncClient, config.dynamoDbTables().verificationSessions(), Clock.systemUTC());
return new IntegrationTools(
new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords),
new VerificationSessionManager(verificationSessions)
);
}
private IntegrationTools(
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
final VerificationSessionManager verificationSessionManager) {
this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
this.verificationSessionManager = verificationSessionManager;
}
public CompletableFuture<Void> populateRecoveryPassword(final String e164, final byte[] password) {
return registrationRecoveryPasswordsManager.storeForCurrentNumber(e164, password);
}
public CompletableFuture<Optional<String>> peekVerificationSessionPushChallenge(final String sessionId) {
return verificationSessionManager.findForId(sessionId)
.thenApply(maybeSession -> maybeSession.map(VerificationSession::pushChallenge));
}
}

View File

@@ -0,0 +1,344 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.integration;
import static java.util.Objects.requireNonNull;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.io.Resources;
import com.google.common.net.HttpHeaders;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.net.URI;
import java.net.URL;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Base64;
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;
import org.signal.integration.config.Config;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECKeyPair;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.signal.libsignal.protocol.kem.KEMKeyPair;
import org.signal.libsignal.protocol.kem.KEMKeyType;
import org.signal.libsignal.protocol.kem.KEMPublicKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
import org.whispersystems.textsecuregcm.entities.RegistrationRequest;
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.HeaderUtils;
import org.whispersystems.textsecuregcm.util.SystemMapper;
public final class Operations {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final Config CONFIG = loadConfigFromClasspath("config.yml");
private static final IntegrationTools INTEGRATION_TOOLS = IntegrationTools.create(CONFIG);
private static final String USER_AGENT = "integration-test";
private static final FaultTolerantHttpClient CLIENT = buildClient();
private Operations() {
// utility class
}
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.PRIMARY_ID, false);
apiPut("/v2/keys", preKeySetPublicView)
.authorized(user, Device.PRIMARY_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 TestUser user = TestUser.create(number, accountPassword, registrationPassword);
final AccountAttributes accountAttributes = user.accountAttributes();
INTEGRATION_TOOLS.populateRecoveryPassword(number, registrationPassword).join();
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
// register account
final RegistrationRequest registrationRequest = new RegistrationRequest(null,
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)),
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());
return user;
}
public static void deleteUser(final TestUser user) {
apiDelete("/v1/accounts/me").authorized(user).executeExpectSuccess();
}
public static String peekVerificationSessionPushChallenge(final String sessionId) {
return INTEGRATION_TOOLS.peekVerificationSessionPushChallenge(sessionId).join()
.orElseThrow(() -> new RuntimeException("push challenge not found for the verification session"));
}
public static <T> T sendEmptyRequestAuthenticated(
final String endpoint,
final String method,
final String username,
final String password,
final Class<T> outputType) {
try {
final HttpRequest request = HttpRequest.newBuilder()
.uri(serverUri(endpoint, Collections.emptyList()))
.method(method, HttpRequest.BodyPublishers.noBody())
.header(HttpHeaders.AUTHORIZATION, HeaderUtils.basicAuthHeader(username, password))
.header(HttpHeaders.CONTENT_TYPE, "application/json")
.build();
return CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))
.whenComplete((response, error) -> {
if (error != null) {
logger.error("request error", error);
error.printStackTrace();
} else {
logger.info("response: {}", response.statusCode());
System.out.println("response: " + response.statusCode() + ", " + response.body());
}
})
.thenApply(response -> {
try {
return outputType.equals(Void.class)
? null
: SystemMapper.jsonMapper().readValue(response.body(), outputType);
} catch (final IOException e) {
throw new RuntimeException(e);
}
})
.get();
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
public static RequestBuilder apiGet(final String endpoint) {
return new RequestBuilder(HttpRequest.newBuilder().GET(), endpoint);
}
public static RequestBuilder apiDelete(final String endpoint) {
return new RequestBuilder(HttpRequest.newBuilder().DELETE(), endpoint);
}
public static <R> RequestBuilder apiPost(final String endpoint, final R input) {
return RequestBuilder.withJsonBody(endpoint, "POST", input);
}
public static <R> RequestBuilder apiPut(final String endpoint, final R input) {
return RequestBuilder.withJsonBody(endpoint, "PUT", input);
}
public static <R> RequestBuilder apiPatch(final String endpoint, final R input) {
return RequestBuilder.withJsonBody(endpoint, "PATCH", input);
}
private static URI serverUri(final String endpoint, final List<String> queryParams) {
final String query = queryParams.isEmpty()
? StringUtils.EMPTY
: "?" + String.join("&", queryParams);
return URI.create("https://" + CONFIG.domain() + endpoint + query);
}
public static class RequestBuilder {
private final HttpRequest.Builder builder;
private final String endpoint;
private final List<String> queryParams = new ArrayList<>();
private RequestBuilder(final HttpRequest.Builder builder, final String endpoint) {
this.builder = builder;
this.endpoint = endpoint;
}
private static <R> RequestBuilder withJsonBody(final String endpoint, final String method, final R input) {
try {
final byte[] body = SystemMapper.jsonMapper().writeValueAsBytes(input);
return new RequestBuilder(HttpRequest.newBuilder()
.header(HttpHeaders.CONTENT_TYPE, "application/json")
.method(method, HttpRequest.BodyPublishers.ofByteArray(body)), endpoint);
} catch (final JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public RequestBuilder authorized(final TestUser user) {
return authorized(user, Device.PRIMARY_ID);
}
public RequestBuilder authorized(final TestUser user, final long deviceId) {
final String username = "%s.%d".formatted(user.aciUuid().toString(), deviceId);
return authorized(username, user.accountPassword());
}
public RequestBuilder authorized(final String username, final String password) {
builder.header(HttpHeaders.AUTHORIZATION, HeaderUtils.basicAuthHeader(username, password));
return this;
}
public RequestBuilder queryParam(final String key, final String value) {
queryParams.add("%s=%s".formatted(key, value));
return this;
}
public RequestBuilder header(final String name, final String value) {
builder.header(name, value);
return this;
}
public Pair<Integer, Void> execute() {
return execute(Void.class);
}
public Pair<Integer, Void> executeExpectSuccess() {
final Pair<Integer, Void> execute = execute();
Validate.isTrue(
execute.getLeft() >= 200 && execute.getLeft() < 300,
"Unexpected response code: %d",
execute.getLeft());
return execute;
}
public <T> T executeExpectSuccess(final Class<T> expectedType) {
final Pair<Integer, T> execute = execute(expectedType);
return requireNonNull(execute.getRight());
}
public void executeExpectStatusCode(final int expectedStatusCode) {
final Pair<Integer, Void> execute = execute(Void.class);
Validate.isTrue(
execute.getLeft() == expectedStatusCode,
"Unexpected response code: %d",
execute.getLeft()
);
}
public <T> Pair<Integer, T> execute(final Class<T> expectedType) {
builder.uri(serverUri(endpoint, queryParams))
.header(HttpHeaders.USER_AGENT, USER_AGENT);
return CLIENT.sendAsync(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))
.whenComplete((response, error) -> {
if (error != null) {
logger.error("request error", error);
error.printStackTrace();
}
})
.thenApply(response -> {
try {
final T result = expectedType.equals(Void.class)
? null
: SystemMapper.jsonMapper().readValue(response.body(), expectedType);
return Pair.of(response.statusCode(), result);
} catch (final IOException e) {
throw new RuntimeException(e);
}
})
.join();
}
}
private static FaultTolerantHttpClient buildClient() {
try {
return FaultTolerantHttpClient.newBuilder()
.withName("integration-test")
.withExecutor(Executors.newFixedThreadPool(16))
.withRetryExecutor(Executors.newSingleThreadScheduledExecutor())
.withCircuitBreaker(new CircuitBreakerConfiguration())
.withTrustedServerCertificates(CONFIG.rootCert())
.build();
} catch (final CertificateException e) {
throw new RuntimeException(e);
}
}
private static Config loadConfigFromClasspath(final String filename) {
try {
final URL configFileUrl = Resources.getResource(filename);
return SystemMapper.yamlMapper().readValue(Resources.toByteArray(configFileUrl), Config.class);
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
private static ECSignedPreKey generateSignedECPreKey(long id, final ECKeyPair identityKeyPair) {
final ECPublicKey pubKey = Curve.generateKeyPair().getPublicKey();
final byte[] sig = identityKeyPair.getPrivateKey().calculateSignature(pubKey.serialize());
return new ECSignedPreKey(id, pubKey, sig);
}
private static KEMSignedPreKey generateSignedKEMPreKey(long id, final ECKeyPair identityKeyPair) {
final KEMPublicKey pubKey = KEMKeyPair.generate(KEMKeyType.KYBER_1024).getPublicKey();
final byte[] sig = identityKeyPair.getPrivateKey().calculateSignature(pubKey.serialize());
return new KEMSignedPreKey(id, pubKey, sig);
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.integration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang3.tuple.Pair;
import org.signal.libsignal.protocol.IdentityKeyPair;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECKeyPair;
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
public class TestDevice {
private final long deviceId;
private final Map<Integer, Pair<IdentityKeyPair, SignedPreKeyRecord>> signedPreKeys = new ConcurrentHashMap<>();
public static TestDevice create(
final long deviceId,
final IdentityKeyPair aciIdentityKeyPair,
final IdentityKeyPair pniIdentityKeyPair) {
final TestDevice device = new TestDevice(deviceId);
device.addSignedPreKey(aciIdentityKeyPair);
device.addSignedPreKey(pniIdentityKeyPair);
return device;
}
public TestDevice(final long deviceId) {
this.deviceId = deviceId;
}
public long deviceId() {
return deviceId;
}
public SignedPreKeyRecord latestSignedPreKey(final IdentityKeyPair identity) {
final int id = signedPreKeys.entrySet()
.stream()
.filter(p -> p.getValue().getLeft().equals(identity))
.mapToInt(Map.Entry::getKey)
.max()
.orElseThrow();
return signedPreKeys.get(id).getRight();
}
public SignedPreKeyRecord addSignedPreKey(final IdentityKeyPair identity) {
try {
final int nextId = signedPreKeys.keySet().stream().mapToInt(k -> k + 1).max().orElse(0);
final ECKeyPair keyPair = Curve.generateKeyPair();
final byte[] signature = Curve.calculateSignature(identity.getPrivateKey(), keyPair.getPublicKey().serialize());
final SignedPreKeyRecord signedPreKeyRecord = new SignedPreKeyRecord(nextId, System.currentTimeMillis(), keyPair, signature);
signedPreKeys.put(nextId, Pair.of(identity, signedPreKeyRecord));
return signedPreKeyRecord;
} catch (InvalidKeyException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,184 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.integration;
import static java.util.Objects.requireNonNull;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
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;
public class TestUser {
private final int registrationId;
private final IdentityKeyPair aciIdentityKey;
private final Map<Long, TestDevice> devices = new ConcurrentHashMap<>();
private final byte[] unidentifiedAccessKey;
private String phoneNumber;
private IdentityKeyPair pniIdentityKey;
private String accountPassword;
private byte[] registrationPassword;
private UUID aciUuid;
private UUID pniUuid;
public static TestUser create(final String phoneNumber, final String accountPassword, final byte[] registrationPassword) {
// ACI identity key pair
final IdentityKeyPair aciIdentityKey = IdentityKeyPair.generate();
// PNI identity key pair
final IdentityKeyPair pniIdentityKey = IdentityKeyPair.generate();
// registration id
final int registrationId = KeyHelper.generateRegistrationId(false);
// uak
final byte[] unidentifiedAccessKey = RandomUtils.nextBytes(UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH);
return new TestUser(
registrationId,
aciIdentityKey,
phoneNumber,
pniIdentityKey,
unidentifiedAccessKey,
accountPassword,
registrationPassword);
}
public TestUser(
final int registrationId,
final IdentityKeyPair aciIdentityKey,
final String phoneNumber,
final IdentityKeyPair pniIdentityKey,
final byte[] unidentifiedAccessKey,
final String accountPassword,
final byte[] registrationPassword) {
this.registrationId = registrationId;
this.aciIdentityKey = aciIdentityKey;
this.phoneNumber = phoneNumber;
this.pniIdentityKey = pniIdentityKey;
this.unidentifiedAccessKey = unidentifiedAccessKey;
this.accountPassword = accountPassword;
this.registrationPassword = registrationPassword;
devices.put(Device.PRIMARY_ID, TestDevice.create(Device.PRIMARY_ID, aciIdentityKey, pniIdentityKey));
}
public int registrationId() {
return registrationId;
}
public IdentityKeyPair aciIdentityKey() {
return aciIdentityKey;
}
public String phoneNumber() {
return phoneNumber;
}
public IdentityKeyPair pniIdentityKey() {
return pniIdentityKey;
}
public String accountPassword() {
return accountPassword;
}
public byte[] registrationPassword() {
return registrationPassword;
}
public UUID aciUuid() {
return aciUuid;
}
public UUID pniUuid() {
return pniUuid;
}
public AccountAttributes accountAttributes() {
return new AccountAttributes(true, registrationId, "", "", true, new Device.DeviceCapabilities(false, false, false, false))
.withUnidentifiedAccessKey(unidentifiedAccessKey)
.withRecoveryPassword(registrationPassword);
}
public void setAciUuid(final UUID aciUuid) {
this.aciUuid = aciUuid;
}
public void setPniUuid(final UUID pniUuid) {
this.pniUuid = pniUuid;
}
public void setPhoneNumber(final String phoneNumber) {
this.phoneNumber = phoneNumber;
}
public void setPniIdentityKey(final IdentityKeyPair pniIdentityKey) {
this.pniIdentityKey = pniIdentityKey;
}
public void setAccountPassword(final String accountPassword) {
this.accountPassword = accountPassword;
}
public void setRegistrationPassword(final byte[] registrationPassword) {
this.registrationPassword = registrationPassword;
}
public PreKeySetPublicView preKeys(final long deviceId, final boolean pni) {
final IdentityKeyPair identity = pni
? pniIdentityKey
: aciIdentityKey;
final TestDevice device = requireNonNull(devices.get(deviceId));
final SignedPreKeyRecord signedPreKeyRecord = device.latestSignedPreKey(identity);
return new PreKeySetPublicView(
Collections.emptyList(),
identity.getPublicKey(),
new SignedPreKeyPublicView(
signedPreKeyRecord.getId(),
signedPreKeyRecord.getKeyPair().getPublicKey(),
signedPreKeyRecord.getSignature()
)
);
}
public record SignedPreKeyPublicView(
int keyId,
@JsonSerialize(using = Codecs.ECPublicKeySerializer.class)
@JsonDeserialize(using = Codecs.ECPublicKeyDeserializer.class)
ECPublicKey publicKey,
@JsonSerialize(using = Codecs.ByteArraySerializer.class)
@JsonDeserialize(using = Codecs.ByteArrayDeserializer.class)
byte[] signature) {
}
public record PreKeySetPublicView(
List<String> preKeys,
@JsonSerialize(using = Codecs.IdentityKeySerializer.class)
@JsonDeserialize(using = Codecs.IdentityKeyDeserializer.class)
IdentityKey identityKey,
SignedPreKeyPublicView signedPreKey) {
}
}

View File

@@ -0,0 +1,14 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.integration.config;
import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguration;
public record Config(String domain,
String rootCert,
DynamoDbClientConfiguration dynamoDbClientConfiguration,
DynamoDbTables dynamoDbTables) {
}

View File

@@ -0,0 +1,10 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.integration.config;
public record DynamoDbTables(String registrationRecovery,
String verificationSessions) {
}

View File

@@ -0,0 +1,124 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.integration;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.http.HttpStatus;
import org.junit.jupiter.api.Test;
import org.signal.libsignal.usernames.BaseUsernameException;
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 {
@Test
public void testCreateAccount() throws Exception {
final TestUser user = Operations.newRegisteredUser("+19995550101");
try {
final Pair<Integer, AccountIdentityResponse> execute = Operations.apiGet("/v1/accounts/whoami")
.authorized(user)
.execute(AccountIdentityResponse.class);
assertEquals(HttpStatus.SC_OK, execute.getLeft());
} finally {
Operations.deleteUser(user);
}
}
@Test
public void testCreateAccountAtomic() throws Exception {
final TestUser user = Operations.newRegisteredUserAtomic("+19995550201");
try {
final Pair<Integer, AccountIdentityResponse> execute = Operations.apiGet("/v1/accounts/whoami")
.authorized(user)
.execute(AccountIdentityResponse.class);
assertEquals(HttpStatus.SC_OK, execute.getLeft());
} finally {
Operations.deleteUser(user);
}
}
@Test
public void testUsernameOperations() throws Exception {
final TestUser user = Operations.newRegisteredUser("+19995550102");
try {
verifyFullUsernameLifecycle(user);
// no do it again to check changing usernames
verifyFullUsernameLifecycle(user);
} finally {
Operations.deleteUser(user);
}
}
private static void verifyFullUsernameLifecycle(final TestUser user) throws BaseUsernameException {
final String preferred = "test";
final List<Username> candidates = Username.candidatesFrom(preferred, preferred.length(), preferred.length() + 1);
// reserve a username
final ReserveUsernameHashRequest reserveUsernameHashRequest = new ReserveUsernameHashRequest(
candidates.stream().map(Username::getHash).toList());
// try unauthorized
Operations
.apiPut("/v1/accounts/username_hash/reserve", reserveUsernameHashRequest)
.executeExpectStatusCode(HttpStatus.SC_UNAUTHORIZED);
final ReserveUsernameHashResponse reserveUsernameHashResponse = Operations
.apiPut("/v1/accounts/username_hash/reserve", reserveUsernameHashRequest)
.authorized(user)
.executeExpectSuccess(ReserveUsernameHashResponse.class);
// find which one is the reserved username
final byte[] reservedHash = reserveUsernameHashResponse.usernameHash();
final Username reservedUsername = candidates.stream()
.filter(u -> Arrays.equals(u.getHash(), reservedHash))
.findAny()
.orElseThrow();
// confirm a username
final ConfirmUsernameHashRequest confirmUsernameHashRequest = new ConfirmUsernameHashRequest(
reservedUsername.getHash(),
reservedUsername.generateProof(),
"cluck cluck i'm a parrot".getBytes()
);
// try unauthorized
Operations
.apiPut("/v1/accounts/username_hash/confirm", confirmUsernameHashRequest)
.executeExpectStatusCode(HttpStatus.SC_UNAUTHORIZED);
Operations
.apiPut("/v1/accounts/username_hash/confirm", confirmUsernameHashRequest)
.authorized(user)
.executeExpectSuccess(UsernameHashResponse.class);
// lookup username
final AccountIdentifierResponse accountIdentifierResponse = Operations
.apiGet("/v1/accounts/username_hash/" + Base64.getUrlEncoder().encodeToString(reservedHash))
.executeExpectSuccess(AccountIdentifierResponse.class);
assertEquals(new AciServiceIdentifier(user.aciUuid()), accountIdentifierResponse.uuid());
// try authorized
Operations
.apiGet("/v1/accounts/username_hash/" + Base64.getUrlEncoder().encodeToString(reservedHash))
.authorized(user)
.executeExpectStatusCode(HttpStatus.SC_BAD_REQUEST);
// delete username
Operations
.apiDelete("/v1/accounts/username_hash")
.authorized(user)
.executeExpectSuccess();
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.integration;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
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.whispersystems.textsecuregcm.entities.IncomingMessage;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
import org.whispersystems.textsecuregcm.entities.SendMessageResponse;
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");
}
try {
final byte[] expectedContent = "Hello, World!".getBytes(StandardCharsets.UTF_8);
final String contentBase64 = Base64.getEncoder().encodeToString(expectedContent);
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
.apiPut("/v1/messages/%s".formatted(userB.aciUuid().toString()), messages)
.authorized(userA)
.execute(SendMessageResponse.class);
final Pair<Integer, OutgoingMessageEntityList> receiveMessages = Operations.apiGet("/v1/messages")
.authorized(userB)
.execute(OutgoingMessageEntityList.class);
final byte[] actualContent = receiveMessages.getRight().messages().get(0).content();
assertArrayEquals(expectedContent, actualContent);
} finally {
Operations.deleteUser(userA);
Operations.deleteUser(userB);
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.integration;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.whispersystems.textsecuregcm.entities.CreateVerificationSessionRequest;
import org.whispersystems.textsecuregcm.entities.SubmitVerificationCodeRequest;
import org.whispersystems.textsecuregcm.entities.UpdateVerificationSessionRequest;
import org.whispersystems.textsecuregcm.entities.VerificationCodeRequest;
import org.whispersystems.textsecuregcm.entities.VerificationSessionResponse;
public class RegistrationTest {
@Test
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 VerificationSessionResponse verificationSessionResponse = Operations
.apiPost("/v1/verification/session", input)
.executeExpectSuccess(VerificationSessionResponse.class);
final String sessionId = verificationSessionResponse.id();
final String pushChallenge = Operations.peekVerificationSessionPushChallenge(sessionId);
// supply push challenge
final UpdateVerificationSessionRequest updatedRequest = new UpdateVerificationSessionRequest(
"test", UpdateVerificationSessionRequest.PushTokenType.FCM, pushChallenge, null, null, null);
final VerificationSessionResponse pushChallengeSupplied = Operations
.apiPatch("/v1/verification/session/%s".formatted(sessionId), updatedRequest)
.executeExpectSuccess(VerificationSessionResponse.class);
Assertions.assertTrue(pushChallengeSupplied.allowedToRequestCode());
// request code
final VerificationCodeRequest verificationCodeRequest = new VerificationCodeRequest(
VerificationCodeRequest.Transport.SMS, "android-ng");
final VerificationSessionResponse codeRequested = Operations
.apiPost("/v1/verification/session/%s/code".formatted(sessionId), verificationCodeRequest)
.executeExpectSuccess(VerificationSessionResponse.class);
// verify code
final SubmitVerificationCodeRequest submitVerificationCodeRequest = new SubmitVerificationCodeRequest("265402");
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=%*

202
pom.xml
View File

@@ -14,11 +14,6 @@
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>dynamodb-local-oregon</id>
<name>DynamoDB Local Release Repository</name>
<url>https://s3-us-west-2.amazonaws.com/dynamodb-local/release</url>
</repository>
</repositories>
<pluginRepositories>
@@ -35,39 +30,50 @@
</pluginRepositories>
<modules>
<module>redis-dispatch</module>
<module>websocket-resources</module>
<module>api-doc</module>
<module>event-logger</module>
<module>integration-tests</module>
<module>service</module>
<module>websocket-resources</module>
</modules>
<properties>
<aws.sdk.version>1.12.154</aws.sdk.version>
<aws.sdk2.version>2.17.125</aws.sdk2.version>
<commons-codec.version>1.15</commons-codec.version>
<commons-csv.version>1.8</commons-csv.version>
<commons-io.version>2.9.0</commons-io.version>
<dropwizard.version>2.0.28</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>2.1.9</dropwizard.version>
<dropwizard-metrics-datadog.version>1.1.13</dropwizard-metrics-datadog.version>
<gson.version>2.9.0</gson.version>
<guava.version>30.1.1-jre</guava.version>
<jackson.version>2.13.2.20220328</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>
<jackson.version>2.13.5</jackson.version>
<jaxb.version>2.3.1</jaxb.version>
<jedis.version>2.9.0</jedis.version>
<lettuce.version>6.1.9.RELEASE</lettuce.version>
<libphonenumber.version>8.12.50</libphonenumber.version>
<logstash.logback.version>7.0.1</logstash.logback.version>
<micrometer.version>1.5.3</micrometer.version>
<mockito.version>4.3.1</mockito.version>
<netty.version>4.1.65.Final</netty.version>
<opentest4j.version>1.2.0</opentest4j.version>
<protobuf.version>3.19.4</protobuf.version>
<pushy.version>0.15.1</pushy.version>
<resilience4j.version>1.5.0</resilience4j.version>
<junit-pioneer.version>2.1.0</junit-pioneer.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.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>20.79.0</stripe.version>
<slf4j.version>1.7.36</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.8_7-jre-jammy -->
<docker.image.sha256>b8af44d6a7e0615a7486d7307dd54bba23ff24e3aea14893fd2795e8c436d44e</docker.image.sha256>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
@@ -80,7 +86,7 @@
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>2.13.2.20220328</version>
<version>${jackson.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@@ -91,6 +97,13 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Needed for gRPC with Java 9+ -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>annotations-api</artifactId>
<version>6.0.53</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-bom</artifactId>
@@ -98,13 +111,6 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-bom</artifactId>
<version>${aws.sdk.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
@@ -115,10 +121,15 @@
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>libraries-bom</artifactId>
<version>20.9.0</version>
<version>${google-cloud-libraries.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.salesforce.servicelibs</groupId>
<artifactId>reactor-grpc-stub</artifactId>
<version>${reactive.grpc.version}</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bom</artifactId>
@@ -133,7 +144,20 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-bom</artifactId>
<version>${reactor-bom.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-bom</artifactId>
<version>${kotlin.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.eatthepath</groupId>
<artifactId>pushy</artifactId>
@@ -144,11 +168,6 @@
<artifactId>pushy-dropwizard-metrics-listener</artifactId>
<version>${pushy.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
@@ -164,11 +183,6 @@
<artifactId>semver4j</artifactId>
<version>${semver4j.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
@@ -210,18 +224,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>
@@ -239,11 +241,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>
@@ -252,7 +249,7 @@
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.2</version>
<version>9.5</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -260,6 +257,11 @@
<artifactId>stripe-java</artifactId>
<version>${stripe.version}</version>
</dependency>
<dependency>
<groupId>com.braintreepayments.gateway</groupId>
<artifactId>braintree-java</artifactId>
<version>${braintree.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
@@ -274,12 +276,12 @@
<dependency>
<groupId>org.signal</groupId>
<artifactId>libsignal-server</artifactId>
<version>0.18.0</version>
<version>0.33.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>
@@ -296,7 +298,7 @@
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>2.32.0</version>
<version>2.35.1</version>
<scope>test</scope>
<exclusions>
<exclusion>
@@ -312,7 +314,6 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -325,27 +326,33 @@
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit-pioneer</groupId>
<artifactId>junit-pioneer</artifactId>
<version>${junit-pioneer.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<profile>
<id>include-abusive-message-filter</id>
<id>include-spam-filter</id>
<activation>
<file>
<exists>abusive-message-filter/pom.xml</exists>
<exists>spam-filter/pom.xml</exists>
</file>
</activation>
<modules>
<module>abusive-message-filter</module>
<module>spam-filter</module>
</modules>
</profile>
<profile>
<id>exclude-abusive-message-filter</id>
<id>exclude-spam-filter</id>
<activation>
<file>
<missing>abusive-message-filter/pom.xml</missing>
<missing>spam-filter/pom.xml</missing>
</file>
</activation>
</profile>
@@ -359,6 +366,15 @@
<version>1.7.0</version>
</extension>
</extensions>
<pluginManagement>
<plugins>
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.4.0</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
@@ -366,14 +382,28 @@
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.18.0:exe:${os.detected.classifier}</protocArtifact>
<checkStaleness>true</checkStaleness>
<checkStaleness>false</checkStaleness>
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
<protocPlugins>
<protocPlugin>
<id>reactor-grpc</id>
<groupId>com.salesforce.servicelibs</groupId>
<artifactId>reactor-grpc</artifactId>
<version>${reactive.grpc.version}</version>
<mainClass>com.salesforce.reactorgrpc.ReactorGrpcGenerator</mainClass>
</protocPlugin>
</protocPlugins>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
<goal>test-compile</goal>
<goal>test-compile-custom</goal>
</goals>
</execution>
</executions>
@@ -382,7 +412,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>
@@ -391,7 +421,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>
@@ -404,7 +434,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>
@@ -424,7 +454,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>
@@ -438,7 +468,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>
@@ -448,7 +478,7 @@
<rules>
<dependencyConvergence/>
<requireMavenVersion>
<version>3.8.3</version>
<version>3.8.6</version>
</requireMavenVersion>
</rules>
</configuration>
@@ -459,7 +489,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>
@@ -468,7 +498,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

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>redis-dispatch</artifactId>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -1,11 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.dispatch;
public interface DispatchChannel {
void onDispatchMessage(String channel, byte[] message);
void onDispatchSubscribed(String channel);
void onDispatchUnsubscribed(String channel);
}

View File

@@ -1,157 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.dispatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.dispatch.io.RedisPubSubConnectionFactory;
import org.whispersystems.dispatch.redis.PubSubConnection;
import org.whispersystems.dispatch.redis.PubSubReply;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class DispatchManager extends Thread {
private final Logger logger = LoggerFactory.getLogger(DispatchManager.class);
private final Executor executor = Executors.newCachedThreadPool();
private final Map<String, DispatchChannel> subscriptions = new ConcurrentHashMap<>();
private final Optional<DispatchChannel> deadLetterChannel;
private final RedisPubSubConnectionFactory redisPubSubConnectionFactory;
private PubSubConnection pubSubConnection;
private volatile boolean running;
public DispatchManager(RedisPubSubConnectionFactory redisPubSubConnectionFactory,
Optional<DispatchChannel> deadLetterChannel)
{
this.redisPubSubConnectionFactory = redisPubSubConnectionFactory;
this.deadLetterChannel = deadLetterChannel;
}
@Override
public void start() {
this.pubSubConnection = redisPubSubConnectionFactory.connect();
this.running = true;
super.start();
}
public void shutdown() {
this.running = false;
this.pubSubConnection.close();
}
public synchronized void subscribe(String name, DispatchChannel dispatchChannel) {
Optional<DispatchChannel> previous = Optional.ofNullable(subscriptions.get(name));
subscriptions.put(name, dispatchChannel);
try {
pubSubConnection.subscribe(name);
} catch (IOException e) {
logger.warn("Subscription error", e);
}
previous.ifPresent(channel -> dispatchUnsubscription(name, channel));
}
public synchronized void unsubscribe(String name, DispatchChannel channel) {
Optional<DispatchChannel> subscription = Optional.ofNullable(subscriptions.get(name));
if (subscription.isPresent() && subscription.get() == channel) {
subscriptions.remove(name);
try {
pubSubConnection.unsubscribe(name);
} catch (IOException e) {
logger.warn("Unsubscribe error", e);
}
dispatchUnsubscription(name, subscription.get());
}
}
public boolean hasSubscription(String name) {
return subscriptions.containsKey(name);
}
@Override
public void run() {
while (running) {
try {
PubSubReply reply = pubSubConnection.read();
switch (reply.getType()) {
case UNSUBSCRIBE: break;
case SUBSCRIBE: dispatchSubscribe(reply); break;
case MESSAGE: dispatchMessage(reply); break;
default: throw new AssertionError("Unknown pubsub reply type! " + reply.getType());
}
} catch (IOException e) {
logger.warn("***** PubSub Connection Error *****", e);
if (running) {
this.pubSubConnection.close();
this.pubSubConnection = redisPubSubConnectionFactory.connect();
resubscribeAll();
}
}
}
logger.warn("DispatchManager Shutting Down...");
}
private void dispatchSubscribe(final PubSubReply reply) {
Optional<DispatchChannel> subscription = Optional.ofNullable(subscriptions.get(reply.getChannel()));
if (subscription.isPresent()) {
dispatchSubscription(reply.getChannel(), subscription.get());
} else {
logger.info("Received subscribe event for non-existing channel: " + reply.getChannel());
}
}
private void dispatchMessage(PubSubReply reply) {
Optional<DispatchChannel> subscription = Optional.ofNullable(subscriptions.get(reply.getChannel()));
if (subscription.isPresent()) {
dispatchMessage(reply.getChannel(), subscription.get(), reply.getContent().get());
} else if (deadLetterChannel.isPresent()) {
dispatchMessage(reply.getChannel(), deadLetterChannel.get(), reply.getContent().get());
} else {
logger.warn("Received message for non-existing channel, with no dead letter handler: " + reply.getChannel());
}
}
private void resubscribeAll() {
new Thread(() -> {
synchronized (DispatchManager.this) {
try {
for (String name : subscriptions.keySet()) {
pubSubConnection.subscribe(name);
}
} catch (IOException e) {
logger.warn("***** RESUBSCRIPTION ERROR *****", e);
}
}
}).start();
}
private void dispatchMessage(final String name, final DispatchChannel channel, final byte[] message) {
executor.execute(() -> channel.onDispatchMessage(name, message));
}
private void dispatchSubscription(final String name, final DispatchChannel channel) {
executor.execute(() -> channel.onDispatchSubscribed(name));
}
private void dispatchUnsubscription(final String name, final DispatchChannel channel) {
executor.execute(() -> channel.onDispatchUnsubscribed(name));
}
}

View File

@@ -1,68 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.dispatch.io;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class RedisInputStream {
private static final byte CR = 0x0D;
private static final byte LF = 0x0A;
private final InputStream inputStream;
public RedisInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
public String readLine() throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
boolean foundCr = false;
while (true) {
int character = inputStream.read();
if (character == -1) {
throw new IOException("Stream closed!");
}
baos.write(character);
if (foundCr && character == LF) break;
else if (character == CR) foundCr = true;
else if (foundCr) foundCr = false;
}
byte[] data = baos.toByteArray();
return new String(data, 0, data.length-2);
}
public byte[] readFully(int size) throws IOException {
byte[] result = new byte[size];
int offset = 0;
int remaining = result.length;
while (remaining > 0) {
int read = inputStream.read(result, offset, remaining);
if (read < 0) {
throw new IOException("Stream closed!");
}
offset += read;
remaining -= read;
}
return result;
}
public void close() throws IOException {
inputStream.close();
}
}

View File

@@ -1,13 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.dispatch.io;
import org.whispersystems.dispatch.redis.PubSubConnection;
public interface RedisPubSubConnectionFactory {
PubSubConnection connect();
}

View File

@@ -1,123 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.dispatch.redis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.dispatch.io.RedisInputStream;
import org.whispersystems.dispatch.redis.protocol.ArrayReplyHeader;
import org.whispersystems.dispatch.redis.protocol.IntReply;
import org.whispersystems.dispatch.redis.protocol.StringReplyHeader;
import org.whispersystems.dispatch.util.Util;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
public class PubSubConnection {
private final Logger logger = LoggerFactory.getLogger(PubSubConnection.class);
private static final byte[] UNSUBSCRIBE_TYPE = {'u', 'n', 's', 'u', 'b', 's', 'c', 'r', 'i', 'b', 'e' };
private static final byte[] SUBSCRIBE_TYPE = {'s', 'u', 'b', 's', 'c', 'r', 'i', 'b', 'e' };
private static final byte[] MESSAGE_TYPE = {'m', 'e', 's', 's', 'a', 'g', 'e' };
private static final byte[] SUBSCRIBE_COMMAND = {'S', 'U', 'B', 'S', 'C', 'R', 'I', 'B', 'E', ' ' };
private static final byte[] UNSUBSCRIBE_COMMAND = {'U', 'N', 'S', 'U', 'B', 'S', 'C', 'R', 'I', 'B', 'E', ' '};
private static final byte[] CRLF = {'\r', '\n' };
private final OutputStream outputStream;
private final RedisInputStream inputStream;
private final Socket socket;
private final AtomicBoolean closed;
public PubSubConnection(Socket socket) throws IOException {
this.socket = socket;
this.outputStream = socket.getOutputStream();
this.inputStream = new RedisInputStream(new BufferedInputStream(socket.getInputStream()));
this.closed = new AtomicBoolean(false);
}
public void subscribe(String channelName) throws IOException {
if (closed.get()) throw new IOException("Connection closed!");
byte[] command = Util.combine(SUBSCRIBE_COMMAND, channelName.getBytes(), CRLF);
outputStream.write(command);
}
public void unsubscribe(String channelName) throws IOException {
if (closed.get()) throw new IOException("Connection closed!");
byte[] command = Util.combine(UNSUBSCRIBE_COMMAND, channelName.getBytes(), CRLF);
outputStream.write(command);
}
public PubSubReply read() throws IOException {
if (closed.get()) throw new IOException("Connection closed!");
ArrayReplyHeader replyHeader = new ArrayReplyHeader(inputStream.readLine());
if (replyHeader.getElementCount() != 3) {
throw new IOException("Received array reply header with strange count: " + replyHeader.getElementCount());
}
StringReplyHeader replyTypeHeader = new StringReplyHeader(inputStream.readLine());
byte[] replyType = inputStream.readFully(replyTypeHeader.getStringLength());
inputStream.readLine();
if (Arrays.equals(SUBSCRIBE_TYPE, replyType)) return readSubscribeReply();
else if (Arrays.equals(UNSUBSCRIBE_TYPE, replyType)) return readUnsubscribeReply();
else if (Arrays.equals(MESSAGE_TYPE, replyType)) return readMessageReply();
else throw new IOException("Unknown reply type: " + new String(replyType));
}
public void close() {
try {
this.closed.set(true);
this.inputStream.close();
this.outputStream.close();
this.socket.close();
} catch (IOException e) {
logger.warn("Exception while closing", e);
}
}
private PubSubReply readMessageReply() throws IOException {
StringReplyHeader channelNameHeader = new StringReplyHeader(inputStream.readLine());
byte[] channelName = inputStream.readFully(channelNameHeader.getStringLength());
inputStream.readLine();
StringReplyHeader messageHeader = new StringReplyHeader(inputStream.readLine());
byte[] message = inputStream.readFully(messageHeader.getStringLength());
inputStream.readLine();
return new PubSubReply(PubSubReply.Type.MESSAGE, new String(channelName), Optional.of(message));
}
private PubSubReply readUnsubscribeReply() throws IOException {
String channelName = readSubscriptionReply();
return new PubSubReply(PubSubReply.Type.UNSUBSCRIBE, channelName, Optional.empty());
}
private PubSubReply readSubscribeReply() throws IOException {
String channelName = readSubscriptionReply();
return new PubSubReply(PubSubReply.Type.SUBSCRIBE, channelName, Optional.empty());
}
private String readSubscriptionReply() throws IOException {
StringReplyHeader channelNameHeader = new StringReplyHeader(inputStream.readLine());
byte[] channelName = inputStream.readFully(channelNameHeader.getStringLength());
inputStream.readLine();
new IntReply(inputStream.readLine());
return new String(channelName);
}
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.dispatch.redis;
import java.util.Optional;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class PubSubReply {
public enum Type {
MESSAGE,
SUBSCRIBE,
UNSUBSCRIBE
}
private final Type type;
private final String channel;
private final Optional<byte[]> content;
public PubSubReply(Type type, String channel, Optional<byte[]> content) {
this.type = type;
this.channel = channel;
this.content = content;
}
public Type getType() {
return type;
}
public String getChannel() {
return channel;
}
public Optional<byte[]> getContent() {
return content;
}
}

View File

@@ -1,28 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.dispatch.redis.protocol;
import java.io.IOException;
public class ArrayReplyHeader {
private final int elementCount;
public ArrayReplyHeader(String header) throws IOException {
if (header == null || header.length() < 2 || header.charAt(0) != '*') {
throw new IOException("Invalid array reply header: " + header);
}
try {
this.elementCount = Integer.parseInt(header.substring(1));
} catch (NumberFormatException e) {
throw new IOException(e);
}
}
public int getElementCount() {
return elementCount;
}
}

View File

@@ -1,28 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.dispatch.redis.protocol;
import java.io.IOException;
public class IntReply {
private final int value;
public IntReply(String reply) throws IOException {
if (reply == null || reply.length() < 2 || reply.charAt(0) != ':') {
throw new IOException("Invalid int reply: " + reply);
}
try {
this.value = Integer.parseInt(reply.substring(1));
} catch (NumberFormatException e) {
throw new IOException(e);
}
}
public int getValue() {
return value;
}
}

View File

@@ -1,28 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.dispatch.redis.protocol;
import java.io.IOException;
public class StringReplyHeader {
private final int stringLength;
public StringReplyHeader(String header) throws IOException {
if (header == null || header.length() < 2 || header.charAt(0) != '$') {
throw new IOException("Invalid string reply header: " + header);
}
try {
this.stringLength = Integer.parseInt(header.substring(1));
} catch (NumberFormatException e) {
throw new IOException(e);
}
}
public int getStringLength() {
return stringLength;
}
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.dispatch.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class Util {
public static byte[] combine(byte[]... elements) {
try {
int sum = 0;
for (byte[] element : elements) {
sum += element.length;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream(sum);
for (byte[] element : elements) {
baos.write(element);
}
return baos.toByteArray();
} catch (IOException e) {
throw new AssertionError(e);
}
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -1,123 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.dispatch;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.stubbing.Answer;
import org.whispersystems.dispatch.io.RedisPubSubConnectionFactory;
import org.whispersystems.dispatch.redis.PubSubConnection;
import org.whispersystems.dispatch.redis.PubSubReply;
public class DispatchManagerTest {
private PubSubConnection pubSubConnection;
private RedisPubSubConnectionFactory socketFactory;
private DispatchManager dispatchManager;
private PubSubReplyInputStream pubSubReplyInputStream;
@BeforeEach
void setUp() throws Exception {
pubSubConnection = mock(PubSubConnection.class );
socketFactory = mock(RedisPubSubConnectionFactory.class);
pubSubReplyInputStream = new PubSubReplyInputStream();
when(socketFactory.connect()).thenReturn(pubSubConnection);
when(pubSubConnection.read()).thenAnswer((Answer<PubSubReply>) invocationOnMock -> pubSubReplyInputStream.read());
dispatchManager = new DispatchManager(socketFactory, Optional.empty());
dispatchManager.start();
}
@AfterEach
void tearDown() {
dispatchManager.shutdown();
}
@Test
public void testConnect() {
verify(socketFactory).connect();
}
@Test
public void testSubscribe() {
DispatchChannel dispatchChannel = mock(DispatchChannel.class);
dispatchManager.subscribe("foo", dispatchChannel);
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.empty()));
verify(dispatchChannel, timeout(1000)).onDispatchSubscribed(eq("foo"));
}
@Test
public void testSubscribeUnsubscribe() {
DispatchChannel dispatchChannel = mock(DispatchChannel.class);
dispatchManager.subscribe("foo", dispatchChannel);
dispatchManager.unsubscribe("foo", dispatchChannel);
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.empty()));
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.UNSUBSCRIBE, "foo", Optional.empty()));
verify(dispatchChannel, timeout(1000)).onDispatchUnsubscribed(eq("foo"));
}
@Test
public void testMessages() {
DispatchChannel fooChannel = mock(DispatchChannel.class);
DispatchChannel barChannel = mock(DispatchChannel.class);
dispatchManager.subscribe("foo", fooChannel);
dispatchManager.subscribe("bar", barChannel);
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.empty()));
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "bar", Optional.empty()));
verify(fooChannel, timeout(1000)).onDispatchSubscribed(eq("foo"));
verify(barChannel, timeout(1000)).onDispatchSubscribed(eq("bar"));
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.MESSAGE, "foo", Optional.of("hello".getBytes())));
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.MESSAGE, "bar", Optional.of("there".getBytes())));
ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
verify(fooChannel, timeout(1000)).onDispatchMessage(eq("foo"), captor.capture());
assertArrayEquals("hello".getBytes(), captor.getValue());
verify(barChannel, timeout(1000)).onDispatchMessage(eq("bar"), captor.capture());
assertArrayEquals("there".getBytes(), captor.getValue());
}
private static class PubSubReplyInputStream {
private final List<PubSubReply> pubSubReplyList = new LinkedList<>();
public synchronized PubSubReply read() {
try {
while (pubSubReplyList.isEmpty()) wait();
return pubSubReplyList.remove(0);
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
public synchronized void write(PubSubReply pubSubReply) {
pubSubReplyList.add(pubSubReply);
notifyAll();
}
}
}

View File

@@ -1,264 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.dispatch.redis;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.security.SecureRandom;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
class PubSubConnectionTest {
private static final String REPLY = "*3\r\n" +
"$9\r\n" +
"subscribe\r\n" +
"$5\r\n" +
"abcde\r\n" +
":1\r\n" +
"*3\r\n" +
"$9\r\n" +
"subscribe\r\n" +
"$5\r\n" +
"fghij\r\n" +
":2\r\n" +
"*3\r\n" +
"$9\r\n" +
"subscribe\r\n" +
"$5\r\n" +
"klmno\r\n" +
":2\r\n" +
"*3\r\n" +
"$7\r\n" +
"message\r\n" +
"$5\r\n" +
"abcde\r\n" +
"$10\r\n" +
"1234567890\r\n" +
"*3\r\n" +
"$7\r\n" +
"message\r\n" +
"$5\r\n" +
"klmno\r\n" +
"$10\r\n" +
"0987654321\r\n";
@Test
void testSubscribe() throws IOException {
OutputStream outputStream = mock(OutputStream.class);
Socket socket = mock(Socket.class );
when(socket.getOutputStream()).thenReturn(outputStream);
PubSubConnection connection = new PubSubConnection(socket);
connection.subscribe("foobar");
ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
verify(outputStream).write(captor.capture());
assertArrayEquals(captor.getValue(), "SUBSCRIBE foobar\r\n".getBytes());
}
@Test
void testUnsubscribe() throws IOException {
OutputStream outputStream = mock(OutputStream.class);
Socket socket = mock(Socket.class );
when(socket.getOutputStream()).thenReturn(outputStream);
PubSubConnection connection = new PubSubConnection(socket);
connection.unsubscribe("bazbar");
ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
verify(outputStream).write(captor.capture());
assertArrayEquals(captor.getValue(), "UNSUBSCRIBE bazbar\r\n".getBytes());
}
@Test
void testTricklyResponse() throws Exception {
InputStream inputStream = mockInputStreamFor(new TrickleInputStream(REPLY.getBytes()));
OutputStream outputStream = mock(OutputStream.class);
Socket socket = mock(Socket.class );
when(socket.getOutputStream()).thenReturn(outputStream);
when(socket.getInputStream()).thenReturn(inputStream);
PubSubConnection pubSubConnection = new PubSubConnection(socket);
readResponses(pubSubConnection);
}
@Test
void testFullResponse() throws Exception {
InputStream inputStream = mockInputStreamFor(new FullInputStream(REPLY.getBytes()));
OutputStream outputStream = mock(OutputStream.class);
Socket socket = mock(Socket.class );
when(socket.getOutputStream()).thenReturn(outputStream);
when(socket.getInputStream()).thenReturn(inputStream);
PubSubConnection pubSubConnection = new PubSubConnection(socket);
readResponses(pubSubConnection);
}
@Test
void testRandomLengthResponse() throws Exception {
InputStream inputStream = mockInputStreamFor(new RandomInputStream(REPLY.getBytes()));
OutputStream outputStream = mock(OutputStream.class);
Socket socket = mock(Socket.class );
when(socket.getOutputStream()).thenReturn(outputStream);
when(socket.getInputStream()).thenReturn(inputStream);
PubSubConnection pubSubConnection = new PubSubConnection(socket);
readResponses(pubSubConnection);
}
private InputStream mockInputStreamFor(final MockInputStream stub) throws IOException {
InputStream result = mock(InputStream.class);
when(result.read()).thenAnswer(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocationOnMock) throws Throwable {
return stub.read();
}
});
when(result.read(any(byte[].class))).thenAnswer(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocationOnMock) throws Throwable {
byte[] buffer = (byte[])invocationOnMock.getArguments()[0];
return stub.read(buffer, 0, buffer.length);
}
});
when(result.read(any(byte[].class), anyInt(), anyInt())).thenAnswer(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocationOnMock) throws Throwable {
byte[] buffer = (byte[]) invocationOnMock.getArguments()[0];
int offset = (int) invocationOnMock.getArguments()[1];
int length = (int) invocationOnMock.getArguments()[2];
return stub.read(buffer, offset, length);
}
});
return result;
}
private void readResponses(PubSubConnection pubSubConnection) throws Exception {
PubSubReply reply = pubSubConnection.read();
assertEquals(reply.getType(), PubSubReply.Type.SUBSCRIBE);
assertEquals(reply.getChannel(), "abcde");
assertFalse(reply.getContent().isPresent());
reply = pubSubConnection.read();
assertEquals(reply.getType(), PubSubReply.Type.SUBSCRIBE);
assertEquals(reply.getChannel(), "fghij");
assertFalse(reply.getContent().isPresent());
reply = pubSubConnection.read();
assertEquals(reply.getType(), PubSubReply.Type.SUBSCRIBE);
assertEquals(reply.getChannel(), "klmno");
assertFalse(reply.getContent().isPresent());
reply = pubSubConnection.read();
assertEquals(reply.getType(), PubSubReply.Type.MESSAGE);
assertEquals(reply.getChannel(), "abcde");
assertArrayEquals(reply.getContent().get(), "1234567890".getBytes());
reply = pubSubConnection.read();
assertEquals(reply.getType(), PubSubReply.Type.MESSAGE);
assertEquals(reply.getChannel(), "klmno");
assertArrayEquals(reply.getContent().get(), "0987654321".getBytes());
}
private interface MockInputStream {
public int read();
public int read(byte[] input, int offset, int length);
}
private static class TrickleInputStream implements MockInputStream {
private final byte[] data;
private int index = 0;
private TrickleInputStream(byte[] data) {
this.data = data;
}
public int read() {
return data[index++];
}
public int read(byte[] input, int offset, int length) {
input[offset] = data[index++];
return 1;
}
}
private static class FullInputStream implements MockInputStream {
private final byte[] data;
private int index = 0;
private FullInputStream(byte[] data) {
this.data = data;
}
public int read() {
return data[index++];
}
public int read(byte[] input, int offset, int length) {
int amount = Math.min(data.length - index, length);
System.arraycopy(data, index, input, offset, amount);
index += length;
return amount;
}
}
private static class RandomInputStream implements MockInputStream {
private final byte[] data;
private int index = 0;
private RandomInputStream(byte[] data) {
this.data = data;
}
public int read() {
return data[index++];
}
public int read(byte[] input, int offset, int length) {
int maxCopy = Math.min(data.length - index, length);
int randomCopy = new SecureRandom().nextInt(maxCopy) + 1;
int copyAmount = Math.min(maxCopy, randomCopy);
System.arraycopy(data, index, input, offset, copyAmount);
index += copyAmount;
return copyAmount;
}
}
}

View File

@@ -1,54 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.dispatch.redis.protocol;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import org.junit.jupiter.api.Test;
class ArrayReplyHeaderTest {
@Test
void testNull() {
assertThrows(IOException.class, () -> new ArrayReplyHeader(null));
}
@Test
void testBadPrefix() {
assertThrows(IOException.class, () -> new ArrayReplyHeader(":3"));
}
@Test
void testEmpty() {
assertThrows(IOException.class, () -> new ArrayReplyHeader(""));
}
@Test
void testTruncated() {
assertThrows(IOException.class, () -> new ArrayReplyHeader("*"));
}
@Test
void testBadNumber() {
assertThrows(IOException.class, () -> new ArrayReplyHeader("*ABC"));
}
@Test
void testValid() throws IOException {
assertEquals(4, new ArrayReplyHeader("*4").getElementCount());
}
}

View File

@@ -1,39 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.dispatch.redis.protocol;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import org.junit.jupiter.api.Test;
class IntReplyHeaderTest {
@Test
void testNull() {
assertThrows(IOException.class, () -> new IntReply(null));
}
@Test
void testEmpty() {
assertThrows(IOException.class, () -> new IntReply(""));
}
@Test
void testBadNumber() {
assertThrows(IOException.class, () -> new IntReply(":A"));
}
@Test
void testBadFormat() {
assertThrows(IOException.class, () -> new IntReply("*"));
}
@Test
void testValid() throws IOException {
assertEquals(23, new IntReply(":23").getValue());
}
}

View File

@@ -1,35 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.dispatch.redis.protocol;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import org.junit.jupiter.api.Test;
class StringReplyHeaderTest {
@Test
void testNull() {
assertThrows(IOException.class, () -> new StringReplyHeader(null));
}
@Test
void testBadNumber() {
assertThrows(IOException.class, () -> new StringReplyHeader("$100A"));
}
@Test
void testBadPrefix() {
assertThrows(IOException.class, () -> new StringReplyHeader("*"));
}
@Test
void testValid() throws IOException {
assertEquals(1000, new StringReplyHeader("$1000").getStringLength());
}
}

View File

@@ -0,0 +1,90 @@
datadog.apiKey: unset
stripe.apiKey: unset
stripe.idempotencyKeyGenerator: abcdefg12345678= # base64 for creating request idempotency hash
braintree.privateKey: unset
directoryV2.client.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with CDS to generate auth tokens for Signal users
directoryV2.client.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with CDS to generate auth identity tokens for Signal users
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
tus.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG=
awsAttachments.accessKey: test
awsAttachments.accessSecret: test
gcpAttachments.rsaSigningKey: |
-----BEGIN PRIVATE KEY-----
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
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
AAAAAAAA
-----END PRIVATE KEY-----
apn.teamId: team-id
apn.keyId: key-id
apn.signingKey: |
-----BEGIN PRIVATE KEY-----
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
AAAAAAAA
-----END PRIVATE KEY-----
fcm.credentials: |
{ "json": true }
cdn.accessKey: test # AWS Access Key ID
cdn.accessSecret: test # AWS Access Secret
unidentifiedDelivery.certificate: ABCD1234
unidentifiedDelivery.privateKey: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789AAAAAAA
hCaptcha.apiKey: unset
storageService.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==
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
paymentsService.coinMarketCapApiKey: unset
artService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= # base64-encoded 32-byte secret not shared with any external service, but used in ArtController
artService.userAuthenticationTokenUserIdSecret: AAAAAAAAAAA= # base64-encoded secret to obscure user phone numbers from Sticker Creator
currentReportingKey.secret: AAAAAAAAAAA=
currentReportingKey.salt: AAAAAAAAAAA=
turn.secret: AAAAAAAAAAA=
linkDevice.secret: AAAAAAAAAAA=

View File

@@ -3,11 +3,78 @@
# `unset` values will need to be set to work properly.
# Most other values are technically valid for a local/demonstration environment, but are probably not production-ready.
logging:
level: INFO
appenders:
- type: console
threshold: ALL
timeZone: UTC
target: stdout
- type: logstashtcpsocket
destination: example.com:10516
apiKey: secret://datadog.apiKey
environment: staging
metrics:
reporters:
- type: signal-datadog
frequency: 10 seconds
tags:
- "env:staging"
- "service:chat"
udpTransport:
statsdHost: localhost
port: 8125
excludesAttributes:
- m1_rate
- m5_rate
- m15_rate
- mean_rate
- stddev
useRegexFilters: true
excludes:
- ^.+\.total$
- ^.+\.request\.filtering$
- ^.+\.response\.filtering$
- ^executor\..+$
- ^lettuce\..+$
reportOnStop: true
adminEventLoggingConfiguration:
credentials: |
{
"key": "value"
}
projectId: some-project-id
logName: some-log-name
grpcPort: 8080
stripe:
apiKey: unset
idempotencyKeyGenerator: abcdefg12345678= # base64 for creating request idempotency hash
apiKey: secret://stripe.apiKey
idempotencyKeyGenerator: secret://stripe.idempotencyKeyGenerator
boostDescription: >
Example
supportedCurrenciesByPaymentMethod:
CARD:
- usd
- eur
SEPA_DEBIT:
- eur
braintree:
merchantId: unset
publicKey: unset
privateKey: secret://braintree.privateKey
environment: unset
graphqlUrl: unset
merchantAccounts:
# ISO 4217 currency code and its corresponding sub-merchant account
'xts': unset
supportedCurrenciesByPaymentMethod:
PAYPAL:
- usd
dynamoDbClientConfiguration:
region: us-west-2 # AWS Region
@@ -18,25 +85,29 @@ dynamoDbTables:
phoneNumberTableName: Example_Accounts_PhoneNumbers
phoneNumberIdentifierTableName: Example_Accounts_PhoneNumberIdentifiers
usernamesTableName: Example_Accounts_Usernames
scanPageSize: 100
backups:
tableName: Example_Backups
clientReleases:
tableName: Example_ClientReleases
deletedAccounts:
tableName: Example_DeletedAccounts
needsReconciliationIndexName: NeedsReconciliation
deletedAccountsLock:
tableName: Example_DeletedAccountsLock
issuedReceipts:
tableName: Example_IssuedReceipts
expiration: P30D # Duration of time until rows expire
generator: abcdefg12345678= # random base64-encoded binary sequence
keys:
ecKeys:
tableName: Example_Keys
ecSignedPreKeys:
tableName: Example_EC_Signed_Pre_Keys
pqKeys:
tableName: Example_PQ_Keys
pqLastResortKeys:
tableName: Example_PQ_Last_Resort_Keys
messages:
tableName: Example_Messages
expiration: P30D # Duration of time until rows expire
pendingAccounts:
tableName: Example_PendingAccounts
pendingDevices:
tableName: Example_PendingDevices
phoneNumberIdentifiers:
tableName: Example_PhoneNumberIdentifiers
profiles:
@@ -46,37 +117,17 @@ dynamoDbTables:
redeemedReceipts:
tableName: Example_RedeemedReceipts
expiration: P30D # Duration of time until rows expire
registrationRecovery:
tableName: Example_RegistrationRecovery
expiration: P300D # Duration of time until rows expire
remoteConfig:
tableName: Example_RemoteConfig
reportMessage:
tableName: Example_ReportMessage
reservedUsernames:
tableName: Example_ReservedUsernames
subscriptions:
tableName: Example_Subscriptions
twilio: # Twilio gateway configuration
accountId: unset
accountToken: unset
nanpaMessagingServiceSid: unset # Twilio SID for the messaging service to use for NANPA.
messagingServiceSid: unset # Twilio SID for the message service to use for non-NANPA.
verifyServiceSid: unset # Twilio SID for a Verify service
localDomain: example.com # Domain Twilio can connect back to for calls. Should be domain of your service.
defaultClientVerificationTexts:
ios: example %1$s # Text to use for the verification message on iOS. Will be passed to String.format with the verification code as argument 1.
androidNg: example %1$s # Text to use for the verification message on android-ng client types. Will be passed to String.format with the verification code as argument 1.
android202001: example %1$s # Text to use for the verification message on android-2020-01 client types. Will be passed to String.format with the verification code as argument 1.
android202103: example %1$s # Text to use for the verification message on android-2021-03 client types. Will be passed to String.format with the verification code as argument 1.
generic: example %1$s # Text to use when the client type is unrecognized. Will be passed to String.format with the verification code as argument 1.
regionalClientVerificationTexts: # Map of country codes to custom texts
999: # example country code
ios: example %1$s # all keys from defaultClientVerificationTexts are required
androidNg: example %1$s
android202001: example %1$s
android202103: example %1$s
generic: example %1$s
androidAppHash: example # Hash appended to Android
verifyServiceFriendlyName: example # Service name used in template. Requires Twilio account rep to enable
verificationSessions:
tableName: Example_VerificationSessions
cacheCluster: # Redis server configuration for cache cluster
configurationUri: redis://redis.example.com:6379/
@@ -85,9 +136,7 @@ clientPresenceCluster: # Redis server configuration for client presence cluster
configurationUri: redis://redis.example.com:6379/
pubsub: # Redis server configuration for pubsub cluster
url: redis://redis.example.com:6379/
replicaUrls:
- redis://redis.example.com:6379/
uri: redis://redis.example.com:6379/
pushSchedulerCluster: # Redis server configuration for push scheduler cluster
configurationUri: redis://redis.example.com:6379/
@@ -95,46 +144,39 @@ pushSchedulerCluster: # Redis server configuration for push scheduler cluster
rateLimitersCluster: # Redis server configuration for rate limiters cluster
configurationUri: redis://redis.example.com:6379/
directory:
client: # Configuration for interfacing with Contact Discovery Service cluster
userAuthenticationTokenSharedSecret: 00000f # hex-encoded secret shared with CDS used to generate auth tokens for Signal users
userAuthenticationTokenUserIdSecret: 00000f # hex-encoded secret shared among Signal-Servers to obscure user phone numbers from CDS
sqs:
accessKey: test # AWS SQS accessKey
accessSecret: test # AWS SQS accessSecret
queueUrls: # AWS SQS queue urls
- https://sqs.example.com/directory.fifo
server: # One or more CDS servers
- replicationName: example # CDS replication name
replicationUrl: cds.example.com # CDS replication endpoint base url
replicationPassword: example # CDS replication endpoint password
replicationCaCertificate: | # CDS replication endpoint TLS certificate trust root
-----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-----
directoryV2:
client: # Configuration for interfacing with Contact Discovery Service v2 cluster
userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with CDS to generate auth tokens for Signal users
userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with CDS to generate auth identity tokens for Signal users
userAuthenticationTokenSharedSecret: secret://directoryV2.client.userAuthenticationTokenSharedSecret
userIdTokenSharedSecret: secret://directoryV2.client.userIdTokenSharedSecret
svr2:
uri: svr2.example.com
userAuthenticationTokenSharedSecret: secret://svr2.userAuthenticationTokenSharedSecret
userIdTokenSharedSecret: secret://svr2.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
@@ -145,8 +187,8 @@ metricsCluster:
configurationUri: redis://redis.example.com:6379/
awsAttachments: # AWS S3 configuration
accessKey: test
accessSecret: test
accessKey: secret://awsAttachments.accessKey
accessSecret: secret://awsAttachments.accessSecret
bucket: aws-attachments
region: us-west-2
@@ -155,136 +197,82 @@ gcpAttachments: # GCP Storage configuration
email: user@example.cocm
maxSizeInBytes: 1024
pathPrefix:
rsaSigningKey: |
-----BEGIN PRIVATE KEY-----
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
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
AAAAAAAA
-----END PRIVATE KEY-----
rsaSigningKey: secret://gcpAttachments.rsaSigningKey
accountDatabaseCrawler:
chunkSize: 10 # accounts per run
chunkIntervalMs: 60000 # time 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
signingKey: |
-----BEGIN PRIVATE KEY-----
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
AAAAAAAA
-----END PRIVATE KEY-----
keyId: secret://apn.keyId
teamId: secret://apn.teamId
signingKey: secret://apn.signingKey
fcm: # FCM configuration
credentials: |
{ "json": true }
credentials: secret://fcm.credentials
cdn:
accessKey: test # AWS Access Key ID
accessSecret: test # AWS Access Secret
accessKey: secret://cdn.accessKey
accessSecret: secret://cdn.accessSecret
bucket: cdn # S3 Bucket name
region: us-west-2 # AWS region
datadog:
apiKey: unset
dogstatsd:
environment: dev
unidentifiedDelivery:
certificate: ABCD1234
privateKey: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789AAAAAAA
certificate: secret://unidentifiedDelivery.certificate
privateKey: secret://unidentifiedDelivery.privateKey
expiresDays: 7
voiceVerification:
url: https://cdn-ca.signal.org/verification/
locales:
- en
recaptcha:
projectPath: projects/example
credentialConfigurationJson: "{ }" # service account configuration for backend authentication
hCaptcha:
apiKey: secret://hCaptcha.apiKey
shortCode:
baseUrl: https://example.com/shortcodes/
storageService:
uri: storage.example.com
userAuthenticationTokenSharedSecret: 00000f
storageCaCertificate: |
-----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-----
backupService:
uri: backup.example.com
userAuthenticationTokenSharedSecret: 00000f
backupCaCertificate: |
-----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-----
userAuthenticationTokenSharedSecret: secret://storageService.userAuthenticationTokenSharedSecret
storageCaCertificates:
- |
-----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: 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==
serverSecret: secret://zkConfig.serverSecret
callingZkConfig:
serverSecret: secret://callingZkConfig.serverSecret
backupsZkConfig:
serverSecret: secret://backupsZkConfig.serverSecret
appConfig:
application: example
@@ -292,28 +280,33 @@ appConfig:
configuration: example
remoteConfig:
authorizedTokens:
- # 1st authorized token
- # 2nd authorized token
authorizedUsers:
- # 1st authorized user
- # 2nd authorized user
- # ...
- # Nth authorized token
- # 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
paymentsService:
userAuthenticationTokenSharedSecret: 0000000f0000000f0000000f0000000f0000000f0000000f0000000f0000000f # hex-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users
fixerApiKey: unset
userAuthenticationTokenSharedSecret: secret://paymentsService.userAuthenticationTokenSharedSecret
fixerApiKey: secret://paymentsService.fixerApiKey
coinMarketCapApiKey: secret://paymentsService.coinMarketCapApiKey
coinMarketCapCurrencyIds:
MOB: 7878
paymentCurrencies:
# list of symbols for supported currencies
- MOB
donation:
uri: donation.example.com # value
supportedCurrencies:
- # 1st supported currency
- # 2nd supported currency
- # ...
- # Nth supported currency
artService:
userAuthenticationTokenSharedSecret: secret://artService.userAuthenticationTokenSharedSecret
userAuthenticationTokenUserIdSecret: secret://artService.userAuthenticationTokenUserIdSecret
badges:
badges:
@@ -344,26 +337,69 @@ subscription: # configuration for Stripe subscriptions
# list of ISO 4217 currency codes and amounts for the given badge level
xts:
amount: '10'
id: price_example # stripe ID
processorIds:
STRIPE: price_example # stripe Price ID
BRAINTREE: plan_example # braintree Plan ID
boost:
level: 1
expiration: P90D
badge: EXAMPLE
oneTimeDonations:
sepaMaxTransactionSizeEuros: '10000'
boost:
level: 1
expiration: P90D
badge: EXAMPLE
gift:
level: 10
expiration: P90D
badge: EXAMPLE
currencies:
# ISO 4217 currency codes and amounts in those currencies
xts:
- '1'
- '2'
- '4'
- '8'
- '20'
- '40'
minimum: '0.5'
gift: '2'
boosts:
- '1'
- '2'
- '4'
- '8'
- '20'
- '40'
gift:
level: 10
expiration: P90D
badge: EXAMPLE
currencies:
# ISO 4217 currency codes and amounts in those currencies
xts: '2'
registrationService:
host: registration.example.com
port: 443
credentialConfigurationJson: |
{
"example": "example"
}
identityTokenAudience: https://registration.example.com
registrationCaCertificate: | # Registration service TLS certificate trust root
-----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-----
turn:
secret: secret://turn.secret
commandStopListener:
path: /example/path
linkDevice:
secret: secret://linkDevice.secret

View File

@@ -10,7 +10,25 @@
<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>${swagger.version}</version>
<exclusions>
<!-- org.yaml:snakeyaml is causing a dependency convergence error -->
<exclusion>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
@@ -26,7 +44,7 @@
<dependency>
<groupId>org.whispersystems.textsecure</groupId>
<artifactId>redis-dispatch</artifactId>
<artifactId>event-logger</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
@@ -43,10 +61,6 @@
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-core</artifactId>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-jdbi3</artifactId>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-auth</artifactId>
@@ -118,19 +132,10 @@
<artifactId>logstash-logback-encoder</artifactId>
</dependency>
<dependency>
<groupId>org.jdbi</groupId>
<artifactId>jdbi3-core</artifactId>
</dependency>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-core</artifactId>
</dependency>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-jdbi3</artifactId>
</dependency>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-healthchecks</artifactId>
@@ -168,6 +173,26 @@
</exclusions>
</dependency>
<dependency>
<groupId>party.iroiro.luajava</groupId>
<artifactId>luajava</artifactId>
<version>${luajava.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>party.iroiro.luajava</groupId>
<artifactId>lua51</artifactId>
<version>${luajava.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>party.iroiro.luajava</groupId>
<artifactId>lua51-platform</artifactId>
<version>${luajava.version}</version>
<classifier>natives-desktop</classifier>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-api</artifactId>
@@ -181,10 +206,6 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
@@ -193,34 +214,7 @@
<dependency>
<groupId>com.google.firebase</groupId>
<artifactId>firebase-admin</artifactId>
<version>9.0.0</version>
<!-- firebase-admin has conflicting versions of these artifacts in its dependency tree; for firebase-admin
9.0.0, we'll need to depend directly on com.google.api-client:google-api-client:1.35.1 and
com.google.oauth-client:google-oauth-client:1.34.1 -->
<exclusions>
<exclusion>
<groupId>com.google.api-client</groupId>
<artifactId>google-api-client</artifactId>
</exclusion>
<exclusion>
<groupId>com.google.oauth-client</groupId>
<artifactId>google-oauth-client</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.google.api-client</groupId>
<artifactId>google-api-client</artifactId>
<version>1.35.1</version>
</dependency>
<dependency>
<groupId>com.google.oauth-client</groupId>
<artifactId>google-oauth-client</artifactId>
<version>1.34.1</version>
<version>${firebase-admin.version}</version>
</dependency>
<dependency>
@@ -236,6 +230,30 @@
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-retry</artifactId>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-reactor</artifactId>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
</dependency>
<!-- Needed for gRPC with Java 9+ -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>annotations-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
@@ -243,7 +261,7 @@
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-datadog</artifactId>
<artifactId>micrometer-registry-statsd</artifactId>
</dependency>
<dependency>
<groupId>org.coursera</groupId>
@@ -274,6 +292,11 @@
<artifactId>jackson-jaxrs-json-provider</artifactId>
</dependency>
<dependency>
<groupId>com.salesforce.servicelibs</groupId>
<artifactId>reactor-grpc-stub</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sts</artifactId>
@@ -282,10 +305,6 @@
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sqs</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>dynamodb</artifactId>
@@ -298,18 +317,10 @@
<groupId>software.amazon.awssdk</groupId>
<artifactId>appconfigdata</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-core</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>dynamodb-lock-client</artifactId>
<version>1.1.0</version>
<version>1.2.0</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
@@ -318,11 +329,6 @@
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
@@ -392,14 +398,17 @@
<dependency>
<groupId>com.almworks.sqlite4java</groupId>
<artifactId>sqlite4java</artifactId>
<version>1.0.392</version>
<version>${sqlite4java.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.3.22.RELEASE</version>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core-micrometer</artifactId>
</dependency>
<dependency>
<groupId>io.vavr</groupId>
@@ -412,6 +421,11 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
</dependency>
<dependency>
<groupId>org.signal</groupId>
<artifactId>embedded-redis</artifactId>
@@ -421,21 +435,27 @@
<dependency>
<groupId>com.fasterxml.uuid</groupId>
<artifactId>java-uuid-generator</artifactId>
<version>3.2.0</version>
<version>${java-uuid-generator.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>DynamoDBLocal</artifactId>
<version>1.16.0</version>
<version>1.23.0</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.github.ganadist.sqlite4java</groupId>
<artifactId>libsqlite4java-osx-aarch64</artifactId>
<version>${sqlite4java.version}</version>
<type>dylib</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
</dependency>
<dependency>
@@ -447,17 +467,29 @@
<groupId>com.stripe</groupId>
<artifactId>stripe-java</artifactId>
</dependency>
<dependency>
<groupId>com.braintreepayments.gateway</groupId>
<artifactId>braintree-java</artifactId>
</dependency>
<dependency>
<groupId>com.apollographql.apollo3</groupId>
<artifactId>apollo-api-jvm</artifactId>
<version>3.8.2</version>
</dependency>
</dependencies>
<profiles>
<profile>
<id>exclude-abusive-message-filter</id>
<id>exclude-spam-filter</id>
<build>
<plugins>
<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>
@@ -492,7 +524,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>
@@ -512,7 +544,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>
@@ -528,24 +560,64 @@
</plugin>
<plugin>
<groupId>org.signal</groupId>
<artifactId>s3-upload-maven-plugin</artifactId>
<version>1.6-SNAPSHOT</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>
</configuration>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<executions>
<execution>
<id>deploy-to-s3</id>
<phase>deploy</phase>
<goals>
<goal>s3-upload</goal>
<goal>build</goal>
</goals>
</execution>
</executions>
<configuration>
<from>
<image>eclipse-temurin@sha256:${docker.image.sha256}</image>
</from>
<to>
<image>${docker.repo}:${project.version}</image>
</to>
<container>
<mainClass>org.whispersystems.textsecuregcm.WhisperServerService</mainClass>
<jvmFlags>
<jvmFlag>-server</jvmFlag>
<jvmFlag>-Djava.awt.headless=true</jvmFlag>
<jvmFlag>-Djdk.nio.maxCachedBufferSize=262144</jvmFlag>
<jvmFlag>-Dlog4j2.formatMsgNoLookups=true</jvmFlag>
<jvmFlag>-XX:MaxRAMPercentage=75</jvmFlag>
<jvmFlag>-XX:+HeapDumpOnOutOfMemoryError</jvmFlag>
<jvmFlag>-XX:HeapDumpPath=/tmp/heapdump.bin</jvmFlag>
</jvmFlags>
<ports>
<port>8080</port>
</ports>
<creationTime>USE_CURRENT_TIMESTAMP</creationTime>
</container>
<extraDirectories>
<paths>
<path>
<from>${project.basedir}/config</from>
<includes>*.yml</includes>
<into>/usr/share/signal/</into>
</path>
</paths>
</extraDirectories>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>include-spam-filter</id>
<build>
<plugins>
<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>
@@ -569,6 +641,16 @@
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<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>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
@@ -584,7 +666,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>
@@ -602,6 +684,31 @@
</arguments>
</configuration>
</plugin>
<plugin>
<groupId>com.github.aoudiamoncef</groupId>
<artifactId>apollo-client-maven-plugin</artifactId>
<version>5.0.0</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<services>
<braintree>
<compilationUnit>
<name>braintree</name>
<compilerParams>
<schemaPackageName>com.braintree.graphql.client</schemaPackageName>
</compilerParams>
</compilationUnit>
</braintree>
</services>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,9 @@
# https://graphql.braintreepayments.com/reference/#Mutation--chargePaymentMethod
mutation ChargePayPalOneTimePayment($input: ChargePaymentMethodInput!) {
chargePaymentMethod(input: $input) {
transaction {
id,
status
}
}
}

View File

@@ -0,0 +1,6 @@
mutation CreatePayPalBillingAgreement($input: CreatePayPalBillingAgreementInput!) {
createPayPalBillingAgreement(input: $input) {
approvalUrl,
billingAgreementToken
}
}

View File

@@ -0,0 +1,7 @@
# https://graphql.braintreepayments.com/reference/#Mutation--createPayPalOneTimePayment
mutation CreatePayPalOneTimePayment($input: CreatePayPalOneTimePaymentInput!) {
createPayPalOneTimePayment(input: $input) {
approvalUrl,
paymentId
}
}

View File

@@ -0,0 +1,7 @@
mutation TokenizePayPalBillingAgreement($input: TokenizePayPalBillingAgreementInput!) {
tokenizePayPalBillingAgreement(input: $input) {
paymentMethod {
id
}
}
}

View File

@@ -0,0 +1,8 @@
# https://graphql.braintreepayments.com/reference/#Mutation--tokenizePayPalOneTimePayment
mutation TokenizePayPalOneTimePayment($input: TokenizePayPalOneTimePaymentInput!) {
tokenizePayPalOneTimePayment(input: $input) {
paymentMethod {
id
}
}
}

View File

@@ -0,0 +1,7 @@
mutation VaultPaymentMethod($input: VaultPaymentMethodInput!) {
vaultPaymentMethod(input: $input) {
paymentMethod {
id
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,62 +1,81 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.dropwizard.Configuration;
import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.AbusiveMessageFilterConfiguration;
import org.whispersystems.textsecuregcm.configuration.AccountDatabaseCrawlerConfiguration;
import org.whispersystems.textsecuregcm.attachments.TusConfiguration;
import org.whispersystems.textsecuregcm.configuration.AdminEventLoggingConfiguration;
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
import org.whispersystems.textsecuregcm.configuration.AppConfigConfiguration;
import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration;
import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration;
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
import org.whispersystems.textsecuregcm.configuration.BoostConfiguration;
import org.whispersystems.textsecuregcm.configuration.BraintreeConfiguration;
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration;
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
import org.whispersystems.textsecuregcm.configuration.ClientReleaseConfiguration;
import org.whispersystems.textsecuregcm.configuration.CommandStopListenerConfiguration;
import org.whispersystems.textsecuregcm.configuration.DogstatsdConfiguration;
import org.whispersystems.textsecuregcm.configuration.DirectoryV2Configuration;
import org.whispersystems.textsecuregcm.configuration.DonationConfiguration;
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.GiftConfiguration;
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;
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
import org.whispersystems.textsecuregcm.configuration.RecaptchaConfiguration;
import org.whispersystems.textsecuregcm.configuration.RedisClusterConfiguration;
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.ShortCodeExpanderConfiguration;
import org.whispersystems.textsecuregcm.configuration.SpamFilterConfiguration;
import org.whispersystems.textsecuregcm.configuration.StripeConfiguration;
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import org.whispersystems.textsecuregcm.configuration.TurnSecretConfiguration;
import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration;
import org.whispersystems.textsecuregcm.configuration.VoiceVerificationConfiguration;
import org.whispersystems.textsecuregcm.configuration.ZkConfig;
import org.whispersystems.textsecuregcm.limits.RateLimiterConfig;
import org.whispersystems.websocket.configuration.WebSocketConfiguration;
/** @noinspection MismatchedQueryAndUpdateOfCollection, WeakerAccess */
public class WhisperServerConfiguration extends Configuration {
@NotNull
@Valid
@JsonProperty
private AdminEventLoggingConfiguration adminEventLoggingConfiguration;
@NotNull
@Valid
@JsonProperty
private StripeConfiguration stripe;
@NotNull
@Valid
@JsonProperty
private BraintreeConfiguration braintree;
@NotNull
@Valid
@JsonProperty
@@ -67,11 +86,6 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private DynamoDbTables dynamoDbTables;
@NotNull
@Valid
@JsonProperty
private TwilioConfiguration twilio;
@NotNull
@Valid
@JsonProperty
@@ -90,7 +104,7 @@ public class WhisperServerConfiguration extends Configuration {
@NotNull
@Valid
@JsonProperty
private DatadogConfiguration datadog;
private DogstatsdConfiguration dogstatsd = new DogstatsdConfiguration();
@NotNull
@Valid
@@ -107,11 +121,6 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private RedisClusterConfiguration metricsCluster;
@NotNull
@Valid
@JsonProperty
private DirectoryConfiguration directory;
@NotNull
@Valid
@JsonProperty
@@ -120,7 +129,7 @@ public class WhisperServerConfiguration extends Configuration {
@NotNull
@Valid
@JsonProperty
private AccountDatabaseCrawlerConfiguration accountDatabaseCrawler;
private SecureValueRecovery2Configuration svr2;
@NotNull
@Valid
@@ -145,7 +154,7 @@ public class WhisperServerConfiguration extends Configuration {
@Valid
@NotNull
@JsonProperty
private List<TestDeviceConfiguration> testDevices = new LinkedList<>();
private Set<String> testDevices = new HashSet<>();
@Valid
@NotNull
@@ -155,7 +164,7 @@ public class WhisperServerConfiguration extends Configuration {
@Valid
@NotNull
@JsonProperty
private RateLimitsConfiguration limits = new RateLimitsConfiguration();
private Map<String, RateLimiterConfig> limits = new HashMap<>();
@Valid
@NotNull
@@ -180,33 +189,48 @@ public class WhisperServerConfiguration extends Configuration {
@Valid
@NotNull
@JsonProperty
private VoiceVerificationConfiguration voiceVerification;
private RecaptchaConfiguration recaptcha;
@Valid
@NotNull
@JsonProperty
private RecaptchaConfiguration recaptcha;
private HCaptchaConfiguration hCaptcha;
@Valid
@NotNull
@JsonProperty
private ShortCodeExpanderConfiguration shortCode;
@Valid
@NotNull
@JsonProperty
private SecureStorageServiceConfiguration storageService;
@Valid
@NotNull
@JsonProperty
private SecureBackupServiceConfiguration backupService;
@Valid
@NotNull
@JsonProperty
private PaymentsServiceConfiguration paymentsService;
@Valid
@NotNull
@JsonProperty
private ArtServiceConfiguration artService;
@Valid
@NotNull
@JsonProperty
private ZkConfig zkConfig;
@Valid
@NotNull
@JsonProperty
private GenericZkConfig callingZkConfig;
@Valid
@NotNull
@JsonProperty
private GenericZkConfig backupsZkConfig;
@Valid
@NotNull
@JsonProperty
@@ -217,11 +241,6 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private AppConfigConfiguration appConfig;
@Valid
@NotNull
@JsonProperty
private DonationConfiguration donation;
@Valid
@NotNull
@JsonProperty
@@ -235,12 +254,7 @@ public class WhisperServerConfiguration extends Configuration {
@Valid
@JsonProperty
@NotNull
private BoostConfiguration boost;
@Valid
@JsonProperty
@NotNull
private GiftConfiguration gift;
private OneTimeDonationConfiguration oneTimeDonations;
@Valid
@NotNull
@@ -249,12 +263,60 @@ public class WhisperServerConfiguration extends Configuration {
@Valid
@JsonProperty
private AbusiveMessageFilterConfiguration abusiveMessageFilter;
private SpamFilterConfiguration spamFilterConfiguration;
@Valid
@NotNull
@JsonProperty
private RegistrationServiceConfiguration registrationService;
@Valid
@NotNull
@JsonProperty
private TurnSecretConfiguration turn;
@Valid
@NotNull
@JsonProperty
private TusConfiguration tus;
@Valid
@NotNull
@JsonProperty
private int grpcPort;
@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 AdminEventLoggingConfiguration getAdminEventLoggingConfiguration() {
return adminEventLoggingConfiguration;
}
public StripeConfiguration getStripe() {
return stripe;
}
public BraintreeConfiguration getBraintree() {
return braintree;
}
public DynamoDbClientConfiguration getDynamoDbClientConfiguration() {
return dynamoDbClientConfiguration;
}
@@ -267,18 +329,18 @@ public class WhisperServerConfiguration extends Configuration {
return recaptcha;
}
public VoiceVerificationConfiguration getVoiceVerificationConfiguration() {
return voiceVerification;
public HCaptchaConfiguration getHCaptchaConfiguration() {
return hCaptcha;
}
public ShortCodeExpanderConfiguration getShortCodeRetrieverConfiguration() {
return shortCode;
}
public WebSocketConfiguration getWebSocketConfiguration() {
return webSocket;
}
public TwilioConfiguration getTwilioConfiguration() {
return twilio;
}
public AwsAttachmentsConfiguration getAwsAttachmentsConfiguration() {
return awsAttachments;
}
@@ -299,8 +361,8 @@ public class WhisperServerConfiguration extends Configuration {
return metricsCluster;
}
public DirectoryConfiguration getDirectoryConfiguration() {
return directory;
public SecureValueRecovery2Configuration getSvr2Configuration() {
return svr2;
}
public DirectoryV2Configuration getDirectoryV2Configuration() {
@@ -311,10 +373,6 @@ public class WhisperServerConfiguration extends Configuration {
return storageService;
}
public AccountDatabaseCrawlerConfiguration getAccountDatabaseCrawlerConfiguration() {
return accountDatabaseCrawler;
}
public MessageCacheConfiguration getMessageCacheConfiguration() {
return messageCache;
}
@@ -331,7 +389,7 @@ public class WhisperServerConfiguration extends Configuration {
return rateLimitersCluster;
}
public RateLimitsConfiguration getLimitsConfiguration() {
public Map<String, RateLimiterConfig> getLimitsConfiguration() {
return limits;
}
@@ -347,23 +405,16 @@ public class WhisperServerConfiguration extends Configuration {
return cdn;
}
public DatadogConfiguration getDatadogConfiguration() {
return datadog;
public DogstatsdConfiguration getDatadogConfiguration() {
return dogstatsd;
}
public UnidentifiedDeliveryConfiguration getDeliveryCertificate() {
return unidentifiedDelivery;
}
public Map<String, Integer> getTestDevices() {
Map<String, Integer> results = new HashMap<>();
for (TestDeviceConfiguration testDeviceConfiguration : testDevices) {
results.put(testDeviceConfiguration.getNumber(),
testDeviceConfiguration.getCode());
}
return results;
public Set<String> getTestDevices() {
return testDevices;
}
public Map<String, Integer> getMaxDevices() {
@@ -377,18 +428,26 @@ public class WhisperServerConfiguration extends Configuration {
return results;
}
public SecureBackupServiceConfiguration getSecureBackupServiceConfiguration() {
return backupService;
}
public PaymentsServiceConfiguration getPaymentsServiceConfiguration() {
return paymentsService;
}
public ArtServiceConfiguration getArtServiceConfiguration() {
return artService;
}
public ZkConfig getZkConfig() {
return zkConfig;
}
public GenericZkConfig getCallingZkConfig() {
return callingZkConfig;
}
public GenericZkConfig getBackupsZkConfig() {
return backupsZkConfig;
}
public RemoteConfigConfiguration getRemoteConfigConfiguration() {
return remoteConfig;
}
@@ -397,10 +456,6 @@ public class WhisperServerConfiguration extends Configuration {
return appConfig;
}
public DonationConfiguration getDonationConfiguration() {
return donation;
}
public BadgesConfiguration getBadges() {
return badges;
}
@@ -409,19 +464,47 @@ public class WhisperServerConfiguration extends Configuration {
return subscription;
}
public BoostConfiguration getBoost() {
return boost;
}
public GiftConfiguration getGift() {
return gift;
public OneTimeDonationConfiguration getOneTimeDonations() {
return oneTimeDonations;
}
public ReportMessageConfiguration getReportMessageConfiguration() {
return reportMessage;
}
public AbusiveMessageFilterConfiguration getAbusiveMessageFilterConfiguration() {
return abusiveMessageFilter;
public SpamFilterConfiguration getSpamFilterConfiguration() {
return spamFilterConfiguration;
}
public RegistrationServiceConfiguration getRegistrationServiceConfiguration() {
return registrationService;
}
public TurnSecretConfiguration getTurnSecretConfiguration() {
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

@@ -1,33 +0,0 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.abuse;
import io.dropwizard.lifecycle.Managed;
import javax.ws.rs.container.ContainerRequestFilter;
import java.io.IOException;
/**
* An abusive message filter is a {@link ContainerRequestFilter} that filters requests to message-sending endpoints to
* detect and respond to patterns of abusive behavior.
* <p/>
* Abusive message filters are managed components that are generally loaded dynamically via a
* {@link java.util.ServiceLoader}. Their {@link #configure(String)} method will be called prior to be adding to the
* server's pool of {@link Managed} objects.
* <p/>
* Abusive message filters must be annotated with {@link FilterAbusiveMessages}, a name binding annotation that
* restricts the endpoints to which the filter may apply.
*/
public interface AbusiveMessageFilter extends ContainerRequestFilter, Managed {
/**
* Configures this abusive message filter. This method will be called before the filter is added to the server's pool
* of managed objects and before the server processes any requests.
*
* @param environmentName the name of the environment in which this filter is running (e.g. "staging" or "production")
* @throws IOException if the filter could not read its configuration source for any reason
*/
void configure(String environmentName) throws IOException;
}

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

@@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.auth;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicCredentials;
import java.util.Optional;
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
public class AccountAuthenticator extends BaseAccountAuthenticator implements

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

@@ -1,50 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import org.apache.commons.codec.binary.Hex;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public class AuthenticationCredentials {
private final String hashedAuthenticationToken;
private final String salt;
public AuthenticationCredentials(String hashedAuthenticationToken, String salt) {
this.hashedAuthenticationToken = hashedAuthenticationToken;
this.salt = salt;
}
public AuthenticationCredentials(String authenticationToken) {
this.salt = String.valueOf(Math.abs(new SecureRandom().nextInt()));
this.hashedAuthenticationToken = getHashedValue(salt, authenticationToken);
}
public String getHashedAuthenticationToken() {
return hashedAuthenticationToken;
}
public String getSalt() {
return salt;
}
public boolean verify(String authenticationToken) {
String theirValue = getHashedValue(salt, authenticationToken);
return MessageDigest.isEqual(theirValue.getBytes(StandardCharsets.UTF_8), this.hashedAuthenticationToken.getBytes(StandardCharsets.UTF_8));
}
private static String getHashedValue(String salt, String token) {
try {
return new String(Hex.encodeHex(MessageDigest.getInstance("SHA1").digest((salt + token).getBytes(StandardCharsets.UTF_8))));
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -27,13 +27,18 @@ import org.whispersystems.textsecuregcm.util.Util;
public class BaseAccountAuthenticator {
private static final String AUTHENTICATION_COUNTER_NAME = name(BaseAccountAuthenticator.class, "authentication");
private static final String ENABLED_NOT_REQUIRED_AUTHENTICATION_COUNTER_NAME = name(BaseAccountAuthenticator.class,
"enabledNotRequiredAuthentication");
private static final String AUTHENTICATION_SUCCEEDED_TAG_NAME = "succeeded";
private static final String AUTHENTICATION_FAILURE_REASON_TAG_NAME = "reason";
private static final String AUTHENTICATION_ENABLED_REQUIRED_TAG_NAME = "enabledRequired";
private static final String ENABLED_TAG_NAME = "enabled";
private static final String DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME = name(BaseAccountAuthenticator.class, "daysSinceLastSeen");
private static final String IS_PRIMARY_DEVICE_TAG = "isPrimary";
@VisibleForTesting
static final char DEVICE_ID_SEPARATOR = '.';
private final AccountsManager accountsManager;
private final Clock clock;
@@ -43,19 +48,19 @@ public class BaseAccountAuthenticator {
@VisibleForTesting
public BaseAccountAuthenticator(AccountsManager accountsManager, Clock clock) {
this.accountsManager = accountsManager;
this.clock = clock;
this.accountsManager = accountsManager;
this.clock = clock;
}
static Pair<String, Long> getIdentifierAndDeviceId(final String basicUsername) {
final String identifier;
final long deviceId;
final int deviceIdSeparatorIndex = basicUsername.indexOf('.');
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));
@@ -93,20 +98,35 @@ public class BaseAccountAuthenticator {
}
if (enabledRequired) {
if (!device.get().isEnabled()) {
final boolean deviceDisabled = !device.get().isEnabled();
if (deviceDisabled) {
failureReason = "deviceDisabled";
return Optional.empty();
}
if (!account.get().isEnabled()) {
final boolean accountDisabled = !account.get().isEnabled();
if (accountDisabled) {
failureReason = "accountDisabled";
}
if (accountDisabled || deviceDisabled) {
return Optional.empty();
}
} 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().isPrimary()))
.increment();
}
if (device.get().getAuthenticationCredentials().verify(basicCredentials.getPassword())) {
SaltedTokenHash deviceSaltedTokenHash = device.get().getAuthTokenHash();
if (deviceSaltedTokenHash.verify(basicCredentials.getPassword())) {
succeeded = true;
final Account authenticatedAccount = updateLastSeen(account.get(), device.get());
Account authenticatedAccount = updateLastSeen(account.get(), device.get());
if (deviceSaltedTokenHash.getVersion() != SaltedTokenHash.CURRENT_VERSION) {
authenticatedAccount = accountsManager.updateDeviceAuthentication(
authenticatedAccount,
device.get(),
SaltedTokenHash.generateFor(basicCredentials.getPassword())); // new credentials have current version
}
return Optional.of(new AuthenticatedAccount(
new RefreshingAccountAndDeviceSupplier(authenticatedAccount, device.get().getId(), accountsManager)));
}
@@ -117,8 +137,7 @@ public class BaseAccountAuthenticator {
return Optional.empty();
} finally {
Tags tags = Tags.of(
AUTHENTICATION_SUCCEEDED_TAG_NAME, String.valueOf(succeeded),
AUTHENTICATION_ENABLED_REQUIRED_TAG_NAME, String.valueOf(enabledRequired));
AUTHENTICATION_SUCCEEDED_TAG_NAME, String.valueOf(succeeded));
if (StringUtils.isNotBlank(failureReason)) {
tags = tags.and(AUTHENTICATION_FAILURE_REASON_TAG_NAME, failureReason);
@@ -130,11 +149,20 @@ public class BaseAccountAuthenticator {
@VisibleForTesting
public Account updateLastSeen(Account account, Device device) {
final long lastSeenOffsetSeconds = Math.abs(account.getUuid().getLeastSignificantBits()) % ChronoUnit.DAYS.getDuration().toSeconds();
// compute a non-negative integer between 0 and 86400.
long n = Util.ensureNonNegativeLong(account.getUuid().getLeastSignificantBits());
final long lastSeenOffsetSeconds = n % ChronoUnit.DAYS.getDuration().toSeconds();
// produce a truncated timestamp which is either today at UTC midnight
// or yesterday at UTC midnight, based on per-user randomized offset used.
final long todayInMillisWithOffset = Util.todayInMillisGivenOffsetFromNow(clock, Duration.ofSeconds(lastSeenOffsetSeconds).negated());
// only update the device's last seen time when it falls behind the truncated timestamp.
// this ensure a few things:
// (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));
@@ -142,5 +170,4 @@ public class BaseAccountAuthenticator {
return account;
}
}

View File

@@ -8,12 +8,12 @@ package org.whispersystems.textsecuregcm.auth;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import java.security.InvalidKeyException;
import java.util.Base64;
import java.util.concurrent.TimeUnit;
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;
@@ -33,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(Base64.getDecoder().decode(account.getIdentityKey())))
.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

@@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.auth;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicCredentials;
import java.util.Optional;
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
public class DisabledPermittedAccountAuthenticator extends BaseAccountAuthenticator implements

View File

@@ -1,86 +0,0 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import com.google.common.annotations.VisibleForTesting;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Clock;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Hex;
import org.whispersystems.textsecuregcm.util.Util;
public class ExternalServiceCredentialGenerator {
private final byte[] key;
private final byte[] userIdKey;
private final boolean usernameDerivation;
private final boolean prependUsername;
private final Clock clock;
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey) {
this(key, userIdKey, true, true);
}
public ExternalServiceCredentialGenerator(byte[] key, boolean prependUsername) {
this(key, new byte[0], false, prependUsername);
}
@VisibleForTesting
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation) {
this(key, userIdKey, usernameDerivation, true);
}
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation,
boolean prependUsername) {
this(key, userIdKey, usernameDerivation, prependUsername, Clock.systemUTC());
}
@VisibleForTesting
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation,
boolean prependUsername, Clock clock) {
this.key = key;
this.userIdKey = userIdKey;
this.usernameDerivation = usernameDerivation;
this.prependUsername = prependUsername;
this.clock = clock;
}
public ExternalServiceCredentials generateFor(String identity) {
Mac mac = getMacInstance();
String username = getUserId(identity, mac, usernameDerivation);
long currentTimeSeconds = clock.millis() / 1000;
String prefix = username + ":" + currentTimeSeconds;
String output = Hex.encodeHexString(Util.truncate(getHmac(key, prefix.getBytes(), mac), 10));
String token = (prependUsername ? prefix : currentTimeSeconds) + ":" + output;
return new ExternalServiceCredentials(username, token);
}
private String getUserId(String number, Mac mac, boolean usernameDerivation) {
if (usernameDerivation) return Hex.encodeHexString(Util.truncate(getHmac(userIdKey, number.getBytes(), mac), 10));
else return number;
}
private Mac getMacInstance() {
try {
return Mac.getInstance("HmacSHA256");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private byte[] getHmac(byte[] key, byte[] input, Mac mac) {
try {
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(input);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -6,28 +6,6 @@
package org.whispersystems.textsecuregcm.auth;
import com.fasterxml.jackson.annotation.JsonProperty;
public record ExternalServiceCredentials(String username, String password) {
public class ExternalServiceCredentials {
@JsonProperty
private String username;
@JsonProperty
private String password;
public ExternalServiceCredentials(String username, String password) {
this.username = username;
this.password = password;
}
public ExternalServiceCredentials() {}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}

View File

@@ -0,0 +1,293 @@
/*
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import static java.util.Objects.requireNonNull;
import static org.whispersystems.textsecuregcm.util.HmacUtils.hmac256ToHexString;
import static org.whispersystems.textsecuregcm.util.HmacUtils.hmac256TruncatedToHexString;
import static org.whispersystems.textsecuregcm.util.HmacUtils.hmacHexStringsEqual;
import com.google.common.annotations.VisibleForTesting;
import java.time.Clock;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
public class ExternalServiceCredentialsGenerator {
private static final String DELIMITER = ":";
private static final int TRUNCATED_SIGNATURE_LENGTH = 10;
private final byte[] key;
private final byte[] userDerivationKey;
private final boolean prependUsername;
private final boolean truncateSignature;
private final String usernameTimestampPrefix;
private final Function<Instant, Instant> usernameTimestampTruncator;
private final Clock clock;
private final int derivedUsernameTruncateLength;
public static ExternalServiceCredentialsGenerator.Builder builder(final SecretBytes key) {
return builder(key.value());
}
@VisibleForTesting
public static ExternalServiceCredentialsGenerator.Builder builder(final byte[] key) {
return new Builder(key);
}
private ExternalServiceCredentialsGenerator(
final byte[] key,
final byte[] userDerivationKey,
final boolean prependUsername,
final boolean truncateSignature,
final int derivedUsernameTruncateLength,
final String usernameTimestampPrefix,
final Function<Instant, Instant> usernameTimestampTruncator,
final Clock clock) {
this.key = requireNonNull(key);
this.userDerivationKey = requireNonNull(userDerivationKey);
this.prependUsername = prependUsername;
this.truncateSignature = truncateSignature;
this.usernameTimestampPrefix = usernameTimestampPrefix;
this.usernameTimestampTruncator = usernameTimestampTruncator;
this.clock = requireNonNull(clock);
this.derivedUsernameTruncateLength = derivedUsernameTruncateLength;
if (hasUsernameTimestampPrefix() ^ hasUsernameTimestampTruncator()) {
throw new RuntimeException("Configured to have only one of (usernameTimestampPrefix, usernameTimestampTruncator)");
}
}
/**
* A convenience method for the case of identity in the form of {@link UUID}.
* @param uuid identity to generate credentials for
* @return an instance of {@link ExternalServiceCredentials}
*/
public ExternalServiceCredentials generateForUuid(final UUID uuid) {
return generateFor(uuid.toString());
}
/**
* Generates `ExternalServiceCredentials` for the given identity following this generator's configuration.
* @param identity identity string to generate credentials for
* @return an instance of {@link ExternalServiceCredentials}
*/
public ExternalServiceCredentials generateFor(final String identity) {
if (usernameIsTimestamp()) {
throw new RuntimeException("Configured to use timestamp as username");
}
return generate(identity);
}
/**
* Generates `ExternalServiceCredentials` using a prefix concatenated with a truncated timestamp as the username, following this generator's configuration.
* @return an instance of {@link ExternalServiceCredentials}
*/
public ExternalServiceCredentials generateWithTimestampAsUsername() {
if (!usernameIsTimestamp()) {
throw new RuntimeException("Not configured to use timestamp as username");
}
final String truncatedTimestampSeconds = String.valueOf(usernameTimestampTruncator.apply(clock.instant()).getEpochSecond());
return generate(usernameTimestampPrefix + DELIMITER + truncatedTimestampSeconds);
}
private ExternalServiceCredentials generate(final String identity) {
final String username = shouldDeriveUsername()
? hmac256TruncatedToHexString(userDerivationKey, identity, derivedUsernameTruncateLength)
: identity;
final long currentTimeSeconds = currentTimeSeconds();
final String dataToSign = usernameIsTimestamp() ? username : username + DELIMITER + currentTimeSeconds;
final String signature = truncateSignature
? hmac256TruncatedToHexString(key, dataToSign, TRUNCATED_SIGNATURE_LENGTH)
: hmac256ToHexString(key, dataToSign);
final String token = (prependUsername ? dataToSign : currentTimeSeconds) + DELIMITER + signature;
return new ExternalServiceCredentials(username, token);
}
/**
* In certain cases, identity (as it was passed to `generate` method)
* is a part of the signature (`password`, in terms of `ExternalServiceCredentials`) string itself.
* For such cases, this method returns the value of the identity string.
* @param password `password` part of `ExternalServiceCredentials`
* @return non-empty optional with an identity string value, or empty if value can't be extracted.
*/
public Optional<String> identityFromSignature(final String password) {
// for some generators, identity in the clear is just not a part of the password
if (!prependUsername || shouldDeriveUsername() || StringUtils.isBlank(password)) {
return Optional.empty();
}
// checking for the case of unexpected format
if (StringUtils.countMatches(password, DELIMITER) == 2) {
if (usernameIsTimestamp()) {
final int indexOfSecondDelimiter = password.indexOf(DELIMITER, password.indexOf(DELIMITER) + 1);
return Optional.of(password.substring(0, indexOfSecondDelimiter));
} else {
return Optional.of(password.substring(0, password.indexOf(DELIMITER)));
}
}
return Optional.empty();
}
/**
* Given an instance of {@link ExternalServiceCredentials} object, checks that the password
* matches the username taking into accound this generator's configuration.
* @param credentials an instance of {@link ExternalServiceCredentials}
* @return An optional with a timestamp (seconds) of when the credentials were generated,
* or an empty optional if the password doesn't match the username for any reason (including malformed data)
*/
public Optional<Long> validateAndGetTimestamp(final ExternalServiceCredentials credentials) {
final String[] parts = requireNonNull(credentials).password().split(DELIMITER);
final String timestampSeconds;
final String actualSignature;
// making sure password format matches our expectations based on the generator configuration
if (parts.length == 3 && prependUsername) {
final String username = usernameIsTimestamp() ? parts[0] + DELIMITER + parts[1] : parts[0];
// username has to match the one from `credentials`
if (!credentials.username().equals(username)) {
return Optional.empty();
}
timestampSeconds = parts[1];
actualSignature = parts[2];
} else if (parts.length == 2 && !prependUsername) {
timestampSeconds = parts[0];
actualSignature = parts[1];
} else {
// unexpected password format
return Optional.empty();
}
final String signedData = usernameIsTimestamp() ? credentials.username() : credentials.username() + DELIMITER + timestampSeconds;
final String expectedSignature = truncateSignature
? hmac256TruncatedToHexString(key, signedData, TRUNCATED_SIGNATURE_LENGTH)
: hmac256ToHexString(key, signedData);
// if the signature is valid it's safe to parse the `timestampSeconds` string into Long
return hmacHexStringsEqual(expectedSignature, actualSignature)
? Optional.of(Long.valueOf(timestampSeconds))
: Optional.empty();
}
/**
* Given an instance of {@link ExternalServiceCredentials} object and the max allowed age for those credentials,
* checks if credentials are valid and not expired.
* @param credentials an instance of {@link ExternalServiceCredentials}
* @param maxAgeSeconds age in seconds
* @return An optional with a timestamp (seconds) of when the credentials were generated,
* or an empty optional if the password doesn't match the username for any reason (including malformed data)
*/
public Optional<Long> validateAndGetTimestamp(final ExternalServiceCredentials credentials, final long maxAgeSeconds) {
return validateAndGetTimestamp(credentials)
.filter(ts -> currentTimeSeconds() - ts <= maxAgeSeconds);
}
private boolean shouldDeriveUsername() {
return userDerivationKey.length > 0;
}
private boolean hasUsernameTimestampPrefix() {
return usernameTimestampPrefix != null;
}
private boolean hasUsernameTimestampTruncator() {
return usernameTimestampTruncator != null;
}
private boolean usernameIsTimestamp() {
return hasUsernameTimestampPrefix() && hasUsernameTimestampTruncator();
}
private long currentTimeSeconds() {
return clock.instant().getEpochSecond();
}
public static class Builder {
private final byte[] key;
private byte[] userDerivationKey = new byte[0];
private boolean prependUsername = true;
private boolean truncateSignature = true;
private int derivedUsernameTruncateLength = 10;
private String usernameTimestampPrefix = null;
private Function<Instant, Instant> usernameTimestampTruncator = null;
private Clock clock = Clock.systemUTC();
private Builder(final byte[] key) {
this.key = requireNonNull(key);
}
public Builder withUserDerivationKey(final SecretBytes userDerivationKey) {
return withUserDerivationKey(userDerivationKey.value());
}
public Builder withUserDerivationKey(final byte[] userDerivationKey) {
Validate.isTrue(requireNonNull(userDerivationKey).length > 0, "userDerivationKey must not be empty");
this.userDerivationKey = userDerivationKey;
return this;
}
public Builder withClock(final Clock clock) {
this.clock = requireNonNull(clock);
return this;
}
public Builder withDerivedUsernameTruncateLength(int truncateLength) {
Validate.inclusiveBetween(10, 32, truncateLength);
this.derivedUsernameTruncateLength = truncateLength;
return this;
}
public Builder prependUsername(final boolean prependUsername) {
this.prependUsername = prependUsername;
return this;
}
public Builder truncateSignature(final boolean truncateSignature) {
this.truncateSignature = truncateSignature;
return this;
}
public Builder withUsernameTimestampTruncatorAndPrefix(final Function<Instant, Instant> truncator, final String prefix) {
this.usernameTimestampTruncator = truncator;
this.usernameTimestampPrefix = prefix;
return this;
}
public ExternalServiceCredentialsGenerator build() {
return new ExternalServiceCredentialsGenerator(
key, userDerivationKey, prependUsername, truncateSignature, derivedUsernameTruncateLength, usernameTimestampPrefix, usernameTimestampTruncator, clock);
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class ExternalServiceCredentialsSelector {
private ExternalServiceCredentialsSelector() {}
public record CredentialInfo(String token, boolean valid, ExternalServiceCredentials credentials, long timestamp) {
/**
* @return a copy of this record with valid=false
*/
private CredentialInfo invalidate() {
return new CredentialInfo(token, false, credentials, timestamp);
}
}
/**
* Validate a list of username:password credentials.
* A credential is valid if it passes validation by the provided credentialsGenerator AND it is the most recent
* credential in the provided list for a username.
*
* @param tokens A list of credentials, potentially with different usernames
* @param credentialsGenerator To validate these credentials
* @param maxAgeSeconds The maximum allowable age of the credential
* @return A {@link CredentialInfo} for each provided token
*/
public static List<CredentialInfo> check(
final List<String> tokens,
final ExternalServiceCredentialsGenerator credentialsGenerator,
final long maxAgeSeconds) {
// the credential for the username with the latest timestamp (so far)
final Map<String, CredentialInfo> bestForUsername = new HashMap<>();
final List<CredentialInfo> results = new ArrayList<>();
for (String token : tokens) {
// each token is supposed to be in a "${username}:${password}" form,
// (note that password part may also contain ':' characters)
final String[] parts = token.split(":", 2);
if (parts.length != 2) {
results.add(new CredentialInfo(token, false, null, 0L));
continue;
}
final ExternalServiceCredentials credentials = new ExternalServiceCredentials(parts[0], parts[1]);
final Optional<Long> maybeTimestamp = credentialsGenerator.validateAndGetTimestamp(credentials, maxAgeSeconds);
if (maybeTimestamp.isEmpty()) {
results.add(new CredentialInfo(token, false, null, 0L));
continue;
}
// now that we validated signature and token age, we will also find the latest of the tokens
// for each username
final long timestamp = maybeTimestamp.get();
final CredentialInfo best = bestForUsername.get(credentials.username());
if (best == null) {
bestForUsername.put(credentials.username(), new CredentialInfo(token, true, credentials, timestamp));
continue;
}
if (best.timestamp() < timestamp) {
// we found a better credential for the username
bestForUsername.put(credentials.username(), new CredentialInfo(token, true, credentials, timestamp));
// mark the previous best as an invalid credential, since we have a better credential now
results.add(best.invalidate());
} else {
// the credential we already had was more recent, this one can be marked invalid
results.add(new CredentialInfo(token, false, null, 0L));
}
}
// all invalid tokens should be in results, just add the valid ones
results.addAll(bestForUsername.values());
return results;
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import java.security.MessageDigest;
import java.time.Duration;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.ServerErrorException;
import javax.ws.rs.core.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PhoneVerificationRequest;
import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession;
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
public class PhoneVerificationTokenManager {
private static final Logger logger = LoggerFactory.getLogger(PhoneVerificationTokenManager.class);
private static final Duration REGISTRATION_RPC_TIMEOUT = Duration.ofSeconds(15);
private static final long VERIFICATION_TIMEOUT_SECONDS = REGISTRATION_RPC_TIMEOUT.plusSeconds(1).getSeconds();
private final RegistrationServiceClient registrationServiceClient;
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
public PhoneVerificationTokenManager(final RegistrationServiceClient registrationServiceClient,
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager) {
this.registrationServiceClient = registrationServiceClient;
this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
}
/**
* Checks if a {@link PhoneVerificationRequest} has a token that verifies the caller has confirmed access to the e164
* number
*
* @param number the e164 presented for verification
* @param request the request with exactly one verification token (RegistrationService sessionId or registration
* recovery password)
* @return if verification was successful, returns the verification type
* @throws BadRequestException if the number does not match the sessionIds number, or the remote service rejects
* the session ID as invalid
* @throws NotAuthorizedException if the session is not verified
* @throws ForbiddenException if the recovery password is not valid
* @throws InterruptedException if verification did not complete before a timeout
*/
public PhoneVerificationRequest.VerificationType verify(final String number, final PhoneVerificationRequest request)
throws InterruptedException {
final PhoneVerificationRequest.VerificationType verificationType = request.verificationType();
switch (verificationType) {
case SESSION -> verifyBySessionId(number, request.decodeSessionId());
case RECOVERY_PASSWORD -> verifyByRecoveryPassword(number, request.recoveryPassword());
}
return verificationType;
}
private void verifyBySessionId(final String number, final byte[] sessionId) throws InterruptedException {
try {
final RegistrationServiceSession session = registrationServiceClient
.getSession(sessionId, REGISTRATION_RPC_TIMEOUT)
.get(VERIFICATION_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.orElseThrow(() -> new NotAuthorizedException("session not verified"));
if (!MessageDigest.isEqual(number.getBytes(), session.number().getBytes())) {
throw new BadRequestException("number does not match session");
}
if (!session.verified()) {
throw new NotAuthorizedException("session not verified");
}
} catch (final ExecutionException e) {
if (e.getCause() instanceof StatusRuntimeException grpcRuntimeException) {
if (grpcRuntimeException.getStatus().getCode() == Status.Code.INVALID_ARGUMENT) {
throw new BadRequestException();
}
}
logger.error("Registration service failure", e);
throw new ServerErrorException(Response.Status.SERVICE_UNAVAILABLE);
} catch (final CancellationException | TimeoutException e) {
logger.error("Registration service failure", e);
throw new ServerErrorException(Response.Status.SERVICE_UNAVAILABLE);
}
}
private void verifyByRecoveryPassword(final String number, final byte[] recoveryPassword)
throws InterruptedException {
try {
final boolean verified = registrationRecoveryPasswordsManager.verify(number, recoveryPassword)
.get(VERIFICATION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
if (!verified) {
throw new ForbiddenException("recoveryPassword couldn't be verified");
}
} catch (final ExecutionException | TimeoutException e) {
throw new ServerErrorException(Response.Status.SERVICE_UNAVAILABLE);
}
}
}

View File

@@ -0,0 +1,178 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
import com.google.common.annotations.VisibleForTesting;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import java.time.Duration;
import java.time.Instant;
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;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
public class RegistrationLockVerificationManager {
public enum Flow {
REGISTRATION,
CHANGE_NUMBER
}
@VisibleForTesting
public static final int FAILURE_HTTP_STATUS = 423;
private static final String EXPIRED_REGISTRATION_LOCK_COUNTER_NAME =
name(RegistrationLockVerificationManager.class, "expiredRegistrationLock");
private static final String REQUIRED_REGISTRATION_LOCK_COUNTER_NAME =
name(RegistrationLockVerificationManager.class, "requiredRegistrationLock");
private static final String CHALLENGED_DEVICE_NOT_PUSH_REGISTERED_COUNTER_NAME =
name(RegistrationLockVerificationManager.class, "challengedDeviceNotPushRegistered");
private static final String ALREADY_LOCKED_TAG_NAME = "alreadyLocked";
private static final String REGISTRATION_LOCK_VERIFICATION_FLOW_TAG_NAME = "flow";
private static final String REGISTRATION_LOCK_MATCHES_TAG_NAME = "registrationLockMatches";
private static final String PHONE_VERIFICATION_TYPE_TAG_NAME = "phoneVerificationType";
private final AccountsManager accounts;
private final ClientPresenceManager clientPresenceManager;
private final ExternalServiceCredentialsGenerator svr2CredentialGenerator;
private final RateLimiters rateLimiters;
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
private final PushNotificationManager pushNotificationManager;
public RegistrationLockVerificationManager(
final AccountsManager accounts, final ClientPresenceManager clientPresenceManager,
final ExternalServiceCredentialsGenerator svr2CredentialGenerator,
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
final PushNotificationManager pushNotificationManager,
final RateLimiters rateLimiters) {
this.accounts = accounts;
this.clientPresenceManager = clientPresenceManager;
this.svr2CredentialGenerator = svr2CredentialGenerator;
this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
this.pushNotificationManager = pushNotificationManager;
this.rateLimiters = rateLimiters;
}
/**
* Verifies the given registration lock credentials against the accounts current registration lock, if any
*
* @param account
* @param clientRegistrationLock
* @throws RateLimitExceededException
* @throws WebApplicationException
*/
public void verifyRegistrationLock(final Account account, @Nullable final String clientRegistrationLock,
final String userAgent,
final Flow flow,
final PhoneVerificationRequest.VerificationType phoneVerificationType
) throws RateLimitExceededException, WebApplicationException {
final Tags expiredTags = Tags.of(UserAgentTagUtil.getPlatformTag(userAgent),
Tag.of(REGISTRATION_LOCK_VERIFICATION_FLOW_TAG_NAME, flow.name()),
Tag.of(PHONE_VERIFICATION_TYPE_TAG_NAME, phoneVerificationType.name())
);
final StoredRegistrationLock existingRegistrationLock = account.getRegistrationLock();
switch (existingRegistrationLock.getStatus()) {
case EXPIRED:
Metrics.counter(EXPIRED_REGISTRATION_LOCK_COUNTER_NAME, expiredTags).increment();
return;
case ABSENT:
return;
case REQUIRED:
break;
default:
throw new RuntimeException("Unexpected status: " + existingRegistrationLock.getStatus());
}
if (StringUtils.isNotEmpty(clientRegistrationLock)) {
rateLimiters.getPinLimiter().validate(account.getNumber());
}
final String phoneNumber = account.getNumber();
final boolean registrationLockMatches = existingRegistrationLock.verify(clientRegistrationLock);
final boolean alreadyLocked = account.hasLockedCredentials();
final Tags additionalTags = expiredTags.and(
REGISTRATION_LOCK_MATCHES_TAG_NAME, Boolean.toString(registrationLockMatches),
ALREADY_LOCKED_TAG_NAME, Boolean.toString(alreadyLocked)
);
Metrics.counter(REQUIRED_REGISTRATION_LOCK_COUNTER_NAME, additionalTags).increment();
final DistributionSummary registrationLockIdleDays = DistributionSummary
.builder(name(RegistrationLockVerificationManager.class, "registrationLockIdleDays"))
.tags(additionalTags)
.publishPercentiles(0.75, 0.95, 0.99, 0.999)
.distributionStatisticExpiry(Duration.ofHours(2))
.register(Metrics.globalRegistry);
final Instant accountLastSeen = Instant.ofEpochMilli(account.getLastSeen());
final Duration timeSinceLastSeen = Duration.between(accountLastSeen, Instant.now());
registrationLockIdleDays.record(timeSinceLastSeen.toDays());
if (!registrationLockMatches) {
// At this point, the client verified ownership of the phone number but doesnt have the reglock PIN.
// 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 existingSvr2Credentials = svr2CredentialGenerator.generateForUuid(account.getUuid());
final Account updatedAccount;
if (!alreadyLocked) {
updatedAccount = accounts.update(account, Account::lockAuthTokenHash);
} else {
updatedAccount = account;
}
// The client often sends an empty registration lock token on the first request
// and sends an actual token if the server returns a 423 indicating that one is required.
// This logic accounts for that behavior by not deleting the registration recovery password
// if the user verified correctly via registration recovery password and sent an empty token.
// This allows users to re-register via registration recovery password
// instead of always being forced to fall back to SMS verification.
if (!phoneVerificationType.equals(PhoneVerificationRequest.VerificationType.RECOVERY_PASSWORD) || clientRegistrationLock != null) {
registrationRecoveryPasswordsManager.removeForNumber(updatedAccount.getNumber());
}
final List<Long> deviceIds = updatedAccount.getDevices().stream().map(Device::getId).toList();
clientPresenceManager.disconnectAllPresences(updatedAccount.getUuid(), deviceIds);
try {
// Send a push notification that prompts the client to attempt login and fail due to locked credentials
pushNotificationManager.sendAttemptLoginNotification(updatedAccount, "failedRegistrationLock");
} catch (final NotPushRegisteredException e) {
Metrics.counter(CHALLENGED_DEVICE_NOT_PUSH_REGISTERED_COUNTER_NAME).increment();
}
throw new WebApplicationException(Response.status(FAILURE_HTTP_STATUS)
.entity(new RegistrationLockFailure(existingRegistrationLock.getTimeRemaining().toMillis(),
existingRegistrationLock.needsFailureCredentials() ? existingSvr2Credentials : null))
.build());
}
rateLimiters.getPinLimiter().clear(phoneNumber);
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.HexFormat;
import org.signal.libsignal.protocol.kdf.HKDF;
public record SaltedTokenHash(String hash, String salt) {
public enum Version {
V1,
V2,
}
public static final Version CURRENT_VERSION = Version.V2;
private static final String V2_PREFIX = "2.";
private static final byte[] AUTH_TOKEN_HKDF_INFO = "authtoken".getBytes(StandardCharsets.UTF_8);
private static final int SALT_SIZE = 16;
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
public static SaltedTokenHash generateFor(final String token) {
final String salt = generateSalt();
final String hash = calculateV2Hash(salt, token);
return new SaltedTokenHash(hash, salt);
}
public Version getVersion() {
return hash.startsWith(V2_PREFIX) ? Version.V2 : Version.V1;
}
public boolean verify(final String token) {
final String theirValue = switch (getVersion()) {
case V1 -> calculateV1Hash(salt, token);
case V2 -> calculateV2Hash(salt, token);
};
return MessageDigest.isEqual(
theirValue.getBytes(StandardCharsets.UTF_8),
hash.getBytes(StandardCharsets.UTF_8));
}
private static String generateSalt() {
final byte[] salt = new byte[SALT_SIZE];
SECURE_RANDOM.nextBytes(salt);
return HexFormat.of().formatHex(salt);
}
private static String calculateV1Hash(final String salt, final String token) {
try {
return HexFormat.of()
.formatHex(MessageDigest.getInstance("SHA1").digest((salt + token).getBytes(StandardCharsets.UTF_8)));
} catch (final NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private static String calculateV2Hash(final String salt, final String token) {
final byte[] secret = HKDF.deriveSecrets(
token.getBytes(StandardCharsets.UTF_8), // key
salt.getBytes(StandardCharsets.UTF_8), // salt
AUTH_TOKEN_HKDF_INFO,
32);
return V2_PREFIX + HexFormat.of().formatHex(secret);
}
}

View File

@@ -1,51 +1,81 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import com.google.common.annotations.VisibleForTesting;
import org.whispersystems.textsecuregcm.util.Util;
import javax.annotation.Nullable;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class StoredRegistrationLock {
public enum Status {
REQUIRED,
EXPIRED,
ABSENT
}
@VisibleForTesting
static final Duration REGISTRATION_LOCK_EXPIRATION_DAYS = Duration.ofDays(7);
private final Optional<String> registrationLock;
private final Optional<String> registrationLockSalt;
private final long lastSeen;
private final Instant lastSeen;
public StoredRegistrationLock(Optional<String> registrationLock, Optional<String> registrationLockSalt, long lastSeen) {
/**
* @return milliseconds since the last time the account was seen.
*/
private long timeSinceLastSeen() {
return System.currentTimeMillis() - lastSeen.toEpochMilli();
}
/**
* @return true if the registration lock and salt are both set.
*/
private boolean hasLockAndSalt() {
return registrationLock.isPresent() && registrationLockSalt.isPresent();
}
public boolean isPresent() {
return hasLockAndSalt();
}
public StoredRegistrationLock(Optional<String> registrationLock, Optional<String> registrationLockSalt, Instant lastSeen) {
this.registrationLock = registrationLock;
this.registrationLockSalt = registrationLockSalt;
this.lastSeen = lastSeen;
}
public boolean requiresClientRegistrationLock() {
return registrationLock.isPresent() && registrationLockSalt.isPresent() && System.currentTimeMillis() - lastSeen < TimeUnit.DAYS.toMillis(7);
public Status getStatus() {
if (!isPresent()) {
return Status.ABSENT;
}
if (getTimeRemaining().toMillis() > 0) {
return Status.REQUIRED;
}
return Status.EXPIRED;
}
public boolean needsFailureCredentials() {
return registrationLock.isPresent() && registrationLockSalt.isPresent();
return hasLockAndSalt();
}
public long getTimeRemaining() {
return TimeUnit.DAYS.toMillis(7) - (System.currentTimeMillis() - lastSeen);
public Duration getTimeRemaining() {
return REGISTRATION_LOCK_EXPIRATION_DAYS.minus(timeSinceLastSeen(), ChronoUnit.MILLIS);
}
public boolean verify(@Nullable String clientRegistrationLock) {
if (Util.isEmpty(clientRegistrationLock)) {
return false;
}
if (registrationLock.isPresent() && registrationLockSalt.isPresent() && !Util.isEmpty(clientRegistrationLock)) {
return new AuthenticationCredentials(registrationLock.get(), registrationLockSalt.get()).verify(clientRegistrationLock);
if (hasLockAndSalt() && StringUtils.isNotEmpty(clientRegistrationLock)) {
SaltedTokenHash credentials = new SaltedTokenHash(registrationLock.get(), registrationLockSalt.get());
return credentials.verify(clientRegistrationLock);
} else {
return false;
}
@@ -53,6 +83,6 @@ public class StoredRegistrationLock {
@VisibleForTesting
public StoredRegistrationLock forTime(long timestamp) {
return new StoredRegistrationLock(registrationLock, registrationLockSalt, timestamp);
return new StoredRegistrationLock(registrationLock, registrationLockSalt, Instant.ofEpochMilli(timestamp));
}
}

View File

@@ -1,72 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.security.MessageDigest;
import java.time.Duration;
import java.util.Optional;
import javax.annotation.Nullable;
import org.whispersystems.textsecuregcm.util.Util;
public class StoredVerificationCode {
@JsonProperty
private final String code;
@JsonProperty
private final long timestamp;
@JsonProperty
private final String pushCode;
@JsonProperty
@Nullable
private final String twilioVerificationSid;
public static final Duration EXPIRATION = Duration.ofMinutes(10);
@JsonCreator
public StoredVerificationCode(
@JsonProperty("code") final String code,
@JsonProperty("timestamp") final long timestamp,
@JsonProperty("pushCode") final String pushCode,
@JsonProperty("twilioVerificationSid") @Nullable final String twilioVerificationSid) {
this.code = code;
this.timestamp = timestamp;
this.pushCode = pushCode;
this.twilioVerificationSid = twilioVerificationSid;
}
public String getCode() {
return code;
}
public long getTimestamp() {
return timestamp;
}
public String getPushCode() {
return pushCode;
}
public Optional<String> getTwilioVerificationSid() {
return Optional.ofNullable(twilioVerificationSid);
}
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

@@ -10,6 +10,7 @@ import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfigurati
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicTurnConfiguration;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.WeightedRandomSelect;
import javax.crypto.Mac;
@@ -17,44 +18,53 @@ import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.UUID;
public class TurnTokenGenerator {
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfiguration;
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
public TurnTokenGenerator(final DynamicConfigurationManager<DynamicConfiguration> config) {
this.dynamicConfiguration = config;
private final byte[] turnSecret;
private static final String ALGORITHM = "HmacSHA1";
public TurnTokenGenerator(final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
final byte[] turnSecret) {
this.dynamicConfigurationManager = dynamicConfigurationManager;
this.turnSecret = turnSecret;
}
public TurnToken generate(final String e164) {
public TurnToken generate(final UUID aci) {
try {
byte[] key = dynamicConfiguration.getConfiguration().getTurnConfiguration().getSecret().getBytes();
List<String> urls = urls(e164);
Mac mac = Mac.getInstance("HmacSHA1");
long validUntilSeconds = (System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1)) / 1000;
long user = Math.abs(new SecureRandom().nextInt());
String userTime = validUntilSeconds + ":" + user;
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());
final String userTime = validUntilSeconds + ":" + user;
mac.init(new SecretKeySpec(key, "HmacSHA1"));
String password = Base64.getEncoder().encodeToString(mac.doFinal(userTime.getBytes()));
mac.init(new SecretKeySpec(turnSecret, ALGORITHM));
final String password = Base64.getEncoder().encodeToString(mac.doFinal(userTime.getBytes()));
return new TurnToken(userTime, password, urls);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
} catch (final NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
private List<String> urls(final String e164) {
final DynamicTurnConfiguration turnConfig = dynamicConfiguration.getConfiguration().getTurnConfiguration();
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()) {
return enrolled.get().getUris();
}
@@ -63,6 +73,6 @@ public class TurnTokenGenerator {
return WeightedRandomSelect.select(turnConfig
.getUriConfigs()
.stream()
.map(c -> new Pair<List<String>, Long>(c.getUris(), c.getWeight())).toList());
.map(c -> new Pair<>(c.getUris(), c.getWeight())).toList());
}
}

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

@@ -0,0 +1,34 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import org.whispersystems.textsecuregcm.storage.Account;
import java.security.MessageDigest;
public class UnidentifiedAccessUtil {
public static final int UNIDENTIFIED_ACCESS_KEY_LENGTH = 16;
private UnidentifiedAccessUtil() {
}
/**
* Checks whether an action (e.g. sending a message or retrieving pre-keys) may be taken on the target account by an
* actor presenting the given unidentified access key.
*
* @param targetAccount the account on which an actor wishes to take an action
* @param unidentifiedAccessKey the unidentified access key presented by the actor
*
* @return {@code true} if an actor presenting the given unidentified access key has permission to take an action on
* the target account or {@code false} otherwise
*/
public static boolean checkUnidentifiedAccess(final Account targetAccount, final byte[] unidentifiedAccessKey) {
return targetAccount.isUnrestrictedUnidentifiedAccess()
|| targetAccount.getUnidentifiedAccessKey()
.map(targetUnidentifiedAccessKey -> MessageDigest.isEqual(targetUnidentifiedAccessKey, unidentifiedAccessKey))
.orElse(false);
}
}

View File

@@ -0,0 +1,11 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth.grpc;
import java.util.UUID;
public record AuthenticatedDevice(UUID accountIdentifier, long deviceId) {
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth.grpc;
import io.grpc.Context;
import io.grpc.Status;
import java.util.UUID;
import javax.annotation.Nullable;
import org.whispersystems.textsecuregcm.storage.Device;
/**
* Provides utility methods for working with authentication in the context of gRPC calls.
*/
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");
/**
* 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 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();
if (accountIdentifier != null && deviceId != null) {
return new AuthenticatedDevice(accountIdentifier, deviceId);
}
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

@@ -0,0 +1,85 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth.grpc;
import com.google.common.annotations.VisibleForTesting;
import io.dropwizard.auth.basic.BasicCredentials;
import io.grpc.Context;
import io.grpc.Contexts;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.grpc.Status;
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.
* Callers supply credentials by providing a username (UUID and optional device ID) and password pair in the
* {@code x-signal-basic-auth-credentials} call header.
* <p/>
* Downstream services can retrieve the identity of the authenticated caller using methods in
* {@link AuthenticationUtil}.
* <p/>
* Note that this authentication, while fully functional, is intended only for development and testing purposes and is
* intended to be replaced with a more robust and efficient strategy before widespread client adoption.
*
* @see AuthenticationUtil
* @see BaseAccountAuthenticator
*/
public class BasicCredentialAuthenticationInterceptor implements ServerInterceptor {
private final BaseAccountAuthenticator baseAccountAuthenticator;
@VisibleForTesting
static final Metadata.Key<String> BASIC_CREDENTIALS =
Metadata.Key.of("x-signal-auth", Metadata.ASCII_STRING_MARSHALLER);
private static final Metadata EMPTY_TRAILERS = new Metadata();
public BasicCredentialAuthenticationInterceptor(final BaseAccountAuthenticator baseAccountAuthenticator) {
this.baseAccountAuthenticator = baseAccountAuthenticator;
}
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
final ServerCall<ReqT, RespT> call,
final Metadata headers,
final ServerCallHandler<ReqT, RespT> next) {
final String authHeader = headers.get(BASIC_CREDENTIALS);
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(maybeCredentials.get(), false);
if (maybeAuthenticatedAccount.isPresent()) {
final AuthenticatedAccount authenticatedAccount = maybeAuthenticatedAccount.get();
final Context context = Context.current()
.withValue(AuthenticationUtil.CONTEXT_AUTHENTICATED_ACCOUNT_IDENTIFIER_KEY, authenticatedAccount.getAccount().getUuid())
.withValue(AuthenticationUtil.CONTEXT_AUTHENTICATED_DEVICE_IDENTIFIER_KEY, authenticatedAccount.getAuthenticatedDevice().getId());
return Contexts.interceptCall(context, call, headers, next);
} else {
call.close(Status.UNAUTHENTICATED.withDescription("Credentials not accepted"), EMPTY_TRAILERS);
}
}
} else {
call.close(Status.UNAUTHENTICATED.withDescription("No credentials provided"), EMPTY_TRAILERS);
}
return new ServerCall.Listener<>() {};
}
}

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,391 @@
/*
* 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.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Clock;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
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.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.ConditionalCheckFailedException;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
public class BackupManager {
private static final Logger logger = LoggerFactory.getLogger(BackupManager.class);
static final String MESSAGE_BACKUP_NAME = "messageBackup";
private static final int BACKUP_CDN = 3;
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 GenericServerSecretParams serverSecretParams;
private final TusBackupCredentialGenerator tusBackupCredentialGenerator;
private final DynamoDbAsyncClient dynamoClient;
private final String backupTableName;
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";
public BackupManager(
final GenericServerSecretParams serverSecretParams,
final TusBackupCredentialGenerator tusBackupCredentialGenerator,
final DynamoDbAsyncClient dynamoClient,
final String backupTableName,
final Clock clock) {
this.serverSecretParams = serverSecretParams;
this.dynamoClient = dynamoClient;
this.tusBackupCredentialGenerator = tusBackupCredentialGenerator;
this.backupTableName = backupTableName;
this.clock = clock;
}
/**
* 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();
}
final byte[] hashedBackupId = hashedBackupId(presentation.getBackupId());
return dynamoClient.updateItem(UpdateItemRequest.builder()
.tableName(backupTableName)
.key(Map.of(KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId)))
.updateExpression("SET #publicKey = :publicKey")
.expressionAttributeNames(Map.of("#publicKey", ATTR_PUBLIC_KEY))
.expressionAttributeValues(Map.of(":publicKey", AttributeValues.b(publicKey.serialize())))
.conditionExpression("attribute_not_exists(#publicKey) OR #publicKey = :publicKey")
.build())
.exceptionally(throwable -> {
// There was already a row for this backup-id and it contained a different publicKey
if (ExceptionUtils.unwrap(throwable) instanceof ConditionalCheckFailedException) {
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();
}
throw ExceptionUtils.wrap(throwable);
})
.thenRun(Util.NOOP);
}
/**
* 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 byte[] hashedBackupId = hashedBackupId(backupUser);
final String encodedBackupId = encodeForCdn(hashedBackupId);
final long refreshTimeSecs = clock.instant().getEpochSecond();
final List<String> updates = new ArrayList<>(List.of("#cdn = :cdn", "#lastRefresh = :expiration"));
final Map<String, String> expressionAttributeNames = new HashMap<>(Map.of(
"#cdn", ATTR_CDN,
"#lastRefresh", ATTR_LAST_REFRESH));
if (backupUser.backupTier().compareTo(BackupTier.MEDIA) >= 0) {
updates.add("#lastMediaRefresh = :expiration");
expressionAttributeNames.put("#lastMediaRefresh", ATTR_LAST_MEDIA_REFRESH);
}
// this could race with concurrent updates, but the only effect would be last-writer-wins on the timestamp
return dynamoClient.updateItem(UpdateItemRequest.builder()
.tableName(backupTableName)
.key(Map.of(KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId)))
.updateExpression("SET %s".formatted(String.join(",", updates)))
.expressionAttributeNames(expressionAttributeNames)
.expressionAttributeValues(Map.of(
":cdn", AttributeValues.n(BACKUP_CDN),
":expiration", AttributeValues.n(refreshTimeSecs)))
.build())
.thenApply(result -> tusBackupCredentialGenerator.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();
}
final long refreshTimeSecs = clock.instant().getEpochSecond();
// update message backup TTL
final List<String> updates = new ArrayList<>(Collections.singletonList("#lastRefresh = :expiration"));
final Map<String, String> expressionAttributeNames = new HashMap<>(Map.of("#lastRefresh", ATTR_LAST_REFRESH));
if (backupUser.backupTier().compareTo(BackupTier.MEDIA) >= 0) {
// update media TTL
expressionAttributeNames.put("#lastMediaRefresh", ATTR_LAST_MEDIA_REFRESH);
updates.add("#lastMediaRefresh = :expiration");
}
return dynamoClient.updateItem(UpdateItemRequest.builder()
.tableName(backupTableName)
.key(Map.of(KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId(backupUser))))
.updateExpression("SET %s".formatted(String.join(",", updates)))
.expressionAttributeNames(expressionAttributeNames)
.expressionAttributeValues(Map.of(":expiration", AttributeValues.n(refreshTimeSecs)))
.build())
.thenRun(Util.NOOP);
}
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 backupInfoHelper(backupUser);
}
private CompletableFuture<BackupInfo> backupInfoHelper(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))
.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 BackupInfo(cdn, encodeForCdn(hashedBackupId(backupUser)), MESSAGE_BACKUP_NAME, mediaUsed);
});
}
/**
* 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 = encodeForCdn(hashedBackupId(backupUser));
return tusBackupCredentialGenerator.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) {
final byte[] hashedBackupId = hashedBackupId(presentation.getBackupId());
return dynamoClient.getItem(GetItemRequest.builder()
.tableName(backupTableName)
.key(Map.of(KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId)))
.projectionExpression("#publicKey")
.expressionAttributeNames(Map.of("#publicKey", ATTR_PUBLIC_KEY))
.build())
.thenApply(response -> {
if (!response.hasItem()) {
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
SUCCESS_TAG_NAME, String.valueOf(false),
FAILURE_REASON_TAG_NAME, "missing_public_key")
.increment();
throw Status.NOT_FOUND.withDescription("Backup not found").asRuntimeException();
}
final byte[] publicKeyBytes = AttributeValues.get(response.item(), ATTR_PUBLIC_KEY)
.map(AttributeValue::b)
.map(SdkBytes::asByteArray)
.orElseThrow(() -> Status.INTERNAL
.withDescription("Stored backup missing public key")
.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(hashedBackupId), 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 byte[] hashedBackupId(final AuthenticatedBackupUser backupId) {
return hashedBackupId(backupId.backupId());
}
private static byte[] hashedBackupId(final byte[] backupId) {
try {
return Arrays.copyOf(MessageDigest.getInstance("SHA-256").digest(backupId), 16);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private static String encodeForCdn(final byte[] bytes) {
return Base64.getUrlEncoder().encodeToString(bytes);
}
}

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,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,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 TusBackupCredentialGenerator {
private static final int BACKUP_CDN = 3;
private static String READ_PERMISSION = "read";
private static String WRITE_PERMISSION = "write";
private static String CDN_PATH = "backups";
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 TusBackupCredentialGenerator(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,43 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.captcha;
import com.fasterxml.jackson.annotation.JsonCreator;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
public enum Action {
CHALLENGE("challenge"),
REGISTRATION("registration");
private final String actionName;
Action(String actionName) {
this.actionName = actionName;
}
public String getActionName() {
return actionName;
}
private static final Map<String, Action> ENUM_MAP = Arrays
.stream(Action.values())
.collect(Collectors.toMap(
a -> a.actionName,
Function.identity()));
@JsonCreator
public static Action fromString(String key) {
return ENUM_MAP.get(key.toLowerCase(Locale.ROOT).strip());
}
static Optional<Action> parse(final String action) {
return Optional.ofNullable(fromString(action));
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2022 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.captcha;
import java.util.Objects;
import java.util.Optional;
public class AssessmentResult {
private final boolean solved;
private final float actualScore;
private final float defaultScoreThreshold;
private final String scoreString;
/**
* A captcha assessment
*
* @param solved if false, the captcha was not successfully completed
* @param actualScore float representation of the risk level from [0, 1.0], with 1.0 being the least risky
* @param defaultScoreThreshold the score threshold which the score will be evaluated against by default
* @param scoreString a quantized string representation of the risk level, suitable for use in metrics
*/
private AssessmentResult(boolean solved, float actualScore, float defaultScoreThreshold, final String scoreString) {
this.solved = solved;
this.actualScore = actualScore;
this.defaultScoreThreshold = defaultScoreThreshold;
this.scoreString = scoreString;
}
/**
* Construct an {@link AssessmentResult} from a captcha evaluation score
*
* @param actualScore the score
* @param defaultScoreThreshold the threshold to compare the score against by default
*/
public static AssessmentResult fromScore(float actualScore, float defaultScoreThreshold) {
if (actualScore < 0 || actualScore > 1.0 || defaultScoreThreshold < 0 || defaultScoreThreshold > 1.0) {
throw new IllegalArgumentException("invalid captcha score");
}
return new AssessmentResult(true, actualScore, defaultScoreThreshold, AssessmentResult.scoreString(actualScore));
}
/**
* Construct a captcha assessment that will always be invalid
*/
public static AssessmentResult invalid() {
return new AssessmentResult(false, 0.0f, 0.0f, "");
}
/**
* Construct a captcha assessment that will always be valid
*/
public static AssessmentResult alwaysValid() {
return new AssessmentResult(true, 1.0f, 0.0f, "1.0");
}
/**
* Check if the captcha assessment should be accepted using the default score threshold
*
* @return true if this assessment should be accepted under the default score threshold
*/
public boolean isValid() {
return isValid(Optional.empty());
}
/**
* Check if the captcha assessment should be accepted
*
* @param scoreThreshold the minimum score the assessment requires to pass, uses default if empty
* @return true if the assessment scored higher than the provided scoreThreshold
*/
public boolean isValid(Optional<Float> scoreThreshold) {
if (!solved) {
return false;
}
return this.actualScore >= scoreThreshold.orElse(this.defaultScoreThreshold);
}
public String getScoreString() {
return scoreString;
}
public float getScore() {
return this.actualScore;
}
/**
* Map a captcha score in [0.0, 1.0] to a low cardinality discrete space in [0, 100] suitable for use in metrics
*/
private static String scoreString(final float score) {
final int x = Math.round(score * 10); // [0, 10]
return Integer.toString(x * 10); // [0, 100] in increments of 10
}
@Override
public boolean equals(final Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
AssessmentResult that = (AssessmentResult) o;
return solved == that.solved && Float.compare(that.actualScore, actualScore) == 0
&& Float.compare(that.defaultScoreThreshold, defaultScoreThreshold) == 0 && Objects.equals(scoreString,
that.scoreString);
}
@Override
public int hashCode() {
return Objects.hash(solved, actualScore, defaultScoreThreshold, scoreString);
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2022 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
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.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.ws.rs.BadRequestException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CaptchaChecker {
private static final Logger logger = LoggerFactory.getLogger(CaptchaChecker.class);
private static final String INVALID_SITEKEY_COUNTER_NAME = name(CaptchaChecker.class, "invalidSiteKey");
private static final String ASSESSMENTS_COUNTER_NAME = name(RecaptchaClient.class, "assessments");
private static final String INVALID_ACTION_COUNTER_NAME = name(CaptchaChecker.class, "invalidActions");
@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 ShortCodeExpander shortCodeRetriever,
final List<CaptchaClient> captchaClients) {
this.shortCodeExpander = shortCodeRetriever;
this.captchaClientMap = captchaClients.stream()
.collect(Collectors.toMap(CaptchaClient::scheme, Function.identity()));
}
/**
* Check if a solved captcha should be accepted
*
* @param expectedAction the {@link Action} for which this captcha solution is intended
* @param input expected to contain a prefix indicating the captcha scheme, sitekey, token, and action. The
* expected format is {@code version-prefix.sitekey.action.token}
* @param ip IP of the solver
* @return An {@link AssessmentResult} indicating whether the solution should be accepted, and a score that can be
* used for metrics
* @throws IOException if there is an error validating the captcha with the underlying service
* @throws BadRequestException if input is not in the expected format
*/
public AssessmentResult verify(
final Action expectedAction,
final String input,
final String ip) throws IOException {
final String[] parts = input.split("\\" + SEPARATOR, 4);
// we allow missing actions, if we're missing 1 part, assume it's the action
if (parts.length < 4) {
throw new BadRequestException("too few parts");
}
final String prefix = parts[0];
final String siteKey = parts[1].toLowerCase(Locale.ROOT).strip();
final String action = parts[2];
String token = parts[3];
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");
}
final Action parsedAction = Action.parse(action)
.orElseThrow(() -> {
Metrics.counter(INVALID_ACTION_COUNTER_NAME, "action", action).increment();
throw new BadRequestException("invalid captcha action");
});
if (!parsedAction.equals(expectedAction)) {
Metrics.counter(INVALID_ACTION_COUNTER_NAME, "action", action).increment();
throw new BadRequestException("invalid captcha action");
}
final Set<String> allowedSiteKeys = client.validSiteKeys(parsedAction);
if (!allowedSiteKeys.contains(siteKey)) {
logger.debug("invalid site-key {}, action={}, token={}", siteKey, action, token);
Metrics.counter(INVALID_SITEKEY_COUNTER_NAME, "action", action).increment();
throw new BadRequestException("invalid captcha site-key");
}
final AssessmentResult result = client.verify(siteKey, parsedAction, token, ip);
Metrics.counter(ASSESSMENTS_COUNTER_NAME,
"action", action,
"score", result.getScoreString(),
"provider", provider)
.increment();
return result;
}
}

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