Compare commits

...
74 Commits
Author SHA1 Message Date
Ehren Kret bf1190696e Add badge workaround for old Android builds
Service CI / build (push) Canceled after 0s
2021-11-23 09:58:06 -06:00
Jon Chambers 71dd0890de Restore an accidentally-removed PNI consistency check 2021-11-23 10:53:40 -05:00
Jon Chambers e5acdf1402 Don't update the PNI attribute during general account updates 2021-11-23 10:53:40 -05:00
Jon Chambers 0f08b6bb59 Drop "got successful captcha" messages from INFO to DEBUG 2021-11-22 17:06:34 -05:00
Jon Chambers 6198a7b69a Remove spurious @JsonProperty annotations
Service CI / build (push) Canceled after 0s
2021-11-22 15:43:09 -05:00
Jon Chambers 067aee6664 Remove unused properties from OutgoingMessageEntity 2021-11-22 15:43:09 -05:00
Jon Chambers 138a2ebbd0 Drop transactional logic from phone number identifier migration 2021-11-22 15:32:24 -05:00
Jon Chambers 296f6a7a88 Make phone number identifiers non-optional 2021-11-22 15:32:24 -05:00
Jon Chambers 069ffa9921 Drop PNI migration tools 2021-11-22 15:32:24 -05:00
Jon Chambers f42fd8a840 Retire unused diagnostic metrics 2021-11-22 15:31:16 -05:00
Jon Chambers 10f27af6f2 Retire old unsealed-sender meters 2021-11-22 15:31:16 -05:00
Jon Chambers 0bbd34d060 Use text blocks where possible 2021-11-22 15:30:31 -05:00
Jon Chambers 282daeb0dc Add a command to assign PNIs to accounts that don't already have one
Service CI / build (push) Canceled after 0s
2021-11-22 15:03:19 -05:00
Jon Chambers d33b313c11 Break down legacy "get profile" requests by platform
Service CI / build (push) Canceled after 0s
2021-11-19 12:37:39 -05:00
Ehren Kret fb7316c9ae Return subscription status string in GET 2021-11-19 11:36:01 -06:00
Ehren Kret 279b0a51d9 Use latest invoice on subscription to generate receipts 2021-11-19 11:25:38 -06:00
Ehren Kret 6547d5ebf3 More consistent naming of receipt credentials endpoints 2021-11-19 10:14:00 -06:00
Ehren Kret 4f1ef9a039 Add additional http status codes to /v1/subscription/boost/receipt_credentials 2021-11-19 10:11:33 -06:00
Ehren Kret 4c80714d19 Update sample.yml
Service CI / build (push) Canceled after 0s
2021-11-18 10:49:30 -06:00
Jon Chambers 077ead71a5 Rename legacy profile methods to separate them in metrics
Service CI / build (push) Canceled after 0s
2021-11-18 11:31:15 -05:00
Ehren Kret caba110266 Revert "Revert "Remove transparent SVG for badging""
This reverts commit 0fdb23c1e9.
2021-11-18 10:23:09 -06:00
Ehren Kret 0fdb23c1e9 Revert "Remove transparent SVG for badging"
This reverts commit 13a84f0c72.
2021-11-18 10:09:34 -06:00
Ehren Kret 13a84f0c72 Remove transparent SVG for badging 2021-11-18 10:04:42 -06:00
Jon Chambers 669bd58e33 Drop the unused Key utility class 2021-11-17 10:57:29 -05:00
Chris Eager 6e82740a9b Update sample.yml
Service CI / build (push) Canceled after 0s
2021-11-16 17:25:45 -07:00
Ehren Kret 7ea43a728d Set boost description from configuration 2021-11-16 17:21:57 -06:00
Chris Eager 71b38356b1 Update to Mockito 4.0.0 2021-11-16 15:56:35 -07:00
Chris Eager 5a99708f56 Update some deprecated usages 2021-11-16 15:56:13 -07:00
Chris Eager 24191d9599 Update Dropwizard to 2.0.25 2021-11-16 15:56:00 -07:00
Chris Eager 482ea8eb40 Update minimum required maven to 3.8.3 2021-11-16 15:55:52 -07:00
Jon Chambers 1dae05651f Add PNIs to account creation and whoami responses
Service CI / build (push) Canceled after 0s
2021-11-16 15:08:10 -05:00
Jon Chambers 5164e92538 Shorten metric names 2021-11-16 15:08:10 -05:00
Jon Chambers f89a20dbc7 Allow callers to set/retrieve keys by ACI or PNI 2021-11-16 15:08:10 -05:00
Jon Chambers 3a4c5a2bfb Store and retrieve one-time pre-keys by UUID 2021-11-16 15:08:10 -05:00
Jon Chambers 5e1334e8de s/KeysDynamoDb/Keys/ 2021-11-16 15:08:10 -05:00
Jon Chambers fa6e3d3690 Allow clients to request PNI-based group credentials 2021-11-16 15:08:10 -05:00
Jon Chambers 9383e7716b Resolve CertificateControllerTest warnings and recommendations 2021-11-16 15:08:10 -05:00
Jon Chambers cfe34fbf0f Allow unsealed-sender messages to be addressed by PNI 2021-11-16 15:08:10 -05:00
Jon Chambers 9fe110625c Add a destinationUuid field to envelopes 2021-11-16 15:08:10 -05:00
Jon Chambers 975f753c2b Add an endpoint for testing whether an account with a given ACI or PNI exists 2021-11-16 15:08:10 -05:00
Jon Chambers e6237480f8 Require that unidentified access keys be exactly 16 bytes 2021-11-16 15:08:10 -05:00
Chris Eager 966d4e29d4 Update sample.yml config to pass mvn verify 2021-11-16 11:43:07 -07:00
Chris Eager 26f876a2cb Check service configurations in verify phase 2021-11-16 11:43:07 -07:00
Jon Chambers ab9e6ac48a Revert "Replace zkgroup with libsignal-client"
Service CI / build (push) Canceled after 0s
This reverts commit 73ea6e4251.
2021-11-16 11:35:10 -05:00
Jon Chambers c1d6c04ab2 Revert "Replace curve25519-java with libsignal-client"
This reverts commit 0011b8925b.
2021-11-16 11:35:10 -05:00
Jon Chambers 888cec3d56 Introduce a filter for correcting numeric "online" flags
Service CI / build (push) Canceled after 0s
2021-11-16 10:15:14 -05:00
Jon Chambers 1461bcc2c2 Correct envelope types for certain iOS builds 2021-11-16 10:15:14 -05:00
Jon Chambers 11f1cf80bd Move MessageControllerTest out of the tests sub-package to expose package-private elements from the class under test 2021-11-16 10:15:14 -05:00
Jordan Rose c675cc8b26 Test the response code for invalid serialized zkgroup objects
Test by Jon, making sure this is consistent even without up-front size
checking.
2021-11-16 09:52:38 -05:00
Jordan Rose 0011b8925b Replace curve25519-java with libsignal-client
These APIs stemmed from a common source long ago, so there's not much
to change!
2021-11-16 09:52:38 -05:00
Jordan Rose 73ea6e4251 Replace zkgroup with libsignal-client 2021-11-16 09:52:38 -05:00
Jon Chambers e4441dddbb Consolidate Redis client resources 2021-11-16 09:52:12 -05:00
Chris Eager 8d1d56f694 Update to Java 17 2021-11-15 16:42:43 -07:00
Jon Chambers 2015ba77ca Switch to a disallowed prefix model instead of a disallowed country code model
Service CI / build (push) Canceled after 0s
2021-11-15 15:44:55 -05:00
Chris Eager 7033a0f68f Set checkStaleness to true for protoc
Service CI / build (push) Canceled after 0s
2021-11-12 13:34:15 -07:00
Jon Chambers 6ada76da7f Parallelize assignment of phone number identifiers
Service CI / build (push) Canceled after 0s
2021-11-12 11:03:46 -05:00
Jon Chambers cbdec0cb22 Remove legacy push latency measurement pathways
Service CI / build (push) Canceled after 0s
2021-11-11 15:44:07 -05:00
Chris Eager de6e9d31c9 Add dedicated crawler for directory reconciler 2021-11-11 13:38:13 -07:00
Jon Chambers f0a6be32fc Add a crawler to assign PNIs to existing accounts
Service CI / build (push) Canceled after 0s
2021-11-10 11:15:05 -05:00
Ehren Kret 5c4855cca6 Remove trailing space 2021-11-10 10:11:49 -06:00
Ehren Kret 2e1e380418 Also update description text of boost badge 2021-11-10 10:10:53 -06:00
Ehren Kret d07f0b4f71 Update badge description text 2021-11-10 09:51:51 -06:00
Jon Chambers aaa2a6eef1 Break down push latency metrics by VOIP/not-VOIP and optionally by client version
Service CI / build (push) Canceled after 0s
2021-11-10 10:35:41 -05:00
Jon Chambers b1f56c3324 Resolve formatting complaints 2021-11-10 10:15:14 -05:00
Jon Chambers da5c0ae4b6 Enable Payments Beta for more country codes 2021-11-10 10:15:14 -05:00
Jon Chambers 1e1394560d Check length of cancellation reason list before getting reason codes
Service CI / build (push) Canceled after 0s
2021-11-09 11:42:44 -05:00
Jon Chambers bae0196bcf Tolerate null UUID attribute values
Service CI / build (push) Canceled after 0s
2021-11-09 11:00:27 -05:00
Jon Chambers 3398955c1a Add basic support for phone number identifiers
Service CI / build (push) Canceled after 0s
2021-11-09 10:23:08 -05:00
Chris Eager a1b925d1e0 Reduce visiblity on one constructor
Service CI / build (push) Canceled after 0s
2021-11-08 14:30:52 -07:00
Chris Eager 31c0c3275f Use the latest and in config, @JsonCreator 2021-11-08 14:30:52 -07:00
Chris Eager 0a4392f700 Streamline ExternalServiceCredentialsGenerator construction 2021-11-08 14:30:52 -07:00
Chris Eager eb86986cf4 Add /v2/directory/auth endpoint 2021-11-08 14:30:52 -07:00
Ehren Kret 1053a47e42 Add an exception mapper for CompletionExceptions
Service CI / build (push) Canceled after 0s
2021-11-04 19:12:31 -05:00
Ehren Kret 99b1f48e0e Copy badges from existing account on re-reg
Service CI / build (push) Canceled after 0s
2021-11-04 18:00:47 -05:00
115 changed files with 3505 additions and 1749 deletions
+2 -2
View File
@@ -11,8 +11,8 @@ jobs:
- name: Set up JDK 11
uses: actions/setup-java@3bc31aaf88e8fc94dc1e632d48af61be5ca8721c
with:
distribution: 'adopt'
java-version: 11
distribution: 'temurin'
java-version: 17
cache: 'maven'
- name: Build with Maven
run: mvn -e -B verify
+5 -5
View File
@@ -34,7 +34,7 @@
<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.22</dropwizard.version>
<dropwizard.version>2.0.25</dropwizard.version>
<dropwizard-metrics-datadog.version>1.1.13</dropwizard-metrics-datadog.version>
<gson.version>2.8.8</gson.version>
<guava.version>30.1.1-jre</guava.version>
@@ -44,7 +44,7 @@
<libphonenumber.version>8.12.33</libphonenumber.version>
<logstash.logback.version>6.6</logstash.logback.version>
<micrometer.version>1.5.3</micrometer.version>
<mockito.version>3.11.1</mockito.version>
<mockito.version>4.0.0</mockito.version>
<netty.version>4.1.65.Final</netty.version>
<netty.tcnative-boringssl-static.version>2.0.39.Final</netty.tcnative-boringssl-static.version>
<opentest4j.version>1.2.0</opentest4j.version>
@@ -337,6 +337,7 @@
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.18.0:exe:${os.detected.classifier}</protocArtifact>
<checkStaleness>true</checkStaleness>
</configuration>
<executions>
<execution>
@@ -353,8 +354,7 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>11</source>
<target>11</target>
<release>17</release>
</configuration>
</plugin>
@@ -418,7 +418,7 @@
<rules>
<dependencyConvergence/>
<requireMavenVersion>
<version>3.0.0</version>
<version>3.8.3</version>
</requireMavenVersion>
</rules>
</configuration>
+271 -164
View File
@@ -1,59 +1,67 @@
# Example, relatively minimal, configuration that passes validation (see `io.dropwizard.cli.CheckCommand`)
#
# `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.
stripe:
apiKey:
idempotencyKeyGenerator:
apiKey: unset
idempotencyKeyGenerator: abcdefg12345678= # base64 for creating request idempotency hash
boostDescription: >
Example
dynamoDbClientConfiguration:
region: # AWS Region
region: us-west-2 # AWS Region
dynamoDbTables:
issuedReceipts:
tableName: # DDB Table Name
expiration: # Duration of time until rows expire
generator: # random binary sequence
tableName: Example_IssuedReceipts
expiration: P30D # Duration of time until rows expire
generator: abcdefg12345678= # random base64-encoded binary sequence
redeemedReceipts:
tableName: # DDB Table Name
expiration: # Duration of time until rows expire
tableName: Example_RedeemedReceipts
expiration: P30D # Duration of time until rows expire
subscriptions:
tableName: # DDB Table Name
tableName: Example_Subscriptions
twilio: # Twilio gateway configuration
accountId:
accountToken:
nanpaMessagingServiceSid: # Twilio SID for the messaging service to use for NANPA.
messagingServiceSid: # Twilio SID for the message service to use for non-NANPA.
verifyServiceSid: # Twilio SID for a Verify service
localDomain: # Domain Twilio can connect back to for calls. Should be domain of your service.
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: # Text to use for the verification message on iOS. Will be passed to String.format with the verification code as argument 1.
androidNg: # 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: # 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: # 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: # Text to use when the client type is unrecognized. Will be passed to String.format with the verification code as argument 1.
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:
# … all keys from defaultClientVerificationTexts are required
androidAppHash: # Hash appended to Android
verifyServiceFriendlyName: # Service name used in template. Requires Twilio account rep to enable
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
push:
queueSize: # Size of push pending queue
queueSize: 1000 # Size of push pending queue
turn: # TURN server configuration
secret: # TURN server secret
secret: example # TURN server secret
uris:
- stun:yourdomain:80
- stun:yourdomain.com:443
- turn:yourdomain:443?transport=udp
- turn:etc.com:80?transport=udp
- stun:example.com:80
- stun:another.example.com:443
- turn:example.com:443?transport=udp
- turn:ya.example.com:80?transport=udp
cacheCluster: # Redis server configuration for cache cluster
urls:
- redis://redis.example.com:6379/
configurationUri: redis://redis.example.com:6379/
clientPresenceCluster: # Redis server configuration for client presence cluster
urls:
- redis://redis.example.com:6379/
configurationUri: redis://redis.example.com:6379/
pubsub: # Redis server configuration for pubsub cluster
url: redis://redis.example.com:6379/
@@ -61,148 +69,190 @@ pubsub: # Redis server configuration for pubsub cluster
- redis://redis.example.com:6379/
pushSchedulerCluster: # Redis server configuration for push scheduler cluster
urls:
- redis://redis.example.com:6379/
configurationUri: redis://redis.example.com:6379/
rateLimitersCluster: # Redis server configuration for rate limiters cluster
urls:
- redis://redis.example.com:6379/
configurationUri: redis://redis.example.com:6379/
directory:
client: # Configuration for interfacing with Contact Discovery Service cluster
userAuthenticationTokenSharedSecret: # hex-encoded secret shared with CDS used to generate auth tokens for Signal users
userAuthenticationTokenUserIdSecret: # hex-encoded secret shared among Signal-Servers to obscure user phone numbers from CDS
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: # AWS SQS accessKey
accessSecret: # AWS SQS accessSecret
queueUrls: # AWS SQS queue urls
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: # CDS replication name
replicationUrl: # CDS replication endpoint base url
replicationPassword: # CDS replication endpoint password
replicationCaCertificate: # CDS replication endpoint TLS certificate trust root
- 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
messageCache: # Redis server configuration for message store cache
persistDelayMinutes:
persistDelayMinutes: 1
cluster:
urls:
- redis://redis.example.com:6379/
configurationUri: redis://redis.example.com:6379/
metricsCluster:
urls:
- redis://redis.example.com:6379/
configurationUri: redis://redis.example.com:6379/
messageDynamoDb: # DynamoDB table configuration
region:
tableName:
region: us-west-2
tableName: Example_Messages
keysDynamoDb: # DynamoDB table configuration
region:
tableName:
region: us-west-2
tableName: Example_PreKeys
accountsDynamoDb: # DynamoDB table configuration
region:
tableName:
phoneNumberTableName:
region: us-west-2
tableName: Example_Accounts
phoneNumberTableName: Example_Accounts_PhoneNumbers
phoneNumberIdentifierTableName: Example_Accounts_PhoneNumberIdentifiers
deletedAccountsDynamoDb: # DynamoDb table configuration
region:
tableName:
needsReconciliationIndexName:
region: us-west-2
tableName: Example_DeletedAccounts
needsReconciliationIndexName: NeedsReconciliation
deletedAccountsLockDynamoDb: # DynamoDb table configuration
region:
tableName:
redeemedReceiptsDynamoDb: # DynamoDB table configuration
region:
tableName:
expirationTime: # ISO8601 Duration
migrationDeletedAccountsDynamoDb: # DynamoDB table configuration
region:
tableName:
migrationRetryAccountsDynamoDb: # DynamoDB table configuration
region:
tableName:
region: us-west-2
tableName: Example_DeletedAccountsLock
pendingAccountsDynamoDb: # DynamoDB table configuration
region:
tableName:
region: us-west-2
tableName: Example_PendingAccounts
pendingDevicesDynamoDb: # DynamoDB table configuration
region:
tableName:
region: us-west-2
tableName: Example_PendingDevices
phoneNumberIdentifiersDynamoDb: # DynamoDB table configuration
region: us-west-2
tableName: Example_PhoneNumberIdentifiers
pushChallengeDynamoDb: # DynamoDB table configuration
region:
tableName:
region: us-west-2
tableName: Example_PushChallenges
reportMessageDynamoDb: # DynamoDB table configuration
region:
tableName:
region: us-west-2
tableName: Example_ReportedMessages
awsAttachments: # AWS S3 configuration
accessKey:
accessSecret:
bucket:
region:
accessKey: test
accessSecret: test
bucket: aws-attachments
region: us-west-2
gcpAttachments: # GCP Storage configuration
domain:
email:
maxSizeInBytes:
domain: example.com
email: user@example.cocm
maxSizeInBytes: 1024
pathPrefix:
rsaSigningKey:
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-----
abuseDatabase: # Postgresql database configuration
driverClass: org.postgresql.Driver
user:
password:
url:
user: example
password: password
url: jdbc:postgresql://example.com:5432/abusedb
accountsDatabase: # Postgresql database configuration
driverClass: org.postgresql.Driver
user:
password:
url:
user: example
password: password
url: jdbc:postgresql://example.com:5432/accountsdb
accountDatabaseCrawler:
chunkSize: # accounts per run
chunkIntervalMs: # time per run
dynamoDbMigrationCrawler:
chunkSize: # accounts per run
chunkIntervalMs: # time per run
chunkSize: 10 # accounts per run
chunkIntervalMs: 60000 # time per run
apn: # Apple Push Notifications configuration
sandbox: true
bundleId:
keyId:
teamId:
signingKey:
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-----
gcm: # GCM Configuration
senderId:
apiKey:
senderId: 123456789
apiKey: unset
cdn:
accessKey: # AWS Access Key ID
accessSecret: # AWS Access Secret
bucket: # S3 Bucket name
region: # AWS region
accessKey: test # AWS Access Key ID
accessSecret: test # AWS Access Secret
bucket: cdn # S3 Bucket name
region: us-west-2 # AWS region
datadog:
apiKey:
environment:
apiKey: unset
environment: dev
unidentifiedDelivery:
certificate:
privateKey:
expiresDays:
certificate: ABCD1234
privateKey: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789AAAAAAA
expiresDays: 7
voiceVerification:
url: https://cdn-ca.signal.org/verification/
@@ -210,33 +260,74 @@ voiceVerification:
- en
recaptcha:
secret:
secret: unset
recaptchaV2:
siteKey:
scoreFloor:
projectPath:
credentialConfigurationJson:
siteKey: unset
scoreFloor: 1.0
projectPath: projects/example
credentialConfigurationJson: "{ }" # service account configuration for backend authentication
storageService:
uri:
userAuthenticationTokenSharedSecret:
storageCaCertificate:
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:
userAuthenticationTokenSharedSecret:
backupCaCertificate:
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-----
zkConfig:
serverPublic:
serverSecret:
enabled:
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==
appConfig:
application:
environment:
configuration:
application: example
environment: example
configuration: example
remoteConfig:
authorizedTokens:
@@ -245,48 +336,64 @@ remoteConfig:
- # ...
- # Nth authorized token
globalConfig: # keys and values that are given to clients on GET /v1/config
EXAMPLE_KEY: VALUE
paymentsService:
userAuthenticationTokenSharedSecret: # hex-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users
fixerApiKey:
userAuthenticationTokenSharedSecret: 0000000f0000000f0000000f0000000f0000000f0000000f0000000f0000000f # hex-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users
fixerApiKey: unset
paymentCurrencies:
-
torExitNodeList:
s3Region:
s3Bucket:
objectKey:
maxSize:
asnTable:
s3Region:
s3Bucket:
objectKey:
maxSize:
# list of symbols for supported currencies
- MOB
donation:
uri: # value
uri: donation.example.com # value
supportedCurrencies:
- # 1st supported currency
- # 2nd supported currency
- # ...
- # Nth supported currency
circuitBreaker:
failureRateThreshold: # value
ringBufferSizeInHalfOpenState: # value
ringBufferSizeInClosedState: # value
waitDurationInOpenStateInSeconds: # value
retry:
maxAttempts: # value
waitDuration: # value
badges:
badges:
- id: TEST
imageUrl: https://example.com/test-badge
category: other
sprites: # exactly 6
- sprite-1.png
- sprite-2.png
- sprite-3.png
- sprite-4.png
- sprite-5.png
- sprite-6.png
svg: example.svg
svgs:
- light: example-light.svg
dark: example-dark.svg
badgeIdsEnabledForAll:
- TEST
receiptLevels:
'1': TEST
subscription: # configuration for Stripe subscriptions
badgeGracePeriod: P15D
levels:
500:
badge: EXAMPLE
prices:
# list of ISO 4217 currency codes and amounts for the given badge level
xts:
amount: '10'
id: price_example # stripe ID
boost:
level: 1
expiration: P90D
badge: EXAMPLE
currencies:
# ISO 4217 currency codes and amounts in those currencies
xts:
- '1'
- '2'
- '4'
- '8'
- '20'
- '40'
+22
View File
@@ -593,6 +593,28 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>check-all-service-config</id>
<phase>verify</phase>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>org.whispersystems.textsecuregcm.CheckServiceConfigurations</mainClass>
<classpathScope>test</classpathScope>
<arguments>
<argument>${project.basedir}/config</argument>
</arguments>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -27,6 +27,7 @@ import org.whispersystems.textsecuregcm.configuration.DatabaseConfiguration;
import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration;
import org.whispersystems.textsecuregcm.configuration.DeletedAccountsDynamoDbConfiguration;
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
import org.whispersystems.textsecuregcm.configuration.DirectoryV2Configuration;
import org.whispersystems.textsecuregcm.configuration.DonationConfiguration;
import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguration;
import org.whispersystems.textsecuregcm.configuration.DynamoDbConfiguration;
@@ -125,6 +126,11 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private DirectoryConfiguration directory;
@NotNull
@Valid
@JsonProperty
private DirectoryV2Configuration directoryV2;
@NotNull
@Valid
@JsonProperty
@@ -165,6 +171,11 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private AccountsDynamoDbConfiguration accountsDynamoDb;
@Valid
@NotNull
@JsonProperty
private DynamoDbConfiguration phoneNumberIdentifiersDynamoDb;
@Valid
@NotNull
@JsonProperty
@@ -390,6 +401,10 @@ public class WhisperServerConfiguration extends Configuration {
return directory;
}
public DirectoryV2Configuration getDirectoryV2Configuration() {
return directoryV2;
}
public SecureStorageServiceConfiguration getSecureStorageServiceConfiguration() {
return storageService;
}
@@ -426,6 +441,10 @@ public class WhisperServerConfiguration extends Configuration {
return accountsDynamoDb;
}
public DynamoDbConfiguration getPhoneNumberIdentifiersDynamoDbConfiguration() {
return phoneNumberIdentifiersDynamoDb;
}
public DeletedAccountsDynamoDbConfiguration getDeletedAccountsDynamoDbConfiguration() {
return deletedAccountsDynamoDb;
}
@@ -53,7 +53,6 @@ import java.util.concurrent.TimeUnit;
import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletRegistration;
import javax.ws.rs.container.DynamicFeature;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.glassfish.jersey.server.ServerProperties;
import org.jdbi.v3.core.Jdbi;
@@ -80,6 +79,7 @@ import org.whispersystems.textsecuregcm.badges.ConfiguredProfileBadgeConverter;
import org.whispersystems.textsecuregcm.badges.ResourceBundleLevelTranslator;
import org.whispersystems.textsecuregcm.configuration.DirectoryServerConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.controllers.AcceptNumericOnlineFlagRequestFilter;
import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV1;
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV2;
@@ -88,6 +88,7 @@ import org.whispersystems.textsecuregcm.controllers.CertificateController;
import org.whispersystems.textsecuregcm.controllers.ChallengeController;
import org.whispersystems.textsecuregcm.controllers.DeviceController;
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
import org.whispersystems.textsecuregcm.controllers.DirectoryV2Controller;
import org.whispersystems.textsecuregcm.controllers.DonationController;
import org.whispersystems.textsecuregcm.controllers.KeepAliveController;
import org.whispersystems.textsecuregcm.controllers.KeysController;
@@ -115,6 +116,7 @@ import org.whispersystems.textsecuregcm.limits.RateLimitResetMetricsManager;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.limits.UnsealedSenderRateLimiter;
import org.whispersystems.textsecuregcm.liquibase.NameableMigrationsBundle;
import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper;
@@ -179,12 +181,13 @@ import org.whispersystems.textsecuregcm.storage.DirectoryReconciliationClient;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.storage.MessagePersister;
import org.whispersystems.textsecuregcm.storage.MessagesCache;
import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.NonNormalizedAccountCrawlerListener;
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
import org.whispersystems.textsecuregcm.storage.Profiles;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
@@ -329,6 +332,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
DynamoDbClient accountsDynamoDbClient = DynamoDbFromConfig.client(config.getAccountsDynamoDbConfiguration(),
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
DynamoDbClient phoneNumberIdentifiersDynamoDbClient =
DynamoDbFromConfig.client(config.getPhoneNumberIdentifiersDynamoDbConfiguration(),
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig.client(config.getDeletedAccountsDynamoDbConfiguration(),
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
@@ -364,11 +371,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
Accounts accounts = new Accounts(accountsDynamoDbClient,
config.getAccountsDynamoDbConfiguration().getTableName(),
config.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
config.getAccountsDynamoDbConfiguration().getPhoneNumberIdentifierTableName(),
config.getAccountsDynamoDbConfiguration().getScanPageSize());
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(phoneNumberIdentifiersDynamoDbClient,
config.getPhoneNumberIdentifiersDynamoDbConfiguration().getTableName());
Usernames usernames = new Usernames(accountDatabase);
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
Profiles profiles = new Profiles(accountDatabase);
KeysDynamoDb keysDynamoDb = new KeysDynamoDb(preKeyDynamoDb, config.getKeysDynamoDbConfiguration().getTableName());
Keys keys = new Keys(preKeyDynamoDb, config.getKeysDynamoDbConfiguration().getTableName());
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(messageDynamoDb,
config.getMessageDynamoDbConfiguration().getTableName(),
config.getMessageDynamoDbConfiguration().getTimeToLive());
@@ -382,24 +392,15 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
RedisClientFactory pubSubClientFactory = new RedisClientFactory("pubsub_cache", config.getPubsubCacheConfiguration().getUrl(), config.getPubsubCacheConfiguration().getReplicaUrls(), config.getPubsubCacheConfiguration().getCircuitBreakerConfiguration());
ReplicatedJedisPool pubsubClient = pubSubClientFactory.getRedisClientPool();
ClientResources generalCacheClientResources = ClientResources.builder().build();
ClientResources messageCacheClientResources = ClientResources.builder().build();
ClientResources presenceClientResources = ClientResources.builder().build();
ClientResources metricsCacheClientResources = ClientResources.builder().build();
ClientResources pushSchedulerCacheClientResources = ClientResources.builder().ioThreadPoolSize(4).build();
ClientResources rateLimitersCacheClientResources = ClientResources.builder().build();
ClientResources redisClientResources = ClientResources.builder().build();
ConnectionEventLogger.logConnectionEvents(redisClientResources);
ConnectionEventLogger.logConnectionEvents(generalCacheClientResources);
ConnectionEventLogger.logConnectionEvents(messageCacheClientResources);
ConnectionEventLogger.logConnectionEvents(presenceClientResources);
ConnectionEventLogger.logConnectionEvents(metricsCacheClientResources);
FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster", config.getCacheClusterConfiguration(), generalCacheClientResources);
FaultTolerantRedisCluster messagesCluster = new FaultTolerantRedisCluster("messages_cluster", config.getMessageCacheConfiguration().getRedisClusterConfiguration(), messageCacheClientResources);
FaultTolerantRedisCluster clientPresenceCluster = new FaultTolerantRedisCluster("client_presence_cluster", config.getClientPresenceClusterConfiguration(), presenceClientResources);
FaultTolerantRedisCluster metricsCluster = new FaultTolerantRedisCluster("metrics_cluster", config.getMetricsClusterConfiguration(), metricsCacheClientResources);
FaultTolerantRedisCluster pushSchedulerCluster = new FaultTolerantRedisCluster("push_scheduler", config.getPushSchedulerCluster(), pushSchedulerCacheClientResources);
FaultTolerantRedisCluster rateLimitersCluster = new FaultTolerantRedisCluster("rate_limiters", config.getRateLimitersCluster(), rateLimitersCacheClientResources);
FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster", config.getCacheClusterConfiguration(), redisClientResources);
FaultTolerantRedisCluster messagesCluster = new FaultTolerantRedisCluster("messages_cluster", config.getMessageCacheConfiguration().getRedisClusterConfiguration(), redisClientResources);
FaultTolerantRedisCluster clientPresenceCluster = new FaultTolerantRedisCluster("client_presence_cluster", config.getClientPresenceClusterConfiguration(), redisClientResources);
FaultTolerantRedisCluster metricsCluster = new FaultTolerantRedisCluster("metrics_cluster", config.getMetricsClusterConfiguration(), redisClientResources);
FaultTolerantRedisCluster pushSchedulerCluster = new FaultTolerantRedisCluster("push_scheduler", config.getPushSchedulerCluster(), redisClientResources);
FaultTolerantRedisCluster rateLimitersCluster = new FaultTolerantRedisCluster("rate_limiters", config.getRateLimitersCluster(), redisClientResources);
BlockingQueue<Runnable> keyspaceNotificationDispatchQueue = new ArrayBlockingQueue<>(10_000);
Metrics.gaugeCollectionSize(name(getClass(), "keyspaceNotificationDispatchQueueSize"), Collections.emptyList(), keyspaceNotificationDispatchQueue);
@@ -421,11 +422,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
build();
StripeManager stripeManager = new StripeManager(config.getStripe().getApiKey(), stripeExecutor,
config.getStripe().getIdempotencyKeyGenerator());
config.getStripe().getIdempotencyKeyGenerator(), config.getStripe().getBoostDescription());
ExternalServiceCredentialGenerator directoryCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenSharedSecret(),
config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenUserIdSecret(),
true);
ExternalServiceCredentialGenerator directoryCredentialsGenerator = new ExternalServiceCredentialGenerator(
config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenSharedSecret(),
config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenUserIdSecret());
ExternalServiceCredentialGenerator directoryV2CredentialsGenerator = new ExternalServiceCredentialGenerator(
config.getDirectoryV2Configuration().getDirectoryV2ClientConfiguration()
.getUserAuthenticationTokenSharedSecret(), false);
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager =
new DynamicConfigurationManager<>(config.getAppConfig().getApplication(),
@@ -440,9 +444,12 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager = new TwilioVerifyExperimentEnrollmentManager(
config.getVoiceVerificationConfiguration(), experimentEnrollmentManager);
ExternalServiceCredentialGenerator storageCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0], false);
ExternalServiceCredentialGenerator backupCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getSecureBackupServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0], false);
ExternalServiceCredentialGenerator paymentsCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getPaymentsServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0], false);
ExternalServiceCredentialGenerator storageCredentialsGenerator = new ExternalServiceCredentialGenerator(
config.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
ExternalServiceCredentialGenerator backupCredentialsGenerator = new ExternalServiceCredentialGenerator(
config.getSecureBackupServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
ExternalServiceCredentialGenerator paymentsCredentialsGenerator = new ExternalServiceCredentialGenerator(
config.getPaymentsServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
SecureBackupClient secureBackupClient = new SecureBackupClient(backupCredentialsGenerator, backupServiceExecutor, config.getSecureBackupServiceConfiguration());
SecureStorageClient secureStorageClient = new SecureStorageClient(storageCredentialsGenerator, storageServiceExecutor, config.getSecureStorageServiceConfiguration());
@@ -453,14 +460,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheCluster);
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheCluster);
MessagesCache messagesCache = new MessagesCache(messagesCluster, messagesCluster, keyspaceNotificationDispatchExecutor);
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster);
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster, dynamicConfigurationManager);
ReportMessageManager reportMessageManager = new ReportMessageManager(reportMessageDynamoDb, rateLimitersCluster, Metrics.globalRegistry, config.getReportMessageConfiguration().getCounterTtl());
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, pushLatencyManager, reportMessageManager);
DeletedAccountsManager deletedAccountsManager = new DeletedAccountsManager(deletedAccounts,
deletedAccountsLockDynamoDbClient, config.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster,
deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager,
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager);
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
deletedAccountsManager, directoryQueue, keys, messagesManager, usernamesManager, profilesManager,
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager, clock);
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(accountsManager, messagesManager);
DispatchManager dispatchManager = new DispatchManager(pubSubClientFactory, Optional.of(deadLetterHandler));
@@ -510,9 +517,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
MessagePersister messagePersister = new MessagePersister(messagesCache, messagesManager, accountsManager, dynamicConfigurationManager, Duration.ofMinutes(config.getMessageCacheConfiguration().getPersistDelayMinutes()));
// TODO listeners must be ordered so that ones that directly update accounts come last, so that read-only ones are not working with stale data
final List<AccountDatabaseCrawlerListener> accountDatabaseCrawlerListeners = new ArrayList<>();
final List<AccountDatabaseCrawlerListener> directoryReconciliationAccountDatabaseCrawlerListeners = new ArrayList<>();
final List<DeletedAccountsDirectoryReconciler> deletedAccountsDirectoryReconcilers = new ArrayList<>();
for (DirectoryServerConfiguration directoryServerConfiguration : config.getDirectoryConfiguration()
.getDirectoryServerConfiguration()) {
@@ -522,25 +527,33 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
directoryServerConfiguration.getReplicationName(), directoryReconciliationClient,
dynamicConfigurationManager);
// reconcilers are read-only
accountDatabaseCrawlerListeners.add(directoryReconciler);
directoryReconciliationAccountDatabaseCrawlerListeners.add(directoryReconciler);
final DeletedAccountsDirectoryReconciler deletedAccountsDirectoryReconciler = new DeletedAccountsDirectoryReconciler(
directoryServerConfiguration.getReplicationName(), directoryReconciliationClient);
deletedAccountsDirectoryReconcilers.add(deletedAccountsDirectoryReconciler);
}
accountDatabaseCrawlerListeners.add(new NonNormalizedAccountCrawlerListener(accountsManager, metricsCluster));
accountDatabaseCrawlerListeners.add(new ContactDiscoveryWriter(accountsManager));
// PushFeedbackProcessor may update device properties
accountDatabaseCrawlerListeners.add(new PushFeedbackProcessor(accountsManager));
// delete accounts last
accountDatabaseCrawlerListeners.add(new AccountCleaner(accountsManager));
HttpClient currencyClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).connectTimeout(Duration.ofSeconds(10)).build();
FixerClient fixerClient = new FixerClient(currencyClient, config.getPaymentsServiceConfiguration().getFixerApiKey());
FtxClient ftxClient = new FtxClient(currencyClient);
CurrencyConversionManager currencyManager = new CurrencyConversionManager(fixerClient, ftxClient, config.getPaymentsServiceConfiguration().getPaymentCurrencies());
AccountDatabaseCrawlerCache directoryReconciliationAccountDatabaseCrawlerCache = new AccountDatabaseCrawlerCache(
cacheCluster, AccountDatabaseCrawlerCache.DIRECTORY_RECONCILER_PREFIX);
AccountDatabaseCrawler directoryReconciliationAccountDatabaseCrawler = new AccountDatabaseCrawler(accountsManager,
directoryReconciliationAccountDatabaseCrawlerCache, directoryReconciliationAccountDatabaseCrawlerListeners,
config.getAccountDatabaseCrawlerConfiguration().getChunkSize(),
config.getAccountDatabaseCrawlerConfiguration().getChunkIntervalMs()
);
AccountDatabaseCrawlerCache accountDatabaseCrawlerCache = new AccountDatabaseCrawlerCache(cacheCluster);
// TODO listeners must be ordered so that ones that directly update accounts come last, so that read-only ones are not working with stale data
final List<AccountDatabaseCrawlerListener> accountDatabaseCrawlerListeners = List.of(
new NonNormalizedAccountCrawlerListener(accountsManager, metricsCluster),
new ContactDiscoveryWriter(accountsManager),
// PushFeedbackProcessor may update device properties
new PushFeedbackProcessor(accountsManager),
// delete accounts last
new AccountCleaner(accountsManager)
);
AccountDatabaseCrawlerCache accountDatabaseCrawlerCache = new AccountDatabaseCrawlerCache(cacheCluster,
AccountDatabaseCrawlerCache.GENERAL_PURPOSE_PREFIX);
AccountDatabaseCrawler accountDatabaseCrawler = new AccountDatabaseCrawler(accountsManager,
accountDatabaseCrawlerCache, accountDatabaseCrawlerListeners,
config.getAccountDatabaseCrawlerConfiguration().getChunkSize(),
@@ -549,12 +562,18 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
DeletedAccountsTableCrawler deletedAccountsTableCrawler = new DeletedAccountsTableCrawler(deletedAccountsManager, deletedAccountsDirectoryReconcilers, cacheCluster, recurringJobExecutor);
HttpClient currencyClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).connectTimeout(Duration.ofSeconds(10)).build();
FixerClient fixerClient = new FixerClient(currencyClient, config.getPaymentsServiceConfiguration().getFixerApiKey());
FtxClient ftxClient = new FtxClient(currencyClient);
CurrencyConversionManager currencyManager = new CurrencyConversionManager(fixerClient, ftxClient, config.getPaymentsServiceConfiguration().getPaymentCurrencies());
apnSender.setApnFallbackManager(apnFallbackManager);
environment.lifecycle().manage(new ApplicationShutdownMonitor());
environment.lifecycle().manage(apnFallbackManager);
environment.lifecycle().manage(pubSubManager);
environment.lifecycle().manage(messageSender);
environment.lifecycle().manage(accountDatabaseCrawler);
environment.lifecycle().manage(directoryReconciliationAccountDatabaseCrawler);
environment.lifecycle().manage(deletedAccountsTableCrawler);
environment.lifecycle().manage(remoteConfigsManager);
environment.lifecycle().manage(messagesCache);
@@ -590,7 +609,12 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
.addFilter("RemoteDeprecationFilter", new RemoteDeprecationFilter(dynamicConfigurationManager))
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
// TODO Remove on or after 2022-03-01
final AcceptNumericOnlineFlagRequestFilter acceptNumericOnlineFlagRequestFilter =
new AcceptNumericOnlineFlagRequestFilter("v1/messages/multi_recipient");
environment.jersey().register(new ContentLengthFilter(TrafficSource.HTTP));
environment.jersey().register(acceptNumericOnlineFlagRequestFilter);
environment.jersey().register(MultiRecipientMessageProvider.class);
environment.jersey().register(new MetricsApplicationEventListener(TrafficSource.HTTP));
environment.jersey()
@@ -612,6 +636,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
clientPresenceManager, retrySchedulingExecutor));
webSocketEnvironment.jersey().register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
webSocketEnvironment.jersey().register(new ContentLengthFilter(TrafficSource.WEBSOCKET));
webSocketEnvironment.jersey().register(acceptNumericOnlineFlagRequestFilter);
webSocketEnvironment.jersey().register(MultiRecipientMessageProvider.class);
webSocketEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET));
webSocketEnvironment.jersey().register(new KeepAliveController(clientPresenceManager));
@@ -622,7 +647,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
smsSender, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(),
transitionalRecaptchaClient, gcmSender, apnSender, backupCredentialsGenerator,
verifyExperimentEnrollmentManager));
environment.jersey().register(new KeysController(rateLimiters, keysDynamoDb, accountsManager, preKeyRateLimiter, rateLimitChallengeManager));
environment.jersey().register(new KeysController(rateLimiters, keys, accountsManager, preKeyRateLimiter, rateLimitChallengeManager));
final List<Object> commonControllers = Lists.newArrayList(
new AttachmentControllerV1(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getBucket()),
@@ -630,8 +655,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
new AttachmentControllerV3(rateLimiters, config.getGcpAttachmentsConfiguration().getDomain(), config.getGcpAttachmentsConfiguration().getEmail(), config.getGcpAttachmentsConfiguration().getMaxSizeInBytes(), config.getGcpAttachmentsConfiguration().getPathPrefix(), config.getGcpAttachmentsConfiguration().getRsaSigningKey()),
new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().getCertificate(), config.getDeliveryCertificate().getPrivateKey(), config.getDeliveryCertificate().getExpiresDays()), zkAuthOperations),
new ChallengeController(rateLimitChallengeManager),
new DeviceController(pendingDevicesManager, accountsManager, messagesManager, keysDynamoDb, rateLimiters, config.getMaxDevices()),
new DeviceController(pendingDevicesManager, accountsManager, messagesManager, keys, rateLimiters, config.getMaxDevices()),
new DirectoryController(directoryCredentialsGenerator),
new DirectoryV2Controller(directoryV2CredentialsGenerator),
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
ReceiptCredentialPresentation::new, stripeExecutor, config.getDonationConfiguration(), config.getStripe()),
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, messagesManager, unsealedSenderRateLimiter, apnFallbackManager,
@@ -744,6 +770,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
List.of(
new LoggingUnhandledExceptionMapper(),
new CompletionExceptionMapper(),
new IOExceptionMapper(),
new RateLimitExceededExceptionMapper(),
new InvalidWebsocketAddressExceptionMapper(),
@@ -76,7 +76,7 @@ public class AuthEnablementRefreshRequirementProvider implements WebsocketRefres
@SuppressWarnings("unchecked") final Map<Long, Boolean> initialDevicesEnabled =
(Map<Long, Boolean>) requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED);
return accountsManager.get((UUID) requestEvent.getContainerRequest().getProperty(ACCOUNT_UUID)).map(account -> {
return accountsManager.getByAccountIdentifier((UUID) requestEvent.getContainerRequest().getProperty(ACCOUNT_UUID)).map(account -> {
final Set<Long> deviceIdsToDisplace;
final Map<Long, Boolean> currentDevicesEnabled = buildDevicesEnabledMap(account);
@@ -78,7 +78,7 @@ public class BaseAccountAuthenticator {
deviceId = identifierAndDeviceId.second();
}
Optional<Account> account = accountsManager.get(accountUuid);
Optional<Account> account = accountsManager.getByAccountIdentifier(accountUuid);
if (account.isEmpty()) {
failureReason = "noSuchAccount";
@@ -1,97 +1,71 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.Util;
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 java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.TimeUnit;
import org.apache.commons.codec.binary.Hex;
import org.whispersystems.textsecuregcm.util.Util;
public class ExternalServiceCredentialGenerator {
private final Logger logger = LoggerFactory.getLogger(ExternalServiceCredentialGenerator.class);
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, boolean usernameDerivation) {
this.key = key;
this.userIdKey = userIdKey;
this.usernameDerivation = usernameDerivation;
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey) {
this(key, userIdKey, true, true);
}
public ExternalServiceCredentials generateFor(String number) {
Mac mac = getMacInstance();
String username = getUserId(number, mac, usernameDerivation);
long currentTimeSeconds = System.currentTimeMillis() / 1000;
String prefix = username + ":" + currentTimeSeconds;
String output = Hex.encodeHexString(Util.truncate(getHmac(key, prefix.getBytes(), mac), 10));
String token = prefix + ":" + output;
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);
}
private 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);
}
public boolean isValid(String token, String number, long currentTimeMillis) {
String[] parts = token.split(":");
Mac mac = getMacInstance();
if (parts.length != 3) {
return false;
}
if (!getUserId(number, mac, usernameDerivation).equals(parts[0])) {
return false;
}
if (!isValidTime(parts[1], currentTimeMillis)) {
return false;
}
return isValidSignature(parts[0] + ":" + parts[1], parts[2], mac);
}
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 boolean isValidTime(String timeString, long currentTimeMillis) {
try {
long tokenTime = Long.parseLong(timeString);
long ourTime = TimeUnit.MILLISECONDS.toSeconds(currentTimeMillis);
return TimeUnit.SECONDS.toHours(Math.abs(ourTime - tokenTime)) < 24;
} catch (NumberFormatException e) {
logger.warn("Number Format", e);
return false;
}
}
private boolean isValidSignature(String prefix, String suffix, Mac mac) {
try {
byte[] ourSuffix = Util.truncate(getHmac(key, prefix.getBytes(), mac), 10);
byte[] theirSuffix = Hex.decodeHex(suffix.toCharArray());
return MessageDigest.isEqual(ourSuffix, theirSuffix);
} catch (DecoderException e) {
logger.warn("DirectoryCredentials", e);
return false;
}
}
private Mac getMacInstance() {
try {
return Mac.getInstance("HmacSHA256");
@@ -8,6 +8,9 @@ public class AccountsDynamoDbConfiguration extends DynamoDbConfiguration {
@NotNull
private String phoneNumberTableName;
@NotNull
private String phoneNumberIdentifierTableName;
private int scanPageSize = 100;
@JsonProperty
@@ -15,6 +18,11 @@ public class AccountsDynamoDbConfiguration extends DynamoDbConfiguration {
return phoneNumberTableName;
}
@JsonProperty
public String getPhoneNumberIdentifierTableName() {
return phoneNumberIdentifierTableName;
}
@JsonProperty
public int getScanPageSize() {
return scanPageSize;
@@ -0,0 +1,26 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.util.ExactlySize;
public class DirectoryV2ClientConfiguration {
private final byte[] userAuthenticationTokenSharedSecret;
@JsonCreator
public DirectoryV2ClientConfiguration(
@JsonProperty("userAuthenticationTokenSharedSecret") final byte[] userAuthenticationTokenSharedSecret) {
this.userAuthenticationTokenSharedSecret = userAuthenticationTokenSharedSecret;
}
@ExactlySize({32})
public byte[] getUserAuthenticationTokenSharedSecret() {
return userAuthenticationTokenSharedSecret;
}
}
@@ -0,0 +1,24 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.Valid;
public class DirectoryV2Configuration {
private final DirectoryV2ClientConfiguration clientConfiguration;
@JsonCreator
public DirectoryV2Configuration(@JsonProperty("client") DirectoryV2ClientConfiguration clientConfiguration) {
this.clientConfiguration = clientConfiguration;
}
@Valid
public DirectoryV2ClientConfiguration getDirectoryV2ClientConfiguration() {
return clientConfiguration;
}
}
@@ -63,6 +63,9 @@ public class RateLimitsConfiguration {
@JsonProperty
private RateLimitConfiguration usernameSet = new RateLimitConfiguration(100, 100 / (24.0 * 60.0));
@JsonProperty
private RateLimitConfiguration checkAccountExistence = new RateLimitConfiguration(1_000, 1_000 / 60.0);
public RateLimitConfiguration getAutoBlock() {
return autoBlock;
}
@@ -135,6 +138,10 @@ public class RateLimitsConfiguration {
return usernameSet;
}
public RateLimitConfiguration getCheckAccountExistence() {
return checkAccountExistence;
}
public static class RateLimitConfiguration {
@JsonProperty
private int bucketSize;
@@ -13,13 +13,16 @@ public class StripeConfiguration {
private final String apiKey;
private final byte[] idempotencyKeyGenerator;
private final String boostDescription;
@JsonCreator
public StripeConfiguration(
@JsonProperty("apiKey") final String apiKey,
@JsonProperty("idempotencyKeyGenerator") final byte[] idempotencyKeyGenerator) {
@JsonProperty("idempotencyKeyGenerator") final byte[] idempotencyKeyGenerator,
@JsonProperty("boostDescription") final String boostDescription) {
this.apiKey = apiKey;
this.idempotencyKeyGenerator = idempotencyKeyGenerator;
this.boostDescription = boostDescription;
}
@NotEmpty
@@ -31,4 +34,9 @@ public class StripeConfiguration {
public byte[] getIdempotencyKeyGenerator() {
return idempotencyKeyGenerator;
}
@NotEmpty
public String getBoostDescription() {
return boostDescription;
}
}
@@ -51,6 +51,10 @@ public class DynamicConfiguration {
@JsonProperty
private DynamicDirectoryReconcilerConfiguration directoryReconciler = new DynamicDirectoryReconcilerConfiguration();
@JsonProperty
@Valid
private DynamicPushLatencyConfiguration pushLatency = new DynamicPushLatencyConfiguration(Collections.emptyMap());
public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(
final String experimentName) {
return Optional.ofNullable(experiments.get(experimentName));
@@ -101,4 +105,8 @@ public class DynamicConfiguration {
public DynamicDirectoryReconcilerConfiguration getDirectoryReconcilerConfiguration() {
return directoryReconciler;
}
public DynamicPushLatencyConfiguration getPushLatencyConfiguration() {
return pushLatency;
}
}
@@ -7,14 +7,14 @@ package org.whispersystems.textsecuregcm.configuration.dynamic;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Collections;
import java.util.Set;
import java.util.List;
public class DynamicPaymentsConfiguration {
@JsonProperty
private Set<String> allowedCountryCodes = Collections.emptySet();
private List<String> disallowedPrefixes = Collections.emptyList();
public Set<String> getAllowedCountryCodes() {
return allowedCountryCodes;
public List<String> getDisallowedPrefixes() {
return disallowedPrefixes;
}
}
@@ -0,0 +1,27 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration.dynamic;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.vdurmont.semver4j.Semver;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
import java.util.Map;
import java.util.Set;
public class DynamicPushLatencyConfiguration {
private final Map<ClientPlatform, Set<Semver>> instrumentedVersions;
@JsonCreator
public DynamicPushLatencyConfiguration(@JsonProperty("instrumentedVersions") final Map<ClientPlatform, Set<Semver>> instrumentedVersions) {
this.instrumentedVersions = instrumentedVersions;
}
public Map<ClientPlatform, Set<Semver>> getInstrumentedVersions() {
return instrumentedVersions;
}
}
@@ -0,0 +1,32 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import java.io.IOException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;
@PreMatching
public class AcceptNumericOnlineFlagRequestFilter implements ContainerRequestFilter {
private final String pathPrefix;
public AcceptNumericOnlineFlagRequestFilter(final String pathPrefix) {
this.pathPrefix = pathPrefix;
}
@Override
public void filter(final ContainerRequestContext requestContext) throws IOException {
if (requestContext.getUriInfo().getPath().startsWith(pathPrefix)) {
if ("1".equals(requestContext.getUriInfo().getQueryParameters().getFirst("online"))) {
requestContext.setRequestUri(requestContext.getUriInfo().getRequestUriBuilder()
.replaceQueryParam("online", "true")
.build());
}
}
}
}
@@ -15,6 +15,7 @@ import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
import java.security.SecureRandom;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
@@ -22,13 +23,17 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HEAD;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
@@ -36,8 +41,11 @@ import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
@@ -353,11 +361,11 @@ public class AccountController {
storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid)
.ifPresent(smsSender::reportVerificationSucceeded);
Optional<Account> existingAccount = accounts.get(number);
Optional<Account> existingAccount = accounts.getByE164(number);
if (existingAccount.isPresent()) {
verifyRegistrationLock(existingAccount.get(), accountAttributes.getRegistrationLock());
}
if (existingAccount.isPresent()) {
verifyRegistrationLock(existingAccount.get(), accountAttributes.getRegistrationLock());
}
if (availableForTransfer.orElse(false) && existingAccount.map(Account::isTransferSupported).orElse(false)) {
throw new WebApplicationException(Response.status(409).build());
@@ -365,7 +373,8 @@ public class AccountController {
rateLimiters.getVerifyLimiter().clear(number);
Account account = accounts.create(number, password, signalAgent, accountAttributes);
Account account = accounts.create(number, password, signalAgent, accountAttributes,
existingAccount.map(Account::getBadges).orElseGet(ArrayList::new));
{
metricRegistry.meter(name(AccountController.class, "verify", Util.getCountryCode(number))).mark();
@@ -381,7 +390,10 @@ public class AccountController {
.record(Instant.now().toEpochMilli() - storedVerificationCode.get().getTimestamp(), TimeUnit.MILLISECONDS);
}
return new AccountCreationResult(account.getUuid(), account.getNumber(), existingAccount.map(Account::isStorageSupported).orElse(false));
return new AccountCreationResult(account.getUuid(),
account.getNumber(),
account.getPhoneNumberIdentifier(),
existingAccount.map(Account::isStorageSupported).orElse(false));
}
@Timed
@@ -411,7 +423,7 @@ public class AccountController {
storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid)
.ifPresent(smsSender::reportVerificationSucceeded);
final Optional<Account> existingAccount = accounts.get(request.getNumber());
final Optional<Account> existingAccount = accounts.getByE164(request.getNumber());
if (existingAccount.isPresent()) {
verifyRegistrationLock(existingAccount.get(), request.getRegistrationLock());
@@ -543,6 +555,7 @@ public class AccountController {
@PUT
@Path("/attributes/")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@ChangesDeviceEnabledState
public void setAccountAttributes(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth,
@HeaderParam("X-Signal-Agent") String userAgent,
@@ -591,7 +604,10 @@ public class AccountController {
@Path("/whoami")
@Produces(MediaType.APPLICATION_JSON)
public AccountCreationResult whoAmI(@Auth AuthenticatedAccount auth) {
return new AccountCreationResult(auth.getAccount().getUuid(), auth.getAccount().getNumber(), auth.getAccount().isStorageSupported());
return new AccountCreationResult(auth.getAccount().getUuid(),
auth.getAccount().getNumber(),
auth.getAccount().getPhoneNumberIdentifier(),
auth.getAccount().isStorageSupported());
}
@DELETE
@@ -625,6 +641,30 @@ public class AccountController {
return Response.ok().build();
}
@HEAD
@Path("/account/{uuid}")
public Response accountExists(
@HeaderParam("X-Forwarded-For") final String forwardedFor,
@PathParam("uuid") final UUID uuid,
@Context HttpServletRequest request) throws RateLimitExceededException {
// Disallow clients from making authenticated requests to this endpoint
if (StringUtils.isNotBlank(request.getHeader("Authorization"))) {
throw new BadRequestException();
}
final String mostRecentProxy = ForwardedIpUtil.getMostRecentProxy(forwardedFor)
.orElseThrow(() -> new RateLimitExceededException(Duration.ofHours(1)));
rateLimiters.getCheckAccountExistenceLimiter().validate(mostRecentProxy);
final Status status = accounts.getByAccountIdentifier(uuid)
.or(() -> accounts.getByPhoneNumberIdentifier(uuid))
.isPresent() ? Status.OK : Status.NOT_FOUND;
return Response.status(status).build();
}
private void verifyRegistrationLock(final Account existingAccount, @Nullable final String clientRegistrationLock)
throws RateLimitExceededException, WebApplicationException {
@@ -14,6 +14,7 @@ import java.security.InvalidKeyException;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@@ -70,7 +71,8 @@ public class CertificateController {
@Path("/group/{startRedemptionTime}/{endRedemptionTime}")
public GroupCredentials getAuthenticationCredentials(@Auth AuthenticatedAccount auth,
@PathParam("startRedemptionTime") int startRedemptionTime,
@PathParam("endRedemptionTime") int endRedemptionTime) {
@PathParam("endRedemptionTime") int endRedemptionTime,
@QueryParam("identity") Optional<String> identityType) {
if (startRedemptionTime > endRedemptionTime) {
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
@@ -83,10 +85,13 @@ public class CertificateController {
List<GroupCredentials.GroupCredential> credentials = new LinkedList<>();
final UUID identifier = identityType.map(String::toLowerCase).orElse("aci").equals("pni") ?
auth.getAccount().getPhoneNumberIdentifier() :
auth.getAccount().getUuid();
for (int i = startRedemptionTime; i <= endRedemptionTime; i++) {
credentials.add(new GroupCredentials.GroupCredential(
serverZkAuthOperations.issueAuthCredential(auth.getAccount().getUuid(), i)
.serialize(),
serverZkAuthOperations.issueAuthCredential(identifier, i).serialize(),
i));
}
@@ -41,7 +41,7 @@ import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
import org.whispersystems.textsecuregcm.util.Util;
@@ -57,14 +57,14 @@ public class DeviceController {
private final StoredVerificationCodeManager pendingDevices;
private final AccountsManager accounts;
private final MessagesManager messages;
private final KeysDynamoDb keys;
private final Keys keys;
private final RateLimiters rateLimiters;
private final Map<String, Integer> maxDeviceConfiguration;
public DeviceController(StoredVerificationCodeManager pendingDevices,
AccountsManager accounts,
MessagesManager messages,
KeysDynamoDb keys,
Keys keys,
RateLimiters rateLimiters,
Map<String, Integer> maxDeviceConfiguration)
{
@@ -168,7 +168,7 @@ public class DeviceController {
throw new WebApplicationException(Response.status(403).build());
}
Optional<Account> account = accounts.get(number);
Optional<Account> account = accounts.getByE164(number);
if (!account.isPresent()) {
throw new WebApplicationException(Response.status(403).build());
@@ -0,0 +1,49 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import java.util.Base64;
import java.util.UUID;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.util.ByteUtil;
import org.whispersystems.textsecuregcm.util.UUIDUtil;
import org.whispersystems.textsecuregcm.util.Util;
@Path("/v2/directory")
public class DirectoryV2Controller {
private final ExternalServiceCredentialGenerator directoryServiceTokenGenerator;
public DirectoryV2Controller(ExternalServiceCredentialGenerator userTokenGenerator) {
this.directoryServiceTokenGenerator = userTokenGenerator;
}
@Timed
@GET
@Path("/auth")
@Produces(MediaType.APPLICATION_JSON)
public Response getAuthToken(@Auth AuthenticatedAccount auth) {
final UUID uuid = auth.getAccount().getUuid();
final String e164 = auth.getAccount().getNumber();
final long e164AsLong = Long.parseLong(e164, e164.indexOf('+'), e164.length() - 1, 10);
final byte[] uuidAndNumber = ByteUtil.combine(UUIDUtil.toBytes(uuid), Util.longToByteArray(e164AsLong));
final String username = Base64.getEncoder().encodeToString(uuidAndNumber);
final ExternalServiceCredentials credentials = directoryServiceTokenGenerator.generateFor(username);
return Response.ok().entity(credentials).build();
}
}
@@ -159,7 +159,7 @@ public class DonationController {
@Override
public boolean block() {
final Optional<Account> optionalAccount = accountsManager.get(auth.getAccount().getUuid());
final Optional<Account> optionalAccount = accountsManager.getByAccountIdentifier(auth.getAccount().getUuid());
optionalAccount.ifPresent(account -> {
accountsManager.update(account, a -> {
a.addBadge(clock, new AccountBadge(badgeId, receiptExpiration, request.isVisible()));
@@ -11,6 +11,7 @@ import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tags;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -24,6 +25,7 @@ import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@@ -44,7 +46,7 @@ import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.util.Util;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@@ -52,7 +54,7 @@ import org.whispersystems.textsecuregcm.util.Util;
public class KeysController {
private final RateLimiters rateLimiters;
private final KeysDynamoDb keysDynamoDb;
private final Keys keys;
private final AccountsManager accounts;
private final PreKeyRateLimiter preKeyRateLimiter;
@@ -64,11 +66,11 @@ public class KeysController {
private static final String SOURCE_COUNTRY_TAG_NAME = "sourceCountry";
private static final String INTERNATIONAL_TAG_NAME = "international";
public KeysController(RateLimiters rateLimiters, KeysDynamoDb keysDynamoDb, AccountsManager accounts,
public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
PreKeyRateLimiter preKeyRateLimiter,
RateLimitChallengeManager rateLimitChallengeManager) {
this.rateLimiters = rateLimiters;
this.keysDynamoDb = keysDynamoDb;
this.keys = keys;
this.accounts = accounts;
this.preKeyRateLimiter = preKeyRateLimiter;
this.rateLimitChallengeManager = rateLimitChallengeManager;
@@ -76,8 +78,10 @@ public class KeysController {
@GET
@Produces(MediaType.APPLICATION_JSON)
public PreKeyCount getStatus(@Auth AuthenticatedAccount auth) {
int count = keysDynamoDb.getCount(auth.getAccount(), auth.getAuthenticatedDevice().getId());
public PreKeyCount getStatus(@Auth final AuthenticatedAccount auth,
@QueryParam("identity") final Optional<String> identityType) {
int count = keys.getCount(getIdentifier(auth.getAccount(), identityType), auth.getAuthenticatedDevice().getId());
if (count > 0) {
count = count - 1;
@@ -89,7 +93,9 @@ public class KeysController {
@Timed
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void setKeys(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth, @Valid PreKeyState preKeys) {
public void setKeys(@Auth final DisabledPermittedAuthenticatedAccount disabledPermittedAuth,
@Valid final PreKeyState preKeys,
@QueryParam("identity") final Optional<String> identityType) {
Account account = disabledPermittedAuth.getAccount();
Device device = disabledPermittedAuth.getAuthenticatedDevice();
boolean updateAccount = false;
@@ -102,14 +108,27 @@ public class KeysController {
updateAccount = true;
}
final boolean usePhoneNumberIdentity = usePhoneNumberIdentity(identityType);
if (updateAccount) {
account = accounts.update(account, a -> {
a.getDevice(device.getId()).ifPresent(d -> d.setSignedPreKey(preKeys.getSignedPreKey()));
a.setIdentityKey(preKeys.getIdentityKey());
a.getDevice(device.getId()).ifPresent(d -> {
if (usePhoneNumberIdentity) {
d.setPhoneNumberIdentitySignedPreKey(preKeys.getSignedPreKey());
} else {
d.setSignedPreKey(preKeys.getSignedPreKey());
}
});
if (usePhoneNumberIdentity) {
a.setPhoneNumberIdentityKey(preKeys.getIdentityKey());
} else {
a.setIdentityKey(preKeys.getIdentityKey());
}
});
}
keysDynamoDb.store(account, device.getId(), preKeys.getPreKeys());
keys.store(getIdentifier(account, identityType), device.getId(), preKeys.getPreKeys());
}
@Timed
@@ -129,14 +148,19 @@ public class KeysController {
final Optional<Account> account = auth.map(AuthenticatedAccount::getAccount);
Optional<Account> target = accounts.get(targetUuid);
OptionalAccess.verify(account, accessKey, target, deviceId);
final Account target;
{
final Optional<Account> maybeTarget = accounts.getByAccountIdentifier(targetUuid)
.or(() -> accounts.getByPhoneNumberIdentifier(targetUuid));
assert (target.isPresent());
OptionalAccess.verify(account, accessKey, maybeTarget, deviceId);
target = maybeTarget.orElseThrow();
}
{
final String sourceCountryCode = account.map(a -> Util.getCountryCode(a.getNumber())).orElse("0");
final String targetCountryCode = target.map(a -> Util.getCountryCode(a.getNumber())).orElseThrow();
final String targetCountryCode = Util.getCountryCode(target.getNumber());
Metrics.counter(PREKEY_REQUEST_COUNTER_NAME, Tags.of(
SOURCE_COUNTRY_TAG_NAME, sourceCountryCode,
@@ -146,7 +170,7 @@ public class KeysController {
if (account.isPresent()) {
rateLimiters.getPreKeysLimiter().validate(
account.get().getUuid() + "." + auth.get().getAuthenticatedDevice().getId() + "__" + target.get().getUuid()
account.get().getUuid() + "." + auth.get().getAuthenticatedDevice().getId() + "__" + targetUuid
+ "." + deviceId);
try {
@@ -167,12 +191,14 @@ public class KeysController {
}
}
Map<Long, PreKey> preKeysByDeviceId = getLocalKeys(target.get(), deviceId);
final boolean usePhoneNumberIdentity = target.getPhoneNumberIdentifier().equals(targetUuid);
Map<Long, PreKey> preKeysByDeviceId = getLocalKeys(target, deviceId, usePhoneNumberIdentity);
List<PreKeyResponseItem> responseItems = new LinkedList<>();
for (Device device : target.get().getDevices()) {
for (Device device : target.getDevices()) {
if (device.isEnabled() && (deviceId.equals("*") || device.getId() == Long.parseLong(deviceId))) {
SignedPreKey signedPreKey = device.getSignedPreKey();
SignedPreKey signedPreKey = usePhoneNumberIdentity ? device.getPhoneNumberIdentitySignedPreKey() : device.getSignedPreKey();
PreKey preKey = preKeysByDeviceId.get(device.getId());
if (signedPreKey != null || preKey != null) {
@@ -181,48 +207,80 @@ public class KeysController {
}
}
final String identityKey = usePhoneNumberIdentity ? target.getPhoneNumberIdentityKey() : target.getIdentityKey();
if (responseItems.isEmpty()) return Response.status(404).build();
else return Response.ok().entity(new PreKeyResponse(target.get().getIdentityKey(), responseItems)).build();
else return Response.ok().entity(new PreKeyResponse(identityKey, responseItems)).build();
}
@Timed
@PUT
@Path("/signed")
@Consumes(MediaType.APPLICATION_JSON)
public void setSignedKey(@Auth AuthenticatedAccount auth, @Valid SignedPreKey signedPreKey) {
public void setSignedKey(@Auth final AuthenticatedAccount auth,
@Valid final SignedPreKey signedPreKey,
@QueryParam("identity") final Optional<String> identityType) {
Device device = auth.getAuthenticatedDevice();
accounts.updateDevice(auth.getAccount(), device.getId(), d -> d.setSignedPreKey(signedPreKey));
accounts.updateDevice(auth.getAccount(), device.getId(), d -> {
if (usePhoneNumberIdentity(identityType)) {
d.setPhoneNumberIdentitySignedPreKey(signedPreKey);
} else {
d.setSignedPreKey(signedPreKey);
}
});
}
@Timed
@GET
@Path("/signed")
@Produces(MediaType.APPLICATION_JSON)
public Optional<SignedPreKey> getSignedKey(@Auth AuthenticatedAccount auth) {
Device device = auth.getAuthenticatedDevice();
SignedPreKey signedPreKey = device.getSignedPreKey();
public Optional<SignedPreKey> getSignedKey(@Auth final AuthenticatedAccount auth,
@QueryParam("identity") final Optional<String> identityType) {
if (signedPreKey != null) {
return Optional.of(signedPreKey);
} else {
return Optional.empty();
}
Device device = auth.getAuthenticatedDevice();
SignedPreKey signedPreKey = usePhoneNumberIdentity(identityType) ?
device.getPhoneNumberIdentitySignedPreKey() : device.getSignedPreKey();
return Optional.ofNullable(signedPreKey);
}
private Map<Long, PreKey> getLocalKeys(Account destination, String deviceIdSelector) {
try {
if (deviceIdSelector.equals("*")) {
return keysDynamoDb.take(destination);
private static boolean usePhoneNumberIdentity(final Optional<String> identityType) {
return "pni".equals(identityType.map(String::toLowerCase).orElse("aci"));
}
private static UUID getIdentifier(final Account account, final Optional<String> identityType) {
return usePhoneNumberIdentity(identityType) ?
account.getPhoneNumberIdentifier() :
account.getUuid();
}
private Map<Long, PreKey> getLocalKeys(Account destination, String deviceIdSelector, final boolean usePhoneNumberIdentity) {
final Map<Long, PreKey> preKeys;
final UUID identifier = usePhoneNumberIdentity ?
destination.getPhoneNumberIdentifier() :
destination.getUuid();
if (deviceIdSelector.equals("*")) {
preKeys = new HashMap<>();
for (final Device device : destination.getDevices()) {
keys.take(identifier, device.getId()).ifPresent(preKey -> preKeys.put(device.getId(), preKey));
}
} else {
try {
long deviceId = Long.parseLong(deviceIdSelector);
long deviceId = Long.parseLong(deviceIdSelector);
return keysDynamoDb.take(destination, deviceId)
.map(preKey -> Map.of(deviceId, preKey))
.orElse(Collections.emptyMap());
} catch (NumberFormatException e) {
throw new WebApplicationException(Response.status(422).build());
preKeys = keys.take(identifier, deviceId)
.map(preKey -> Map.of(deviceId, preKey))
.orElse(Collections.emptyMap());
} catch (NumberFormatException e) {
throw new WebApplicationException(Response.status(422).build());
}
}
return preKeys;
}
}
@@ -6,14 +6,10 @@ package org.whispersystems.textsecuregcm.controllers;
import static com.codahale.metrics.MetricRegistry.name;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer;
import com.codahale.metrics.annotation.Timed;
import com.google.common.annotations.VisibleForTesting;
import com.google.protobuf.ByteString;
import com.vdurmont.semver4j.Semver;
import io.dropwizard.auth.Auth;
import io.dropwizard.util.DataSize;
import io.micrometer.core.instrument.Counter;
@@ -42,6 +38,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import javax.validation.Valid;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
@@ -93,10 +90,11 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
import org.whispersystems.textsecuregcm.websocket.WebSocketConnection;
@@ -104,14 +102,7 @@ import org.whispersystems.textsecuregcm.websocket.WebSocketConnection;
@Path("/v1/messages")
public class MessageController {
private final Logger logger = LoggerFactory.getLogger(MessageController.class);
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter unidentifiedMeter = metricRegistry.meter(name(getClass(), "delivery", "unidentified"));
private final Meter identifiedMeter = metricRegistry.meter(name(getClass(), "delivery", "identified" ));
private final Meter rejectOver256kibMessageMeter = metricRegistry.meter(name(getClass(), "rejectOver256kibMessage"));
private final Timer sendMessageInternalTimer = metricRegistry.timer(name(getClass(), "sendMessageInternal"));
private final Timer sendCommonMessageInternalTimer = metricRegistry.timer(name(getClass(), "sendCommonMessageInternal"));
private final Histogram outgoingMessageListSizeHistogram = metricRegistry.histogram(name(getClass(), "outgoingMessageListSize"));
private static final Logger logger = LoggerFactory.getLogger(MessageController.class);
private final RateLimiters rateLimiters;
private final MessageSender messageSender;
@@ -124,6 +115,13 @@ public class MessageController {
private final ReportMessageManager reportMessageManager;
private final ExecutorService multiRecipientMessageExecutor;
@VisibleForTesting
static final Semver FIRST_IOS_VERSION_WITH_INCORRECT_ENVELOPE_TYPE = new Semver("5.22.0.32");
@VisibleForTesting
static final Semver IOS_VERSION_WITH_FIXED_ENVELOPE_TYPE = new Semver("5.25.0.0");
private static final String REJECT_OVERSIZE_MESSAGE_COUNTER = name(MessageController.class, "rejectOversizeMessage");
private static final String LEGACY_MESSAGE_SENT_COUNTER = name(MessageController.class, "legacyMessageSent");
private static final String SENT_MESSAGE_COUNTER_NAME = name(MessageController.class, "sentMessages");
private static final String CONTENT_SIZE_DISTRIBUTION_NAME = name(MessageController.class, "messageContentSize");
@@ -135,6 +133,10 @@ public class MessageController {
private static final String SENDER_COUNTRY_TAG_NAME = "senderCountry";
private static final String RATE_LIMIT_REASON_TAG_NAME = "rateLimitReason";
private static final String SENDER_TYPE_IDENTIFIED = "identified";
private static final String SENDER_TYPE_UNIDENTIFIED = "unidentified";
private static final String SENDER_TYPE_SELF = "self";
private static final long MAX_MESSAGE_SIZE = DataSize.kibibytes(256).toBytes();
public MessageController(
@@ -180,14 +182,14 @@ public class MessageController {
final String senderType;
if (source.isPresent() && !source.get().getAccount().getUuid().equals(destinationUuid)) {
identifiedMeter.mark();
senderType = "identified";
} else if (source.isEmpty()) {
unidentifiedMeter.mark();
senderType = "unidentified";
if (source.isPresent()) {
if (source.get().getAccount().isIdentifiedBy(destinationUuid)) {
senderType = SENDER_TYPE_SELF;
} else {
senderType = SENDER_TYPE_IDENTIFIED;
}
} else {
senderType = "self";
senderType = SENDER_TYPE_UNIDENTIFIED;
}
for (final IncomingMessage message : messages.getMessages()) {
@@ -204,18 +206,19 @@ public class MessageController {
Metrics.summary(CONTENT_SIZE_DISTRIBUTION_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).record(contentLength);
if (contentLength > MAX_MESSAGE_SIZE) {
rejectOver256kibMessageMeter.mark();
Metrics.counter(REJECT_OVERSIZE_MESSAGE_COUNTER, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).increment();
return Response.status(Response.Status.REQUEST_ENTITY_TOO_LARGE).build();
}
}
try {
boolean isSyncMessage = source.isPresent() && source.get().getAccount().getUuid().equals(destinationUuid);
boolean isSyncMessage = source.isPresent() && source.get().getAccount().isIdentifiedBy(destinationUuid);
Optional<Account> destination;
if (!isSyncMessage) {
destination = accountsManager.get(destinationUuid);
destination = accountsManager.getByAccountIdentifier(destinationUuid)
.or(() -> accountsManager.getByPhoneNumberIdentifier(destinationUuid));
} else {
destination = source.map(AuthenticatedAccount::getAccount);
}
@@ -223,7 +226,7 @@ public class MessageController {
OptionalAccess.verify(source.map(AuthenticatedAccount::getAccount), accessKey, destination);
assert (destination.isPresent());
if (source.isPresent() && !source.get().getAccount().getUuid().equals(destinationUuid)) {
if (source.isPresent() && !source.get().getAccount().isIdentifiedBy(destinationUuid)) {
final String senderCountryCode = Util.getCountryCode(source.get().getAccount().getNumber());
try {
@@ -268,8 +271,8 @@ public class MessageController {
if (destinationDevice.isPresent()) {
Metrics.counter(SENT_MESSAGE_COUNTER_NAME, tags).increment();
sendMessage(source, destination.get(), destinationDevice.get(), messages.getTimestamp(), messages.isOnline(),
incomingMessage);
sendMessage(source, destination.get(), destinationDevice.get(), destinationUuid, messages.getTimestamp(),
messages.isOnline(), incomingMessage, userAgent);
}
}
@@ -305,13 +308,11 @@ public class MessageController {
@QueryParam("ts") long timestamp,
@Valid MultiRecipientMessage multiRecipientMessage) {
unidentifiedMeter.mark(multiRecipientMessage.getRecipients().length);
Map<UUID, Account> uuidToAccountMap = Arrays.stream(multiRecipientMessage.getRecipients())
.map(Recipient::getUuid)
.distinct()
.collect(Collectors.toUnmodifiableMap(Function.identity(), uuid -> {
Optional<Account> account = accountsManager.get(uuid);
Optional<Account> account = accountsManager.getByAccountIdentifier(uuid);
if (account.isEmpty()) {
throw new WebApplicationException(Status.NOT_FOUND);
}
@@ -362,20 +363,21 @@ public class MessageController {
.build();
}
List<Tag> tags = List.of(
UserAgentTagUtil.getPlatformTag(userAgent),
Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(online)),
Tag.of(SENDER_TYPE_TAG_NAME, "unidentified"));
List<UUID> uuids404 = Collections.synchronizedList(new ArrayList<>());
final Counter counter = Metrics.counter(SENT_MESSAGE_COUNTER_NAME, tags);
try {
final Counter sentMessageCounter = Metrics.counter(SENT_MESSAGE_COUNTER_NAME, Tags.of(
UserAgentTagUtil.getPlatformTag(userAgent),
Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(online)),
Tag.of(SENDER_TYPE_TAG_NAME, SENDER_TYPE_UNIDENTIFIED)));
multiRecipientMessageExecutor.invokeAll(Arrays.stream(multiRecipientMessage.getRecipients())
.map(recipient -> (Callable<Void>) () -> {
Account destinationAccount = uuidToAccountMap.get(recipient.getUuid());
// we asserted this must exist in validateCompleteDeviceList
Device destinationDevice = destinationAccount.getDevice(recipient.getDeviceId()).orElseThrow();
counter.increment();
sentMessageCounter.increment();
try {
sendMessage(destinationAccount, destinationDevice, timestamp, online, recipient,
multiRecipientMessage.getCommonPayload());
@@ -444,8 +446,6 @@ public class MessageController {
userAgent,
false);
outgoingMessageListSizeHistogram.update(outgoingMessages.getMessages().size());
{
String platform;
@@ -512,33 +512,56 @@ public class MessageController {
private void sendMessage(Optional<AuthenticatedAccount> source,
Account destinationAccount,
Device destinationDevice,
UUID destinationUuid,
long timestamp,
boolean online,
IncomingMessage incomingMessage)
IncomingMessage incomingMessage,
String userAgentString)
throws NoSuchUserException {
try (final Timer.Context ignored = sendMessageInternalTimer.time()) {
try {
Optional<byte[]> messageBody = getMessageBody(incomingMessage);
Optional<byte[]> messageContent = getMessageContent(incomingMessage);
Envelope.Builder messageBuilder = Envelope.newBuilder();
messageBuilder.setType(Envelope.Type.forNumber(incomingMessage.getType()))
.setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
.setServerTimestamp(System.currentTimeMillis());
int envelopeTypeNumber = incomingMessage.getType();
if (source.isPresent()) {
messageBuilder.setSource(source.get().getAccount().getNumber())
.setSourceUuid(source.get().getAccount().getUuid().toString())
.setSourceDevice((int) source.get().getAuthenticatedDevice().getId());
// Some versions of the iOS app incorrectly use the reserved envelope type 7 for PLAINTEXT_CONTENT instead of type
// 8. This check can be removed safely after 2022-03-01.
if (envelopeTypeNumber == 7) {
try {
final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString);
if (userAgent.getPlatform() == ClientPlatform.IOS &&
userAgent.getVersion().isGreaterThanOrEqualTo(FIRST_IOS_VERSION_WITH_INCORRECT_ENVELOPE_TYPE) &&
userAgent.getVersion().isLowerThan(IOS_VERSION_WITH_FIXED_ENVELOPE_TYPE)) {
envelopeTypeNumber = Type.PLAINTEXT_CONTENT.getNumber();
}
} catch (final UnrecognizedUserAgentException ignored2) {
}
}
if (messageBody.isPresent()) {
final Envelope.Type envelopeType = Envelope.Type.forNumber(envelopeTypeNumber);
if (envelopeType == null) {
logger.warn("Received bad envelope type {} from {}", incomingMessage.getType(), userAgentString);
throw new BadRequestException();
}
messageBuilder.setType(envelopeType)
.setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
.setServerTimestamp(System.currentTimeMillis())
.setDestinationUuid(destinationUuid.toString());
source.ifPresent(authenticatedAccount ->
messageBuilder.setSource(authenticatedAccount.getAccount().getNumber())
.setSourceUuid(authenticatedAccount.getAccount().getUuid().toString())
.setSourceDevice((int) authenticatedAccount.getAuthenticatedDevice().getId()));
messageBody.ifPresent(bytes -> {
Metrics.counter(LEGACY_MESSAGE_SENT_COUNTER).increment();
messageBuilder.setLegacyMessage(ByteString.copyFrom(messageBody.get()));
}
});
if (messageContent.isPresent()) {
messageBuilder.setContent(ByteString.copyFrom(messageContent.get()));
}
messageContent.ifPresent(bytes -> messageBuilder.setContent(ByteString.copyFrom(bytes)));
messageSender.sendMessage(destinationAccount, destinationDevice, messageBuilder.build(), online);
} catch (NotPushRegisteredException e) {
@@ -547,9 +570,13 @@ public class MessageController {
}
}
private void sendMessage(Account destinationAccount, Device destinationDevice, long timestamp, boolean online,
Recipient recipient, byte[] commonPayload) throws NoSuchUserException {
try (final Timer.Context ignored = sendCommonMessageInternalTimer.time()) {
private void sendMessage(Account destinationAccount,
Device destinationDevice,
long timestamp,
boolean online,
Recipient recipient,
byte[] commonPayload) throws NoSuchUserException {
try {
Envelope.Builder messageBuilder = Envelope.newBuilder();
long serverTimestamp = System.currentTimeMillis();
byte[] recipientKeyMaterial = recipient.getPerRecipientKeyMaterial();
@@ -563,7 +590,8 @@ public class MessageController {
.setType(Type.UNIDENTIFIED_SENDER)
.setTimestamp(timestamp == 0 ? serverTimestamp : timestamp)
.setServerTimestamp(serverTimestamp)
.setContent(ByteString.copyFrom(payload));
.setContent(ByteString.copyFrom(payload))
.setDestinationUuid(destinationAccount.getUuid().toString());
messageSender.sendMessage(destinationAccount, destinationDevice, messageBuilder.build(), online);
} catch (NotPushRegisteredException e) {
@@ -19,7 +19,6 @@ import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -40,6 +39,8 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tags;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
@@ -65,6 +66,7 @@ import org.whispersystems.textsecuregcm.entities.Profile;
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
import org.whispersystems.textsecuregcm.entities.UserCapabilities;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
import org.whispersystems.textsecuregcm.s3.PolicySigner;
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
import org.whispersystems.textsecuregcm.storage.Account;
@@ -76,10 +78,11 @@ import org.whispersystems.textsecuregcm.storage.UsernamesManager;
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
import org.whispersystems.textsecuregcm.util.ExactlySize;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.Util;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@Path("/v1/profile")
public class ProfileController {
@@ -102,6 +105,8 @@ public class ProfileController {
private final S3Client s3client;
private final String bucket;
private static final String LEGACY_GET_PROFILE_COUNTER_NAME = name(ProfileController.class, "legacyGetProfileByPlatform");
public ProfileController(
Clock clock,
RateLimiters rateLimiters,
@@ -137,13 +142,14 @@ public class ProfileController {
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response setProfile(@Auth AuthenticatedAccount auth, @Valid CreateProfileRequest request) {
final Set<String> allowedPaymentsCountryCodes =
dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration().getAllowedCountryCodes();
if (StringUtils.isNotBlank(request.getPaymentAddress())) {
final boolean hasDisallowedPrefix =
dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration().getDisallowedPrefixes().stream()
.anyMatch(prefix -> auth.getAccount().getNumber().startsWith(prefix));
if (StringUtils.isNotBlank(request.getPaymentAddress()) &&
!allowedPaymentsCountryCodes.contains(Util.getCountryCode(auth.getAccount().getNumber()))) {
return Response.status(Status.FORBIDDEN).build();
if (hasDisallowedPrefix) {
return Response.status(Status.FORBIDDEN).build();
}
}
Optional<VersionedProfile> currentProfile = profilesManager.get(auth.getAccount().getUuid(), request.getVersion());
@@ -251,7 +257,7 @@ public class ProfileController {
isSelf = uuid.equals(authedUuid);
}
Optional<Account> accountProfile = accountsManager.get(uuid);
Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(uuid);
OptionalAccess.verify(requestAccount, accessKey, accountProfile);
assert(accountProfile.isPresent());
@@ -316,7 +322,7 @@ public class ProfileController {
final boolean isSelf = auth.getAccount().getUuid().equals(uuid.get());
Optional<Account> accountProfile = accountsManager.get(uuid.get());
Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(uuid.get());
if (accountProfile.isEmpty()) {
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
@@ -368,7 +374,7 @@ public class ProfileController {
@PUT
@Produces(MediaType.APPLICATION_JSON)
@Path("/name/{name}")
public void setProfile(@Auth AuthenticatedAccount auth,
public void setLegacyProfile(@Auth AuthenticatedAccount auth,
@PathParam("name") @ExactlySize(value = {72, 108}, payload = {Unwrapping.Unwrap.class}) Optional<String> name) {
accountsManager.update(auth.getAccount(), a -> a.setProfileName(name.orElse(null)));
}
@@ -378,7 +384,7 @@ public class ProfileController {
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/{identifier}")
public Profile getProfile(
public Profile getLegacyProfile(
@Auth Optional<AuthenticatedAccount> auth,
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
@Context ContainerRequestContext containerRequestContext,
@@ -391,6 +397,8 @@ public class ProfileController {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
Metrics.counter(LEGACY_GET_PROFILE_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).increment();
boolean isSelf = false;
if (auth.isPresent()) {
UUID authedUuid = auth.get().getAccount().getUuid();
@@ -398,7 +406,7 @@ public class ProfileController {
isSelf = authedUuid.equals(identifier);
}
Optional<Account> accountProfile = accountsManager.get(identifier);
Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(identifier);
OptionalAccess.verify(auth.map(AuthenticatedAccount::getAccount), accessKey, accountProfile);
Optional<String> username = usernamesManager.get(accountProfile.get().getUuid());
@@ -427,7 +435,7 @@ public class ProfileController {
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/form/avatar")
public ProfileAvatarUploadAttributes getAvatarUploadForm(@Auth AuthenticatedAccount auth) {
public ProfileAvatarUploadAttributes getLegacyAvatarUploadForm(@Auth AuthenticatedAccount auth) {
String previousAvatar = auth.getAccount().getAvatar();
String objectName = generateAvatarObjectName();
ProfileAvatarUploadAttributes profileAvatarUploadAttributes = generateAvatarUploadForm(objectName);
@@ -21,7 +21,6 @@ import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -55,6 +54,7 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang3.StringUtils;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.receipts.ReceiptCredentialRequest;
@@ -562,10 +562,13 @@ public class SubscriptionController {
return stripeManager.getPaymentIntent(request.getPaymentIntentId())
.thenCompose(paymentIntent -> {
if (paymentIntent == null) {
throw new WebApplicationException(Status.NOT_FOUND);
}
if (StringUtils.equalsIgnoreCase("processing", paymentIntent.getStatus())) {
throw new WebApplicationException(Status.NO_CONTENT);
}
if (!"succeeded".equalsIgnoreCase(paymentIntent.getStatus())) {
throw new WebApplicationException(Status.NO_CONTENT);
if (!StringUtils.equalsIgnoreCase("succeeded", paymentIntent.getStatus())) {
throw new WebApplicationException(Status.PAYMENT_REQUIRED);
}
ReceiptCredentialRequest receiptCredentialRequest;
try {
@@ -603,6 +606,7 @@ public class SubscriptionController {
private final boolean cancelAtPeriodEnd;
private final String currency;
private final BigDecimal amount;
private final String status;
public Subscription(
@JsonProperty("level") long level,
@@ -611,7 +615,8 @@ public class SubscriptionController {
@JsonProperty("active") boolean active,
@JsonProperty("cancelAtPeriodEnd") boolean cancelAtPeriodEnd,
@JsonProperty("currency") String currency,
@JsonProperty("amount") BigDecimal amount) {
@JsonProperty("amount") BigDecimal amount,
@JsonProperty("status") String status) {
this.level = level;
this.billingCycleAnchor = billingCycleAnchor;
this.endOfCurrentPeriod = endOfCurrentPeriod;
@@ -619,6 +624,7 @@ public class SubscriptionController {
this.cancelAtPeriodEnd = cancelAtPeriodEnd;
this.currency = currency;
this.amount = amount;
this.status = status;
}
public long getLevel() {
@@ -648,6 +654,10 @@ public class SubscriptionController {
public BigDecimal getAmount() {
return amount;
}
public String getStatus() {
return status;
}
}
private final Subscription subscription;
@@ -688,7 +698,8 @@ public class SubscriptionController {
Objects.equals(subscription.getStatus(), "active"),
subscription.getCancelAtPeriodEnd(),
price.getCurrency().toUpperCase(Locale.ROOT),
price.getUnitAmountDecimal()
price.getUnitAmountDecimal(),
subscription.getStatus()
))).build())));
});
}
@@ -730,7 +741,7 @@ public class SubscriptionController {
@Path("/{subscriberId}/receipt_credentials")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public CompletableFuture<Response> getReceiptCredentials(
public CompletableFuture<Response> createSubscriptionReceiptCredentials(
@Auth Optional<AuthenticatedAccount> authenticatedAccount,
@PathParam("subscriberId") String subscriberId,
@Valid GetReceiptCredentialsRequest request) {
@@ -739,7 +750,7 @@ public class SubscriptionController {
.thenApply(this::requireRecordFromGetResult)
.thenCompose(record -> {
if (record.subscriptionId == null) {
return CompletableFuture.completedFuture(Response.noContent().build());
return CompletableFuture.completedFuture(Response.status(Status.NOT_FOUND).build());
}
ReceiptCredentialRequest receiptCredentialRequest;
try {
@@ -747,29 +758,20 @@ public class SubscriptionController {
} catch (InvalidInputException e) {
throw new BadRequestException("invalid receipt credential request", e);
}
return stripeManager.getPaidInvoicesForSubscription(record.subscriptionId, requestData.now)
.thenCompose(invoices -> checkNextInvoice(invoices.iterator(), record.subscriptionId))
.thenCompose(receipt -> {
if (receipt == null) {
return CompletableFuture.completedFuture(null);
}
return issuedReceiptsManager.recordIssuance(
receipt.getInvoiceLineItemId(), receiptCredentialRequest, requestData.now)
.thenApply(unused -> receipt);
})
return stripeManager.getLatestInvoiceForSubscription(record.subscriptionId)
.thenCompose(invoice -> convertInvoiceToReceipt(invoice, record.subscriptionId))
.thenCompose(receipt -> issuedReceiptsManager.recordIssuance(
receipt.getInvoiceLineItemId(), receiptCredentialRequest, requestData.now)
.thenApply(unused -> receipt))
.thenApply(receipt -> {
if (receipt == null) {
return Response.noContent().build();
} else {
ReceiptCredentialResponse receiptCredentialResponse;
try {
receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential(
receiptCredentialRequest, receipt.getExpiration().getEpochSecond(), receipt.getLevel());
} catch (VerificationFailedException e) {
throw new BadRequestException("receipt credential request failed verification", e);
}
return Response.ok(new GetReceiptCredentialsResponse(receiptCredentialResponse.serialize())).build();
ReceiptCredentialResponse receiptCredentialResponse;
try {
receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential(
receiptCredentialRequest, receipt.getExpiration().getEpochSecond(), receipt.getLevel());
} catch (VerificationFailedException e) {
throw new BadRequestException("receipt credential request failed verification", e);
}
return Response.ok(new GetReceiptCredentialsResponse(receiptCredentialResponse.serialize())).build();
});
});
}
@@ -799,35 +801,46 @@ public class SubscriptionController {
}
}
private CompletableFuture<Receipt> checkNextInvoice(Iterator<Invoice> invoiceIterator, String subscriptionId) {
if (!invoiceIterator.hasNext()) {
return null;
private CompletableFuture<Receipt> convertInvoiceToReceipt(Invoice latestSubscriptionInvoice, String subscriptionId) {
if (latestSubscriptionInvoice == null) {
throw new WebApplicationException(Status.NO_CONTENT);
}
if (StringUtils.equalsIgnoreCase("open", latestSubscriptionInvoice.getStatus())) {
throw new WebApplicationException(Status.NO_CONTENT);
}
if (!StringUtils.equalsIgnoreCase("paid", latestSubscriptionInvoice.getStatus())) {
throw new WebApplicationException(Status.PAYMENT_REQUIRED);
}
Invoice invoice = invoiceIterator.next();
return stripeManager.getInvoiceLineItemsForInvoice(invoice).thenCompose(invoiceLineItems -> {
return stripeManager.getInvoiceLineItemsForInvoice(latestSubscriptionInvoice).thenCompose(invoiceLineItems -> {
Collection<InvoiceLineItem> subscriptionLineItems = invoiceLineItems.stream()
.filter(invoiceLineItem -> Objects.equals("subscription", invoiceLineItem.getType()))
.collect(Collectors.toList());
if (subscriptionLineItems.isEmpty()) {
return checkNextInvoice(invoiceIterator, subscriptionId);
throw new IllegalStateException("latest subscription invoice has no subscription line items; subscriptionId="
+ subscriptionId + "; invoiceId=" + latestSubscriptionInvoice.getId());
}
if (subscriptionLineItems.size() > 1) {
throw new IllegalStateException("invoice has more than one subscription; subscriptionId=" + subscriptionId
+ "; count=" + subscriptionLineItems.size());
throw new IllegalStateException(
"latest subscription invoice has too many subscription line items; subscriptionId=" + subscriptionId
+ "; invoiceId=" + latestSubscriptionInvoice.getId() + "; count=" + subscriptionLineItems.size());
}
InvoiceLineItem subscriptionLineItem = subscriptionLineItems.stream().findAny().get();
return stripeManager.getProductForPrice(subscriptionLineItem.getPrice().getId()).thenApply(product -> new Receipt(
Instant.ofEpochSecond(subscriptionLineItem.getPeriod().getEnd())
.plus(subscriptionConfiguration.getBadgeGracePeriod())
.truncatedTo(ChronoUnit.DAYS)
.plus(1, ChronoUnit.DAYS),
stripeManager.getLevelForProduct(product),
subscriptionLineItem.getId()));
return getReceiptForSubscriptionInvoiceLineItem(subscriptionLineItem);
});
}
private CompletableFuture<Receipt> getReceiptForSubscriptionInvoiceLineItem(InvoiceLineItem subscriptionLineItem) {
return stripeManager.getProductForPrice(subscriptionLineItem.getPrice().getId()).thenApply(product -> new Receipt(
Instant.ofEpochSecond(subscriptionLineItem.getPeriod().getEnd())
.plus(subscriptionConfiguration.getBadgeGracePeriod())
.truncatedTo(ChronoUnit.DAYS)
.plus(1, ChronoUnit.DAYS),
stripeManager.getLevelForProduct(product),
subscriptionLineItem.getId()));
}
private SubscriptionManager.Record requireRecordFromGetResult(SubscriptionManager.GetResult getResult) {
if (getResult == GetResult.PASSWORD_MISMATCH) {
throw new ForbiddenException("subscriberId mismatch");
@@ -23,32 +23,34 @@ import org.whispersystems.textsecuregcm.util.Util;
@Path("/v1/voice/")
public class VoiceVerificationController {
private static final String PLAY_TWIML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<Response>\n" +
" <Play>%s</Play>\n" +
" <Play>%s</Play>\n" +
" <Play>%s</Play>\n" +
" <Play>%s</Play>\n" +
" <Play>%s</Play>\n" +
" <Play>%s</Play>\n" +
" <Play>%s</Play>\n" +
" <Pause length=\"1\"/>\n" +
" <Play>%s</Play>\n" +
" <Play>%s</Play>\n" +
" <Play>%s</Play>\n" +
" <Play>%s</Play>\n" +
" <Play>%s</Play>\n" +
" <Play>%s</Play>\n" +
" <Play>%s</Play>\n" +
" <Pause length=\"1\"/>\n" +
" <Play>%s</Play>\n" +
" <Play>%s</Play>\n" +
" <Play>%s</Play>\n" +
" <Play>%s</Play>\n" +
" <Play>%s</Play>\n" +
" <Play>%s</Play>\n" +
" <Play>%s</Play>\n" +
"</Response>";
private static final String PLAY_TWIML = """
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Play>%s</Play>
<Play>%s</Play>
<Play>%s</Play>
<Play>%s</Play>
<Play>%s</Play>
<Play>%s</Play>
<Play>%s</Play>
<Pause length="1"/>
<Play>%s</Play>
<Play>%s</Play>
<Play>%s</Play>
<Play>%s</Play>
<Play>%s</Play>
<Play>%s</Play>
<Play>%s</Play>
<Pause length="1"/>
<Play>%s</Play>
<Play>%s</Play>
<Play>%s</Play>
<Play>%s</Play>
<Play>%s</Play>
<Play>%s</Play>
<Play>%s</Play>
</Response>
""";
private static final String DEFAULT_LOCALE = "en-US";
@@ -8,6 +8,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import javax.validation.constraints.Size;
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
import org.whispersystems.textsecuregcm.util.ExactlySize;
public class AccountAttributes {
@@ -25,6 +26,7 @@ public class AccountAttributes {
private String registrationLock;
@JsonProperty
@ExactlySize({0, 16})
private byte[] unidentifiedAccessKey;
@JsonProperty
@@ -80,4 +82,9 @@ public class AccountAttributes {
public boolean isDiscoverableByPhoneNumber() {
return discoverableByPhoneNumber;
}
@VisibleForTesting
public void setUnidentifiedAccessKey(final byte[] unidentifiedAccessKey) {
this.unidentifiedAccessKey = unidentifiedAccessKey;
}
}
@@ -8,29 +8,25 @@ package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import java.util.UUID;
public class AccountCreationResult {
@JsonProperty
private final UUID uuid;
@JsonProperty
private final String number;
@JsonProperty
private final UUID pni;
private final boolean storageCapable;
@JsonCreator
public AccountCreationResult(
@JsonProperty("uuid") final UUID uuid,
@JsonProperty("number") final String number,
@JsonProperty("pni") final UUID pni,
@JsonProperty("storageCapable") final boolean storageCapable) {
this.uuid = uuid;
this.number = number;
this.pni = pni;
this.storageCapable = storageCapable;
}
@@ -42,6 +38,10 @@ public class AccountCreationResult {
return number;
}
public UUID getPni() {
return pni;
}
public boolean isStorageCapable() {
return storageCapable;
}
@@ -72,6 +72,15 @@ public class Badge {
return svgs;
}
/**
* Workaround for old Android builds that expect this field to exist but don't care it's an empty string.
*/
@Deprecated
@JsonProperty
public String getImageUrl() {
return "";
}
@Override
public boolean equals(final Object o) {
if (this == o) {
@@ -14,13 +14,11 @@ import javax.validation.constraints.NotEmpty;
public class BadgeSvg {
private final String light;
private final String dark;
private final String transparent;
@JsonCreator
public BadgeSvg(
@JsonProperty("light") final String light,
@JsonProperty("dark") final String dark,
@JsonProperty("transparent") final String transparent) {
@JsonProperty("dark") final String dark) {
if (Strings.isNullOrEmpty(light)) {
throw new IllegalArgumentException("light cannot be empty");
}
@@ -29,10 +27,6 @@ public class BadgeSvg {
throw new IllegalArgumentException("dark cannot be empty");
}
this.dark = dark;
if (Strings.isNullOrEmpty(transparent)) {
throw new IllegalArgumentException("transparent cannot be empty");
}
this.transparent = transparent;
}
@NotEmpty
@@ -45,11 +39,6 @@ public class BadgeSvg {
return dark;
}
@NotEmpty
public String getTransparent() {
return transparent;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
@@ -60,13 +49,12 @@ public class BadgeSvg {
}
BadgeSvg badgeSvg = (BadgeSvg) o;
return Objects.equals(light, badgeSvg.light)
&& Objects.equals(dark, badgeSvg.dark)
&& Objects.equals(transparent, badgeSvg.transparent);
&& Objects.equals(dark, badgeSvg.dark);
}
@Override
public int hashCode() {
return Objects.hash(light, dark, transparent);
return Objects.hash(light, dark);
}
@Override
@@ -74,7 +62,6 @@ public class BadgeSvg {
return "BadgeSvg{" +
"light='" + light + '\'' +
", dark='" + dark + '\'' +
", transparent='" + transparent + '\'' +
'}';
}
}
@@ -5,60 +5,39 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Arrays;
import java.util.Objects;
import java.util.UUID;
public class OutgoingMessageEntity {
@JsonIgnore
private long id;
private final UUID guid;
private final int type;
private final String relay;
private final long timestamp;
private final String source;
private final UUID sourceUuid;
private final int sourceDevice;
private final UUID destinationUuid;
private final byte[] message;
private final byte[] content;
private final long serverTimestamp;
@JsonIgnore
private boolean cached;
@JsonProperty
private UUID guid;
@JsonProperty
private int type;
@JsonProperty
private String relay;
@JsonProperty
private long timestamp;
@JsonProperty
private String source;
@JsonProperty
private UUID sourceUuid;
@JsonProperty
private int sourceDevice;
@JsonProperty
private byte[] message;
@JsonProperty
private byte[] content;
@JsonProperty
private long serverTimestamp;
public OutgoingMessageEntity() {}
public OutgoingMessageEntity(long id, boolean cached,
UUID guid, int type, String relay, long timestamp,
String source, UUID sourceUuid, int sourceDevice,
byte[] message, byte[] content, long serverTimestamp)
@JsonCreator
public OutgoingMessageEntity(@JsonProperty("guid") final UUID guid,
@JsonProperty("type") final int type,
@JsonProperty("relay") final String relay,
@JsonProperty("timestamp") final long timestamp,
@JsonProperty("source") final String source,
@JsonProperty("sourceUuid") final UUID sourceUuid,
@JsonProperty("sourceDevice") final int sourceDevice,
@JsonProperty("destinationUuid") final UUID destinationUuid,
@JsonProperty("message") final byte[] message,
@JsonProperty("content") final byte[] content,
@JsonProperty("serverTimestamp") final long serverTimestamp)
{
this.id = id;
this.cached = cached;
this.guid = guid;
this.type = type;
this.relay = relay;
@@ -66,6 +45,7 @@ public class OutgoingMessageEntity {
this.source = source;
this.sourceUuid = sourceUuid;
this.sourceDevice = sourceDevice;
this.destinationUuid = destinationUuid;
this.message = message;
this.content = content;
this.serverTimestamp = serverTimestamp;
@@ -99,6 +79,10 @@ public class OutgoingMessageEntity {
return sourceDevice;
}
public UUID getDestinationUuid() {
return destinationUuid;
}
public byte[] getMessage() {
return message;
}
@@ -107,16 +91,6 @@ public class OutgoingMessageEntity {
return content;
}
@JsonIgnore
public long getId() {
return id;
}
@JsonIgnore
public boolean isCached() {
return cached;
}
public long getServerTimestamp() {
return serverTimestamp;
}
@@ -126,23 +100,22 @@ public class OutgoingMessageEntity {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final OutgoingMessageEntity that = (OutgoingMessageEntity)o;
return id == that.id &&
cached == that.cached &&
type == that.type &&
return type == that.type &&
timestamp == that.timestamp &&
sourceDevice == that.sourceDevice &&
serverTimestamp == that.serverTimestamp &&
Objects.equals(guid, that.guid) &&
guid.equals(that.guid) &&
Objects.equals(relay, that.relay) &&
Objects.equals(source, that.source) &&
Objects.equals(sourceUuid, that.sourceUuid) &&
destinationUuid.equals(that.destinationUuid) &&
Arrays.equals(message, that.message) &&
Arrays.equals(content, that.content);
}
@Override
public int hashCode() {
int result = Objects.hash(id, cached, guid, type, relay, timestamp, source, sourceUuid, sourceDevice, serverTimestamp);
int result = Objects.hash(guid, type, relay, timestamp, source, sourceUuid, sourceDevice, destinationUuid, serverTimestamp);
result = 31 * result + Arrays.hashCode(message);
result = 31 * result + Arrays.hashCode(content);
return result;
@@ -39,6 +39,8 @@ public class RateLimiters {
private final RateLimiter usernameLookupLimiter;
private final RateLimiter usernameSetLimiter;
private final RateLimiter checkAccountExistenceLimiter;
private final AtomicReference<CardinalityRateLimiter> unsealedSenderCardinalityLimiter;
private final AtomicReference<RateLimiter> unsealedIpLimiter;
private final AtomicReference<RateLimiter> rateLimitResetLimiter;
@@ -127,6 +129,10 @@ public class RateLimiters {
config.getUsernameSet().getBucketSize(),
config.getUsernameSet().getLeakRatePerMinute());
this.checkAccountExistenceLimiter = new RateLimiter(cacheCluster, "checkAccountExistence",
config.getCheckAccountExistence().getBucketSize(),
config.getCheckAccountExistence().getLeakRatePerMinute());
this.dailyPreKeysLimiter = new AtomicReference<>(createDailyPreKeysLimiter(cacheCluster, dynamicConfig.getConfiguration().getLimits().getDailyPreKeys()));
this.unsealedSenderCardinalityLimiter = new AtomicReference<>(createUnsealedSenderCardinalityLimiter(cacheCluster, dynamicConfig.getConfiguration().getLimits().getUnsealedSenderNumber()));
@@ -287,6 +293,10 @@ public class RateLimiters {
return usernameSetLimiter;
}
public RateLimiter getCheckAccountExistenceLimiter() {
return checkAccountExistenceLimiter;
}
private CardinalityRateLimiter createUnsealedSenderCardinalityLimiter(FaultTolerantRedisCluster cacheCluster, CardinalityRateLimitConfiguration configuration) {
return new CardinalityRateLimiter(cacheCluster, "unsealedSender", configuration.getTtl(), configuration.getMaxCardinality());
}
@@ -0,0 +1,32 @@
/*
* Copyright 2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.mappers;
import java.util.concurrent.CompletionException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.Providers;
@Provider
public class CompletionExceptionMapper implements ExceptionMapper<CompletionException> {
@Context
private Providers providers;
@Override
public Response toResponse(final CompletionException exception) {
final Throwable cause = exception.getCause();
if (cause != null) {
final Class type = cause.getClass();
return providers.getExceptionMapper(type).toResponse(cause);
}
return Response.serverError().build();
}
}
@@ -24,6 +24,6 @@ public class CpuUsageGauge extends CachedGauge<Integer> {
@Override
protected Integer loadValue() {
return (int) Math.ceil(operatingSystemMXBean.getSystemCpuLoad() * 100);
return (int) Math.ceil(operatingSystemMXBean.getCpuLoad() * 100);
}
}
@@ -7,7 +7,6 @@ package org.whispersystems.textsecuregcm.metrics;
import com.codahale.metrics.Gauge;
import com.sun.management.OperatingSystemMXBean;
import java.lang.management.ManagementFactory;
public class FreeMemoryGauge implements Gauge<Long> {
@@ -21,6 +20,6 @@ public class FreeMemoryGauge implements Gauge<Long> {
@Override
public Long getValue() {
return operatingSystemMXBean.getFreePhysicalMemorySize();
return operatingSystemMXBean.getFreeMemorySize();
}
}
@@ -6,17 +6,34 @@
package org.whispersystems.textsecuregcm.metrics;
import com.codahale.metrics.MetricRegistry;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.annotations.VisibleForTesting;
import com.vdurmont.semver4j.Semver;
import io.lettuce.core.SetArgs;
import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tags;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
import io.micrometer.core.instrument.Tag;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
/**
* Measures and records the latency between sending a push notification to a device and that device draining its queue
@@ -28,48 +45,139 @@ import java.util.concurrent.TimeUnit;
* device and records the time elapsed since the push notification timestamp as a latency observation.
*/
public class PushLatencyManager {
private static final String TIMER_NAME = MetricRegistry.name(PushLatencyManager.class, "latency");
private static final int TTL = (int)Duration.ofDays(1).toSeconds();
private final FaultTolerantRedisCluster redisCluster;
private final FaultTolerantRedisCluster redisCluster;
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
public PushLatencyManager(final FaultTolerantRedisCluster redisCluster) {
this.redisCluster = redisCluster;
private final Clock clock;
private static final String TIMER_NAME = MetricRegistry.name(PushLatencyManager.class, "latency");
private static final int TTL = (int) Duration.ofDays(1).toSeconds();
private static final Logger log = LoggerFactory.getLogger(PushLatencyManager.class);
@VisibleForTesting
enum PushType {
STANDARD,
VOIP
}
@VisibleForTesting
static class PushRecord {
private final Instant timestamp;
@Nullable
private final PushType pushType;
@JsonCreator
PushRecord(@JsonProperty("timestamp") final Instant timestamp,
@JsonProperty("pushType") @Nullable final PushType pushType) {
this.timestamp = timestamp;
this.pushType = pushType;
}
public void recordPushSent(final UUID accountUuid, final long deviceId) {
recordPushSent(accountUuid, deviceId, System.currentTimeMillis());
public Instant getTimestamp() {
return timestamp;
}
@VisibleForTesting
void recordPushSent(final UUID accountUuid, final long deviceId, final long currentTime) {
redisCluster.useCluster(connection ->
connection.async().set(getFirstUnacknowledgedPushKey(accountUuid, deviceId), String.valueOf(currentTime), SetArgs.Builder.nx().ex(TTL)));
@Nullable
public PushType getPushType() {
return pushType;
}
}
public void recordQueueRead(final UUID accountUuid, final long deviceId, final String userAgent) {
getLatencyAndClearTimestamp(accountUuid, deviceId, System.currentTimeMillis()).thenAccept(latency -> {
if (latency != null) {
Metrics.timer(TIMER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).record(latency, TimeUnit.MILLISECONDS);
public PushLatencyManager(final FaultTolerantRedisCluster redisCluster,
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
this(redisCluster, dynamicConfigurationManager, Clock.systemUTC());
}
@VisibleForTesting
PushLatencyManager(final FaultTolerantRedisCluster redisCluster,
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
final Clock clock) {
this.redisCluster = redisCluster;
this.dynamicConfigurationManager = dynamicConfigurationManager;
this.clock = clock;
}
public void recordPushSent(final UUID accountUuid, final long deviceId, final boolean isVoip) {
try {
final String recordJson = SystemMapper.getMapper().writeValueAsString(
new PushRecord(Instant.now(clock), isVoip ? PushType.VOIP : PushType.STANDARD));
redisCluster.useCluster(connection ->
connection.async().set(getFirstUnacknowledgedPushKey(accountUuid, deviceId),
recordJson,
SetArgs.Builder.nx().ex(TTL)));
} catch (final JsonProcessingException e) {
// This should never happen
log.error("Failed to write push latency record JSON", e);
}
}
public void recordQueueRead(final UUID accountUuid, final long deviceId, final String userAgentString) {
takePushRecord(accountUuid, deviceId).thenAccept(pushRecord -> {
if (pushRecord != null) {
final Duration latency = Duration.between(pushRecord.getTimestamp(), Instant.now());
final List<Tag> tags = new ArrayList<>(2);
tags.add(UserAgentTagUtil.getPlatformTag(userAgentString));
if (pushRecord.getPushType() != null) {
tags.add(Tag.of("pushType", pushRecord.getPushType().name().toLowerCase()));
}
try {
final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString);
final Set<Semver> instrumentedVersions =
dynamicConfigurationManager.getConfiguration().getPushLatencyConfiguration().getInstrumentedVersions()
.getOrDefault(userAgent.getPlatform(), Collections.emptySet());
if (instrumentedVersions.contains(userAgent.getVersion())) {
tags.add(Tag.of("clientVersion", userAgent.getVersion().toString()));
}
} catch (UnrecognizedUserAgentException ignored) {
}
Metrics.timer(TIMER_NAME, tags).record(latency);
}
});
}
@VisibleForTesting
CompletableFuture<PushRecord> takePushRecord(final UUID accountUuid, final long deviceId) {
final String key = getFirstUnacknowledgedPushKey(accountUuid, deviceId);
return redisCluster.withCluster(connection -> {
final CompletableFuture<PushRecord> getFuture = connection.async().get(key).toCompletableFuture()
.thenApply(recordJson -> {
if (StringUtils.isNotEmpty(recordJson)) {
try {
return SystemMapper.getMapper().readValue(recordJson, PushRecord.class);
} catch (JsonProcessingException e) {
return null;
}
} else {
return null;
}
});
}
});
@VisibleForTesting
CompletableFuture<Long> getLatencyAndClearTimestamp(final UUID accountUuid, final long deviceId, final long currentTimeMillis) {
final String key = getFirstUnacknowledgedPushKey(accountUuid, deviceId);
getFuture.whenComplete((record, cause) -> {
if (cause == null) {
connection.async().del(key);
}
});
return redisCluster.withCluster(connection -> {
final RedisAdvancedClusterAsyncCommands<String, String> commands = connection.async();
return getFuture;
});
}
final CompletableFuture<String> getFuture = commands.get(key).toCompletableFuture();
commands.del(key);
return getFuture.thenApply(timestampString -> timestampString != null ? currentTimeMillis - Long.parseLong(timestampString, 10) : null);
});
}
private static String getFirstUnacknowledgedPushKey(final UUID accountUuid, final long deviceId) {
return "push_latency::" + accountUuid.toString() + "::" + deviceId;
}
private static String getFirstUnacknowledgedPushKey(final UUID accountUuid, final long deviceId) {
return "push_latency::v2::" + accountUuid.toString() + "::" + deviceId;
}
}
@@ -125,7 +125,7 @@ public class APNSender implements Managed {
private void handleUnregisteredUser(String registrationId, UUID uuid, long deviceId) {
// logger.info("Got APN Unregistered: " + number + "," + deviceId);
Optional<Account> account = accountsManager.get(uuid);
Optional<Account> account = accountsManager.getByAccountIdentifier(uuid);
if (account.isEmpty()) {
logger.info("No account found: {}", uuid);
@@ -99,7 +99,7 @@ public class ApnFallbackManager implements Managed {
final Optional<Account> maybeAccount = separated.map(Pair::first)
.map(UUID::fromString)
.flatMap(accountsManager::get);
.flatMap(accountsManager::getByAccountIdentifier);
final Optional<Device> maybeDevice = separated.map(Pair::second)
.flatMap(deviceId -> maybeAccount.flatMap(account -> account.getDevice(deviceId)));
@@ -139,7 +139,7 @@ public class GCMSender {
}
private Optional<Account> getAccountForEvent(GcmMessage message) {
Optional<Account> account = message.getUuid().flatMap(accountsManager::get);
Optional<Account> account = message.getUuid().flatMap(accountsManager::getByAccountIdentifier);
if (account.isPresent()) {
Optional<Device> device = account.get().getDevice(message.getDeviceId());
@@ -124,22 +124,24 @@ public class MessageSender implements Managed {
gcmSender.sendMessage(gcmMessage);
RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId()));
RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId(), false));
}
private void sendApnNotification(Account account, Device device) {
ApnMessage apnMessage;
if (!Util.isEmpty(device.getVoipApnId())) {
apnMessage = new ApnMessage(device.getVoipApnId(), account.getUuid(), device.getId(), true, Type.NOTIFICATION, Optional.empty());
final boolean useVoip = !Util.isEmpty(device.getVoipApnId());
if (useVoip) {
apnMessage = new ApnMessage(device.getVoipApnId(), account.getUuid(), device.getId(), useVoip, Type.NOTIFICATION, Optional.empty());
RedisOperation.unchecked(() -> apnFallbackManager.schedule(account, device));
} else {
apnMessage = new ApnMessage(device.getApnId(), account.getUuid(), device.getId(), false, Type.NOTIFICATION, Optional.empty());
apnMessage = new ApnMessage(device.getApnId(), account.getUuid(), device.getId(), useVoip, Type.NOTIFICATION, Optional.empty());
}
apnSender.sendMessage(apnMessage);
RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId()));
RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId(), useVoip));
}
@Override
@@ -33,7 +33,7 @@ public class ReceiptSender {
return;
}
final Account destinationAccount = accountManager.get(destinationUuid)
final Account destinationAccount = accountManager.getByAccountIdentifier(destinationUuid)
.orElseThrow(() -> new NoSuchUserException(destinationUuid));
final Envelope.Builder message = Envelope.newBuilder()
@@ -41,7 +41,7 @@ public class LegacyRecaptchaClient implements RecaptchaClient {
.post(Entity.form(formData), VerifyResponse.class);
if (response.success) {
logger.info("Got successful captcha time: " + response.challenge_ts + ", current time: " + System.currentTimeMillis());
logger.debug("Got successful captcha time: " + response.challenge_ts + ", current time: " + System.currentTimeMillis());
}
return response.success;
@@ -32,6 +32,9 @@ public class Account {
@JsonIgnore
private UUID uuid;
@JsonProperty("pni")
private UUID phoneNumberIdentifier;
@JsonProperty
private String number;
@@ -41,6 +44,9 @@ public class Account {
@JsonProperty
private String identityKey;
@JsonProperty("pniIdentityKey")
private String phoneNumberIdentityKey;
@JsonProperty("cpv")
private String currentProfileVersion;
@@ -80,9 +86,10 @@ public class Account {
public Account() {}
@VisibleForTesting
public Account(String number, UUID uuid, Set<Device> devices, byte[] unidentifiedAccessKey) {
public Account(String number, UUID uuid, final UUID phoneNumberIdentifier, Set<Device> devices, byte[] unidentifiedAccessKey) {
this.number = number;
this.uuid = uuid;
this.phoneNumberIdentifier = phoneNumberIdentifier;
this.devices = devices;
this.unidentifiedAccessKey = unidentifiedAccessKey;
}
@@ -98,16 +105,20 @@ public class Account {
this.uuid = uuid;
}
public void setNumber(String number) {
public UUID getPhoneNumberIdentifier() {
requireNotStale();
this.number = number;
return phoneNumberIdentifier;
}
public void setCanonicallyDiscoverable(boolean canonicallyDiscoverable) {
requireNotStale();
this.canonicallyDiscoverable = canonicallyDiscoverable;
/**
* Tests whether this account's account identifier or phone number identifier matches the given UUID.
*
* @param identifier the identifier to test
* @return {@code true} if this account's identifier or phone number identifier matches
*/
public boolean isIdentifiedBy(final UUID identifier) {
return uuid.equals(identifier) || (phoneNumberIdentifier != null && phoneNumberIdentifier.equals(identifier));
}
public String getNumber() {
@@ -116,6 +127,13 @@ public class Account {
return number;
}
public void setNumber(String number, UUID phoneNumberIdentifier) {
requireNotStale();
this.number = number;
this.phoneNumberIdentifier = phoneNumberIdentifier;
}
public void addDevice(Device device) {
requireNotStale();
@@ -247,6 +265,12 @@ public class Account {
return canonicallyDiscoverable;
}
public void setCanonicallyDiscoverable(boolean canonicallyDiscoverable) {
requireNotStale();
this.canonicallyDiscoverable = canonicallyDiscoverable;
}
public Optional<String> getRelay() {
requireNotStale();
@@ -265,6 +289,14 @@ public class Account {
return identityKey;
}
public String getPhoneNumberIdentityKey() {
return phoneNumberIdentityKey;
}
public void setPhoneNumberIdentityKey(final String phoneNumberIdentityKey) {
this.phoneNumberIdentityKey = phoneNumberIdentityKey;
}
public long getLastSeen() {
requireNotStale();
@@ -481,5 +513,4 @@ public class Account {
logger.error("Accessor called on stale account", new RuntimeException());
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
@@ -16,6 +16,9 @@ import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class AccountDatabaseCrawlerCache {
public static final String GENERAL_PURPOSE_PREFIX = "";
public static final String DIRECTORY_RECONCILER_PREFIX = "directory-reconciler";
private static final String ACTIVE_WORKER_KEY = "account_database_crawler_cache_active_worker";
private static final String LAST_UUID_KEY = "account_database_crawler_cache_last_uuid";
private static final String ACCELERATE_KEY = "account_database_crawler_cache_accelerate";
@@ -27,17 +30,21 @@ public class AccountDatabaseCrawlerCache {
private final FaultTolerantRedisCluster cacheCluster;
private final ClusterLuaScript unlockClusterScript;
public AccountDatabaseCrawlerCache(FaultTolerantRedisCluster cacheCluster) throws IOException {
private final String prefix;
public AccountDatabaseCrawlerCache(FaultTolerantRedisCluster cacheCluster, String prefix) throws IOException {
this.cacheCluster = cacheCluster;
this.unlockClusterScript = ClusterLuaScript.fromResource(cacheCluster, "lua/account_database_crawler/unlock.lua",
ScriptOutputType.INTEGER);
this.prefix = prefix + "::";
}
public void setAccelerated(final boolean accelerated) {
if (accelerated) {
cacheCluster.useCluster(connection -> connection.sync().set(ACCELERATE_KEY, "1"));
cacheCluster.useCluster(connection -> connection.sync().set(getPrefixedKey(ACCELERATE_KEY), "1"));
} else {
cacheCluster.useCluster(connection -> connection.sync().del(ACCELERATE_KEY));
cacheCluster.useCluster(connection -> connection.sync().del(getPrefixedKey(ACCELERATE_KEY)));
}
}
@@ -47,16 +54,16 @@ public class AccountDatabaseCrawlerCache {
public boolean claimActiveWork(String workerId, long ttlMs) {
return "OK".equals(cacheCluster.withCluster(connection -> connection.sync()
.set(ACTIVE_WORKER_KEY, workerId, SetArgs.Builder.nx().px(ttlMs))));
.set(getPrefixedKey(ACTIVE_WORKER_KEY), workerId, SetArgs.Builder.nx().px(ttlMs))));
}
public void releaseActiveWork(String workerId) {
unlockClusterScript.execute(List.of(ACTIVE_WORKER_KEY), List.of(workerId));
unlockClusterScript.execute(List.of(getPrefixedKey(ACTIVE_WORKER_KEY)), List.of(workerId));
}
public Optional<UUID> getLastUuid() {
final String lastUuidString = cacheCluster.withCluster(
connection -> connection.sync().get(LAST_UUID_KEY));
connection -> connection.sync().get(getPrefixedKey(LAST_UUID_KEY)));
if (lastUuidString == null) {
return Optional.empty();
@@ -68,15 +75,15 @@ public class AccountDatabaseCrawlerCache {
public void setLastUuid(Optional<UUID> lastUuid) {
if (lastUuid.isPresent()) {
cacheCluster.useCluster(connection -> connection.sync()
.psetex(LAST_UUID_KEY, LAST_NUMBER_TTL_MS, lastUuid.get().toString()));
.psetex(getPrefixedKey(LAST_UUID_KEY), LAST_NUMBER_TTL_MS, lastUuid.get().toString()));
} else {
cacheCluster.useCluster(connection -> connection.sync().del(LAST_UUID_KEY));
cacheCluster.useCluster(connection -> connection.sync().del(getPrefixedKey(LAST_UUID_KEY)));
}
}
public Optional<UUID> getLastUuidDynamo() {
final String lastUuidString = cacheCluster.withCluster(
connection -> connection.sync().get(LAST_UUID_DYNAMO_KEY));
connection -> connection.sync().get(getPrefixedKey(LAST_UUID_DYNAMO_KEY)));
if (lastUuidString == null) {
return Optional.empty();
@@ -89,9 +96,14 @@ public class AccountDatabaseCrawlerCache {
if (lastUuid.isPresent()) {
cacheCluster.useCluster(
connection -> connection.sync()
.psetex(LAST_UUID_DYNAMO_KEY, LAST_NUMBER_TTL_MS, lastUuid.get().toString()));
.psetex(getPrefixedKey(LAST_UUID_DYNAMO_KEY), LAST_NUMBER_TTL_MS, lastUuid.get().toString()));
} else {
cacheCluster.useCluster(connection -> connection.sync().del(LAST_UUID_DYNAMO_KEY));
cacheCluster.useCluster(connection -> connection.sync().del(getPrefixedKey(LAST_UUID_DYNAMO_KEY)));
}
}
private String getPrefixedKey(final String key) {
return prefix + key;
}
}
@@ -13,6 +13,7 @@ import io.micrometer.core.instrument.Timer;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -29,7 +30,6 @@ import software.amazon.awssdk.services.dynamodb.model.Delete;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
import software.amazon.awssdk.services.dynamodb.model.Put;
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure;
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem;
@@ -44,6 +44,8 @@ public class Accounts extends AbstractDynamoDbStore {
// uuid, primary key
static final String KEY_ACCOUNT_UUID = "U";
// uuid, attribute on account table, primary key for PNI table
static final String ATTR_PNI_UUID = "PNI";
// phone number
static final String ATTR_ACCOUNT_E164 = "P";
// account, serialized to JSON
@@ -55,7 +57,8 @@ public class Accounts extends AbstractDynamoDbStore {
private final DynamoDbClient client;
private final String phoneNumbersTableName;
private final String phoneNumberConstraintTableName;
private final String phoneNumberIdentifierConstraintTableName;
private final String accountsTableName;
private final int scanPageSize;
@@ -64,19 +67,20 @@ public class Accounts extends AbstractDynamoDbStore {
private static final Timer CHANGE_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "changeNumber"));
private static final Timer UPDATE_TIMER = Metrics.timer(name(Accounts.class, "update"));
private static final Timer GET_BY_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "getByNumber"));
private static final Timer GET_BY_PNI_TIMER = Metrics.timer(name(Accounts.class, "getByPni"));
private static final Timer GET_BY_UUID_TIMER = Metrics.timer(name(Accounts.class, "getByUuid"));
private static final Timer GET_ALL_FROM_START_TIMER = Metrics.timer(name(Accounts.class, "getAllFrom"));
private static final Timer GET_ALL_FROM_OFFSET_TIMER = Metrics.timer(name(Accounts.class, "getAllFromOffset"));
private static final Timer DELETE_TIMER = Metrics.timer(name(Accounts.class, "delete"));
public Accounts(DynamoDbClient client, String accountsTableName, String phoneNumbersTableName,
final int scanPageSize) {
public Accounts(DynamoDbClient client, String accountsTableName, String phoneNumberConstraintTableName,
String phoneNumberIdentifierConstraintTableName, final int scanPageSize) {
super(client);
this.client = client;
this.phoneNumbersTableName = phoneNumbersTableName;
this.phoneNumberConstraintTableName = phoneNumberConstraintTableName;
this.phoneNumberIdentifierConstraintTableName = phoneNumberIdentifierConstraintTableName;
this.accountsTableName = accountsTableName;
this.scanPageSize = scanPageSize;
}
@@ -85,35 +89,91 @@ public class Accounts extends AbstractDynamoDbStore {
return CREATE_TIMER.record(() -> {
try {
TransactWriteItem phoneNumberConstraintPut = buildPutWriteItemForPhoneNumberConstraint(account, account.getUuid());
TransactWriteItem accountPut = buildPutWriteItemForAccount(account, account.getUuid(), Put.builder()
.conditionExpression("attribute_not_exists(#number) OR #number = :number")
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164))
.expressionAttributeValues(Map.of(":number", AttributeValues.fromString(account.getNumber()))));
TransactWriteItem phoneNumberConstraintPut = TransactWriteItem.builder()
.put(
Put.builder()
.tableName(phoneNumberConstraintTableName)
.item(Map.of(
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.conditionExpression(
"attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)")
.expressionAttributeNames(
Map.of("#uuid", KEY_ACCOUNT_UUID,
"#number", ATTR_ACCOUNT_E164))
.expressionAttributeValues(
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build();
TransactWriteItem phoneNumberIdentifierConstraintPut = TransactWriteItem.builder()
.put(
Put.builder()
.tableName(phoneNumberIdentifierConstraintTableName)
.item(Map.of(
ATTR_PNI_UUID, AttributeValues.fromUUID(account.getPhoneNumberIdentifier()),
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.conditionExpression(
"attribute_not_exists(#pni) OR (attribute_exists(#pni) AND #uuid = :uuid)")
.expressionAttributeNames(
Map.of("#uuid", KEY_ACCOUNT_UUID,
"#pni", ATTR_PNI_UUID))
.expressionAttributeValues(
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build();
final Map<String, AttributeValue> item = new HashMap<>(Map.of(
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
ATTR_PNI_UUID, AttributeValues.fromUUID(account.getPhoneNumberIdentifier()),
ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())));
TransactWriteItem accountPut = TransactWriteItem.builder()
.put(Put.builder()
.conditionExpression("attribute_not_exists(#number) OR #number = :number")
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164))
.expressionAttributeValues(Map.of(":number", AttributeValues.fromString(account.getNumber())))
.tableName(accountsTableName)
.item(item)
.build())
.build();
final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
.transactItems(phoneNumberConstraintPut, accountPut)
.transactItems(phoneNumberConstraintPut, phoneNumberIdentifierConstraintPut, accountPut)
.build();
try {
client.transactWriteItems(request);
} catch (TransactionCanceledException e) {
final CancellationReason accountCancellationReason = e.cancellationReasons().get(1);
final CancellationReason accountCancellationReason = e.cancellationReasons().get(2);
if ("ConditionalCheckFailed".equals(accountCancellationReason.code())) {
throw new IllegalArgumentException("uuid present with different phone number");
throw new IllegalArgumentException("account identifier present with different phone number");
}
final CancellationReason phoneNumberConstraintCancellationReason = e.cancellationReasons().get(0);
final CancellationReason phoneNumberIdentifierConstraintCancellationReason = e.cancellationReasons().get(1);
if ("ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code())) {
if ("ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code()) ||
"ConditionalCheckFailed".equals(phoneNumberIdentifierConstraintCancellationReason.code())) {
ByteBuffer actualAccountUuid = phoneNumberConstraintCancellationReason.item().get(KEY_ACCOUNT_UUID).b().asByteBuffer();
// In theory, both reasons should trip in tandem and either should give us the information we need. Even so,
// we'll be cautious here and make sure we're choosing a condition check that really failed.
final CancellationReason reason = "ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code()) ?
phoneNumberConstraintCancellationReason : phoneNumberIdentifierConstraintCancellationReason;
ByteBuffer actualAccountUuid = reason.item().get(KEY_ACCOUNT_UUID).b().asByteBuffer();
account.setUuid(UUIDUtil.fromByteBuffer(actualAccountUuid));
final int version = get(account.getUuid()).get().getVersion();
account.setVersion(version);
final Account existingAccount = getByAccountIdentifier(account.getUuid()).orElseThrow();
account.setNumber(existingAccount.getNumber(), existingAccount.getPhoneNumberIdentifier());
account.setVersion(existingAccount.getVersion());
update(account);
@@ -125,7 +185,7 @@ public class Accounts extends AbstractDynamoDbStore {
throw new ContestedOptimisticLockException();
}
// this shouldn’t happen
// this shouldn't happen
throw new RuntimeException("could not create account: " + extractCancellationReasonCodes(e));
}
} catch (JsonProcessingException e) {
@@ -136,44 +196,11 @@ public class Accounts extends AbstractDynamoDbStore {
});
}
private TransactWriteItem buildPutWriteItemForAccount(Account account, UUID uuid, Put.Builder putBuilder) throws JsonProcessingException {
return TransactWriteItem.builder()
.put(putBuilder
.tableName(accountsTableName)
.item(Map.of(
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid),
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())))
.build())
.build();
}
private TransactWriteItem buildPutWriteItemForPhoneNumberConstraint(Account account, UUID uuid) {
return TransactWriteItem.builder()
.put(
Put.builder()
.tableName(phoneNumbersTableName)
.item(Map.of(
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid)))
.conditionExpression(
"attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)")
.expressionAttributeNames(
Map.of("#uuid", KEY_ACCOUNT_UUID,
"#number", ATTR_ACCOUNT_E164))
.expressionAttributeValues(
Map.of(":uuid", AttributeValues.fromUUID(uuid)))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build();
}
/**
* Changes the phone number for the given account. The given account's number should be its current, pre-change
* number. If this method succeeds, the account's number will be changed to the new number. If the update fails for
* any reason, the account's number will be unchanged.
* number. If this method succeeds, the account's number will be changed to the new number and its phone number
* identifier will be changed to the given phone number identifier. If the update fails for any reason, the account's
* number and PNI will be unchanged.
* <p/>
* This method expects that any accounts with conflicting numbers will have been removed by the time this method is
* called. This method may fail with an unspecified {@link RuntimeException} if another account with the same number
@@ -182,26 +209,28 @@ public class Accounts extends AbstractDynamoDbStore {
* @param account the account for which to change the phone number
* @param number the new phone number
*/
public void changeNumber(final Account account, final String number) {
public void changeNumber(final Account account, final String number, final UUID phoneNumberIdentifier) {
CHANGE_NUMBER_TIMER.record(() -> {
final String originalNumber = account.getNumber();
final UUID originalPni = account.getPhoneNumberIdentifier();
boolean succeeded = false;
account.setNumber(number);
account.setNumber(number, phoneNumberIdentifier);
try {
final List<TransactWriteItem> writeItems = new ArrayList<>();
writeItems.add(TransactWriteItem.builder()
.delete(Delete.builder()
.tableName(phoneNumbersTableName)
.tableName(phoneNumberConstraintTableName)
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(originalNumber)))
.build())
.build());
writeItems.add(TransactWriteItem.builder()
.put(Put.builder()
.tableName(phoneNumbersTableName)
.tableName(phoneNumberConstraintTableName)
.item(Map.of(
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
@@ -211,20 +240,41 @@ public class Accounts extends AbstractDynamoDbStore {
.build())
.build());
writeItems.add(TransactWriteItem.builder()
.delete(Delete.builder()
.tableName(phoneNumberIdentifierConstraintTableName)
.key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(originalPni)))
.build())
.build());
writeItems.add(TransactWriteItem.builder()
.put(Put.builder()
.tableName(phoneNumberIdentifierConstraintTableName)
.item(Map.of(
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
.conditionExpression("attribute_not_exists(#pni)")
.expressionAttributeNames(Map.of("#pni", ATTR_PNI_UUID))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build());
writeItems.add(
TransactWriteItem.builder()
.update(Update.builder()
.tableName(accountsTableName)
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.updateExpression("SET #data = :data, #number = :number, #cds = :cds ADD #version :version_increment")
.updateExpression("SET #data = :data, #number = :number, #pni = :pni, #cds = :cds ADD #version :version_increment")
.conditionExpression("attribute_exists(#number) AND #version = :version")
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
"#data", ATTR_ACCOUNT_DATA,
"#cds", ATTR_CANONICALLY_DISCOVERABLE,
"#pni", ATTR_PNI_UUID,
"#version", ATTR_VERSION))
.expressionAttributeValues(Map.of(
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
":number", AttributeValues.fromString(number),
":pni", AttributeValues.fromUUID(phoneNumberIdentifier),
":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
":version", AttributeValues.fromInt(account.getVersion()),
":version_increment", AttributeValues.fromInt(1)))
@@ -243,7 +293,7 @@ public class Accounts extends AbstractDynamoDbStore {
throw new IllegalArgumentException(e);
} finally {
if (!succeeded) {
account.setNumber(originalNumber);
account.setNumber(originalNumber, originalPni);
}
}
});
@@ -251,57 +301,69 @@ public class Accounts extends AbstractDynamoDbStore {
public void update(Account account) throws ContestedOptimisticLockException {
UPDATE_TIMER.record(() -> {
UpdateItemRequest updateItemRequest;
final UpdateItemRequest updateItemRequest;
try {
updateItemRequest = UpdateItemRequest.builder()
.tableName(accountsTableName)
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.updateExpression("SET #data = :data, #cds = :cds ADD #version :version_increment")
.conditionExpression("attribute_exists(#number) AND #version = :version")
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
"#data", ATTR_ACCOUNT_DATA,
"#cds", ATTR_CANONICALLY_DISCOVERABLE,
"#version", ATTR_VERSION))
.expressionAttributeValues(Map.of(
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
":version", AttributeValues.fromInt(account.getVersion()),
":version_increment", AttributeValues.fromInt(1)))
.returnValues(ReturnValue.UPDATED_NEW)
.build();
.tableName(accountsTableName)
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.updateExpression("SET #data = :data, #cds = :cds ADD #version :version_increment")
.conditionExpression("attribute_exists(#number) AND #version = :version")
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
"#data", ATTR_ACCOUNT_DATA,
"#cds", ATTR_CANONICALLY_DISCOVERABLE,
"#version", ATTR_VERSION))
.expressionAttributeValues(Map.of(
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
":version", AttributeValues.fromInt(account.getVersion()),
":version_increment", AttributeValues.fromInt(1)))
.build();
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e);
}
try {
UpdateItemResponse response = client.updateItem(updateItemRequest);
final UpdateItemResponse response = client.updateItem(updateItemRequest);
account.setVersion(AttributeValues.getInt(response.attributes(), "V", account.getVersion() + 1));
} catch (final TransactionConflictException e) {
throw new ContestedOptimisticLockException();
} catch (final ConditionalCheckFailedException e) {
// the exception doesn’t give details about which condition failed,
// the exception doesn't give details about which condition failed,
// but we can infer it was an optimistic locking failure if the UUID is known
throw get(account.getUuid()).isPresent() ? new ContestedOptimisticLockException() : e;
throw getByAccountIdentifier(account.getUuid()).isPresent() ? new ContestedOptimisticLockException() : e;
}
});
}
public Optional<Account> get(String number) {
public Optional<Account> getByE164(String number) {
return GET_BY_NUMBER_TIMER.record(() -> {
final GetItemResponse response = client.getItem(GetItemRequest.builder()
.tableName(phoneNumbersTableName)
.tableName(phoneNumberConstraintTableName)
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
.build());
return Optional.ofNullable(response.item())
.map(item -> item.get(KEY_ACCOUNT_UUID))
.map(uuid -> accountByUuid(uuid))
.map(this::accountByUuid)
.map(Accounts::fromItem);
});
}
public Optional<Account> getByPhoneNumberIdentifier(final UUID phoneNumberIdentifier) {
return GET_BY_PNI_TIMER.record(() -> {
final GetItemResponse response = client.getItem(GetItemRequest.builder()
.tableName(phoneNumberIdentifierConstraintTableName)
.key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
.build());
return Optional.ofNullable(response.item())
.map(item -> item.get(KEY_ACCOUNT_UUID))
.map(this::accountByUuid)
.map(Accounts::fromItem);
});
}
@@ -315,7 +377,7 @@ public class Accounts extends AbstractDynamoDbStore {
return r.item().isEmpty() ? null : r.item();
}
public Optional<Account> get(UUID uuid) {
public Optional<Account> getByAccountIdentifier(UUID uuid) {
return GET_BY_UUID_TIMER.record(() ->
Optional.ofNullable(accountByUuid(AttributeValues.fromUUID(uuid)))
.map(Accounts::fromItem));
@@ -324,13 +386,11 @@ public class Accounts extends AbstractDynamoDbStore {
public void delete(UUID uuid) {
DELETE_TIMER.record(() -> {
Optional<Account> maybeAccount = get(uuid);
maybeAccount.ifPresent(account -> {
getByAccountIdentifier(uuid).ifPresent(account -> {
TransactWriteItem phoneNumberDelete = TransactWriteItem.builder()
.delete(Delete.builder()
.tableName(phoneNumbersTableName)
.tableName(phoneNumberConstraintTableName)
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber())))
.build())
.build();
@@ -342,8 +402,17 @@ public class Accounts extends AbstractDynamoDbStore {
.build())
.build();
final List<TransactWriteItem> transactWriteItems = new ArrayList<>(List.of(phoneNumberDelete, accountDelete));
transactWriteItems.add(TransactWriteItem.builder()
.delete(Delete.builder()
.tableName(phoneNumberIdentifierConstraintTableName)
.key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(account.getPhoneNumberIdentifier())))
.build())
.build());
TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
.transactItems(phoneNumberDelete, accountDelete).build();
.transactItems(transactWriteItems).build();
client.transactWriteItems(request);
});
@@ -393,7 +462,7 @@ public class Accounts extends AbstractDynamoDbStore {
}
try {
Account account = SystemMapper.getMapper().readValue(item.get(ATTR_ACCOUNT_DATA).b().asByteArray(), Account.class);
account.setNumber(item.get(ATTR_ACCOUNT_E164).s());
account.setNumber(item.get(ATTR_ACCOUNT_E164).s(), AttributeValues.getUUID(item, ATTR_PNI_UUID, null));
account.setUuid(UUIDUtil.fromByteBuffer(item.get(KEY_ACCOUNT_UUID).b().asByteBuffer()));
account.setVersion(Integer.parseInt(item.get(ATTR_VERSION).n()));
account.setCanonicallyDiscoverable(Optional.ofNullable(item.get(ATTR_CANONICALLY_DISCOVERABLE)).map(av -> av.bool()).orElse(false));
@@ -17,6 +17,9 @@ import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tags;
import java.io.IOException;
import java.time.Clock;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -49,6 +52,7 @@ public class AccountsManager {
private static final Timer redisSetTimer = metricRegistry.timer(name(AccountsManager.class, "redisSet" ));
private static final Timer redisNumberGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisNumberGet"));
private static final Timer redisPniGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisPniGet"));
private static final Timer redisUuidGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisUuidGet" ));
private static final Timer redisDeleteTimer = metricRegistry.timer(name(AccountsManager.class, "redisDelete" ));
@@ -60,18 +64,20 @@ public class AccountsManager {
private final Logger logger = LoggerFactory.getLogger(AccountsManager.class);
private final Accounts accounts;
private final PhoneNumberIdentifiers phoneNumberIdentifiers;
private final FaultTolerantRedisCluster cacheCluster;
private final DeletedAccountsManager deletedAccountsManager;
private final DirectoryQueue directoryQueue;
private final KeysDynamoDb keysDynamoDb;
private final DirectoryQueue directoryQueue;
private final Keys keys;
private final MessagesManager messagesManager;
private final UsernamesManager usernamesManager;
private final ProfilesManager profilesManager;
private final ProfilesManager profilesManager;
private final StoredVerificationCodeManager pendingAccounts;
private final SecureStorageClient secureStorageClient;
private final SecureBackupClient secureBackupClient;
private final SecureStorageClient secureStorageClient;
private final SecureBackupClient secureBackupClient;
private final ClientPresenceManager clientPresenceManager;
private final ObjectMapper mapper;
private final ObjectMapper mapper;
private final Clock clock;
public enum DeletionReason {
ADMIN_DELETED("admin"),
@@ -85,21 +91,26 @@ public class AccountsManager {
}
}
public AccountsManager(Accounts accounts, FaultTolerantRedisCluster cacheCluster,
public AccountsManager(final Accounts accounts,
final PhoneNumberIdentifiers phoneNumberIdentifiers,
final FaultTolerantRedisCluster cacheCluster,
final DeletedAccountsManager deletedAccountsManager,
final DirectoryQueue directoryQueue,
final KeysDynamoDb keysDynamoDb, final MessagesManager messagesManager,
final Keys keys,
final MessagesManager messagesManager,
final UsernamesManager usernamesManager,
final ProfilesManager profilesManager,
final StoredVerificationCodeManager pendingAccounts,
final SecureStorageClient secureStorageClient,
final SecureBackupClient secureBackupClient,
final ClientPresenceManager clientPresenceManager) {
final ClientPresenceManager clientPresenceManager,
final Clock clock) {
this.accounts = accounts;
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
this.cacheCluster = cacheCluster;
this.deletedAccountsManager = deletedAccountsManager;
this.directoryQueue = directoryQueue;
this.keysDynamoDb = keysDynamoDb;
this.keys = keys;
this.messagesManager = messagesManager;
this.usernamesManager = usernamesManager;
this.profilesManager = profilesManager;
@@ -108,12 +119,14 @@ public class AccountsManager {
this.secureBackupClient = secureBackupClient;
this.clientPresenceManager = clientPresenceManager;
this.mapper = SystemMapper.getMapper();
this.clock = Objects.requireNonNull(clock);
}
public Account create(final String number,
final String password,
final String signalAgent,
final AccountAttributes accountAttributes) throws InterruptedException {
final AccountAttributes accountAttributes,
final List<AccountBadge> accountBadges) throws InterruptedException {
try (Timer.Context ignored = createTimer.time()) {
final Account account = new Account();
@@ -130,17 +143,18 @@ public class AccountsManager {
device.setLastSeen(Util.todayInMillis());
device.setUserAgent(signalAgent);
account.setNumber(number);
account.setNumber(number, phoneNumberIdentifiers.getPhoneNumberIdentifier(number));
account.setUuid(maybeRecentlyDeletedUuid.orElseGet(UUID::randomUUID));
account.addDevice(device);
account.setRegistrationLockFromAttributes(accountAttributes);
account.setUnidentifiedAccessKey(accountAttributes.getUnidentifiedAccessKey());
account.setUnrestrictedUnidentifiedAccess(accountAttributes.isUnrestrictedUnidentifiedAccess());
account.setDiscoverableByPhoneNumber(accountAttributes.isDiscoverableByPhoneNumber());
account.setBadges(clock, accountBadges);
final UUID originalUuid = account.getUuid();
boolean freshUser = dynamoCreate(account);
boolean freshUser = accounts.create(account);
// create() sometimes updates the UUID, if there was a number conflict.
// for metrics, we want secondary to run with the same original UUID
@@ -164,7 +178,7 @@ public class AccountsManager {
// account and need to clear out messages and keys that may have been stored for the old account.
if (!originalUuid.equals(actualUuid)) {
messagesManager.clear(actualUuid);
keysDynamoDb.delete(actualUuid);
keys.delete(actualUuid);
profilesManager.deleteAll(actualUuid);
}
@@ -202,7 +216,7 @@ public class AccountsManager {
deletedAccountsManager.lockAndPut(account.getNumber(), number, () -> {
redisDelete(account);
final Optional<Account> maybeExistingAccount = get(number);
final Optional<Account> maybeExistingAccount = getByE164(number);
final Optional<UUID> displacedUuid;
if (maybeExistingAccount.isPresent()) {
@@ -213,12 +227,13 @@ public class AccountsManager {
}
final UUID uuid = account.getUuid();
final UUID phoneNumberIdentifier = phoneNumberIdentifiers.getPhoneNumberIdentifier(number);
final Account numberChangedAccount = updateWithRetries(
account,
a -> true,
a -> dynamoChangeNumber(a, number),
() -> dynamoGet(uuid).orElseThrow());
a -> accounts.changeNumber(a, number, phoneNumberIdentifier),
() -> accounts.getByAccountIdentifier(uuid).orElseThrow());
updatedAccount.set(numberChangedAccount);
directoryQueue.changePhoneNumber(numberChangedAccount, originalNumber, number);
@@ -277,8 +292,12 @@ public class AccountsManager {
final UUID uuid = account.getUuid();
final String originalNumber = account.getNumber();
final UUID originalPhoneNumberIdentifier = account.getPhoneNumberIdentifier();
updatedAccount = updateWithRetries(account, updater, this::dynamoUpdate, () -> dynamoGet(uuid).get());
updatedAccount = updateWithRetries(account,
updater,
accounts::update,
() -> accounts.getByAccountIdentifier(uuid).orElseThrow());
assert updatedAccount.getNumber().equals(originalNumber);
@@ -287,6 +306,13 @@ public class AccountsManager {
new RuntimeException());
}
assert updatedAccount.getPhoneNumberIdentifier().equals(originalPhoneNumberIdentifier);
if (!updatedAccount.getPhoneNumberIdentifier().equals(originalPhoneNumberIdentifier)) {
logger.error("Phone number identifier changed via \"normal\" update; PNIs must be changed via changeNumber method",
new RuntimeException());
}
redisSet(updatedAccount);
}
@@ -347,12 +373,12 @@ public class AccountsManager {
});
}
public Optional<Account> get(String number) {
public Optional<Account> getByE164(String number) {
try (Timer.Context ignored = getByNumberTimer.time()) {
Optional<Account> account = redisGet(number);
Optional<Account> account = redisGetByE164(number);
if (account.isEmpty()) {
account = dynamoGet(number);
account = accounts.getByE164(number);
account.ifPresent(this::redisSet);
}
@@ -360,12 +386,25 @@ public class AccountsManager {
}
}
public Optional<Account> get(UUID uuid) {
try (Timer.Context ignored = getByUuidTimer.time()) {
Optional<Account> account = redisGet(uuid);
public Optional<Account> getByPhoneNumberIdentifier(UUID pni) {
try (Timer.Context ignored = getByNumberTimer.time()) {
Optional<Account> account = redisGetByPhoneNumberIdentifier(pni);
if (account.isEmpty()) {
account = dynamoGet(uuid);
account = accounts.getByPhoneNumberIdentifier(pni);
account.ifPresent(this::redisSet);
}
return account;
}
}
public Optional<Account> getByAccountIdentifier(UUID uuid) {
try (Timer.Context ignored = getByUuidTimer.time()) {
Optional<Account> account = redisGetByAccountIdentifier(uuid);
if (account.isEmpty()) {
account = accounts.getByAccountIdentifier(uuid);
account.ifPresent(this::redisSet);
}
@@ -406,22 +445,24 @@ public class AccountsManager {
usernamesManager.delete(account.getUuid());
profilesManager.deleteAll(account.getUuid());
keysDynamoDb.delete(account.getUuid());
keys.delete(account.getUuid());
keys.delete(account.getPhoneNumberIdentifier());
messagesManager.clear(account.getUuid());
messagesManager.clear(account.getPhoneNumberIdentifier());
deleteStorageServiceDataFuture.join();
deleteBackupServiceDataFuture.join();
redisDelete(account);
dynamoDelete(account);
accounts.delete(account.getUuid());
RedisOperation.unchecked(() ->
account.getDevices().forEach(device ->
clientPresenceManager.displacePresence(account.getUuid(), device.getId())));
}
private String getAccountMapKey(String number) {
return "AccountMap::" + number;
private String getAccountMapKey(String key) {
return "AccountMap::" + key;
}
private String getAccountEntityKey(UUID uuid) {
@@ -435,6 +476,8 @@ public class AccountsManager {
cacheCluster.useCluster(connection -> {
final RedisAdvancedClusterCommands<String, String> commands = connection.sync();
commands.set(getAccountMapKey(account.getPhoneNumberIdentifier().toString()), account.getUuid().toString());
commands.set(getAccountMapKey(account.getNumber()), account.getUuid().toString());
commands.set(getAccountEntityKey(account.getUuid()), accountJson);
});
@@ -443,11 +486,19 @@ public class AccountsManager {
}
}
private Optional<Account> redisGet(String number) {
try (Timer.Context ignored = redisNumberGetTimer.time()) {
final String uuid = cacheCluster.withCluster(connection -> connection.sync().get(getAccountMapKey(number)));
private Optional<Account> redisGetByPhoneNumberIdentifier(UUID uuid) {
return redisGetBySecondaryKey(uuid.toString(), redisPniGetTimer);
}
if (uuid != null) return redisGet(UUID.fromString(uuid));
private Optional<Account> redisGetByE164(String e164) {
return redisGetBySecondaryKey(e164, redisNumberGetTimer);
}
private Optional<Account> redisGetBySecondaryKey(String secondaryKey, Timer timer) {
try (Timer.Context ignored = timer.time()) {
final String uuid = cacheCluster.withCluster(connection -> connection.sync().get(getAccountMapKey(secondaryKey)));
if (uuid != null) return redisGetByAccountIdentifier(UUID.fromString(uuid));
else return Optional.empty();
} catch (IllegalArgumentException e) {
logger.warn("Deserialization error", e);
@@ -458,7 +509,7 @@ public class AccountsManager {
}
}
private Optional<Account> redisGet(UUID uuid) {
private Optional<Account> redisGetByAccountIdentifier(UUID uuid) {
try (Timer.Context ignored = redisUuidGetTimer.time()) {
final String json = cacheCluster.withCluster(connection -> connection.sync().get(getAccountEntityKey(uuid)));
@@ -481,32 +532,10 @@ public class AccountsManager {
private void redisDelete(final Account account) {
try (final Timer.Context ignored = redisDeleteTimer.time()) {
cacheCluster.useCluster(connection -> connection.sync()
.del(getAccountMapKey(account.getNumber()), getAccountEntityKey(account.getUuid())));
cacheCluster.useCluster(connection -> connection.sync().del(
getAccountMapKey(account.getNumber()),
getAccountMapKey(account.getPhoneNumberIdentifier().toString()),
getAccountEntityKey(account.getUuid())));
}
}
private Optional<Account> dynamoGet(String number) {
return accounts.get(number);
}
private Optional<Account> dynamoGet(UUID uuid) {
return accounts.get(uuid);
}
private boolean dynamoCreate(Account account) {
return accounts.create(account);
}
private void dynamoUpdate(Account account) {
accounts.update(account);
}
private void dynamoDelete(final Account account) {
accounts.delete(account.getUuid());
}
private void dynamoChangeNumber(final Account account, final String number) {
accounts.changeNumber(account, number);
}
}
@@ -43,7 +43,7 @@ public class ContactDiscoveryWriter extends AccountDatabaseCrawlerListener {
// It’s less than ideal, but crawler listeners currently must not call update()
// with the accounts from the chunk, because updates cause the account instance to become stale. Instead, they
// must get a new copy, which they are free to update.
accounts.get(account.getUuid()).ifPresent(a -> accounts.update(a, NOOP_UPDATER));
accounts.getByAccountIdentifier(account.getUuid()).ifPresent(a -> accounts.update(a, NOOP_UPDATER));
}
}
}
@@ -53,6 +53,9 @@ public class Device {
@JsonProperty
private SignedPreKey signedPreKey;
@JsonProperty("pniSignedPreKey")
private SignedPreKey phoneNumberIdentitySignedPreKey;
@JsonProperty
private long lastSeen;
@@ -215,6 +218,14 @@ public class Device {
this.signedPreKey = signedPreKey;
}
public SignedPreKey getPhoneNumberIdentitySignedPreKey() {
return phoneNumberIdentitySignedPreKey;
}
public void setPhoneNumberIdentitySignedPreKey(final SignedPreKey phoneNumberIdentitySignedPreKey) {
this.phoneNumberIdentitySignedPreKey = phoneNumberIdentitySignedPreKey;
}
public long getPushTimestamp() {
return pushTimestamp;
}
@@ -1,64 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import java.io.IOException;
class Key {
private final byte[] userMessageQueue;
private final byte[] userMessageQueueMetadata;
private final byte[] userMessageQueuePersistInProgress;
private final String address;
private final long deviceId;
Key(String address, long deviceId) {
this.address = address;
this.deviceId = deviceId;
this.userMessageQueue = ("user_queue::" + address + "::" + deviceId).getBytes();
this.userMessageQueueMetadata = ("user_queue_metadata::" + address + "::" + deviceId).getBytes();
this.userMessageQueuePersistInProgress = ("user_queue_persisting::" + address + "::" + deviceId).getBytes();
}
String getAddress() {
return address;
}
long getDeviceId() {
return deviceId;
}
byte[] getUserMessageQueue() {
return userMessageQueue;
}
byte[] getUserMessageQueueMetadata() {
return userMessageQueueMetadata;
}
byte[] getUserMessageQueuePersistInProgress() {
return userMessageQueuePersistInProgress;
}
static byte[] getUserMessageQueueIndex() {
return "user_queue_index".getBytes();
}
static Key fromUserMessageQueue(byte[] userMessageQueue) throws IOException {
try {
String[] parts = new String(userMessageQueue).split("::");
if (parts.length != 3) {
throw new IOException("Malformed key: " + new String(userMessageQueue));
}
return new Key(parts[1], Long.parseLong(parts[2]));
} catch (NumberFormatException e) {
throw new IOException(e);
}
}
}
@@ -5,15 +5,12 @@
package org.whispersystems.textsecuregcm.storage;
import static com.codahale.metrics.MetricRegistry.name;
import com.google.common.annotations.VisibleForTesting;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Timer;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -32,7 +29,9 @@ import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
import software.amazon.awssdk.services.dynamodb.model.Select;
import software.amazon.awssdk.services.dynamodb.model.WriteRequest;
public class KeysDynamoDb extends AbstractDynamoDbStore {
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
public class Keys extends AbstractDynamoDbStore {
private final String tableName;
@@ -40,30 +39,29 @@ public class KeysDynamoDb extends AbstractDynamoDbStore {
static final String KEY_DEVICE_ID_KEY_ID = "DK";
static final String KEY_PUBLIC_KEY = "P";
private static final Timer STORE_KEYS_TIMER = Metrics.timer(name(KeysDynamoDb.class, "storeKeys"));
private static final Timer TAKE_KEY_FOR_DEVICE_TIMER = Metrics.timer(name(KeysDynamoDb.class, "takeKeyForDevice"));
private static final Timer TAKE_KEYS_FOR_ACCOUNT_TIMER = Metrics.timer(name(KeysDynamoDb.class, "takeKeyForAccount"));
private static final Timer GET_KEY_COUNT_TIMER = Metrics.timer(name(KeysDynamoDb.class, "getKeyCount"));
private static final Timer DELETE_KEYS_FOR_DEVICE_TIMER = Metrics.timer(name(KeysDynamoDb.class, "deleteKeysForDevice"));
private static final Timer DELETE_KEYS_FOR_ACCOUNT_TIMER = Metrics.timer(name(KeysDynamoDb.class, "deleteKeysForAccount"));
private static final DistributionSummary CONTESTED_KEY_DISTRIBUTION = Metrics.summary(name(KeysDynamoDb.class, "contestedKeys"));
private static final DistributionSummary KEY_COUNT_DISTRIBUTION = Metrics.summary(name(KeysDynamoDb.class, "keyCount"));
private static final Timer STORE_KEYS_TIMER = Metrics.timer(name(Keys.class, "storeKeys"));
private static final Timer TAKE_KEY_FOR_DEVICE_TIMER = Metrics.timer(name(Keys.class, "takeKeyForDevice"));
private static final Timer GET_KEY_COUNT_TIMER = Metrics.timer(name(Keys.class, "getKeyCount"));
private static final Timer DELETE_KEYS_FOR_DEVICE_TIMER = Metrics.timer(name(Keys.class, "deleteKeysForDevice"));
private static final Timer DELETE_KEYS_FOR_ACCOUNT_TIMER = Metrics.timer(name(Keys.class, "deleteKeysForAccount"));
private static final DistributionSummary CONTESTED_KEY_DISTRIBUTION = Metrics.summary(name(Keys.class, "contestedKeys"));
private static final DistributionSummary KEY_COUNT_DISTRIBUTION = Metrics.summary(name(Keys.class, "keyCount"));
public KeysDynamoDb(final DynamoDbClient dynamoDB, final String tableName) {
public Keys(final DynamoDbClient dynamoDB, final String tableName) {
super(dynamoDB);
this.tableName = tableName;
}
public void store(final Account account, final long deviceId, final List<PreKey> keys) {
public void store(final UUID identifier, final long deviceId, final List<PreKey> keys) {
STORE_KEYS_TIMER.record(() -> {
delete(account.getUuid(), deviceId);
delete(identifier, deviceId);
writeInBatches(keys, batch -> {
List<WriteRequest> items = new ArrayList<>();
for (final PreKey preKey : batch) {
items.add(WriteRequest.builder()
.putRequest(PutRequest.builder()
.item(getItemFromPreKey(account.getUuid(), deviceId, preKey))
.item(getItemFromPreKey(identifier, deviceId, preKey))
.build())
.build());
}
@@ -72,9 +70,9 @@ public class KeysDynamoDb extends AbstractDynamoDbStore {
});
}
public Optional<PreKey> take(final Account account, final long deviceId) {
public Optional<PreKey> take(final UUID identifier, final long deviceId) {
return TAKE_KEY_FOR_DEVICE_TIMER.record(() -> {
final AttributeValue partitionKey = getPartitionKey(account.getUuid());
final AttributeValue partitionKey = getPartitionKey(identifier);
QueryRequest queryRequest = QueryRequest.builder()
.tableName(tableName)
.keyConditionExpression("#uuid = :uuid AND begins_with (#sort, :sortprefix)")
@@ -113,26 +111,14 @@ public class KeysDynamoDb extends AbstractDynamoDbStore {
});
}
public Map<Long, PreKey> take(final Account account) {
return TAKE_KEYS_FOR_ACCOUNT_TIMER.record(() -> {
final Map<Long, PreKey> preKeysByDeviceId = new HashMap<>();
for (final Device device : account.getDevices()) {
take(account, device.getId()).ifPresent(preKey -> preKeysByDeviceId.put(device.getId(), preKey));
}
return preKeysByDeviceId;
});
}
public int getCount(final Account account, final long deviceId) {
public int getCount(final UUID identifier, final long deviceId) {
return GET_KEY_COUNT_TIMER.record(() -> {
QueryRequest queryRequest = QueryRequest.builder()
.tableName(tableName)
.keyConditionExpression("#uuid = :uuid AND begins_with (#sort, :sortprefix)")
.expressionAttributeNames(Map.of("#uuid", KEY_ACCOUNT_UUID, "#sort", KEY_DEVICE_ID_KEY_ID))
.expressionAttributeValues(Map.of(
":uuid", getPartitionKey(account.getUuid()),
":uuid", getPartitionKey(identifier),
":sortprefix", getSortKeyPrefix(deviceId)))
.select(Select.COUNT)
.consistentRead(false)
@@ -146,7 +146,7 @@ public class MessagePersister implements Managed {
@VisibleForTesting
void persistQueue(final UUID accountUuid, final long deviceId) {
final Optional<Account> maybeAccount = accountsManager.get(accountUuid);
final Optional<Account> maybeAccount = accountsManager.getByAccountIdentifier(accountUuid);
if (maybeAccount.isEmpty()) {
logger.error("No account record found for account {}", accountUuid);
@@ -167,7 +167,7 @@ public class MessagesCache extends RedisClusterPubSubAdapter<String, String> imp
for (final byte[] bytes : serialized) {
try {
removedMessages.add(constructEntityFromEnvelope(0, MessageProtos.Envelope.parseFrom(bytes)));
removedMessages.add(constructEntityFromEnvelope(MessageProtos.Envelope.parseFrom(bytes)));
} catch (final InvalidProtocolBufferException e) {
logger.warn("Failed to parse envelope", e);
}
@@ -208,7 +208,7 @@ public class MessagesCache extends RedisClusterPubSubAdapter<String, String> imp
final long id = Long.parseLong(new String(queueItems.get(i + 1), StandardCharsets.UTF_8));
messageEntities.add(constructEntityFromEnvelope(id, message));
messageEntities.add(constructEntityFromEnvelope(message));
} catch (InvalidProtocolBufferException e) {
logger.warn("Failed to parse envelope", e);
}
@@ -376,8 +376,8 @@ public class MessagesCache extends RedisClusterPubSubAdapter<String, String> imp
}
@VisibleForTesting
static OutgoingMessageEntity constructEntityFromEnvelope(long id, MessageProtos.Envelope envelope) {
return new OutgoingMessageEntity(id, true,
static OutgoingMessageEntity constructEntityFromEnvelope(MessageProtos.Envelope envelope) {
return new OutgoingMessageEntity(
envelope.hasServerGuid() ? UUID.fromString(envelope.getServerGuid()) : null,
envelope.getType().getNumber(),
envelope.getRelay(),
@@ -385,6 +385,7 @@ public class MessagesCache extends RedisClusterPubSubAdapter<String, String> imp
envelope.getSource(),
envelope.hasSourceUuid() ? UUID.fromString(envelope.getSourceUuid()) : null,
envelope.getSourceDevice(),
envelope.hasDestinationUuid() ? UUID.fromString(envelope.getDestinationUuid()) : null,
envelope.hasLegacyMessage() ? envelope.getLegacyMessage().toByteArray() : null,
envelope.hasContent() ? envelope.getContent().toByteArray() : null,
envelope.hasServerTimestamp() ? envelope.getServerTimestamp() : 0);
@@ -46,6 +46,7 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
private static final String KEY_SOURCE = "SN";
private static final String KEY_SOURCE_UUID = "SU";
private static final String KEY_SOURCE_DEVICE = "SD";
private static final String KEY_DESTINATION_UUID = "DU";
private static final String KEY_MESSAGE = "M";
private static final String KEY_CONTENT = "C";
private static final String KEY_TTL = "E";
@@ -224,9 +225,10 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
final String source = AttributeValues.getString(message, KEY_SOURCE, null);
final UUID sourceUuid = AttributeValues.getUUID(message, KEY_SOURCE_UUID, null);
final int sourceDevice = AttributeValues.getInt(message, KEY_SOURCE_DEVICE, 0);
final UUID destinationUuid = AttributeValues.getUUID(message, KEY_DESTINATION_UUID, null);
final byte[] messageBytes = AttributeValues.getByteArray(message, KEY_MESSAGE, null);
final byte[] content = AttributeValues.getByteArray(message, KEY_CONTENT, null);
return new OutgoingMessageEntity(-1L, false, messageUuid, type, relay, timestamp, source, sourceUuid, sourceDevice, messageBytes, content, sortKey.getServerTimestamp());
return new OutgoingMessageEntity(messageUuid, type, relay, timestamp, source, sourceUuid, sourceDevice, destinationUuid, messageBytes, content, sortKey.getServerTimestamp());
}
private void deleteRowsMatchingQuery(AttributeValue partitionKey, QueryRequest querySpec) {
@@ -62,7 +62,7 @@ public class NonNormalizedAccountCrawlerListener extends AccountDatabaseCrawlerL
workingNonNormalizedNumbers++;
try {
final Optional<Account> maybeConflictingAccount = accountsManager.get(getNormalizedNumber(account));
final Optional<Account> maybeConflictingAccount = accountsManager.getByE164(getNormalizedNumber(account));
if (maybeConflictingAccount.isPresent()) {
workingConflictingNumbers++;
@@ -0,0 +1,85 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import com.google.common.annotations.VisibleForTesting;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Timer;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse;
import java.util.Map;
import java.util.UUID;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
/**
* Manages a global, persistent mapping of phone numbers to phone number identifiers regardless of whether those
* numbers/identifiers are actually associated with an account.
*/
public class PhoneNumberIdentifiers {
private final DynamoDbClient dynamoDbClient;
private final String tableName;
@VisibleForTesting
static final String KEY_E164 = "P";
private static final String ATTR_PHONE_NUMBER_IDENTIFIER = "PNI";
private static final Timer GET_PNI_TIMER = Metrics.timer(name(PhoneNumberIdentifiers.class, "get"));
private static final Timer SET_PNI_TIMER = Metrics.timer(name(PhoneNumberIdentifiers.class, "set"));
public PhoneNumberIdentifiers(final DynamoDbClient dynamoDbClient, final String tableName) {
this.dynamoDbClient = dynamoDbClient;
this.tableName = tableName;
}
/**
* Returns the phone number identifier (PNI) associated with the given phone number.
*
* @param phoneNumber the phone number for which to retrieve a phone number identifier
* @return the phone number identifier associated with the given phone number
*/
public UUID getPhoneNumberIdentifier(final String phoneNumber) {
final GetItemResponse response = GET_PNI_TIMER.record(() -> dynamoDbClient.getItem(GetItemRequest.builder()
.tableName(tableName)
.key(Map.of(KEY_E164, AttributeValues.fromString(phoneNumber)))
.projectionExpression(ATTR_PHONE_NUMBER_IDENTIFIER)
.build()));
final UUID phoneNumberIdentifier;
if (response.hasItem()) {
phoneNumberIdentifier = AttributeValues.getUUID(response.item(), ATTR_PHONE_NUMBER_IDENTIFIER, null);
} else {
phoneNumberIdentifier = generatePhoneNumberIdentifierIfNotExists(phoneNumber);
}
if (phoneNumberIdentifier == null) {
throw new RuntimeException("Could not retrieve phone number identifier from stored item");
}
return phoneNumberIdentifier;
}
@VisibleForTesting
UUID generatePhoneNumberIdentifierIfNotExists(final String phoneNumber) {
final UpdateItemResponse response = SET_PNI_TIMER.record(() -> dynamoDbClient.updateItem(UpdateItemRequest.builder()
.tableName(tableName)
.key(Map.of(KEY_E164, AttributeValues.fromString(phoneNumber)))
.updateExpression("SET #pni = if_not_exists(#pni, :pni)")
.expressionAttributeNames(Map.of("#pni", ATTR_PHONE_NUMBER_IDENTIFIER))
.expressionAttributeValues(Map.of(":pni", AttributeValues.fromUUID(UUID.randomUUID())))
.returnValues(ReturnValue.ALL_NEW)
.build()));
return AttributeValues.getUUID(response.attributes(), ATTR_PHONE_NUMBER_IDENTIFIER, null);
}
}
@@ -58,7 +58,7 @@ public class PushFeedbackProcessor extends AccountDatabaseCrawlerListener {
if (update) {
// fetch a new version, since the chunk is shared and implicitly read-only
accountsManager.get(account.getUuid()).ifPresent(accountToUpdate -> {
accountsManager.getByAccountIdentifier(account.getUuid()).ifPresent(accountToUpdate -> {
accountsManager.update(accountToUpdate, a -> {
for (Device device : a.getDevices()) {
if (deviceNeedsUpdate(device)) {
@@ -24,7 +24,7 @@ public class RefreshingAccountAndDeviceSupplier implements Supplier<Pair<Account
@Override
public Pair<Account, Device> get() {
if (account.isStale()) {
account = accountsManager.get(account.getUuid())
account = accountsManager.getByAccountIdentifier(account.getUuid())
.orElseThrow(() -> new RuntimeException("Could not find account"));
device = account.getDevice(device.getId())
.orElseThrow(() -> new RefreshingAccountAndDeviceNotFoundException("Could not find device"));
@@ -29,6 +29,7 @@ import com.stripe.param.SetupIntentCreateParams;
import com.stripe.param.SubscriptionCancelParams;
import com.stripe.param.SubscriptionCreateParams;
import com.stripe.param.SubscriptionListParams;
import com.stripe.param.SubscriptionRetrieveParams;
import com.stripe.param.SubscriptionUpdateParams;
import com.stripe.param.SubscriptionUpdateParams.BillingCycleAnchor;
import com.stripe.param.SubscriptionUpdateParams.ProrationBehavior;
@@ -62,11 +63,13 @@ public class StripeManager {
private final String apiKey;
private final Executor executor;
private final byte[] idempotencyKeyGenerator;
private final String boostDescription;
public StripeManager(
@Nonnull String apiKey,
@Nonnull Executor executor,
@Nonnull byte[] idempotencyKeyGenerator) {
@Nonnull byte[] idempotencyKeyGenerator,
@Nonnull String boostDescription) {
this.apiKey = Objects.requireNonNull(apiKey);
if (Strings.isNullOrEmpty(apiKey)) {
throw new IllegalArgumentException("apiKey cannot be empty");
@@ -76,6 +79,7 @@ public class StripeManager {
if (idempotencyKeyGenerator.length == 0) {
throw new IllegalArgumentException("idempotencyKeyGenerator cannot be empty");
}
this.boostDescription = Objects.requireNonNull(boostDescription);
}
private RequestOptions commonOptions() {
@@ -148,6 +152,7 @@ public class StripeManager {
PaymentIntentCreateParams params = PaymentIntentCreateParams.builder()
.setAmount(amount)
.setCurrency(currency.toLowerCase(Locale.ROOT))
.setDescription(boostDescription)
.build();
try {
return PaymentIntent.create(params, commonOptions());
@@ -341,6 +346,19 @@ public class StripeManager {
}, executor);
}
public CompletableFuture<Invoice> getLatestInvoiceForSubscription(String subscriptionId) {
return CompletableFuture.supplyAsync(() -> {
SubscriptionRetrieveParams params = SubscriptionRetrieveParams.builder()
.addExpand("latest_invoice")
.build();
try {
return Subscription.retrieve(subscriptionId, params, commonOptions()).getLatestInvoiceObject();
} catch (StripeException e) {
throw new CompletionException(e);
}
}, executor);
}
public CompletableFuture<Collection<InvoiceLineItem>> getInvoiceLineItemsForInvoice(Invoice invoice) {
return CompletableFuture.supplyAsync(
() -> Lists.newArrayList(invoice.getLines().autoPagingIterable(null, commonOptions())), executor);
@@ -119,6 +119,6 @@ public class AttributeValues {
}
public static UUID getUUID(Map<String, AttributeValue> item, String key, UUID defaultValue) {
return AttributeValues.get(item, key).map(AttributeValues::toUUID).orElse(defaultValue);
return AttributeValues.get(item, key).filter(av -> av.b() != null).map(AttributeValues::toUUID).orElse(defaultValue);
}
}
@@ -10,6 +10,7 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat;
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
@@ -107,16 +108,6 @@ public class Util {
return param == null || param.length() == 0;
}
public static byte[] combine(byte[] one, byte[] two, byte[] three, byte[] four) {
byte[] combined = new byte[one.length + two.length + three.length + four.length];
System.arraycopy(one, 0, combined, 0, one.length);
System.arraycopy(two, 0, combined, one.length, two.length);
System.arraycopy(three, 0, combined, one.length + two.length, three.length);
System.arraycopy(four, 0, combined, one.length + two.length + three.length, four.length);
return combined;
}
public static byte[] truncate(byte[] element, int length) {
byte[] result = new byte[length];
System.arraycopy(element, 0, result, 0, result.length);
@@ -124,7 +115,6 @@ public class Util {
return result;
}
public static byte[][] split(byte[] input, int firstLength, int secondLength) {
byte[][] parts = new byte[2][];
@@ -161,11 +151,19 @@ public class Util {
return data;
}
public static byte[] longToByteArray(long value) {
final ByteBuffer longBuffer = ByteBuffer.allocate(Long.BYTES);
longBuffer.putLong(value);
return longBuffer.array();
}
public static int toIntExact(long value) {
if ((int)value != value) {
if ((int) value != value) {
throw new ArithmeticException("integer overflow");
}
return (int)value;
return (int) value;
}
public static int currentDaysSinceEpoch() {
@@ -47,7 +47,7 @@ public class DeadLetterHandler implements DispatchChannel {
switch (pubSubMessage.getType().getNumber()) {
case PubSubMessage.Type.DELIVER_VALUE:
Envelope message = Envelope.parseFrom(pubSubMessage.getContent());
Optional<Account> maybeAccount = accountsManager.get(address.getNumber());
Optional<Account> maybeAccount = accountsManager.getByE164(address.getNumber());
if (maybeAccount.isPresent()) {
messagesManager.insert(maybeAccount.get().getUuid(), address.getDeviceId(), message);
@@ -289,7 +289,7 @@ public class WebSocketConnection implements MessageAvailabilityListener, Displac
for (int i = 0; i < messages.getMessages().size(); i++) {
final OutgoingMessageEntity message = messages.getMessages().get(i);
final Envelope.Builder builder = Envelope.newBuilder()
.setType(Envelope.Type.valueOf(message.getType()))
.setType(Envelope.Type.forNumber(message.getType()))
.setTimestamp(message.getTimestamp())
.setServerTimestamp(message.getServerTimestamp());
@@ -18,6 +18,7 @@ import io.dropwizard.jdbi3.JdbiFactory;
import io.dropwizard.setup.Environment;
import io.lettuce.core.resource.ClientResources;
import io.micrometer.core.instrument.Metrics;
import java.time.Clock;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -43,10 +44,11 @@ import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.storage.MessagesCache;
import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
import org.whispersystems.textsecuregcm.storage.Profiles;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
@@ -88,6 +90,7 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
throws Exception
{
try {
Clock clock = Clock.systemUTC();
String[] users = namespace.getString("user").split(",");
environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@@ -110,6 +113,9 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig.client(
configuration.getDeletedAccountsDynamoDbConfiguration(),
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
DynamoDbClient phoneNumberIdentifiersDynamoDbClient =
DynamoDbFromConfig.client(configuration.getPhoneNumberIdentifiersDynamoDbConfiguration(),
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster",
configuration.getCacheClusterConfiguration(), redisClusterClientResources);
@@ -122,11 +128,9 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
.executorService(name(getClass(), "storageService-%d")).maxThreads(8).minThreads(1).build();
ExternalServiceCredentialGenerator backupCredentialsGenerator = new ExternalServiceCredentialGenerator(
configuration.getSecureBackupServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0],
false);
configuration.getSecureBackupServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
ExternalServiceCredentialGenerator storageCredentialsGenerator = new ExternalServiceCredentialGenerator(
configuration.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0],
false);
configuration.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager = new DynamicConfigurationManager<>(
configuration.getAppConfig().getApplication(), configuration.getAppConfig().getEnvironment(),
@@ -156,11 +160,14 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
Accounts accounts = new Accounts(accountsDynamoDbClient,
configuration.getAccountsDynamoDbConfiguration().getTableName(),
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberIdentifierTableName(),
configuration.getAccountsDynamoDbConfiguration().getScanPageSize());
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(phoneNumberIdentifiersDynamoDbClient,
configuration.getPhoneNumberIdentifiersDynamoDbConfiguration().getTableName());
Usernames usernames = new Usernames(accountDatabase);
Profiles profiles = new Profiles(accountDatabase);
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
KeysDynamoDb keysDynamoDb = new KeysDynamoDb(preKeysDynamoDb,
Keys keys = new Keys(preKeysDynamoDb,
configuration.getKeysDynamoDbConfiguration().getTableName());
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(messageDynamoDb,
configuration.getMessageDynamoDbConfiguration().getTableName(),
@@ -183,7 +190,7 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
Executors.newSingleThreadScheduledExecutor(), keyspaceNotificationDispatchExecutor);
MessagesCache messagesCache = new MessagesCache(messageInsertCacheCluster, messageReadDeleteCluster,
keyspaceNotificationDispatchExecutor);
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster);
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster, dynamicConfigurationManager);
DirectoryQueue directoryQueue = new DirectoryQueue(
configuration.getDirectoryConfiguration().getSqsConfiguration());
UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheCluster);
@@ -199,12 +206,12 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
deletedAccountsLockDynamoDbClient,
configuration.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster,
deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager,
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager);
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
deletedAccountsManager, directoryQueue, keys, messagesManager, usernamesManager, profilesManager,
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager, clock);
for (String user : users) {
Optional<Account> account = accountsManager.get(user);
Optional<Account> account = accountsManager.getByE164(user);
if (account.isPresent()) {
accountsManager.delete(account.get(), DeletionReason.ADMIN_DELETED);
@@ -18,6 +18,7 @@ import io.dropwizard.jdbi3.JdbiFactory;
import io.dropwizard.setup.Environment;
import io.lettuce.core.resource.ClientResources;
import io.micrometer.core.instrument.Metrics;
import java.time.Clock;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
@@ -41,10 +42,11 @@ import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.storage.MessagesCache;
import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
import org.whispersystems.textsecuregcm.storage.Profiles;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
@@ -91,6 +93,7 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
final WhisperServerConfiguration configuration) throws Exception {
try {
Clock clock = Clock.systemUTC();
environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
JdbiFactory jdbiFactory = new JdbiFactory();
@@ -112,6 +115,9 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig
.client(configuration.getDeletedAccountsDynamoDbConfiguration(),
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
DynamoDbClient phoneNumberIdentifiersDynamoDbClient =
DynamoDbFromConfig.client(configuration.getPhoneNumberIdentifiersDynamoDbConfiguration(),
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster",
configuration.getCacheClusterConfiguration(), redisClusterClientResources);
@@ -126,11 +132,9 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
.executorService(name(getClass(), "storageService-%d")).maxThreads(8).minThreads(1).build();
ExternalServiceCredentialGenerator backupCredentialsGenerator = new ExternalServiceCredentialGenerator(
configuration.getSecureBackupServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0],
false);
configuration.getSecureBackupServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
ExternalServiceCredentialGenerator storageCredentialsGenerator = new ExternalServiceCredentialGenerator(
configuration.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0],
false);
configuration.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager = new DynamicConfigurationManager<>(
configuration.getAppConfig().getApplication(), configuration.getAppConfig().getEnvironment(),
@@ -161,11 +165,14 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
Accounts accounts = new Accounts(accountsDynamoDbClient,
configuration.getAccountsDynamoDbConfiguration().getTableName(),
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberIdentifierTableName(),
configuration.getAccountsDynamoDbConfiguration().getScanPageSize());
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(phoneNumberIdentifiersDynamoDbClient,
configuration.getPhoneNumberIdentifiersDynamoDbConfiguration().getTableName());
Usernames usernames = new Usernames(accountDatabase);
Profiles profiles = new Profiles(accountDatabase);
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
KeysDynamoDb keysDynamoDb = new KeysDynamoDb(preKeysDynamoDb,
Keys keys = new Keys(preKeysDynamoDb,
configuration.getKeysDynamoDbConfiguration().getTableName());
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(messageDynamoDb,
configuration.getMessageDynamoDbConfiguration().getTableName(),
@@ -186,7 +193,7 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
Executors.newSingleThreadScheduledExecutor(), keyspaceNotificationDispatchExecutor);
MessagesCache messagesCache = new MessagesCache(messageInsertCacheCluster, messageReadDeleteCluster,
keyspaceNotificationDispatchExecutor);
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster);
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster, dynamicConfigurationManager);
DirectoryQueue directoryQueue = new DirectoryQueue(
configuration.getDirectoryConfiguration().getSqsConfiguration());
UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheCluster);
@@ -202,16 +209,16 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
deletedAccountsLockDynamoDbClient,
configuration.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster,
deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager,
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager);
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
deletedAccountsManager, directoryQueue, keys, messagesManager, usernamesManager, profilesManager,
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager, clock);
Optional<Account> maybeAccount;
try {
maybeAccount = accountsManager.get(UUID.fromString(namespace.getString("user")));
maybeAccount = accountsManager.getByAccountIdentifier(UUID.fromString(namespace.getString("user")));
} catch (final IllegalArgumentException e) {
maybeAccount = accountsManager.get(namespace.getString("user"));
maybeAccount = accountsManager.getByE164(namespace.getString("user"));
}
maybeAccount.ifPresentOrElse(account -> {
+1
View File
@@ -42,6 +42,7 @@ message Envelope {
optional string serverGuid = 9;
optional uint64 server_timestamp = 10;
optional bool ephemeral = 12; // indicates that the message should not be persisted if the recipient is offline
optional string destinationUuid = 13;
}
message ProvisioningUuid {
@@ -16,13 +16,13 @@ TEST3_name = Test Badge Gamma
TEST3_description = {short_name} is testing the gamma test badge.
R_LOW_name = Signal Star
R_LOW_description = {short_name} supports Signal by making a monthly donation.
R_LOW_description = {short_name} is a Sustainer. Signal is a nonprofit with no advertisers or investors, supported only by people like you.
R_MED_name = Signal Planet
R_MED_description = {short_name} supports Signal by making a monthly donation.
R_MED_description = {short_name} is a Sustainer. Signal is a nonprofit with no advertisers or investors, supported only by people like you.
R_HIGH_name = Signal Sun
R_HIGH_description = {short_name} supports Signal by making a monthly donation.
R_HIGH_description = {short_name} is a Sustainer. Signal is a nonprofit with no advertisers or investors, supported only by people like you.
BOOST_name = Signal Boost
BOOST_description = {short_name} supported Signal by making a donation.
BOOST_description = {short_name} supported Signal with a boost. Signal is a nonprofit with no advertisers or investors, supported only by people like you.
@@ -0,0 +1,56 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm;
import java.io.File;
import java.util.Arrays;
/**
* Checks whether all YAML configuration files in a given directory are valid.
* <p>
* Note: the current implementation fails fast, rather than reporting multiple invalid files
*/
public class CheckServiceConfigurations {
private void checkConfiguration(final File configDirectory) {
final File[] configFiles = configDirectory.listFiles(f ->
!f.isDirectory()
&& f.getPath().endsWith(".yml"));
if (configFiles == null || configFiles.length == 0) {
throw new IllegalArgumentException("No .yml configuration files found at " + configDirectory.getPath());
}
for (File configFile : configFiles) {
String[] args = new String[]{"check", configFile.getAbsolutePath()};
try {
new WhisperServerService().run(args);
} catch (final Exception e) {
// Invalid configuration will cause the "check" command to call `System.exit()`, rather than throwing,
// so this is unexpected
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
if (args.length != 1) {
throw new IllegalArgumentException("Expected single argument with config directory: " + Arrays.toString(args));
}
final File configDirectory = new File(args[0]);
if (!(configDirectory.exists() && configDirectory.isDirectory())) {
throw new IllegalArgumentException("No directory found at " + configDirectory.getPath());
}
new CheckServiceConfigurations().checkConfiguration(configDirectory);
}
}
@@ -128,7 +128,7 @@ class AuthEnablementRefreshRequirementProviderTest {
account.addDevice(authenticatedDevice);
LongStream.range(2, 4).forEach(deviceId -> account.addDevice(DevicesHelper.createDevice(deviceId)));
when(accountsManager.get(uuid)).thenReturn(Optional.of(account));
when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
account.getDevices()
.forEach(device -> when(clientPresenceManager.isPresent(uuid, device.getId())).thenReturn(true));
@@ -310,7 +310,7 @@ class AuthEnablementRefreshRequirementProviderTest {
assertEquals(200, response.getStatus());
verify(accountsManager, never()).get(any(UUID.class));
verify(accountsManager, never()).getByAccountIdentifier(any(UUID.class));
}
@Nested
@@ -31,9 +31,6 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.BaseAccountAuthenticator;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
@@ -62,11 +59,14 @@ class BaseAccountAuthenticatorTest {
clock = mock(Clock.class);
baseAccountAuthenticator = new BaseAccountAuthenticator(accountsManager, clock);
acct1 = new Account("+14088675309", AuthHelper.getRandomUUID(random), Set.of(new Device(1, null, null, null,
acct1 = new Account("+14088675309", AuthHelper.getRandomUUID(random), UUID.randomUUID(),
Set.of(new Device(1, null, null, null,
null, null, null, false, 0, null, yesterday, 0, null, 0, null)), null);
acct2 = new Account("+14098675309", AuthHelper.getRandomUUID(random), Set.of(new Device(1, null, null, null,
acct2 = new Account("+14098675309", AuthHelper.getRandomUUID(random), UUID.randomUUID(),
Set.of(new Device(1, null, null, null,
null, null, null, false, 0, null, yesterday, 0, null, 0, null)), null);
oldAccount = new Account("+14108675309", AuthHelper.getRandomUUID(random), Set.of(new Device(1, null, null, null,
oldAccount = new Account("+14108675309", AuthHelper.getRandomUUID(random), UUID.randomUUID(),
Set.of(new Device(1, null, null, null,
null, null, null, false, 0, null, oldTime, 0, null, 0, null)), null);
AccountsHelper.setupMockUpdate(accountsManager);
@@ -156,7 +156,7 @@ class BaseAccountAuthenticatorTest {
final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class);
when(clock.instant()).thenReturn(Instant.now());
when(accountsManager.get(uuid)).thenReturn(Optional.of(account));
when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
when(account.getUuid()).thenReturn(uuid);
when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
when(account.isEnabled()).thenReturn(true);
@@ -184,7 +184,7 @@ class BaseAccountAuthenticatorTest {
final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class);
when(clock.instant()).thenReturn(Instant.now());
when(accountsManager.get(uuid)).thenReturn(Optional.of(account));
when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
when(account.getUuid()).thenReturn(uuid);
when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
when(account.isEnabled()).thenReturn(true);
@@ -213,7 +213,7 @@ class BaseAccountAuthenticatorTest {
final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class);
when(clock.instant()).thenReturn(Instant.now());
when(accountsManager.get(uuid)).thenReturn(Optional.of(account));
when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
when(account.getUuid()).thenReturn(uuid);
when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
when(account.isEnabled()).thenReturn(false);
@@ -251,7 +251,7 @@ class BaseAccountAuthenticatorTest {
final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class);
when(clock.instant()).thenReturn(Instant.now());
when(accountsManager.get(uuid)).thenReturn(Optional.of(account));
when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
when(account.getUuid()).thenReturn(uuid);
when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
when(account.isEnabled()).thenReturn(true);
@@ -278,7 +278,7 @@ class BaseAccountAuthenticatorTest {
final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class);
when(clock.instant()).thenReturn(Instant.now());
when(accountsManager.get(uuid)).thenReturn(Optional.of(account));
when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
when(account.getUuid()).thenReturn(uuid);
when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
when(account.isEnabled()).thenReturn(true);
@@ -303,7 +303,7 @@ class BaseAccountAuthenticatorTest {
() -> baseAccountAuthenticator.authenticate(new BasicCredentials(username, "password"), true));
assertThat(maybeAuthenticatedAccount).isEmpty();
verify(accountsManager, never()).get(any(UUID.class));
verify(accountsManager, never()).getByAccountIdentifier(any(UUID.class));
}
private static Stream<String> testAuthenticateMalformedCredentials() {
@@ -69,7 +69,7 @@ public class ConfiguredProfileBadgeConverterTest {
private static BadgeConfiguration newBadge(int i) {
return new BadgeConfiguration(
idFor(i), "other", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG",
List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"), new BadgeSvg("ll", "ld", "lt")));
List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")));
}
private BadgesConfiguration createBadges(int count) {
@@ -142,8 +142,7 @@ public class ConfiguredProfileBadgeConverterTest {
arguments(idFor(0), expired, true, false, null),
arguments(idFor(0), notExpired, true, false,
new Badge(idFor(0), "other", nameFor(0), desriptionFor(0), List.of("l", "m", "h", "x", "xx", "xxx"), "SVG",
List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"),
new BadgeSvg("ll", "ld", "lt")))),
List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")))),
arguments(idFor(1), expired, false, false, null),
arguments(idFor(1), notExpired, false, false, null),
arguments(idFor(1), expired, true, false, null),
@@ -152,13 +151,13 @@ public class ConfiguredProfileBadgeConverterTest {
arguments(idFor(0), notExpired, false, true,
new SelfBadge(idFor(0), "other", nameFor(0), desriptionFor(0), List.of("l", "m", "h", "x", "xx", "xxx"),
"SVG",
List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"), new BadgeSvg("ll", "ld", "lt")),
List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")),
notExpired, false)),
arguments(idFor(0), expired, true, true, null),
arguments(idFor(0), notExpired, true, true,
new SelfBadge(idFor(0), "other", nameFor(0), desriptionFor(0), List.of("l", "m", "h", "x", "xx", "xxx"),
"SVG",
List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"), new BadgeSvg("ll", "ld", "lt")),
List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")),
notExpired, true)),
arguments(idFor(1), expired, false, true, null),
arguments(idFor(1), notExpired, false, true, null),
@@ -38,18 +38,19 @@ class DynamicConfigurationTest {
}
{
final String experimentConfigYaml =
"experiments:\n" +
" percentageOnly:\n" +
" enrollmentPercentage: 12\n" +
" uuidsAndPercentage:\n" +
" enrolledUuids:\n" +
" - 717b1c09-ed0b-4120-bb0e-f4697534b8e1\n" +
" - 279f264c-56d7-4bbf-b9da-de718ff90903\n" +
" enrollmentPercentage: 77\n" +
" uuidsOnly:\n" +
" enrolledUuids:\n" +
" - 71618739-114c-4b1f-bb0d-6478a44eb600";
final String experimentConfigYaml = """
experiments:
percentageOnly:
enrollmentPercentage: 12
uuidsAndPercentage:
enrolledUuids:
- 717b1c09-ed0b-4120-bb0e-f4697534b8e1
- 279f264c-56d7-4bbf-b9da-de718ff90903
enrollmentPercentage: 77
uuidsOnly:
enrolledUuids:
- 71618739-114c-4b1f-bb0d-6478a44eb600
""";
final DynamicConfiguration config =
DynamicConfigurationManager.parseConfiguration(experimentConfigYaml, DynamicConfiguration.class).orElseThrow();
@@ -86,27 +87,28 @@ class DynamicConfigurationTest {
}
{
final String experimentConfigYaml =
"preRegistrationExperiments:\n" +
" percentageOnly:\n" +
" enrollmentPercentage: 17\n" +
" e164sCountryCodesAndPercentage:\n" +
" enrolledE164s:\n" +
" - +120255551212\n" +
" - +3655323174\n" +
" excludedE164s:\n" +
" - +120255551213\n" +
" - +3655323175\n" +
" enrollmentPercentage: 46\n" +
" excludedCountryCodes:\n" +
" - 47\n" +
" includedCountryCodes:\n" +
" - 56\n" +
" e164sAndExcludedCodes:\n" +
" enrolledE164s:\n" +
" - +120255551212\n" +
" excludedCountryCodes:\n" +
" - 47";
final String experimentConfigYaml = """
preRegistrationExperiments:
percentageOnly:
enrollmentPercentage: 17
e164sCountryCodesAndPercentage:
enrolledE164s:
- +120255551212
- +3655323174
excludedE164s:
- +120255551213
- +3655323175
enrollmentPercentage: 46
excludedCountryCodes:
- 47
includedCountryCodes:
- 56
e164sAndExcludedCodes:
enrolledE164s:
- +120255551212
excludedCountryCodes:
- 47
""";
final DynamicConfiguration config =
DynamicConfigurationManager.parseConfiguration(experimentConfigYaml, DynamicConfiguration.class).orElseThrow();
@@ -167,18 +169,17 @@ class DynamicConfigurationTest {
}
{
final String remoteDeprecationConfig =
"remoteDeprecation:\n" +
" minimumVersions:\n" +
" IOS: 1.2.3\n" +
" ANDROID: 4.5.6\n" +
" versionsPendingDeprecation:\n" +
" DESKTOP: 7.8.9\n" +
" blockedVersions:\n" +
" DESKTOP:\n" +
" - 1.4.0-beta.2";
final String remoteDeprecationConfig = """
remoteDeprecation:
minimumVersions:
IOS: 1.2.3
ANDROID: 4.5.6
versionsPendingDeprecation:
DESKTOP: 7.8.9
blockedVersions:
DESKTOP:
- 1.4.0-beta.2
""";
final DynamicConfiguration config =
DynamicConfigurationManager.parseConfiguration(remoteDeprecationConfig, DynamicConfiguration.class).orElseThrow();
@@ -207,9 +208,10 @@ class DynamicConfigurationTest {
}
{
final String messageRateConfigYaml =
"messageRate:\n" +
" enforceUnsealedSenderRateLimit: true";
final String messageRateConfigYaml = """
messageRate:
enforceUnsealedSenderRateLimit: true
""";
final DynamicConfiguration emptyConfig =
DynamicConfigurationManager.parseConfiguration(messageRateConfigYaml, DynamicConfiguration.class).orElseThrow();
@@ -229,9 +231,10 @@ class DynamicConfigurationTest {
}
{
final String featureFlagYaml =
"featureFlags:\n"
+ " - testFlag";
final String featureFlagYaml = """
featureFlags:
- testFlag
""";
final DynamicConfiguration emptyConfig =
DynamicConfigurationManager.parseConfiguration(featureFlagYaml, DynamicConfiguration.class).orElseThrow();
@@ -251,11 +254,12 @@ class DynamicConfigurationTest {
}
{
final String twilioConfigYaml =
"twilio:\n"
+ " numbers:\n"
+ " - 2135551212\n"
+ " - 2135551313";
final String twilioConfigYaml = """
twilio:
numbers:
- 2135551212
- 2135551313
""";
final DynamicTwilioConfiguration config =
DynamicConfigurationManager.parseConfiguration(twilioConfigYaml, DynamicConfiguration.class).orElseThrow()
@@ -272,20 +276,21 @@ class DynamicConfigurationTest {
final DynamicConfiguration emptyConfig =
DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow();
assertTrue(emptyConfig.getPaymentsConfiguration().getAllowedCountryCodes().isEmpty());
assertTrue(emptyConfig.getPaymentsConfiguration().getDisallowedPrefixes().isEmpty());
}
{
final String paymentsConfigYaml =
"payments:\n"
+ " allowedCountryCodes:\n"
+ " - 44";
final String paymentsConfigYaml = """
payments:
disallowedPrefixes:
- +44
""";
final DynamicPaymentsConfiguration config =
DynamicConfigurationManager.parseConfiguration(paymentsConfigYaml, DynamicConfiguration.class).orElseThrow()
.getPaymentsConfiguration();
assertEquals(Set.of("44"), config.getAllowedCountryCodes());
assertEquals(List.of("+44"), config.getDisallowedPrefixes());
}
}
@@ -300,10 +305,11 @@ class DynamicConfigurationTest {
}
{
final String signupCaptchaConfig =
"signupCaptcha:\n"
+ " countryCodes:\n"
+ " - 1";
final String signupCaptchaConfig = """
signupCaptcha:
countryCodes:
- 1
""";
final DynamicSignupCaptchaConfiguration config =
DynamicConfigurationManager.parseConfiguration(signupCaptchaConfig, DynamicConfiguration.class).orElseThrow()
@@ -325,11 +331,12 @@ class DynamicConfigurationTest {
}
{
final String limitsConfig =
"limits:\n"
+ " unsealedSenderNumber:\n"
+ " maxCardinality: 99\n"
+ " ttl: PT23H";
final String limitsConfig = """
limits:
unsealedSenderNumber:
maxCardinality: 99
ttl: PT23H
""";
final CardinalityRateLimitConfiguration unsealedSenderNumber =
DynamicConfigurationManager.parseConfiguration(limitsConfig, DynamicConfiguration.class).orElseThrow()
@@ -353,13 +360,14 @@ class DynamicConfigurationTest {
}
{
final String rateLimitChallengeConfig =
"rateLimitChallenge:\n"
+ " preKeyLimitEnforced: true\n"
+ " clientSupportedVersions:\n"
+ " IOS: 5.1.0\n"
+ " ANDROID: 5.2.0\n"
+ " DESKTOP: 5.0.0";
final String rateLimitChallengeConfig = """
rateLimitChallenge:
preKeyLimitEnforced: true
clientSupportedVersions:
IOS: 5.1.0
ANDROID: 5.2.0
DESKTOP: 5.0.0
""";
DynamicRateLimitChallengeConfiguration rateLimitChallengeConfiguration =
DynamicConfigurationManager.parseConfiguration(rateLimitChallengeConfig, DynamicConfiguration.class).orElseThrow()
@@ -386,9 +394,10 @@ class DynamicConfigurationTest {
}
{
final String directoryReconcilerConfig =
"directoryReconciler:\n"
+ " enabled: false";
final String directoryReconcilerConfig = """
directoryReconciler:
enabled: false
""";
DynamicDirectoryReconcilerConfiguration directoryReconcilerConfiguration =
DynamicConfigurationManager.parseConfiguration(directoryReconcilerConfig, DynamicConfiguration.class).orElseThrow()
@@ -0,0 +1,66 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import static org.junit.jupiter.api.Assertions.assertEquals;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
import io.dropwizard.testing.junit5.ResourceExtension;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
@ExtendWith(DropwizardExtensionsSupport.class)
class AcceptNumericOnlineFlagRequestFilterTest {
@Path("/test")
public static class TestResource {
@Path("/convert-boolean")
@GET
public boolean convertBoolean(@QueryParam("online") final boolean online) {
return online;
}
@Path("/no-conversion")
@GET
public boolean noConversion(@QueryParam("online") final boolean online) {
return online;
}
}
private static final ResourceExtension RESOURCE_EXTENSION = ResourceExtension.builder()
.addProvider(new AcceptNumericOnlineFlagRequestFilter("test/convert-boolean"))
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new TestResource())
.build();
@ParameterizedTest
@CsvSource({
"/test/convert-boolean, true, true",
"/test/convert-boolean, false, false",
"/test/convert-boolean, 1, true",
"/test/convert-boolean, 0, false",
"/test/no-conversion, true, true",
"/test/no-conversion, false, false",
"/test/no-conversion, 1, false",
"/test/no-conversion, 0, false"
})
void filter(final String path, final String value, final boolean expected) {
final boolean response =
RESOURCE_EXTENSION.getJerseyTest()
.target(path)
.queryParam("online", value)
.request()
.get(boolean.class);
assertEquals(expected, response);
}
}
@@ -11,7 +11,7 @@ import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.verifyNoInteractions;
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
@@ -56,10 +56,12 @@ class ChallengeControllerTest {
@Test
void testHandlePushChallenge() throws RateLimitExceededException {
final String pushChallengeJson = "{\n"
+ " \"type\": \"rateLimitPushChallenge\",\n"
+ " \"challenge\": \"Hello I am a push challenge token\"\n"
+ "}";
final String pushChallengeJson = """
{
"type": "rateLimitPushChallenge",
"challenge": "Hello I am a push challenge token"
}
""";
final Response response = EXTENSION.target("/v1/challenge")
.request()
@@ -72,10 +74,12 @@ class ChallengeControllerTest {
@Test
void testHandlePushChallengeRateLimited() throws RateLimitExceededException {
final String pushChallengeJson = "{\n"
+ " \"type\": \"rateLimitPushChallenge\",\n"
+ " \"challenge\": \"Hello I am a push challenge token\"\n"
+ "}";
final String pushChallengeJson = """
{
"type": "rateLimitPushChallenge",
"challenge": "Hello I am a push challenge token"
}
""";
final Duration retryAfter = Duration.ofMinutes(17);
doThrow(new RateLimitExceededException(retryAfter)).when(rateLimitChallengeManager).answerPushChallenge(any(), any());
@@ -91,11 +95,13 @@ class ChallengeControllerTest {
@Test
void testHandleRecaptcha() throws RateLimitExceededException {
final String recaptchaChallengeJson = "{\n"
+ " \"type\": \"recaptcha\",\n"
+ " \"token\": \"A server-generated token\",\n"
+ " \"captcha\": \"The value of the solved captcha token\"\n"
+ "}";
final String recaptchaChallengeJson = """
{
"type": "recaptcha",
"token": "A server-generated token",
"captcha": "The value of the solved captcha token"
}
""";
final Response response = EXTENSION.target("/v1/challenge")
.request()
@@ -109,11 +115,13 @@ class ChallengeControllerTest {
@Test
void testHandleRecaptchaRateLimited() throws RateLimitExceededException {
final String recaptchaChallengeJson = "{\n"
+ " \"type\": \"recaptcha\",\n"
+ " \"token\": \"A server-generated token\",\n"
+ " \"captcha\": \"The value of the solved captcha token\"\n"
+ "}";
final String recaptchaChallengeJson = """
{
"type": "recaptcha",
"token": "A server-generated token",
"captcha": "The value of the solved captcha token"
}
""";
final Duration retryAfter = Duration.ofMinutes(17);
doThrow(new RateLimitExceededException(retryAfter)).when(rateLimitChallengeManager).answerRecaptchaChallenge(any(), any(), any());
@@ -130,11 +138,13 @@ class ChallengeControllerTest {
@Test
void testHandleRecaptchaNoForwardedFor() {
final String recaptchaChallengeJson = "{\n"
+ " \"type\": \"recaptcha\",\n"
+ " \"token\": \"A server-generated token\",\n"
+ " \"captcha\": \"The value of the solved captcha token\"\n"
+ "}";
final String recaptchaChallengeJson = """
{
"type": "recaptcha",
"token": "A server-generated token",
"captcha": "The value of the solved captcha token"
}
""";
final Response response = EXTENSION.target("/v1/challenge")
.request()
@@ -142,14 +152,16 @@ class ChallengeControllerTest {
.put(Entity.json(recaptchaChallengeJson));
assertEquals(400, response.getStatus());
verifyZeroInteractions(rateLimitChallengeManager);
verifyNoInteractions(rateLimitChallengeManager);
}
@Test
void testHandleUnrecognizedAnswer() {
final String unrecognizedJson = "{\n"
+ " \"type\": \"unrecognized\"\n"
+ "}";
final String unrecognizedJson = """
{
"type": "unrecognized"
}
""";
final Response response = EXTENSION.target("/v1/challenge")
.request()
@@ -159,7 +171,7 @@ class ChallengeControllerTest {
assertEquals(400, response.getStatus());
verifyZeroInteractions(rateLimitChallengeManager);
verifyNoInteractions(rateLimitChallengeManager);
}
@Test
@@ -187,9 +199,11 @@ class ChallengeControllerTest {
@Test
void testValidationError() {
final String unrecognizedJson = "{\n"
+ " \"type\": \"rateLimitPushChallenge\"\n"
+ "}";
final String unrecognizedJson = """
{
"type": "rateLimitPushChallenge"
}
""";
final Response response = EXTENSION.target("/v1/challenge")
.request()
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.tests.controllers;
package org.whispersystems.textsecuregcm.controllers;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
@@ -64,10 +64,6 @@ import org.mockito.ArgumentCaptor;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
import org.whispersystems.textsecuregcm.controllers.MessageController;
import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.controllers.StaleDevicesException;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
@@ -98,6 +94,7 @@ class MessageControllerTest {
private static final String SINGLE_DEVICE_RECIPIENT = "+14151111111";
private static final UUID SINGLE_DEVICE_UUID = UUID.randomUUID();
private static final UUID SINGLE_DEVICE_PNI = UUID.randomUUID();
private static final String MULTI_DEVICE_RECIPIENT = "+14152222222";
private static final UUID MULTI_DEVICE_UUID = UUID.randomUUID();
@@ -160,13 +157,14 @@ class MessageControllerTest {
false, false, false)));
}};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, SINGLE_DEVICE_UUID, singleDeviceList, "1234".getBytes());
Account multiDeviceAccount = new Account(MULTI_DEVICE_RECIPIENT, MULTI_DEVICE_UUID, multiDeviceList, "1234".getBytes());
internationalAccount = new Account(INTERNATIONAL_RECIPIENT, INTERNATIONAL_UUID, singleDeviceList, "1234".getBytes());
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, SINGLE_DEVICE_UUID, SINGLE_DEVICE_PNI, singleDeviceList, "1234".getBytes());
Account multiDeviceAccount = new Account(MULTI_DEVICE_RECIPIENT, MULTI_DEVICE_UUID, UUID.randomUUID(), multiDeviceList, "1234".getBytes());
internationalAccount = new Account(INTERNATIONAL_RECIPIENT, INTERNATIONAL_UUID, UUID.randomUUID(), singleDeviceList, "1234".getBytes());
when(accountsManager.get(eq(SINGLE_DEVICE_UUID))).thenReturn(Optional.of(singleDeviceAccount));
when(accountsManager.get(eq(MULTI_DEVICE_UUID))).thenReturn(Optional.of(multiDeviceAccount));
when(accountsManager.get(INTERNATIONAL_UUID)).thenReturn(Optional.of(internationalAccount));
when(accountsManager.getByAccountIdentifier(eq(SINGLE_DEVICE_UUID))).thenReturn(Optional.of(singleDeviceAccount));
when(accountsManager.getByPhoneNumberIdentifier(SINGLE_DEVICE_PNI)).thenReturn(Optional.of(singleDeviceAccount));
when(accountsManager.getByAccountIdentifier(eq(MULTI_DEVICE_UUID))).thenReturn(Optional.of(multiDeviceAccount));
when(accountsManager.getByAccountIdentifier(INTERNATIONAL_UUID)).thenReturn(Optional.of(internationalAccount));
when(rateLimiters.getMessagesLimiter()).thenReturn(rateLimiter);
}
@@ -220,6 +218,57 @@ class MessageControllerTest {
assertTrue(captor.getValue().hasSourceDevice());
}
@ParameterizedTest
@MethodSource
void testSingleDeviceCurrentBadType(final String userAgentString, final boolean expectAcceptMessage) throws Exception {
Response response =
resources.getJerseyTest()
.target(String.format("/v1/messages/%s", SINGLE_DEVICE_UUID))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.header("User-Agent", userAgentString)
.put(Entity.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device_bad_type.json"), IncomingMessageList.class),
MediaType.APPLICATION_JSON_TYPE));
if (expectAcceptMessage) {
assertEquals(200, response.getStatus());
final ArgumentCaptor<Envelope> captor = ArgumentCaptor.forClass(Envelope.class);
verify(messageSender).sendMessage(any(Account.class), any(Device.class), captor.capture(), eq(false));
} else {
assertEquals(400, response.getStatus());
verify(messageSender, never()).sendMessage(any(), any(), any(), anyBoolean());
}
}
private static Stream<Arguments> testSingleDeviceCurrentBadType() {
return Stream.of(
Arguments.of(String.format("Signal-iOS/%s iOS/14.2", MessageController.FIRST_IOS_VERSION_WITH_INCORRECT_ENVELOPE_TYPE), true),
Arguments.of(String.format("Signal-iOS/%s iOS/14.2", MessageController.FIRST_IOS_VERSION_WITH_INCORRECT_ENVELOPE_TYPE.nextPatch()), true),
Arguments.of(String.format("Signal-iOS/%s iOS/14.2", MessageController.IOS_VERSION_WITH_FIXED_ENVELOPE_TYPE.withIncMinor(-1)), true),
Arguments.of(String.format("Signal-iOS/%s iOS/14.2", MessageController.IOS_VERSION_WITH_FIXED_ENVELOPE_TYPE), false)
);
}
@Test
void testSingleDeviceCurrentByPni() throws Exception {
Response response =
resources.getJerseyTest()
.target(String.format("/v1/messages/%s", SINGLE_DEVICE_PNI))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class),
MediaType.APPLICATION_JSON_TYPE));
assertThat("Good Response", response.getStatus(), is(equalTo(200)));
ArgumentCaptor<Envelope> captor = ArgumentCaptor.forClass(Envelope.class);
verify(messageSender, times(1)).sendMessage(any(Account.class), any(Device.class), captor.capture(), eq(false));
assertTrue(captor.getValue().hasSource());
assertTrue(captor.getValue().hasSourceDevice());
}
@Test
void testNullMessageInList() throws Exception {
Response response =
@@ -410,8 +459,8 @@ class MessageControllerTest {
final UUID sourceUuid = UUID.randomUUID();
List<OutgoingMessageEntity> messages = new LinkedList<>() {{
add(new OutgoingMessageEntity(1L, false, messageGuidOne, Envelope.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", sourceUuid, 2, "hi there".getBytes(), null, 0));
add(new OutgoingMessageEntity(2L, false, null, Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE, null, timestampTwo, "+14152222222", sourceUuid, 2, null, null, 0));
add(new OutgoingMessageEntity(messageGuidOne, Envelope.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", sourceUuid, 2, AuthHelper.VALID_UUID, "hi there".getBytes(), null, 0));
add(new OutgoingMessageEntity(null, Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE, null, timestampTwo, "+14152222222", sourceUuid, 2, AuthHelper.VALID_UUID, null, null, 0));
}};
OutgoingMessageEntityList messagesList = new OutgoingMessageEntityList(messages, false);
@@ -428,9 +477,6 @@ class MessageControllerTest {
assertEquals(response.getMessages().size(), 2);
assertEquals(response.getMessages().get(0).getId(), 0);
assertEquals(response.getMessages().get(1).getId(), 0);
assertEquals(response.getMessages().get(0).getTimestamp(), timestampOne);
assertEquals(response.getMessages().get(1).getTimestamp(), timestampTwo);
@@ -447,8 +493,8 @@ class MessageControllerTest {
final long timestampTwo = 313388;
List<OutgoingMessageEntity> messages = new LinkedList<OutgoingMessageEntity>() {{
add(new OutgoingMessageEntity(1L, false, UUID.randomUUID(), Envelope.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", UUID.randomUUID(), 2, "hi there".getBytes(), null, 0));
add(new OutgoingMessageEntity(2L, false, UUID.randomUUID(), Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE, null, timestampTwo, "+14152222222", UUID.randomUUID(), 2, null, null, 0));
add(new OutgoingMessageEntity(UUID.randomUUID(), Envelope.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", UUID.randomUUID(), 2, AuthHelper.VALID_UUID, "hi there".getBytes(), null, 0));
add(new OutgoingMessageEntity(UUID.randomUUID(), Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE, null, timestampTwo, "+14152222222", UUID.randomUUID(), 2, AuthHelper.VALID_UUID, null, null, 0));
}};
OutgoingMessageEntityList messagesList = new OutgoingMessageEntityList(messages, false);
@@ -473,13 +519,13 @@ class MessageControllerTest {
UUID uuid1 = UUID.randomUUID();
when(messagesManager.delete(AuthHelper.VALID_UUID, 1, uuid1)).thenReturn(Optional.of(new OutgoingMessageEntity(
31337L, true, uuid1, Envelope.Type.CIPHERTEXT_VALUE,
null, timestamp, "+14152222222", sourceUuid, 1, "hi".getBytes(), null, 0)));
uuid1, Envelope.Type.CIPHERTEXT_VALUE,
null, timestamp, "+14152222222", sourceUuid, 1, AuthHelper.VALID_UUID, "hi".getBytes(), null, 0)));
UUID uuid2 = UUID.randomUUID();
when(messagesManager.delete(AuthHelper.VALID_UUID, 1, uuid2)).thenReturn(Optional.of(new OutgoingMessageEntity(
31337L, true, uuid2, Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE,
null, System.currentTimeMillis(), "+14152222222", sourceUuid, 1, null, null, 0)));
uuid2, Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE,
null, System.currentTimeMillis(), "+14152222222", sourceUuid, 1, AuthHelper.VALID_UUID, null, null, 0)));
UUID uuid3 = UUID.randomUUID();
@@ -82,11 +82,11 @@ class SubscriptionControllerTest {
3L, new SubscriptionLevelConfiguration("B3", "P3", Map.of("USD", new SubscriptionPriceConfiguration("R3", BigDecimal.valueOf(300))))
));
when(BADGE_TRANSLATOR.translate(any(), eq("B1"))).thenReturn(new Badge("B1", "cat1", "name1", "desc1",
List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"), new BadgeSvg("ll", "ld", "lt"))));
List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))));
when(BADGE_TRANSLATOR.translate(any(), eq("B2"))).thenReturn(new Badge("B2", "cat2", "name2", "desc2",
List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"), new BadgeSvg("ll", "ld", "lt"))));
List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))));
when(BADGE_TRANSLATOR.translate(any(), eq("B3"))).thenReturn(new Badge("B3", "cat3", "name3", "desc3",
List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"), new BadgeSvg("ll", "ld", "lt"))));
List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))));
when(LEVEL_TRANSLATOR.translate(any(), eq("B1"))).thenReturn("Z1");
when(LEVEL_TRANSLATOR.translate(any(), eq("B2"))).thenReturn("Z2");
when(LEVEL_TRANSLATOR.translate(any(), eq("B3"))).thenReturn("Z3");
@@ -17,10 +17,12 @@ class AnswerChallengeRequestTest {
@Test
void parse() throws JsonProcessingException {
{
final String pushChallengeJson = "{\n"
+ " \"type\": \"rateLimitPushChallenge\",\n"
+ " \"challenge\": \"Hello I am a push challenge token\"\n"
+ "}";
final String pushChallengeJson = """
{
"type": "rateLimitPushChallenge",
"challenge": "Hello I am a push challenge token"
}
""";
final AnswerChallengeRequest answerChallengeRequest =
SystemMapper.getMapper().readValue(pushChallengeJson, AnswerChallengeRequest.class);
@@ -31,11 +33,13 @@ class AnswerChallengeRequestTest {
}
{
final String recaptchaChallengeJson = "{\n"
+ " \"type\": \"recaptcha\",\n"
+ " \"token\": \"A server-generated token\",\n"
+ " \"captcha\": \"The value of the solved captcha token\"\n"
+ "}";
final String recaptchaChallengeJson = """
{
"type": "recaptcha",
"token": "A server-generated token",
"captcha": "The value of the solved captcha token"
}
""";
final AnswerChallengeRequest answerChallengeRequest =
SystemMapper.getMapper().readValue(recaptchaChallengeJson, AnswerChallengeRequest.class);
@@ -50,11 +54,13 @@ class AnswerChallengeRequestTest {
}
{
final String unrecognizedTypeJson = "{\n"
+ " \"type\": \"unrecognized\",\n"
+ " \"token\": \"A server-generated token\",\n"
+ " \"captcha\": \"The value of the solved captcha token\"\n"
+ "}";
final String unrecognizedTypeJson = """
{
"type": "unrecognized",
"token": "A server-generated token",
"captcha": "The value of the solved captcha token"
}
""";
assertThrows(InvalidTypeIdException.class,
() -> SystemMapper.getMapper().readValue(unrecognizedTypeJson, AnswerChallengeRequest.class));
@@ -18,59 +18,61 @@ import static org.junit.Assert.*;
public class OperatingSystemMemoryGaugeTest {
private static final String MEMINFO =
"MemTotal: 16052208 kB\n" +
"MemFree: 4568468 kB\n" +
"MemAvailable: 7702848 kB\n" +
"Buffers: 636372 kB\n" +
"Cached: 5019116 kB\n" +
"SwapCached: 6692 kB\n" +
"Active: 7746436 kB\n" +
"Inactive: 2729876 kB\n" +
"Active(anon): 5580980 kB\n" +
"Inactive(anon): 1648108 kB\n" +
"Active(file): 2165456 kB\n" +
"Inactive(file): 1081768 kB\n" +
"Unevictable: 443948 kB\n" +
"Mlocked: 4924 kB\n" +
"SwapTotal: 1003516 kB\n" +
"SwapFree: 935932 kB\n" +
"Dirty: 28308 kB\n" +
"Writeback: 0 kB\n" +
"AnonPages: 5258396 kB\n" +
"Mapped: 1530740 kB\n" +
"Shmem: 2419340 kB\n" +
"KReclaimable: 229392 kB\n" +
"Slab: 408156 kB\n" +
"SReclaimable: 229392 kB\n" +
"SUnreclaim: 178764 kB\n" +
"KernelStack: 17360 kB\n" +
"PageTables: 50436 kB\n" +
"NFS_Unstable: 0 kB\n" +
"Bounce: 0 kB\n" +
"WritebackTmp: 0 kB\n" +
"CommitLimit: 9029620 kB\n" +
"Committed_AS: 16681884 kB\n" +
"VmallocTotal: 34359738367 kB\n" +
"VmallocUsed: 41944 kB\n" +
"VmallocChunk: 0 kB\n" +
"Percpu: 4240 kB\n" +
"HardwareCorrupted: 0 kB\n" +
"AnonHugePages: 0 kB\n" +
"ShmemHugePages: 0 kB\n" +
"ShmemPmdMapped: 0 kB\n" +
"FileHugePages: 0 kB\n" +
"FilePmdMapped: 0 kB\n" +
"CmaTotal: 0 kB\n" +
"CmaFree: 0 kB\n" +
"HugePages_Total: 0\n" +
"HugePages_Free: 7\n" +
"HugePages_Rsvd: 0\n" +
"HugePages_Surp: 0\n" +
"Hugepagesize: 2048 kB\n" +
"Hugetlb: 0 kB\n" +
"DirectMap4k: 481804 kB\n" +
"DirectMap2M: 14901248 kB\n" +
"DirectMap1G: 2097152 kB\n";
"""
MemTotal: 16052208 kB
MemFree: 4568468 kB
MemAvailable: 7702848 kB
Buffers: 636372 kB
Cached: 5019116 kB
SwapCached: 6692 kB
Active: 7746436 kB
Inactive: 2729876 kB
Active(anon): 5580980 kB
Inactive(anon): 1648108 kB
Active(file): 2165456 kB
Inactive(file): 1081768 kB
Unevictable: 443948 kB
Mlocked: 4924 kB
SwapTotal: 1003516 kB
SwapFree: 935932 kB
Dirty: 28308 kB
Writeback: 0 kB
AnonPages: 5258396 kB
Mapped: 1530740 kB
Shmem: 2419340 kB
KReclaimable: 229392 kB
Slab: 408156 kB
SReclaimable: 229392 kB
SUnreclaim: 178764 kB
KernelStack: 17360 kB
PageTables: 50436 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 9029620 kB
Committed_AS: 16681884 kB
VmallocTotal: 34359738367 kB
VmallocUsed: 41944 kB
VmallocChunk: 0 kB
Percpu: 4240 kB
HardwareCorrupted: 0 kB
AnonHugePages: 0 kB
ShmemHugePages: 0 kB
ShmemPmdMapped: 0 kB
FileHugePages: 0 kB
FilePmdMapped: 0 kB
CmaTotal: 0 kB
CmaFree: 0 kB
HugePages_Total: 0
HugePages_Free: 7
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 0 kB
DirectMap4k: 481804 kB
DirectMap2M: 14901248 kB
DirectMap1G: 2097152 kB
""";
@Test
@Parameters(method = "argumentsForTestGetValue")
@@ -6,38 +6,68 @@
package org.whispersystems.textsecuregcm.metrics;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicPushLatencyConfiguration;
import org.whispersystems.textsecuregcm.metrics.PushLatencyManager.PushRecord;
import org.whispersystems.textsecuregcm.metrics.PushLatencyManager.PushType;
import org.whispersystems.textsecuregcm.redis.RedisClusterExtension;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
class PushLatencyManagerTest {
@RegisterExtension
static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build();
@Test
void testGetLatency() throws ExecutionException, InterruptedException {
private DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
final PushLatencyManager pushLatencyManager = new PushLatencyManager(REDIS_CLUSTER_EXTENSION.getRedisCluster());
@BeforeEach
void setUp() {
//noinspection unchecked
dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
final DynamicPushLatencyConfiguration dynamicPushLatencyConfiguration = mock(DynamicPushLatencyConfiguration.class);
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
when(dynamicConfiguration.getPushLatencyConfiguration()).thenReturn(dynamicPushLatencyConfiguration);
when(dynamicPushLatencyConfiguration.getInstrumentedVersions()).thenReturn(Collections.emptyMap());
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
void testTakeRecord(final boolean isVoip) throws ExecutionException, InterruptedException {
final UUID accountUuid = UUID.randomUUID();
final long deviceId = 1;
final long expectedLatency = 1234;
final long pushSentTimestamp = System.currentTimeMillis();
final long clearQueueTimestamp = pushSentTimestamp + expectedLatency;
assertNull(pushLatencyManager.getLatencyAndClearTimestamp(accountUuid, deviceId, System.currentTimeMillis()).get());
final Instant pushTimestamp = Instant.now();
{
pushLatencyManager.recordPushSent(accountUuid, deviceId, pushSentTimestamp);
final PushLatencyManager pushLatencyManager = new PushLatencyManager(REDIS_CLUSTER_EXTENSION.getRedisCluster(),
dynamicConfigurationManager, Clock.fixed(pushTimestamp, ZoneId.systemDefault()));
assertEquals(expectedLatency,
(long) pushLatencyManager.getLatencyAndClearTimestamp(accountUuid, deviceId, clearQueueTimestamp).get());
assertNull(
pushLatencyManager.getLatencyAndClearTimestamp(accountUuid, deviceId, System.currentTimeMillis()).get());
}
assertNull(pushLatencyManager.takePushRecord(accountUuid, deviceId).get());
pushLatencyManager.recordPushSent(accountUuid, deviceId, isVoip);
final PushRecord pushRecord = pushLatencyManager.takePushRecord(accountUuid, deviceId).get();
assertNotNull(pushRecord);
assertEquals(pushTimestamp, pushRecord.getTimestamp());
assertEquals(isVoip ? PushType.VOIP : PushType.STANDARD, pushRecord.getPushType());
assertNull(pushLatencyManager.takePushRecord(accountUuid, deviceId).get());
}
}
@@ -57,8 +57,8 @@ class ApnFallbackManagerTest {
when(account.getDevice(DEVICE_ID)).thenReturn(Optional.of(device));
final AccountsManager accountsManager = mock(AccountsManager.class);
when(accountsManager.get(ACCOUNT_NUMBER)).thenReturn(Optional.of(account));
when(accountsManager.get(ACCOUNT_UUID)).thenReturn(Optional.of(account));
when(accountsManager.getByE164(ACCOUNT_NUMBER)).thenReturn(Optional.of(account));
when(accountsManager.getByAccountIdentifier(ACCOUNT_UUID)).thenReturn(Optional.of(account));
apnSender = mock(APNSender.class);
@@ -55,22 +55,23 @@ public class SecureBackupClientTest {
config.setUri("http://localhost:" + wireMockRule.port());
// This is a randomly-generated, throwaway certificate that's not actually connected to anything
config.setBackupCaCertificate(
"-----BEGIN CERTIFICATE-----\n" +
"MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEL\n" +
"MAkGA1UECAwCVVMxHjAcBgNVBAoMFVNpZ25hbCBNZXNzZW5nZXIsIExMQzETMBEG\n" +
"A1UEAwwKc2lnbmFsLm9yZzAeFw0yMDEyMjMyMjQ3NTlaFw0zMDEyMjEyMjQ3NTla\n" +
"ME8xCzAJBgNVBAYTAnVzMQswCQYDVQQIDAJVUzEeMBwGA1UECgwVU2lnbmFsIE1l\n" +
"c3NlbmdlciwgTExDMRMwEQYDVQQDDApzaWduYWwub3JnMIGfMA0GCSqGSIb3DQEB\n" +
"AQUAA4GNADCBiQKBgQCfSLcZNHYqbxSsgWp4JvbPRHjQTrlsrKrgD2q7f/OY6O3Y\n" +
"/X0QNcNSOJpliN8rmzwslfsrXHO3q1diGRw4xHogUJZ/7NQrHiP/zhN0VTDh49pD\n" +
"ZpjXVyUbayLS/6qM5arKxBspzEFBb5v8cF6bPr76SO/rpGXiI0j6yJKX6fRiKwID\n" +
"AQABo1AwTjAdBgNVHQ4EFgQU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwHwYDVR0jBBgw\n" +
"FoAU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B\n" +
"AQ0FAAOBgQB+5d5+NtzLILfrc9QmJdIO1YeDP64JmFwTER0kEUouRsb9UwknVWZa\n" +
"y7MTM4NoBV1k0zb5LAk89SIDPr/maW5AsLtEomzjnEiomjoMBUdNe3YCgQReoLnr\n" +
"R/QaUNbrCjTGYfBsjGbIzmkWPUyTec2ZdRyJ8JiVl386+6CZkxnndQ==\n" +
"-----END CERTIFICATE-----");
config.setBackupCaCertificate("""
-----BEGIN CERTIFICATE-----
MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEL
MAkGA1UECAwCVVMxHjAcBgNVBAoMFVNpZ25hbCBNZXNzZW5nZXIsIExMQzETMBEG
A1UEAwwKc2lnbmFsLm9yZzAeFw0yMDEyMjMyMjQ3NTlaFw0zMDEyMjEyMjQ3NTla
ME8xCzAJBgNVBAYTAnVzMQswCQYDVQQIDAJVUzEeMBwGA1UECgwVU2lnbmFsIE1l
c3NlbmdlciwgTExDMRMwEQYDVQQDDApzaWduYWwub3JnMIGfMA0GCSqGSIb3DQEB
AQUAA4GNADCBiQKBgQCfSLcZNHYqbxSsgWp4JvbPRHjQTrlsrKrgD2q7f/OY6O3Y
/X0QNcNSOJpliN8rmzwslfsrXHO3q1diGRw4xHogUJZ/7NQrHiP/zhN0VTDh49pD
ZpjXVyUbayLS/6qM5arKxBspzEFBb5v8cF6bPr76SO/rpGXiI0j6yJKX6fRiKwID
AQABo1AwTjAdBgNVHQ4EFgQU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwHwYDVR0jBBgw
FoAU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B
AQ0FAAOBgQB+5d5+NtzLILfrc9QmJdIO1YeDP64JmFwTER0kEUouRsb9UwknVWZa
y7MTM4NoBV1k0zb5LAk89SIDPr/maW5AsLtEomzjnEiomjoMBUdNe3YCgQReoLnr
R/QaUNbrCjTGYfBsjGbIzmkWPUyTec2ZdRyJ8JiVl386+6CZkxnndQ==
-----END CERTIFICATE-----
""");
secureStorageClient = new SecureBackupClient(credentialGenerator, httpExecutor, config);
}
@@ -55,22 +55,23 @@ public class SecureStorageClientTest {
config.setUri("http://localhost:" + wireMockRule.port());
// This is a randomly-generated, throwaway certificate that's not actually connected to anything
config.setStorageCaCertificate(
"-----BEGIN CERTIFICATE-----\n" +
"MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEL\n" +
"MAkGA1UECAwCVVMxHjAcBgNVBAoMFVNpZ25hbCBNZXNzZW5nZXIsIExMQzETMBEG\n" +
"A1UEAwwKc2lnbmFsLm9yZzAeFw0yMDEyMjMyMjQ3NTlaFw0zMDEyMjEyMjQ3NTla\n" +
"ME8xCzAJBgNVBAYTAnVzMQswCQYDVQQIDAJVUzEeMBwGA1UECgwVU2lnbmFsIE1l\n" +
"c3NlbmdlciwgTExDMRMwEQYDVQQDDApzaWduYWwub3JnMIGfMA0GCSqGSIb3DQEB\n" +
"AQUAA4GNADCBiQKBgQCfSLcZNHYqbxSsgWp4JvbPRHjQTrlsrKrgD2q7f/OY6O3Y\n" +
"/X0QNcNSOJpliN8rmzwslfsrXHO3q1diGRw4xHogUJZ/7NQrHiP/zhN0VTDh49pD\n" +
"ZpjXVyUbayLS/6qM5arKxBspzEFBb5v8cF6bPr76SO/rpGXiI0j6yJKX6fRiKwID\n" +
"AQABo1AwTjAdBgNVHQ4EFgQU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwHwYDVR0jBBgw\n" +
"FoAU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B\n" +
"AQ0FAAOBgQB+5d5+NtzLILfrc9QmJdIO1YeDP64JmFwTER0kEUouRsb9UwknVWZa\n" +
"y7MTM4NoBV1k0zb5LAk89SIDPr/maW5AsLtEomzjnEiomjoMBUdNe3YCgQReoLnr\n" +
"R/QaUNbrCjTGYfBsjGbIzmkWPUyTec2ZdRyJ8JiVl386+6CZkxnndQ==\n" +
"-----END CERTIFICATE-----");
config.setStorageCaCertificate("""
-----BEGIN CERTIFICATE-----
MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEL
MAkGA1UECAwCVVMxHjAcBgNVBAoMFVNpZ25hbCBNZXNzZW5nZXIsIExMQzETMBEG
A1UEAwwKc2lnbmFsLm9yZzAeFw0yMDEyMjMyMjQ3NTlaFw0zMDEyMjEyMjQ3NTla
ME8xCzAJBgNVBAYTAnVzMQswCQYDVQQIDAJVUzEeMBwGA1UECgwVU2lnbmFsIE1l
c3NlbmdlciwgTExDMRMwEQYDVQQDDApzaWduYWwub3JnMIGfMA0GCSqGSIb3DQEB
AQUAA4GNADCBiQKBgQCfSLcZNHYqbxSsgWp4JvbPRHjQTrlsrKrgD2q7f/OY6O3Y
/X0QNcNSOJpliN8rmzwslfsrXHO3q1diGRw4xHogUJZ/7NQrHiP/zhN0VTDh49pD
ZpjXVyUbayLS/6qM5arKxBspzEFBb5v8cF6bPr76SO/rpGXiI0j6yJKX6fRiKwID
AQABo1AwTjAdBgNVHQ4EFgQU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwHwYDVR0jBBgw
FoAU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B
AQ0FAAOBgQB+5d5+NtzLILfrc9QmJdIO1YeDP64JmFwTER0kEUouRsb9UwknVWZa
y7MTM4NoBV1k0zb5LAk89SIDPr/maW5AsLtEomzjnEiomjoMBUdNe3YCgQReoLnr
R/QaUNbrCjTGYfBsjGbIzmkWPUyTec2ZdRyJ8JiVl386+6CZkxnndQ==
-----END CERTIFICATE-----
""");
secureStorageClient = new SecureStorageClient(credentialGenerator, httpExecutor, config);
}
@@ -57,7 +57,7 @@ public class AccountDatabaseCrawlerIntegrationTest extends AbstractRedisClusterT
.thenReturn(new AccountCrawlChunk(List.of(secondAccount), SECOND_UUID))
.thenReturn(new AccountCrawlChunk(Collections.emptyList(), null));
final AccountDatabaseCrawlerCache crawlerCache = new AccountDatabaseCrawlerCache(getRedisCluster());
final AccountDatabaseCrawlerCache crawlerCache = new AccountDatabaseCrawlerCache(getRedisCluster(), "test");
accountDatabaseCrawler = new AccountDatabaseCrawler(accountsManager, crawlerCache, List.of(listener), CHUNK_SIZE,
CHUNK_INTERVAL_MS);
}
@@ -6,6 +6,7 @@
package org.whispersystems.textsecuregcm.storage;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
@@ -15,6 +16,8 @@ import static org.mockito.Mockito.when;
import com.opentable.db.postgres.embedded.LiquibasePreparer;
import com.opentable.db.postgres.junit5.EmbeddedPostgresExtension;
import com.opentable.db.postgres.junit5.PreparedDbExtension;
import java.time.Clock;
import java.util.ArrayList;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -47,6 +50,8 @@ class AccountsManagerChangeNumberIntegrationTest {
private static final String ACCOUNTS_TABLE_NAME = "accounts_test";
private static final String NUMBERS_TABLE_NAME = "numbers_test";
private static final String PNI_ASSIGNMENT_TABLE_NAME = "pni_assignment_test";
private static final String PNI_TABLE_NAME = "pni_test";
private static final String NEEDS_RECONCILIATION_INDEX_NAME = "needs_reconciliation_test";
private static final String DELETED_ACCOUNTS_LOCK_TABLE_NAME = "deleted_accounts_lock_test";
private static final int SCAN_PAGE_SIZE = 1;
@@ -90,6 +95,16 @@ class AccountsManagerChangeNumberIntegrationTest {
.attributeType(ScalarAttributeType.S).build())
.build();
@RegisterExtension
static DynamoDbExtension PNI_DYNAMO_EXTENSION = DynamoDbExtension.builder()
.tableName(PNI_TABLE_NAME)
.hashKey(PhoneNumberIdentifiers.KEY_E164)
.attributeDefinition(AttributeDefinition.builder()
.attributeName(PhoneNumberIdentifiers.KEY_E164)
.attributeType(ScalarAttributeType.S)
.build())
.build();
@RegisterExtension
static RedisClusterExtension CACHE_CLUSTER_EXTENSION = RedisClusterExtension.builder().build();
@@ -118,14 +133,33 @@ class AccountsManagerChangeNumberIntegrationTest {
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient().createTable(createNumbersTableRequest);
}
{
CreateTableRequest createPhoneNumberIdentifierTableRequest = CreateTableRequest.builder()
.tableName(PNI_ASSIGNMENT_TABLE_NAME)
.keySchema(KeySchemaElement.builder()
.attributeName(Accounts.ATTR_PNI_UUID)
.keyType(KeyType.HASH)
.build())
.attributeDefinitions(AttributeDefinition.builder()
.attributeName(Accounts.ATTR_PNI_UUID)
.attributeType(ScalarAttributeType.B)
.build())
.provisionedThroughput(DynamoDbExtension.DEFAULT_PROVISIONED_THROUGHPUT)
.build();
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient().createTable(createPhoneNumberIdentifierTableRequest);
}
final Accounts accounts = new Accounts(
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient(),
ACCOUNTS_DYNAMO_EXTENSION.getTableName(),
NUMBERS_TABLE_NAME,
PNI_ASSIGNMENT_TABLE_NAME,
SCAN_PAGE_SIZE);
{
final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
@SuppressWarnings("unchecked") final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager =
mock(DynamicConfigurationManager.class);
DynamicConfiguration dynamicConfiguration = new DynamicConfiguration();
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
@@ -146,19 +180,24 @@ class AccountsManagerChangeNumberIntegrationTest {
clientPresenceManager = mock(ClientPresenceManager.class);
final PhoneNumberIdentifiers phoneNumberIdentifiers =
new PhoneNumberIdentifiers(PNI_DYNAMO_EXTENSION.getDynamoDbClient(), PNI_TABLE_NAME);
accountsManager = new AccountsManager(
accounts,
phoneNumberIdentifiers,
CACHE_CLUSTER_EXTENSION.getRedisCluster(),
deletedAccountsManager,
mock(DirectoryQueue.class),
mock(KeysDynamoDb.class),
mock(Keys.class),
mock(MessagesManager.class),
mock(UsernamesManager.class),
mock(ProfilesManager.class),
mock(StoredVerificationCodeManager.class),
secureStorageClient,
secureBackupClient,
clientPresenceManager);
clientPresenceManager,
mock(Clock.class));
}
}
@@ -167,17 +206,19 @@ class AccountsManagerChangeNumberIntegrationTest {
final String originalNumber = "+18005551111";
final String secondNumber = "+18005552222";
final Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes());
final Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>());
final UUID originalUuid = account.getUuid();
final UUID originalPni = account.getPhoneNumberIdentifier();
accountsManager.changeNumber(account, secondNumber);
assertTrue(accountsManager.get(originalNumber).isEmpty());
assertTrue(accountsManager.getByE164(originalNumber).isEmpty());
assertTrue(accountsManager.get(secondNumber).isPresent());
assertEquals(Optional.of(originalUuid), accountsManager.get(secondNumber).map(Account::getUuid));
assertTrue(accountsManager.getByE164(secondNumber).isPresent());
assertEquals(originalUuid, accountsManager.getByE164(secondNumber).map(Account::getUuid).orElseThrow());
assertNotEquals(originalPni, accountsManager.getByE164(secondNumber).map(Account::getPhoneNumberIdentifier).orElseThrow());
assertEquals(secondNumber, accountsManager.get(originalUuid).map(Account::getNumber).orElseThrow());
assertEquals(secondNumber, accountsManager.getByAccountIdentifier(originalUuid).map(Account::getNumber).orElseThrow());
assertEquals(Optional.empty(), deletedAccounts.findUuid(originalNumber));
assertEquals(Optional.empty(), deletedAccounts.findUuid(secondNumber));
@@ -188,18 +229,20 @@ class AccountsManagerChangeNumberIntegrationTest {
final String originalNumber = "+18005551111";
final String secondNumber = "+18005552222";
Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes());
Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>());
final UUID originalUuid = account.getUuid();
final UUID originalPni = account.getPhoneNumberIdentifier();
account = accountsManager.changeNumber(account, secondNumber);
accountsManager.changeNumber(account, originalNumber);
assertTrue(accountsManager.get(originalNumber).isPresent());
assertEquals(Optional.of(originalUuid), accountsManager.get(originalNumber).map(Account::getUuid));
assertTrue(accountsManager.getByE164(originalNumber).isPresent());
assertEquals(originalUuid, accountsManager.getByE164(originalNumber).map(Account::getUuid).orElseThrow());
assertEquals(originalPni, accountsManager.getByE164(originalNumber).map(Account::getPhoneNumberIdentifier).orElseThrow());
assertTrue(accountsManager.get(secondNumber).isEmpty());
assertTrue(accountsManager.getByE164(secondNumber).isEmpty());
assertEquals(originalNumber, accountsManager.get(originalUuid).map(Account::getNumber).orElseThrow());
assertEquals(originalNumber, accountsManager.getByAccountIdentifier(originalUuid).map(Account::getNumber).orElseThrow());
assertEquals(Optional.empty(), deletedAccounts.findUuid(originalNumber));
assertEquals(Optional.empty(), deletedAccounts.findUuid(secondNumber));
@@ -210,22 +253,22 @@ class AccountsManagerChangeNumberIntegrationTest {
final String originalNumber = "+18005551111";
final String secondNumber = "+18005552222";
final Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes());
final Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>());
final UUID originalUuid = account.getUuid();
final Account existingAccount = accountsManager.create(secondNumber, "password", null, new AccountAttributes());
final Account existingAccount = accountsManager.create(secondNumber, "password", null, new AccountAttributes(), new ArrayList<>());
final UUID existingAccountUuid = existingAccount.getUuid();
accountsManager.update(existingAccount, a -> a.addDevice(new Device(Device.MASTER_ID, "test", "token", "salt", null, null, null, true, 1, null, 0, 0, null, 0, new DeviceCapabilities())));
accountsManager.changeNumber(account, secondNumber);
assertTrue(accountsManager.get(originalNumber).isEmpty());
assertTrue(accountsManager.getByE164(originalNumber).isEmpty());
assertTrue(accountsManager.get(secondNumber).isPresent());
assertEquals(Optional.of(originalUuid), accountsManager.get(secondNumber).map(Account::getUuid));
assertTrue(accountsManager.getByE164(secondNumber).isPresent());
assertEquals(Optional.of(originalUuid), accountsManager.getByE164(secondNumber).map(Account::getUuid));
assertEquals(secondNumber, accountsManager.get(originalUuid).map(Account::getNumber).orElseThrow());
assertEquals(secondNumber, accountsManager.getByAccountIdentifier(originalUuid).map(Account::getNumber).orElseThrow());
verify(clientPresenceManager).displacePresence(existingAccountUuid, Device.MASTER_ID);
@@ -238,24 +281,28 @@ class AccountsManagerChangeNumberIntegrationTest {
final String originalNumber = "+18005551111";
final String secondNumber = "+18005552222";
final Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes());
final Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>());
final UUID originalUuid = account.getUuid();
final UUID originalPni = account.getPhoneNumberIdentifier();
final Account existingAccount = accountsManager.create(secondNumber, "password", null, new AccountAttributes());
final Account existingAccount = accountsManager.create(secondNumber, "password", null, new AccountAttributes(), new ArrayList<>());
final UUID existingAccountUuid = existingAccount.getUuid();
accountsManager.changeNumber(account, secondNumber);
final Account changedNumberAccount = accountsManager.changeNumber(account, secondNumber);
final UUID secondPni = changedNumberAccount.getPhoneNumberIdentifier();
final Account reRegisteredAccount = accountsManager.create(originalNumber, "password", null, new AccountAttributes());
final Account reRegisteredAccount = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>());
assertEquals(existingAccountUuid, reRegisteredAccount.getUuid());
assertEquals(originalPni, reRegisteredAccount.getPhoneNumberIdentifier());
assertEquals(Optional.empty(), deletedAccounts.findUuid(originalNumber));
assertEquals(Optional.empty(), deletedAccounts.findUuid(secondNumber));
accountsManager.changeNumber(reRegisteredAccount, secondNumber);
final Account changedNumberReRegisteredAccount = accountsManager.changeNumber(reRegisteredAccount, secondNumber);
assertEquals(Optional.of(originalUuid), deletedAccounts.findUuid(originalNumber));
assertEquals(Optional.empty(), deletedAccounts.findUuid(secondNumber));
assertEquals(secondPni, changedNumberReRegisteredAccount.getPhoneNumberIdentifier());
}
}
@@ -15,10 +15,13 @@ import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
import java.io.IOException;
import java.time.Clock;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
@@ -33,6 +36,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.ArgumentCaptor;
import org.mockito.stubbing.Answer;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
@@ -53,6 +57,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
private static final String ACCOUNTS_TABLE_NAME = "accounts_test";
private static final String NUMBERS_TABLE_NAME = "numbers_test";
private static final String PNI_TABLE_NAME = "pni_test";
private static final int SCAN_PAGE_SIZE = 1;
@@ -94,10 +99,28 @@ class AccountsManagerConcurrentModificationIntegrationTest {
dynamoDbExtension.getDynamoDbClient().createTable(createNumbersTableRequest);
}
{
CreateTableRequest createPhoneNumberIdentifierTableRequest = CreateTableRequest.builder()
.tableName(PNI_TABLE_NAME)
.keySchema(KeySchemaElement.builder()
.attributeName(Accounts.ATTR_PNI_UUID)
.keyType(KeyType.HASH)
.build())
.attributeDefinitions(AttributeDefinition.builder()
.attributeName(Accounts.ATTR_PNI_UUID)
.attributeType(ScalarAttributeType.B)
.build())
.provisionedThroughput(DynamoDbExtension.DEFAULT_PROVISIONED_THROUGHPUT)
.build();
dynamoDbExtension.getDynamoDbClient().createTable(createPhoneNumberIdentifierTableRequest);
}
accounts = new Accounts(
dynamoDbExtension.getDynamoDbClient(),
dynamoDbExtension.getTableName(),
NUMBERS_TABLE_NAME,
PNI_TABLE_NAME,
SCAN_PAGE_SIZE);
{
@@ -112,19 +135,25 @@ class AccountsManagerConcurrentModificationIntegrationTest {
return null;
}).when(deletedAccountsManager).lockAndTake(anyString(), any());
final PhoneNumberIdentifiers phoneNumberIdentifiers = mock(PhoneNumberIdentifiers.class);
when(phoneNumberIdentifiers.getPhoneNumberIdentifier(anyString()))
.thenAnswer((Answer<UUID>) invocation -> UUID.randomUUID());
accountsManager = new AccountsManager(
accounts,
phoneNumberIdentifiers,
RedisClusterHelper.buildMockRedisCluster(commands),
deletedAccountsManager,
mock(DirectoryQueue.class),
mock(KeysDynamoDb.class),
mock(Keys.class),
mock(MessagesManager.class),
mock(UsernamesManager.class),
mock(ProfilesManager.class),
mock(StoredVerificationCodeManager.class),
mock(SecureStorageClient.class),
mock(SecureBackupClient.class),
mock(ClientPresenceManager.class)
mock(ClientPresenceManager.class),
mock(Clock.class)
);
}
}
@@ -135,7 +164,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
final UUID uuid;
{
final Account account = accountsManager.update(
accountsManager.create("+14155551212", "password", null, new AccountAttributes()),
accountsManager.create("+14155551212", "password", null, new AccountAttributes(), new ArrayList<>()),
a -> {
a.setUnidentifiedAccessKey(new byte[16]);
@@ -182,8 +211,8 @@ class AccountsManagerConcurrentModificationIntegrationTest {
modifyDevice(uuid, Device.MASTER_ID, device -> device.setName("deviceName"))
).join();
final Account managerAccount = accountsManager.get(uuid).orElseThrow();
final Account dynamoAccount = accounts.get(uuid).orElseThrow();
final Account managerAccount = accountsManager.getByAccountIdentifier(uuid).orElseThrow();
final Account dynamoAccount = accounts.getByAccountIdentifier(uuid).orElseThrow();
final Account redisAccount = getLastAccountFromRedisMock(commands);
@@ -222,7 +251,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
private CompletableFuture<?> modifyAccount(final UUID uuid, final Consumer<Account> accountMutation) {
return CompletableFuture.runAsync(() -> {
final Account account = accountsManager.get(uuid).orElseThrow();
final Account account = accountsManager.getByAccountIdentifier(uuid).orElseThrow();
accountsManager.update(account, accountMutation);
}, mutationExecutor);
}
@@ -230,7 +259,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
private CompletableFuture<?> modifyDevice(final UUID uuid, final long deviceId, final Consumer<Device> deviceMutation) {
return CompletableFuture.runAsync(() -> {
final Account account = accountsManager.get(uuid).orElseThrow();
final Account account = accountsManager.getByAccountIdentifier(uuid).orElseThrow();
accountsManager.updateDevice(account, deviceId, deviceMutation);
}, mutationExecutor);
}
@@ -12,12 +12,14 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.uuid.UUIDComparator;
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -25,11 +27,6 @@ import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.assertj.core.api.AssertionsForClassTypes;
import org.jdbi.v3.core.transaction.TransactionException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
@@ -38,15 +35,22 @@ import org.junit.jupiter.api.extension.RegisterExtension;
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.CancellationReason;
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement;
import software.amazon.awssdk.services.dynamodb.model.KeyType;
import software.amazon.awssdk.services.dynamodb.model.Put;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure;
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem;
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest;
import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException;
import software.amazon.awssdk.services.dynamodb.model.TransactionConflictException;
@@ -55,7 +59,8 @@ import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
class AccountsTest {
private static final String ACCOUNTS_TABLE_NAME = "accounts_test";
private static final String NUMBERS_TABLE_NAME = "numbers_test";
private static final String NUMBER_CONSTRAINT_TABLE_NAME = "numbers_test";
private static final String PNI_CONSTRAINT_TABLE_NAME = "pni_test";
private static final int SCAN_PAGE_SIZE = 1;
@@ -74,7 +79,7 @@ class AccountsTest {
@BeforeEach
void setupAccountsDao() {
CreateTableRequest createNumbersTableRequest = CreateTableRequest.builder()
.tableName(NUMBERS_TABLE_NAME)
.tableName(NUMBER_CONSTRAINT_TABLE_NAME)
.keySchema(KeySchemaElement.builder()
.attributeName(Accounts.ATTR_ACCOUNT_E164)
.keyType(KeyType.HASH)
@@ -88,27 +93,48 @@ class AccountsTest {
dynamoDbExtension.getDynamoDbClient().createTable(createNumbersTableRequest);
CreateTableRequest createPhoneNumberIdentifierTableRequest = CreateTableRequest.builder()
.tableName(PNI_CONSTRAINT_TABLE_NAME)
.keySchema(KeySchemaElement.builder()
.attributeName(Accounts.ATTR_PNI_UUID)
.keyType(KeyType.HASH)
.build())
.attributeDefinitions(AttributeDefinition.builder()
.attributeName(Accounts.ATTR_PNI_UUID)
.attributeType(ScalarAttributeType.B)
.build())
.provisionedThroughput(DynamoDbExtension.DEFAULT_PROVISIONED_THROUGHPUT)
.build();
dynamoDbExtension.getDynamoDbClient().createTable(createPhoneNumberIdentifierTableRequest);
this.accounts = new Accounts(
dynamoDbExtension.getDynamoDbClient(),
dynamoDbExtension.getTableName(),
NUMBERS_TABLE_NAME,
NUMBER_CONSTRAINT_TABLE_NAME,
PNI_CONSTRAINT_TABLE_NAME,
SCAN_PAGE_SIZE);
}
@Test
void testStore() {
Device device = generateDevice (1 );
Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device));
Device device = generateDevice(1);
Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(device));
boolean freshUser = accounts.create(account);
assertThat(freshUser).isTrue();
verifyStoredState("+14151112222", account.getUuid(), account, true);
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, true);
assertPhoneNumberConstraintExists("+14151112222", account.getUuid());
assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier(), account.getUuid());
freshUser = accounts.create(account);
assertThat(freshUser).isTrue();
verifyStoredState("+14151112222", account.getUuid(), account, true);
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, true);
assertPhoneNumberConstraintExists("+14151112222", account.getUuid());
assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier(), account.getUuid());
}
@Test
@@ -117,11 +143,14 @@ class AccountsTest {
devices.add(generateDevice(1));
devices.add(generateDevice(2));
Account account = generateAccount("+14151112222", UUID.randomUUID(), devices);
Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), devices);
accounts.create(account);
verifyStoredState("+14151112222", account.getUuid(), account, true);
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, true);
assertPhoneNumberConstraintExists("+14151112222", account.getUuid());
assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier(), account.getUuid());
}
@Test
@@ -131,46 +160,123 @@ class AccountsTest {
devicesFirst.add(generateDevice(2));
UUID uuidFirst = UUID.randomUUID();
Account accountFirst = generateAccount("+14151112222", uuidFirst, devicesFirst);
UUID pniFirst = UUID.randomUUID();
Account accountFirst = generateAccount("+14151112222", uuidFirst, pniFirst, devicesFirst);
Set<Device> devicesSecond = new HashSet<>();
devicesSecond.add(generateDevice(1));
devicesSecond.add(generateDevice(2));
UUID uuidSecond = UUID.randomUUID();
Account accountSecond = generateAccount("+14152221111", uuidSecond, devicesSecond);
UUID pniSecond = UUID.randomUUID();
Account accountSecond = generateAccount("+14152221111", uuidSecond, pniSecond, devicesSecond);
accounts.create(accountFirst);
accounts.create(accountSecond);
Optional<Account> retrievedFirst = accounts.get("+14151112222");
Optional<Account> retrievedSecond = accounts.get("+14152221111");
Optional<Account> retrievedFirst = accounts.getByE164("+14151112222");
Optional<Account> retrievedSecond = accounts.getByE164("+14152221111");
assertThat(retrievedFirst.isPresent()).isTrue();
assertThat(retrievedSecond.isPresent()).isTrue();
verifyStoredState("+14151112222", uuidFirst, retrievedFirst.get(), accountFirst);
verifyStoredState("+14152221111", uuidSecond, retrievedSecond.get(), accountSecond);
verifyStoredState("+14151112222", uuidFirst, pniFirst, retrievedFirst.get(), accountFirst);
verifyStoredState("+14152221111", uuidSecond, pniSecond, retrievedSecond.get(), accountSecond);
retrievedFirst = accounts.get(uuidFirst);
retrievedSecond = accounts.get(uuidSecond);
retrievedFirst = accounts.getByAccountIdentifier(uuidFirst);
retrievedSecond = accounts.getByAccountIdentifier(uuidSecond);
assertThat(retrievedFirst.isPresent()).isTrue();
assertThat(retrievedSecond.isPresent()).isTrue();
verifyStoredState("+14151112222", uuidFirst, retrievedFirst.get(), accountFirst);
verifyStoredState("+14152221111", uuidSecond, retrievedSecond.get(), accountSecond);
verifyStoredState("+14151112222", uuidFirst, pniFirst, retrievedFirst.get(), accountFirst);
verifyStoredState("+14152221111", uuidSecond, pniSecond, retrievedSecond.get(), accountSecond);
retrievedFirst = accounts.getByPhoneNumberIdentifier(pniFirst);
retrievedSecond = accounts.getByPhoneNumberIdentifier(pniSecond);
assertThat(retrievedFirst.isPresent()).isTrue();
assertThat(retrievedSecond.isPresent()).isTrue();
verifyStoredState("+14151112222", uuidFirst, pniFirst, retrievedFirst.get(), accountFirst);
verifyStoredState("+14152221111", uuidSecond, pniSecond, retrievedSecond.get(), accountSecond);
}
@Test
void testRetrieveNoPni() throws JsonProcessingException {
final Set<Device> devices = new HashSet<>();
devices.add(generateDevice(1));
devices.add(generateDevice(2));
final UUID uuid = UUID.randomUUID();
final Account account = generateAccount("+14151112222", uuid, null, devices);
// Accounts#create enforces that newly-created accounts have a PNI, so we need to make a bit of an end-run around it
// to simulate an existing account with no PNI.
{
final TransactWriteItem phoneNumberConstraintPut = TransactWriteItem.builder()
.put(
Put.builder()
.tableName(NUMBER_CONSTRAINT_TABLE_NAME)
.item(Map.of(
Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.conditionExpression(
"attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)")
.expressionAttributeNames(
Map.of("#uuid", Accounts.KEY_ACCOUNT_UUID,
"#number", Accounts.ATTR_ACCOUNT_E164))
.expressionAttributeValues(
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build();
final TransactWriteItem accountPut = TransactWriteItem.builder()
.put(Put.builder()
.tableName(ACCOUNTS_TABLE_NAME)
.conditionExpression("attribute_not_exists(#number) OR #number = :number")
.expressionAttributeNames(Map.of("#number", Accounts.ATTR_ACCOUNT_E164))
.expressionAttributeValues(Map.of(":number", AttributeValues.fromString(account.getNumber())))
.item(Map.of(
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid),
Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
Accounts.ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
Accounts.ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
Accounts.ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())))
.build())
.build();
dynamoDbExtension.getDynamoDbClient().transactWriteItems(TransactWriteItemsRequest.builder()
.transactItems(phoneNumberConstraintPut, accountPut)
.build());
}
Optional<Account> retrieved = accounts.getByE164("+14151112222");
assertThat(retrieved.isPresent()).isTrue();
verifyStoredState("+14151112222", uuid, null, retrieved.get(), account);
retrieved = accounts.getByAccountIdentifier(uuid);
assertThat(retrieved.isPresent()).isTrue();
verifyStoredState("+14151112222", uuid, null, retrieved.get(), account);
}
@Test
void testOverwrite() {
Device device = generateDevice (1 );
UUID firstUuid = UUID.randomUUID();
Account account = generateAccount("+14151112222", firstUuid, Collections.singleton(device));
Device device = generateDevice(1);
UUID firstUuid = UUID.randomUUID();
UUID firstPni = UUID.randomUUID();
Account account = generateAccount("+14151112222", firstUuid, firstPni, Collections.singleton(device));
accounts.create(account);
verifyStoredState("+14151112222", account.getUuid(), account, true);
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, true);
assertPhoneNumberConstraintExists("+14151112222", firstUuid);
assertPhoneNumberIdentifierConstraintExists(firstPni, firstUuid);
account.setProfileName("name");
@@ -179,14 +285,17 @@ class AccountsTest {
UUID secondUuid = UUID.randomUUID();
device = generateDevice(1);
account = generateAccount("+14151112222", secondUuid, Collections.singleton(device));
account = generateAccount("+14151112222", secondUuid, UUID.randomUUID(), Collections.singleton(device));
final boolean freshUser = accounts.create(account);
assertThat(freshUser).isFalse();
verifyStoredState("+14151112222", firstUuid, account, true);
verifyStoredState("+14151112222", firstUuid, firstPni, account, true);
assertPhoneNumberConstraintExists("+14151112222", firstUuid);
assertPhoneNumberIdentifierConstraintExists(firstPni, firstUuid);
device = generateDevice(1);
Account invalidAccount = generateAccount("+14151113333", firstUuid, Collections.singleton(device));
Account invalidAccount = generateAccount("+14151113333", firstUuid, UUID.randomUUID(), Collections.singleton(device));
assertThatThrownBy(() -> accounts.create(invalidAccount));
}
@@ -194,26 +303,32 @@ class AccountsTest {
@Test
void testUpdate() {
Device device = generateDevice (1 );
Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device));
Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(device));
accounts.create(account);
assertPhoneNumberConstraintExists("+14151112222", account.getUuid());
assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier(), account.getUuid());
device.setName("foobar");
accounts.update(account);
Optional<Account> retrieved = accounts.get("+14151112222");
assertPhoneNumberConstraintExists("+14151112222", account.getUuid());
assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier(), account.getUuid());
Optional<Account> retrieved = accounts.getByE164("+14151112222");
assertThat(retrieved.isPresent()).isTrue();
verifyStoredState("+14151112222", account.getUuid(), retrieved.get(), account);
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), retrieved.get(), account);
retrieved = accounts.get(account.getUuid());
retrieved = accounts.getByAccountIdentifier(account.getUuid());
assertThat(retrieved.isPresent()).isTrue();
verifyStoredState("+14151112222", account.getUuid(), account, true);
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, true);
device = generateDevice(1);
Account unknownAccount = generateAccount("+14151113333", UUID.randomUUID(), Collections.singleton(device));
Account unknownAccount = generateAccount("+14151113333", UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(device));
assertThatThrownBy(() -> accounts.update(unknownAccount)).isInstanceOfAny(ConditionalCheckFailedException.class);
@@ -223,7 +338,7 @@ class AccountsTest {
assertThat(account.getVersion()).isEqualTo(2);
verifyStoredState("+14151112222", account.getUuid(), account, true);
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, true);
account.setVersion(1);
@@ -234,7 +349,7 @@ class AccountsTest {
accounts.update(account);
verifyStoredState("+14151112222", account.getUuid(), account, true);
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, true);
}
@Test
@@ -242,13 +357,12 @@ class AccountsTest {
final DynamoDbClient dynamoDbClient = mock(DynamoDbClient.class);
accounts = new Accounts(dynamoDbClient,
dynamoDbExtension.getTableName(), NUMBERS_TABLE_NAME, SCAN_PAGE_SIZE);
dynamoDbExtension.getTableName(), NUMBER_CONSTRAINT_TABLE_NAME, PNI_CONSTRAINT_TABLE_NAME, SCAN_PAGE_SIZE);
when(dynamoDbClient.updateItem(any(UpdateItemRequest.class)))
.thenThrow(TransactionConflictException.class);
Device device = generateDevice(1);
Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device));
Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID());
assertThatThrownBy(() -> accounts.update(account)).isInstanceOfAny(ContestedOptimisticLockException.class);
}
@@ -258,7 +372,7 @@ class AccountsTest {
List<Account> users = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
Account account = generateAccount("+1" + String.format("%03d", i), UUID.randomUUID());
Account account = generateAccount("+1" + String.format("%03d", i), UUID.randomUUID(), UUID.randomUUID());
users.add(account);
accounts.create(account);
}
@@ -276,7 +390,7 @@ class AccountsTest {
.findAny()
.orElseThrow();
verifyStoredState(expectedAccount.getNumber(), expectedAccount.getUuid(), retrievedAccount, expectedAccount);
verifyStoredState(expectedAccount.getNumber(), expectedAccount.getUuid(), expectedAccount.getPhoneNumberIdentifier(), retrievedAccount, expectedAccount);
users.remove(expectedAccount);
}
@@ -293,7 +407,7 @@ class AccountsTest {
.findAny()
.orElseThrow();
verifyStoredState(expectedAccount.getNumber(), expectedAccount.getUuid(), retrievedAccount, expectedAccount);
verifyStoredState(expectedAccount.getNumber(), expectedAccount.getUuid(), expectedAccount.getPhoneNumberIdentifier(), retrievedAccount, expectedAccount);
users.remove(expectedAccount);
}
@@ -306,48 +420,59 @@ class AccountsTest {
void testDelete() {
final Device deletedDevice = generateDevice(1);
final Account deletedAccount = generateAccount("+14151112222", UUID.randomUUID(),
Collections.singleton(deletedDevice));
UUID.randomUUID(), Collections.singleton(deletedDevice));
final Device retainedDevice = generateDevice(1);
final Account retainedAccount = generateAccount("+14151112345", UUID.randomUUID(),
Collections.singleton(retainedDevice));
UUID.randomUUID(), Collections.singleton(retainedDevice));
accounts.create(deletedAccount);
accounts.create(retainedAccount);
assertThat(accounts.get(deletedAccount.getUuid())).isPresent();
assertThat(accounts.get(retainedAccount.getUuid())).isPresent();
assertPhoneNumberConstraintExists("+14151112222", deletedAccount.getUuid());
assertPhoneNumberIdentifierConstraintExists(deletedAccount.getPhoneNumberIdentifier(), deletedAccount.getUuid());
assertPhoneNumberConstraintExists("+14151112345", retainedAccount.getUuid());
assertPhoneNumberIdentifierConstraintExists(retainedAccount.getPhoneNumberIdentifier(), retainedAccount.getUuid());
assertThat(accounts.getByAccountIdentifier(deletedAccount.getUuid())).isPresent();
assertThat(accounts.getByAccountIdentifier(retainedAccount.getUuid())).isPresent();
accounts.delete(deletedAccount.getUuid());
assertThat(accounts.get(deletedAccount.getUuid())).isNotPresent();
assertThat(accounts.getByAccountIdentifier(deletedAccount.getUuid())).isNotPresent();
verifyStoredState(retainedAccount.getNumber(), retainedAccount.getUuid(),
accounts.get(retainedAccount.getUuid()).get(), retainedAccount);
assertPhoneNumberConstraintDoesNotExist(deletedAccount.getNumber());
assertPhoneNumberIdentifierConstraintDoesNotExist(deletedAccount.getPhoneNumberIdentifier());
verifyStoredState(retainedAccount.getNumber(), retainedAccount.getUuid(), retainedAccount.getPhoneNumberIdentifier(),
accounts.getByAccountIdentifier(retainedAccount.getUuid()).get(), retainedAccount);
{
final Account recreatedAccount = generateAccount(deletedAccount.getNumber(), UUID.randomUUID(),
Collections.singleton(generateDevice(1)));
UUID.randomUUID(), Collections.singleton(generateDevice(1)));
final boolean freshUser = accounts.create(recreatedAccount);
assertThat(freshUser).isTrue();
assertThat(accounts.get(recreatedAccount.getUuid())).isPresent();
verifyStoredState(recreatedAccount.getNumber(), recreatedAccount.getUuid(),
accounts.get(recreatedAccount.getUuid()).get(), recreatedAccount);
assertThat(accounts.getByAccountIdentifier(recreatedAccount.getUuid())).isPresent();
verifyStoredState(recreatedAccount.getNumber(), recreatedAccount.getUuid(), recreatedAccount.getPhoneNumberIdentifier(),
accounts.getByAccountIdentifier(recreatedAccount.getUuid()).get(), recreatedAccount);
assertPhoneNumberConstraintExists(recreatedAccount.getNumber(), recreatedAccount.getUuid());
assertPhoneNumberIdentifierConstraintExists(recreatedAccount.getPhoneNumberIdentifier(), recreatedAccount.getUuid());
}
}
@Test
void testMissing() {
Device device = generateDevice (1 );
Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device));
Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(device));
accounts.create(account);
Optional<Account> retrieved = accounts.get("+11111111");
Optional<Account> retrieved = accounts.getByE164("+11111111");
assertThat(retrieved.isPresent()).isFalse();
retrieved = accounts.get(UUID.randomUUID());
retrieved = accounts.getByAccountIdentifier(UUID.randomUUID());
assertThat(retrieved.isPresent()).isFalse();
}
@@ -369,8 +494,9 @@ class AccountsTest {
when(client.updateItem(any(UpdateItemRequest.class)))
.thenThrow(RuntimeException.class);
Accounts accounts = new Accounts(client, ACCOUNTS_TABLE_NAME, NUMBERS_TABLE_NAME, SCAN_PAGE_SIZE);
Account account = generateAccount("+14151112222", UUID.randomUUID());
Accounts accounts = new Accounts(client, ACCOUNTS_TABLE_NAME, NUMBER_CONSTRAINT_TABLE_NAME,
PNI_CONSTRAINT_TABLE_NAME, SCAN_PAGE_SIZE);
Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID());
try {
accounts.update(account);
@@ -406,17 +532,16 @@ class AccountsTest {
@Test
void testCanonicallyDiscoverableSet() {
Device device = generateDevice(1);
UUID uuid = UUID.randomUUID();
Account account = generateAccount("+14151112222", uuid, Collections.singleton(device));
Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(device));
account.setDiscoverableByPhoneNumber(false);
accounts.create(account);
verifyStoredState("+14151112222", account.getUuid(), account, false);
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, false);
account.setDiscoverableByPhoneNumber(true);
accounts.update(account);
verifyStoredState("+14151112222", account.getUuid(), account, true);
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, true);
account.setDiscoverableByPhoneNumber(false);
accounts.update(account);
verifyStoredState("+14151112222", account.getUuid(), account, false);
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, false);
}
@Test
@@ -424,27 +549,44 @@ class AccountsTest {
final String originalNumber = "+14151112222";
final String targetNumber = "+14151113333";
final UUID originalPni = UUID.randomUUID();
final UUID targetPni = UUID.randomUUID();
final Device device = generateDevice(1);
final Account account = generateAccount(originalNumber, UUID.randomUUID(), Collections.singleton(device));
final Account account = generateAccount(originalNumber, UUID.randomUUID(), originalPni, Collections.singleton(device));
accounts.create(account);
assertThat(accounts.getByPhoneNumberIdentifier(originalPni)).isPresent();
assertPhoneNumberConstraintExists(originalNumber, account.getUuid());
assertPhoneNumberIdentifierConstraintExists(originalPni, account.getUuid());
{
final Optional<Account> retrieved = accounts.get(originalNumber);
final Optional<Account> retrieved = accounts.getByE164(originalNumber);
assertThat(retrieved).isPresent();
verifyStoredState(originalNumber, account.getUuid(), retrieved.get(), account);
verifyStoredState(originalNumber, account.getUuid(), account.getPhoneNumberIdentifier(), retrieved.get(), account);
}
accounts.changeNumber(account, targetNumber);
accounts.changeNumber(account, targetNumber, targetPni);
assertThat(accounts.get(originalNumber)).isEmpty();
assertThat(accounts.getByE164(originalNumber)).isEmpty();
assertThat(accounts.getByAccountIdentifier(originalPni)).isEmpty();
assertPhoneNumberConstraintDoesNotExist(originalNumber);
assertPhoneNumberIdentifierConstraintDoesNotExist(originalPni);
assertPhoneNumberConstraintExists(targetNumber, account.getUuid());
assertPhoneNumberIdentifierConstraintExists(targetPni, account.getUuid());
{
final Optional<Account> retrieved = accounts.get(targetNumber);
final Optional<Account> retrieved = accounts.getByE164(targetNumber);
assertThat(retrieved).isPresent();
verifyStoredState(targetNumber, account.getUuid(), retrieved.get(), account);
verifyStoredState(targetNumber, account.getUuid(), account.getPhoneNumberIdentifier(), retrieved.get(), account);
assertThat(retrieved.get().getPhoneNumberIdentifier()).isEqualTo(targetPni);
assertThat(accounts.getByPhoneNumberIdentifier(targetPni)).isPresent();
}
}
@@ -453,16 +595,55 @@ class AccountsTest {
final String originalNumber = "+14151112222";
final String targetNumber = "+14151113333";
final UUID originalPni = UUID.randomUUID();
final UUID targetPni = UUID.randomUUID();
final Device existingDevice = generateDevice(1);
final Account existingAccount = generateAccount(targetNumber, UUID.randomUUID(), Collections.singleton(existingDevice));
final Account existingAccount = generateAccount(targetNumber, UUID.randomUUID(), targetPni, Collections.singleton(existingDevice));
final Device device = generateDevice(1);
final Account account = generateAccount(originalNumber, UUID.randomUUID(), Collections.singleton(device));
final Account account = generateAccount(originalNumber, UUID.randomUUID(), originalPni, Collections.singleton(device));
accounts.create(account);
accounts.create(existingAccount);
assertThrows(TransactionCanceledException.class, () -> accounts.changeNumber(account, targetNumber));
assertThrows(TransactionCanceledException.class, () -> accounts.changeNumber(account, targetNumber, targetPni));
assertPhoneNumberConstraintExists(originalNumber, account.getUuid());
assertPhoneNumberIdentifierConstraintExists(originalPni, account.getUuid());
assertPhoneNumberConstraintExists(targetNumber, existingAccount.getUuid());
assertPhoneNumberIdentifierConstraintExists(targetPni, existingAccount.getUuid());
}
@Test
public void testChangeNumberPhoneNumberIdentifierConflict() {
final String originalNumber = "+14151112222";
final String targetNumber = "+14151113333";
final Device device = generateDevice(1);
final Account account = generateAccount(originalNumber, UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(device));
accounts.create(account);
final UUID existingAccountIdentifier = UUID.randomUUID();
final UUID existingPhoneNumberIdentifier = UUID.randomUUID();
// Artificially inject a conflicting PNI entry
dynamoDbExtension.getDynamoDbClient().putItem(PutItemRequest.builder()
.tableName(PNI_CONSTRAINT_TABLE_NAME)
.item(Map.of(
Accounts.ATTR_PNI_UUID, AttributeValues.fromUUID(existingPhoneNumberIdentifier),
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(existingAccountIdentifier)))
.conditionExpression(
"attribute_not_exists(#pni) OR (attribute_exists(#pni) AND #uuid = :uuid)")
.expressionAttributeNames(
Map.of("#uuid", Accounts.KEY_ACCOUNT_UUID,
"#pni", Accounts.ATTR_PNI_UUID))
.expressionAttributeValues(
Map.of(":uuid", AttributeValues.fromUUID(existingAccountIdentifier)))
.build());
assertThrows(TransactionCanceledException.class, () -> accounts.changeNumber(account, targetNumber, existingPhoneNumberIdentifier));
}
private Device generateDevice(long id) {
@@ -473,20 +654,62 @@ class AccountsTest {
random.nextBoolean(), random.nextBoolean(), random.nextBoolean()));
}
private Account generateAccount(String number, UUID uuid) {
private Account generateAccount(String number, UUID uuid, final UUID pni) {
Device device = generateDevice(1);
return generateAccount(number, uuid, Collections.singleton(device));
return generateAccount(number, uuid, pni, Collections.singleton(device));
}
private Account generateAccount(String number, UUID uuid, Set<Device> devices) {
private Account generateAccount(String number, UUID uuid, final UUID pni, Set<Device> devices) {
byte[] unidentifiedAccessKey = new byte[16];
Random random = new Random(System.currentTimeMillis());
Arrays.fill(unidentifiedAccessKey, (byte)random.nextInt(255));
return new Account(number, uuid, devices, unidentifiedAccessKey);
return new Account(number, uuid, pni, devices, unidentifiedAccessKey);
}
private void verifyStoredState(String number, UUID uuid, Account expecting, boolean canonicallyDiscoverable) {
private void assertPhoneNumberConstraintExists(final String number, final UUID uuid) {
final GetItemResponse numberConstraintResponse = dynamoDbExtension.getDynamoDbClient().getItem(
GetItemRequest.builder()
.tableName(NUMBER_CONSTRAINT_TABLE_NAME)
.key(Map.of(Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
.build());
assertThat(numberConstraintResponse.hasItem()).isTrue();
assertThat(AttributeValues.getUUID(numberConstraintResponse.item(), Accounts.KEY_ACCOUNT_UUID, null)).isEqualTo(uuid);
}
private void assertPhoneNumberConstraintDoesNotExist(final String number) {
final GetItemResponse numberConstraintResponse = dynamoDbExtension.getDynamoDbClient().getItem(
GetItemRequest.builder()
.tableName(NUMBER_CONSTRAINT_TABLE_NAME)
.key(Map.of(Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
.build());
assertThat(numberConstraintResponse.hasItem()).isFalse();
}
private void assertPhoneNumberIdentifierConstraintExists(final UUID phoneNumberIdentifier, final UUID uuid) {
final GetItemResponse pniConstraintResponse = dynamoDbExtension.getDynamoDbClient().getItem(
GetItemRequest.builder()
.tableName(PNI_CONSTRAINT_TABLE_NAME)
.key(Map.of(Accounts.ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
.build());
assertThat(pniConstraintResponse.hasItem()).isTrue();
assertThat(AttributeValues.getUUID(pniConstraintResponse.item(), Accounts.KEY_ACCOUNT_UUID, null)).isEqualTo(uuid);
}
private void assertPhoneNumberIdentifierConstraintDoesNotExist(final UUID phoneNumberIdentifier) {
final GetItemResponse pniConstraintResponse = dynamoDbExtension.getDynamoDbClient().getItem(
GetItemRequest.builder()
.tableName(PNI_CONSTRAINT_TABLE_NAME)
.key(Map.of(Accounts.ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
.build());
assertThat(pniConstraintResponse.hasItem()).isFalse();
}
private void verifyStoredState(String number, UUID uuid, UUID pni, Account expecting, boolean canonicallyDiscoverable) {
final DynamoDbClient db = dynamoDbExtension.getDynamoDbClient();
final GetItemResponse get = db.getItem(GetItemRequest.builder()
@@ -506,14 +729,15 @@ class AccountsTest {
!canonicallyDiscoverable)).isEqualTo(canonicallyDiscoverable);
Account result = Accounts.fromItem(get.item());
verifyStoredState(number, uuid, result, expecting);
verifyStoredState(number, uuid, pni, result, expecting);
} else {
throw new AssertionError("No data");
}
}
private void verifyStoredState(String number, UUID uuid, Account result, Account expecting) {
private void verifyStoredState(String number, UUID uuid, UUID pni, Account result, Account expecting) {
assertThat(result.getNumber()).isEqualTo(number);
assertThat(result.getPhoneNumberIdentifier()).isEqualTo(pni);
assertThat(result.getLastSeen()).isEqualTo(expecting.getLastSeen());
assertThat(result.getUuid()).isEqualTo(uuid);
assertThat(result.getVersion()).isEqualTo(expecting.getVersion());
@@ -30,7 +30,7 @@ public class ContactDiscoveryWriterTest {
when(acct.getUuid()).thenReturn(uuid);
when(acct.isCanonicallyDiscoverable()).thenReturn(canonicallyDiscoverable);
when(acct.shouldBeVisibleInDirectory()).thenReturn(shouldBeVisible);
when(mgr.get(uuid)).thenReturn(Optional.of(acct));
when(mgr.getByAccountIdentifier(uuid)).thenReturn(Optional.of(acct));
ContactDiscoveryWriter writer = new ContactDiscoveryWriter(mgr);
writer.onCrawlChunk(Optional.empty(), List.of(acct));
verify(mgr, times(updateCalled ? 1 : 0)).update(acct, ContactDiscoveryWriter.NOOP_UPDATER);
@@ -22,15 +22,15 @@ public class KeysDynamoDbRule extends LocalDynamoDbRule {
getDynamoDbClient().createTable(CreateTableRequest.builder()
.tableName(TABLE_NAME)
.keySchema(
KeySchemaElement.builder().attributeName(KeysDynamoDb.KEY_ACCOUNT_UUID).keyType(KeyType.HASH).build(),
KeySchemaElement.builder().attributeName(KeysDynamoDb.KEY_DEVICE_ID_KEY_ID).keyType(KeyType.RANGE)
KeySchemaElement.builder().attributeName(Keys.KEY_ACCOUNT_UUID).keyType(KeyType.HASH).build(),
KeySchemaElement.builder().attributeName(Keys.KEY_DEVICE_ID_KEY_ID).keyType(KeyType.RANGE)
.build())
.attributeDefinitions(AttributeDefinition.builder()
.attributeName(KeysDynamoDb.KEY_ACCOUNT_UUID)
.attributeName(Keys.KEY_ACCOUNT_UUID)
.attributeType(ScalarAttributeType.B)
.build(),
AttributeDefinition.builder()
.attributeName(KeysDynamoDb.KEY_DEVICE_ID_KEY_ID)
.attributeName(Keys.KEY_DEVICE_ID_KEY_ID)
.attributeType(ScalarAttributeType.B)
.build())
.provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(20L).writeCapacityUnits(20L).build())
@@ -1,144 +0,0 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.whispersystems.textsecuregcm.entities.PreKey;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class KeysDynamoDbTest {
private Account account;
private KeysDynamoDb keysDynamoDb;
@ClassRule
public static KeysDynamoDbRule dynamoDbRule = new KeysDynamoDbRule();
private static final String ACCOUNT_NUMBER = "+18005551234";
private static final long DEVICE_ID = 1L;
@Before
public void setup() {
keysDynamoDb = new KeysDynamoDb(dynamoDbRule.getDynamoDbClient(), KeysDynamoDbRule.TABLE_NAME);
account = mock(Account.class);
when(account.getNumber()).thenReturn(ACCOUNT_NUMBER);
when(account.getUuid()).thenReturn(UUID.randomUUID());
}
@Test
public void testStore() {
assertEquals("Initial pre-key count for an account should be zero",
0, keysDynamoDb.getCount(account, DEVICE_ID));
keysDynamoDb.store(account, DEVICE_ID, List.of(new PreKey(1, "public-key")));
assertEquals(1, keysDynamoDb.getCount(account, DEVICE_ID));
keysDynamoDb.store(account, DEVICE_ID, List.of(new PreKey(1, "public-key")));
assertEquals("Repeatedly storing same key should have no effect",
1, keysDynamoDb.getCount(account, DEVICE_ID));
keysDynamoDb.store(account, DEVICE_ID, List.of(new PreKey(2, "different-public-key")));
assertEquals("Inserting a new key should overwrite all prior keys for the given account/device",
1, keysDynamoDb.getCount(account, DEVICE_ID));
keysDynamoDb.store(account, DEVICE_ID, List.of(new PreKey(3, "third-public-key"), new PreKey(4, "fourth-public-key")));
assertEquals("Inserting multiple new keys should overwrite all prior keys for the given account/device",
2, keysDynamoDb.getCount(account, DEVICE_ID));
}
@Test
public void testTakeAccount() {
final Device firstDevice = mock(Device.class);
final Device secondDevice = mock(Device.class);
when(firstDevice.getId()).thenReturn(DEVICE_ID);
when(secondDevice.getId()).thenReturn(DEVICE_ID + 1);
when(account.getDevices()).thenReturn(Set.of(firstDevice, secondDevice));
assertEquals(Collections.emptyMap(), keysDynamoDb.take(account));
final PreKey firstDevicePreKey = new PreKey(1, "public-key");
final PreKey secondDevicePreKey = new PreKey(2, "second-key");
keysDynamoDb.store(account, DEVICE_ID, List.of(firstDevicePreKey));
keysDynamoDb.store(account, DEVICE_ID + 1, List.of(secondDevicePreKey));
final Map<Long, PreKey> expectedKeys = Map.of(DEVICE_ID, firstDevicePreKey,
DEVICE_ID + 1, secondDevicePreKey);
assertEquals(expectedKeys, keysDynamoDb.take(account));
assertEquals(0, keysDynamoDb.getCount(account, DEVICE_ID));
assertEquals(0, keysDynamoDb.getCount(account, DEVICE_ID + 1));
}
@Test
public void testTakeAccountAndDeviceId() {
assertEquals(Optional.empty(), keysDynamoDb.take(account, DEVICE_ID));
final PreKey preKey = new PreKey(1, "public-key");
keysDynamoDb.store(account, DEVICE_ID, List.of(preKey, new PreKey(2, "different-pre-key")));
assertEquals(Optional.of(preKey), keysDynamoDb.take(account, DEVICE_ID));
assertEquals(1, keysDynamoDb.getCount(account, DEVICE_ID));
}
@Test
public void testGetCount() {
assertEquals(0, keysDynamoDb.getCount(account, DEVICE_ID));
keysDynamoDb.store(account, DEVICE_ID, List.of(new PreKey(1, "public-key")));
assertEquals(1, keysDynamoDb.getCount(account, DEVICE_ID));
}
@Test
public void testDeleteByAccount() {
keysDynamoDb.store(account, DEVICE_ID, List.of(new PreKey(1, "public-key"), new PreKey(2, "different-public-key")));
keysDynamoDb.store(account, DEVICE_ID + 1, List.of(new PreKey(3, "public-key-for-different-device")));
assertEquals(2, keysDynamoDb.getCount(account, DEVICE_ID));
assertEquals(1, keysDynamoDb.getCount(account, DEVICE_ID + 1));
keysDynamoDb.delete(account.getUuid());
assertEquals(0, keysDynamoDb.getCount(account, DEVICE_ID));
assertEquals(0, keysDynamoDb.getCount(account, DEVICE_ID + 1));
}
@Test
public void testDeleteByAccountAndDevice() {
keysDynamoDb.store(account, DEVICE_ID, List.of(new PreKey(1, "public-key"), new PreKey(2, "different-public-key")));
keysDynamoDb.store(account, DEVICE_ID + 1, List.of(new PreKey(3, "public-key-for-different-device")));
assertEquals(2, keysDynamoDb.getCount(account, DEVICE_ID));
assertEquals(1, keysDynamoDb.getCount(account, DEVICE_ID + 1));
keysDynamoDb.delete(account.getUuid(), DEVICE_ID);
assertEquals(0, keysDynamoDb.getCount(account, DEVICE_ID));
assertEquals(1, keysDynamoDb.getCount(account, DEVICE_ID + 1));
}
@Test
public void testSortKeyPrefix() {
AttributeValue got = KeysDynamoDb.getSortKeyPrefix(123);
assertArrayEquals(new byte[]{0, 0, 0, 0, 0, 0, 0, 123}, got.b().asByteArray());
}
}
@@ -0,0 +1,108 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.whispersystems.textsecuregcm.entities.PreKey;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
public class KeysTest {
private Keys keys;
@ClassRule
public static KeysDynamoDbRule dynamoDbRule = new KeysDynamoDbRule();
private static final UUID ACCOUNT_UUID = UUID.randomUUID();
private static final long DEVICE_ID = 1L;
@Before
public void setup() {
keys = new Keys(dynamoDbRule.getDynamoDbClient(), KeysDynamoDbRule.TABLE_NAME);
}
@Test
public void testStore() {
assertEquals("Initial pre-key count for an account should be zero",
0, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
keys.store(ACCOUNT_UUID, DEVICE_ID, List.of(new PreKey(1, "public-key")));
assertEquals(1, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
keys.store(ACCOUNT_UUID, DEVICE_ID, List.of(new PreKey(1, "public-key")));
assertEquals("Repeatedly storing same key should have no effect",
1, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
keys.store(ACCOUNT_UUID, DEVICE_ID, List.of(new PreKey(2, "different-public-key")));
assertEquals("Inserting a new key should overwrite all prior keys for the given account/device",
1, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
keys.store(ACCOUNT_UUID, DEVICE_ID, List.of(new PreKey(3, "third-public-key"), new PreKey(4, "fourth-public-key")));
assertEquals("Inserting multiple new keys should overwrite all prior keys for the given account/device",
2, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
}
@Test
public void testTakeAccountAndDeviceId() {
assertEquals(Optional.empty(), keys.take(ACCOUNT_UUID, DEVICE_ID));
final PreKey preKey = new PreKey(1, "public-key");
keys.store(ACCOUNT_UUID, DEVICE_ID, List.of(preKey, new PreKey(2, "different-pre-key")));
assertEquals(Optional.of(preKey), keys.take(ACCOUNT_UUID, DEVICE_ID));
assertEquals(1, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
}
@Test
public void testGetCount() {
assertEquals(0, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
keys.store(ACCOUNT_UUID, DEVICE_ID, List.of(new PreKey(1, "public-key")));
assertEquals(1, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
}
@Test
public void testDeleteByAccount() {
keys.store(ACCOUNT_UUID, DEVICE_ID, List.of(new PreKey(1, "public-key"), new PreKey(2, "different-public-key")));
keys.store(ACCOUNT_UUID, DEVICE_ID + 1, List.of(new PreKey(3, "public-key-for-different-device")));
assertEquals(2, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
assertEquals(1, keys.getCount(ACCOUNT_UUID, DEVICE_ID + 1));
keys.delete(ACCOUNT_UUID);
assertEquals(0, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
assertEquals(0, keys.getCount(ACCOUNT_UUID, DEVICE_ID + 1));
}
@Test
public void testDeleteByAccountAndDevice() {
keys.store(ACCOUNT_UUID, DEVICE_ID, List.of(new PreKey(1, "public-key"), new PreKey(2, "different-public-key")));
keys.store(ACCOUNT_UUID, DEVICE_ID + 1, List.of(new PreKey(3, "public-key-for-different-device")));
assertEquals(2, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
assertEquals(1, keys.getCount(ACCOUNT_UUID, DEVICE_ID + 1));
keys.delete(ACCOUNT_UUID, DEVICE_ID);
assertEquals(0, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
assertEquals(1, keys.getCount(ACCOUNT_UUID, DEVICE_ID + 1));
}
@Test
public void testSortKeyPrefix() {
AttributeValue got = Keys.getSortKeyPrefix(123);
assertArrayEquals(new byte[]{0, 0, 0, 0, 0, 0, 0, 123}, got.b().asByteArray());
}
}
@@ -82,7 +82,7 @@ class MessagePersisterIntegrationTest {
when(account.getNumber()).thenReturn("+18005551234");
when(account.getUuid()).thenReturn(accountUuid);
when(accountsManager.get(accountUuid)).thenReturn(Optional.of(account));
when(accountsManager.getByAccountIdentifier(accountUuid)).thenReturn(Optional.of(account));
when(dynamicConfigurationManager.getConfiguration()).thenReturn(new DynamicConfiguration());
messagesCache.start();
@@ -63,7 +63,7 @@ public class MessagePersisterTest extends AbstractRedisClusterTest {
final Account account = mock(Account.class);
when(accountsManager.get(DESTINATION_ACCOUNT_UUID)).thenReturn(Optional.of(account));
when(accountsManager.getByAccountIdentifier(DESTINATION_ACCOUNT_UUID)).thenReturn(Optional.of(account));
when(account.getNumber()).thenReturn(DESTINATION_ACCOUNT_NUMBER);
when(dynamicConfigurationManager.getConfiguration()).thenReturn(new DynamicConfiguration());
@@ -98,7 +98,7 @@ public class MessagePersisterTest extends AbstractRedisClusterTest {
public void testPersistNextQueuesNoQueues() {
messagePersister.persistNextQueues(Instant.now());
verify(accountsManager, never()).get(any(UUID.class));
verify(accountsManager, never()).getByAccountIdentifier(any(UUID.class));
}
@Test
@@ -147,7 +147,7 @@ public class MessagePersisterTest extends AbstractRedisClusterTest {
final Account account = mock(Account.class);
when(accountsManager.get(accountUuid)).thenReturn(Optional.of(account));
when(accountsManager.getByAccountIdentifier(accountUuid)).thenReturn(Optional.of(account));
when(account.getNumber()).thenReturn(accountNumber);
insertMessages(accountUuid, deviceId, messagesPerQueue, now);
@@ -104,7 +104,7 @@ public class MessagesCacheTest extends AbstractRedisClusterTest {
DESTINATION_DEVICE_ID, messageGuid);
assertTrue(maybeRemovedMessage.isPresent());
assertEquals(MessagesCache.constructEntityFromEnvelope(0, message), maybeRemovedMessage.get());
assertEquals(MessagesCache.constructEntityFromEnvelope(message), maybeRemovedMessage.get());
}
@Test
@@ -136,7 +136,7 @@ public class MessagesCacheTest extends AbstractRedisClusterTest {
messagesToRemove.stream().map(message -> UUID.fromString(message.getServerGuid()))
.collect(Collectors.toList()));
assertEquals(messagesToRemove.stream().map(message -> MessagesCache.constructEntityFromEnvelope(0, message))
assertEquals(messagesToRemove.stream().map(MessagesCache::constructEntityFromEnvelope)
.collect(Collectors.toList()),
removedMessages);
@@ -167,7 +167,7 @@ public class MessagesCacheTest extends AbstractRedisClusterTest {
final MessageProtos.Envelope message = generateRandomMessage(messageGuid, sealedSender);
final long messageId = messagesCache.insert(messageGuid, DESTINATION_UUID, DESTINATION_DEVICE_ID, message);
expectedMessages.add(MessagesCache.constructEntityFromEnvelope(messageId, message));
expectedMessages.add(MessagesCache.constructEntityFromEnvelope(message));
}
assertEquals(expectedMessages, messagesCache.get(DESTINATION_UUID, DESTINATION_DEVICE_ID, messageCount));
@@ -224,7 +224,8 @@ public class MessagesCacheTest extends AbstractRedisClusterTest {
.setServerTimestamp(timestamp)
.setContent(ByteString.copyFromUtf8(RandomStringUtils.randomAlphanumeric(256)))
.setType(MessageProtos.Envelope.Type.CIPHERTEXT)
.setServerGuid(messageGuid.toString());
.setServerGuid(messageGuid.toString())
.setDestinationUuid(UUID.randomUUID().toString());
if (!sealedSender) {
envelopeBuilder.setSourceDevice(random.nextInt(256))
@@ -270,7 +271,7 @@ public class MessagesCacheTest extends AbstractRedisClusterTest {
messagesCache.insert(messageGuid, DESTINATION_UUID, DESTINATION_DEVICE_ID,
generateRandomMessage(messageGuid, sealedSender));
final int slot = SlotHash.getSlot(DESTINATION_UUID.toString() + "::" + DESTINATION_DEVICE_ID);
final int slot = SlotHash.getSlot(DESTINATION_UUID + "::" + DESTINATION_DEVICE_ID);
assertTrue(messagesCache.getQueuesToPersist(slot + 1, Instant.now().plusSeconds(60), 100).isEmpty());
@@ -0,0 +1,58 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
class PhoneNumberIdentifiersTest {
private static final String PNI_TABLE_NAME = "pni_test";
@RegisterExtension
static DynamoDbExtension DYNAMO_DB_EXTENSION = DynamoDbExtension.builder()
.tableName(PNI_TABLE_NAME)
.hashKey(PhoneNumberIdentifiers.KEY_E164)
.attributeDefinition(AttributeDefinition.builder()
.attributeName(PhoneNumberIdentifiers.KEY_E164)
.attributeType(ScalarAttributeType.S)
.build())
.build();
private PhoneNumberIdentifiers phoneNumberIdentifiers;
@BeforeEach
void setUp() {
phoneNumberIdentifiers = new PhoneNumberIdentifiers(DYNAMO_DB_EXTENSION.getDynamoDbClient(), PNI_TABLE_NAME);
}
@Test
void getPhoneNumberIdentifier() {
final String number = "+18005551234";
final String differentNumber = "+18005556789";
final UUID firstPni = phoneNumberIdentifiers.getPhoneNumberIdentifier(number);
final UUID secondPni = phoneNumberIdentifiers.getPhoneNumberIdentifier(number);
assertEquals(firstPni, secondPni);
assertNotEquals(firstPni, phoneNumberIdentifiers.getPhoneNumberIdentifier(differentNumber));
}
@Test
void generatePhoneNumberIdentifierIfNotExists() {
final String number = "+18005551234";
assertEquals(phoneNumberIdentifiers.generatePhoneNumberIdentifierIfNotExists(number),
phoneNumberIdentifiers.generatePhoneNumberIdentifierIfNotExists(number));
}
}
@@ -34,7 +34,7 @@ class RefreshingAccountAndDeviceSupplierTest {
when(initialDevice.getId()).thenReturn(deviceId);
when(initialAccount.getDevice(deviceId)).thenReturn(Optional.of(initialDevice));
when(accountsManager.get(any(UUID.class))).thenAnswer(answer -> {
when(accountsManager.getByAccountIdentifier(any(UUID.class))).thenAnswer(answer -> {
final Account account = mock(Account.class);
final Device device = mock(Device.class);
@@ -6,7 +6,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import io.micrometer.core.instrument.Counter;
@@ -33,7 +33,7 @@ class ReportMessageManagerTest {
assertDoesNotThrow(() -> reportMessageManager.store(null, messageGuid));
verifyZeroInteractions(reportMessageDynamoDb);
verifyNoInteractions(reportMessageDynamoDb);
reportMessageManager.store(number, messageGuid);
@@ -5,17 +5,17 @@
package org.whispersystems.textsecuregcm.tests.auth;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import org.junit.Test;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
public class ExternalServiceCredentialsGeneratorTest {
@Test
public void testGenerateDerivedUsername() {
ExternalServiceCredentialGenerator generator = new ExternalServiceCredentialGenerator(new byte[32], new byte[32], true);
ExternalServiceCredentialGenerator generator = new ExternalServiceCredentialGenerator(new byte[32], new byte[32]);
ExternalServiceCredentials credentials = generator.generateFor("+14152222222");
assertThat(credentials.getUsername()).isNotEqualTo("+14152222222");
@@ -31,6 +31,7 @@ import java.security.SecureRandom;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
@@ -223,19 +224,20 @@ class AccountControllerTest {
when(pendingAccountsManager.getCodeForNumber(SENDER_HAS_STORAGE)).thenReturn(Optional.of(new StoredVerificationCode("666666", System.currentTimeMillis(), null, null)));
when(pendingAccountsManager.getCodeForNumber(SENDER_TRANSFER)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis(), null, null)));
when(accountsManager.get(eq(SENDER_PIN))).thenReturn(Optional.of(senderPinAccount));
when(accountsManager.get(eq(SENDER_REG_LOCK))).thenReturn(Optional.of(senderRegLockAccount));
when(accountsManager.get(eq(SENDER_OVER_PIN))).thenReturn(Optional.of(senderPinAccount));
when(accountsManager.get(eq(SENDER))).thenReturn(Optional.empty());
when(accountsManager.get(eq(SENDER_OLD))).thenReturn(Optional.empty());
when(accountsManager.get(eq(SENDER_PREAUTH))).thenReturn(Optional.empty());
when(accountsManager.get(eq(SENDER_HAS_STORAGE))).thenReturn(Optional.of(senderHasStorage));
when(accountsManager.get(eq(SENDER_TRANSFER))).thenReturn(Optional.of(senderTransfer));
when(accountsManager.getByE164(eq(SENDER_PIN))).thenReturn(Optional.of(senderPinAccount));
when(accountsManager.getByE164(eq(SENDER_REG_LOCK))).thenReturn(Optional.of(senderRegLockAccount));
when(accountsManager.getByE164(eq(SENDER_OVER_PIN))).thenReturn(Optional.of(senderPinAccount));
when(accountsManager.getByE164(eq(SENDER))).thenReturn(Optional.empty());
when(accountsManager.getByE164(eq(SENDER_OLD))).thenReturn(Optional.empty());
when(accountsManager.getByE164(eq(SENDER_PREAUTH))).thenReturn(Optional.empty());
when(accountsManager.getByE164(eq(SENDER_HAS_STORAGE))).thenReturn(Optional.of(senderHasStorage));
when(accountsManager.getByE164(eq(SENDER_TRANSFER))).thenReturn(Optional.of(senderTransfer));
when(accountsManager.create(any(), any(), any(), any())).thenAnswer((Answer<Account>) invocation -> {
when(accountsManager.create(any(), any(), any(), any(), any())).thenAnswer((Answer<Account>) invocation -> {
final Account account = mock(Account.class);
when(account.getUuid()).thenReturn(UUID.randomUUID());
when(account.getNumber()).thenReturn(invocation.getArgument(0, String.class));
when(account.getBadges()).thenReturn(invocation.getArgument(4, List.class));
return account;
});
@@ -1017,7 +1019,7 @@ class AccountControllerTest {
.header("Authorization", AuthHelper.getProvisioningAuthHeader(SENDER, "bar"))
.put(Entity.entity(new AccountAttributes(), MediaType.APPLICATION_JSON_TYPE), AccountCreationResult.class);
verify(accountsManager).create(eq(SENDER), eq("bar"), any(), any());
verify(accountsManager).create(eq(SENDER), eq("bar"), any(), any(), anyList());
if (enrolledInVerifyExperiment) {
verify(smsSender).reportVerificationSucceeded("VerificationSid");
@@ -1095,7 +1097,8 @@ class AccountControllerTest {
verify(pinLimiter).validate(eq(SENDER_REG_LOCK));
verify(accountsManager).create(eq(SENDER_REG_LOCK), eq("bar"), any(), argThat(
attributes -> Hex.toStringCondensed(registration_lock_key).equals(attributes.getRegistrationLock())));
attributes -> Hex.toStringCondensed(registration_lock_key).equals(attributes.getRegistrationLock())),
argThat(List::isEmpty));
}
@Test
@@ -1335,7 +1338,7 @@ class AccountControllerTest {
when(existingAccount.getUuid()).thenReturn(UUID.randomUUID());
when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock);
when(accountsManager.get(number)).thenReturn(Optional.of(existingAccount));
when(accountsManager.getByE164(number)).thenReturn(Optional.of(existingAccount));
final Response response =
resources.getJerseyTest()
@@ -1365,7 +1368,7 @@ class AccountControllerTest {
when(existingAccount.getUuid()).thenReturn(UUID.randomUUID());
when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock);
when(accountsManager.get(number)).thenReturn(Optional.of(existingAccount));
when(accountsManager.getByE164(number)).thenReturn(Optional.of(existingAccount));
final Response response =
resources.getJerseyTest()
@@ -1397,7 +1400,7 @@ class AccountControllerTest {
when(existingAccount.getUuid()).thenReturn(UUID.randomUUID());
when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock);
when(accountsManager.get(number)).thenReturn(Optional.of(existingAccount));
when(accountsManager.getByE164(number)).thenReturn(Optional.of(existingAccount));
final Response response =
resources.getJerseyTest()
@@ -1429,7 +1432,7 @@ class AccountControllerTest {
when(existingAccount.getUuid()).thenReturn(UUID.randomUUID());
when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock);
when(accountsManager.get(number)).thenReturn(Optional.of(existingAccount));
when(accountsManager.getByE164(number)).thenReturn(Optional.of(existingAccount));
final Response response =
resources.getJerseyTest()
@@ -1670,6 +1673,21 @@ class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(204);
}
@Test
void testSetAccountAttributesBadUnidentifiedKeyLength() {
final AccountAttributes attributes = new AccountAttributes(false, 2222, null, null, false, null);
attributes.setUnidentifiedAccessKey(new byte[7]);
Response response =
resources.getJerseyTest()
.target("/v1/accounts/attributes/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.json(attributes));
assertThat(response.getStatus()).isEqualTo(422);
}
@Test
void testDeleteAccount() throws InterruptedException {
Response response =
@@ -1731,4 +1749,51 @@ class AccountControllerTest {
Arguments.of("captcha enforced", true, Set.of("1"), 402)
);
}
@Test
void testAccountExists() {
final Account account = mock(Account.class);
final UUID accountIdentifier = UUID.randomUUID();
final UUID phoneNumberIdentifier = UUID.randomUUID();
when(accountsManager.getByAccountIdentifier(any())).thenReturn(Optional.empty());
when(accountsManager.getByAccountIdentifier(accountIdentifier)).thenReturn(Optional.of(account));
when(accountsManager.getByPhoneNumberIdentifier(any())).thenReturn(Optional.empty());
when(accountsManager.getByPhoneNumberIdentifier(phoneNumberIdentifier)).thenReturn(Optional.of(account));
when(rateLimiters.getCheckAccountExistenceLimiter()).thenReturn(mock(RateLimiter.class));
assertThat(resources.getJerseyTest()
.target(String.format("/v1/accounts/account/%s", accountIdentifier))
.request()
.header("X-Forwarded-For", "127.0.0.1")
.head()
.getStatus()).isEqualTo(200);
assertThat(resources.getJerseyTest()
.target(String.format("/v1/accounts/account/%s", phoneNumberIdentifier))
.request()
.header("X-Forwarded-For", "127.0.0.1")
.head()
.getStatus()).isEqualTo(200);
assertThat(resources.getJerseyTest()
.target(String.format("/v1/accounts/account/%s", UUID.randomUUID()))
.request()
.header("X-Forwarded-For", "127.0.0.1")
.head()
.getStatus()).isEqualTo(404);
}
@Test
void testAccountExistsAuthenticated() {
assertThat(resources.getJerseyTest()
.target(String.format("/v1/accounts/account/%s", UUID.randomUUID()))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.header("X-Forwarded-For", "127.0.0.1")
.head()
.getStatus()).isEqualTo(400);
}
}

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