mirror of
https://github.com/signalapp/Signal-Server.git
synced 2025-12-15 02:00:48 +00:00
Compare commits
352 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c93972a322 | ||
|
|
ca47a7b663 | ||
|
|
9d3d4a3698 | ||
|
|
3b509bf820 | ||
|
|
5b7f91827a | ||
|
|
a44491714c | ||
|
|
06800043a9 | ||
|
|
3090de56b8 | ||
|
|
a37acd1f42 | ||
|
|
372e3f83d2 | ||
|
|
de260a2bef | ||
|
|
e9a130f976 | ||
|
|
43f17414ff | ||
|
|
b259eea8ce | ||
|
|
9cfc2ba09a | ||
|
|
bb347999ce | ||
|
|
3548c3df15 | ||
|
|
1167d0ac2e | ||
|
|
f738bc97e7 | ||
|
|
3f9edfe597 | ||
|
|
a024949311 | ||
|
|
609c901867 | ||
|
|
4ce060a963 | ||
|
|
c4ca0fee40 | ||
|
|
8d4acf0330 | ||
|
|
28a981f29f | ||
|
|
c29113d17a | ||
|
|
951f978447 | ||
|
|
07899f35bd | ||
|
|
3cbbf37468 | ||
|
|
6c7a3df5ae | ||
|
|
2054ab2771 | ||
|
|
44145073f1 | ||
|
|
feb933b4df | ||
|
|
c7cc3002d5 | ||
|
|
049b901d63 | ||
|
|
3cf1b92dfc | ||
|
|
5b0fcbe854 | ||
|
|
cca747a1f6 | ||
|
|
417d99a17e | ||
|
|
e9708b9259 | ||
|
|
e5d3be16b0 | ||
|
|
2ab3c97ee8 | ||
|
|
f20d3043d6 | ||
|
|
4efda89358 | ||
|
|
1fb88271e5 | ||
|
|
a843780f68 | ||
|
|
5ad83da4e0 | ||
|
|
949cc9e214 | ||
|
|
50d92265ea | ||
|
|
e084a9f2b6 | ||
|
|
664f9f36e1 | ||
|
|
4c9efdb936 | ||
|
|
45848e7bfe | ||
|
|
4fa10e5783 | ||
|
|
fc0bc85f4d | ||
|
|
5ae2e5281a | ||
|
|
34a943832a | ||
|
|
db17693ba7 | ||
|
|
6cdf8ebd2c | ||
|
|
072b470f46 | ||
|
|
78b2df2ecc | ||
|
|
51a825f25c | ||
|
|
00e72a30c9 | ||
|
|
69990c23a5 | ||
|
|
df421e0182 | ||
|
|
ede9297139 | ||
|
|
85383fe581 | ||
|
|
4cca7aa4bd | ||
|
|
e2037dea6c | ||
|
|
f10f772e94 | ||
|
|
9ecfe15ac4 | ||
|
|
5f0726af8a | ||
|
|
331bbdd4e6 | ||
|
|
37e3bcfc3e | ||
|
|
4f42c10d60 | ||
|
|
20392a567b | ||
|
|
c03249b411 | ||
|
|
22e6584402 | ||
|
|
c18aca9215 | ||
|
|
aa23a5422a | ||
|
|
01fde4f9ca | ||
|
|
3980dec123 | ||
|
|
c97f837f45 | ||
|
|
9c54d2407b | ||
|
|
a027c4ce1f | ||
|
|
b1fd025ea6 | ||
|
|
a05a230085 | ||
|
|
8fbc1dac74 | ||
|
|
f46842c6c9 | ||
|
|
8b95bb0c03 | ||
|
|
202dd8e92d | ||
|
|
1da3f96d10 | ||
|
|
5f6fe4d670 | ||
|
|
a74438d1ee | ||
|
|
c8033f875d | ||
|
|
07c04006df | ||
|
|
521900c048 | ||
|
|
9069c5abb6 | ||
|
|
ff7a5f471b | ||
|
|
42a47406cc | ||
|
|
de10b6de7b | ||
|
|
d6ade0e1ac | ||
|
|
e04b5e5c9f | ||
|
|
15a6c46d47 | ||
|
|
cb1fc734c2 | ||
|
|
db7f18aae7 | ||
|
|
7fbc327591 | ||
|
|
84b56ae1b2 | ||
|
|
041aa8639a | ||
|
|
216ac72ad0 | ||
|
|
c85ddaeb9c | ||
|
|
e09dec330a | ||
|
|
8f7bae54fe | ||
|
|
ce60f13320 | ||
|
|
1ac0140666 | ||
|
|
6cc8b147a9 | ||
|
|
e078161e2f | ||
|
|
7764185c57 | ||
|
|
d4ef2adf0a | ||
|
|
a83378a44e | ||
|
|
a4a4204762 | ||
|
|
acd1140ef6 | ||
|
|
fbf71c93ff | ||
|
|
38bc0c466a | ||
|
|
71e4351743 | ||
|
|
387e4b94b4 | ||
|
|
201c76b861 | ||
|
|
1c3aa87ca6 | ||
|
|
db63ff6b88 | ||
|
|
115431a486 | ||
|
|
d47ff9b7c7 | ||
|
|
b0818148cf | ||
|
|
2bc4412d66 | ||
|
|
6a428b4da9 | ||
|
|
7299067829 | ||
|
|
5659cb2820 | ||
|
|
570aa4b9e2 | ||
|
|
c4079a3b11 | ||
|
|
6b38b538f1 | ||
|
|
ba139dddd8 | ||
|
|
38b581a231 | ||
|
|
3c2675b41a | ||
|
|
0f5c62ade5 | ||
|
|
54bc3bce96 | ||
|
|
3d92e5b8a9 | ||
|
|
325d145ac3 | ||
|
|
b0654a416a | ||
|
|
19930ec2e4 | ||
|
|
e4de6bf4a7 | ||
|
|
21125c2f5a | ||
|
|
6f166425fe | ||
|
|
cf2353bcf9 | ||
|
|
744eb58071 | ||
|
|
9d47a6f41f | ||
|
|
4f4c23b12f | ||
|
|
fb02815c27 | ||
|
|
fd19299ae0 | ||
|
|
9c053e20da | ||
|
|
19d7b5c65d | ||
|
|
7b9d8829da | ||
|
|
3505ac498c | ||
|
|
f0ab52eb5d | ||
|
|
e8cebad27e | ||
|
|
6441d5838d | ||
|
|
ac0c8b1e9a | ||
|
|
8ec062fbef | ||
|
|
5990a100db | ||
|
|
bc35278684 | ||
|
|
c3c7329ebb | ||
|
|
6fd1c84126 | ||
|
|
0100f0fcc9 | ||
|
|
0cdc32cf65 | ||
|
|
601e9eebbd | ||
|
|
eaa868cf06 | ||
|
|
f55504c665 | ||
|
|
b2ff016cc1 | ||
|
|
33b4f17945 | ||
|
|
e310a3560b | ||
|
|
162b27323e | ||
|
|
ae976ef8d6 | ||
|
|
c6b4e2b71d | ||
|
|
33c8bbd0ce | ||
|
|
f2a3b8dba4 | ||
|
|
207ae6129b | ||
|
|
e1aa734c40 | ||
|
|
9b1b03bbfa | ||
|
|
bb7e0528c4 | ||
|
|
010eadcd10 | ||
|
|
c43e0b54f2 | ||
|
|
6522b74e20 | ||
|
|
8c7975d89a | ||
|
|
407070c9fc | ||
|
|
7821a3cd61 | ||
|
|
a00c2fcfdb | ||
|
|
9cd21d1326 | ||
|
|
aaba95f9b8 | ||
|
|
8d1135a2a3 | ||
|
|
f9fabbedce | ||
|
|
16012e6ffe | ||
|
|
d10a132b0c | ||
|
|
0b3af7d824 | ||
|
|
d0fdae3df7 | ||
|
|
a263611746 | ||
|
|
0e989419c6 | ||
|
|
0fa8276d2d | ||
|
|
b594986241 | ||
|
|
9f3ffa3707 | ||
|
|
8e598c19dc | ||
|
|
2601d6e906 | ||
|
|
de41088051 | ||
|
|
f2752b2a02 | ||
|
|
f0544fab89 | ||
|
|
1b9bf01ab1 | ||
|
|
9945367fa1 | ||
|
|
cbc3887226 | ||
|
|
c11b74e9c0 | ||
|
|
2b764c2abd | ||
|
|
845fc338d7 | ||
|
|
977243ebfd | ||
|
|
29ca544c95 | ||
|
|
94b41d3a2c | ||
|
|
92bb783cbb | ||
|
|
8348263fab | ||
|
|
48f633de11 | ||
|
|
b3b9a629f3 | ||
|
|
5934b7344a | ||
|
|
a9a2e40fed | ||
|
|
656326355a | ||
|
|
b89e2e5355 | ||
|
|
2d187abf13 | ||
|
|
b701412295 | ||
|
|
b4dad81220 | ||
|
|
6bccdad998 | ||
|
|
ecd6b0174a | ||
|
|
a1e534a515 | ||
|
|
ebbe19ba63 | ||
|
|
6a37b73463 | ||
|
|
dd18fcaea2 | ||
|
|
5afc058f90 | ||
|
|
5e221fa9a3 | ||
|
|
0e0cb4d422 | ||
|
|
09f6d60ae9 | ||
|
|
9577d552c6 | ||
|
|
093f17dce2 | ||
|
|
6089f49b9c | ||
|
|
cfb910e87e | ||
|
|
376cffc61d | ||
|
|
d338ba5152 | ||
|
|
f181397664 | ||
|
|
708f23a2ee | ||
|
|
2d1a979eba | ||
|
|
ee0be92967 | ||
|
|
7536b75508 | ||
|
|
7237ae6c54 | ||
|
|
ca05753a3e | ||
|
|
9ca8503eac | ||
|
|
754f71ce00 | ||
|
|
619b05e56c | ||
|
|
8b13826949 | ||
|
|
a96ee57c7e | ||
|
|
ff1ef90a6d | ||
|
|
22905fa8ee | ||
|
|
9e218ddd1c | ||
|
|
6f0462622b | ||
|
|
2f17161163 | ||
|
|
17d48b95ac | ||
|
|
eeea97e2fe | ||
|
|
360e101660 | ||
|
|
3501a944a3 | ||
|
|
76305190a2 | ||
|
|
ab83990170 | ||
|
|
8103a22026 | ||
|
|
1f8e4713ef | ||
|
|
ff9fe2c1be | ||
|
|
7f37c8ee5e | ||
|
|
ed0a723fef | ||
|
|
5c31ef43c9 | ||
|
|
43fd8518c0 | ||
|
|
19a08f01e8 | ||
|
|
33498cf147 | ||
|
|
beeb85cf8d | ||
|
|
ccd860207b | ||
|
|
2c835b5c51 | ||
|
|
5caa951c61 | ||
|
|
4d8c4d6693 | ||
|
|
a9d0574ea8 | ||
|
|
3954494eae | ||
|
|
ed6a2c55eb | ||
|
|
b6ee074149 | ||
|
|
f6b3500e92 | ||
|
|
a71dc48b9b | ||
|
|
bc5eed48c3 | ||
|
|
2ecf3cb303 | ||
|
|
bed33d042a | ||
|
|
d7975626be | ||
|
|
3ac7aba6b2 | ||
|
|
1dde612855 | ||
|
|
4ec97cf006 | ||
|
|
d51c6fd2f8 | ||
|
|
d868e3075c | ||
|
|
ae61ee5486 | ||
|
|
58fd9ddb27 | ||
|
|
a953cb33b7 | ||
|
|
95b90e7c5a | ||
|
|
6a3ecb2881 | ||
|
|
6cf4241283 | ||
|
|
42141e51a1 | ||
|
|
b01945ff50 | ||
|
|
a131f2116f | ||
|
|
625637b888 | ||
|
|
c873f62025 | ||
|
|
43d91e5bd6 | ||
|
|
5c4c729703 | ||
|
|
308da3343d | ||
|
|
48c7572dd5 | ||
|
|
dc5f35460b | ||
|
|
69ea9b0296 | ||
|
|
969c6884c0 | ||
|
|
fcf311aab3 | ||
|
|
888879dfb2 | ||
|
|
e003197f77 | ||
|
|
f57910cd97 | ||
|
|
d85e25dba0 | ||
|
|
89a4034fc6 | ||
|
|
f53743d287 | ||
|
|
2d132128e1 | ||
|
|
6e5ffbe7b5 | ||
|
|
a81c9681a0 | ||
|
|
baf98accd0 | ||
|
|
901c950ee6 | ||
|
|
50ac7f9dc2 | ||
|
|
c2ea4a5290 | ||
|
|
b691b8d37d | ||
|
|
4ead8527c8 | ||
|
|
6f4801fd6f | ||
|
|
10689843b0 | ||
|
|
60cc0c482e | ||
|
|
e1a5105c28 | ||
|
|
ed8a1ed579 | ||
|
|
c3fd2e2284 | ||
|
|
872ef5d0a0 | ||
|
|
b44599cd59 | ||
|
|
7a5dcc700e | ||
|
|
705fb93e45 | ||
|
|
9df923d916 | ||
|
|
dc1cb9093a | ||
|
|
e32043ae79 | ||
|
|
881c921d56 | ||
|
|
abb32bd919 | ||
|
|
4a6c7152cf | ||
|
|
cf92007f66 |
@@ -158,7 +158,7 @@ ij_java_keep_indents_on_empty_lines = false
|
||||
ij_java_keep_line_breaks = true
|
||||
ij_java_keep_multiple_expressions_in_one_line = false
|
||||
ij_java_keep_simple_blocks_in_one_line = false
|
||||
ij_java_keep_simple_classes_in_one_line = false
|
||||
ij_java_keep_simple_classes_in_one_line = true
|
||||
ij_java_keep_simple_lambdas_in_one_line = false
|
||||
ij_java_keep_simple_methods_in_one_line = false
|
||||
ij_java_label_indent_absolute = false
|
||||
|
||||
6
.github/workflows/integration-tests.yml
vendored
6
.github/workflows/integration-tests.yml
vendored
@@ -1,9 +1,13 @@
|
||||
name: Integration Tests
|
||||
|
||||
on: [workflow_dispatch]
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 19 * * MON-FRI'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ vars.INTEGRATION_TESTS_BUCKET != '' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
<extension>
|
||||
<groupId>fr.brouillard.oss</groupId>
|
||||
<artifactId>jgitver-maven-plugin</artifactId>
|
||||
<version>1.7.1</version>
|
||||
<version>1.9.0</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
|
||||
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
Binary file not shown.
6
.mvn/wrapper/maven-wrapper.properties
vendored
6
.mvn/wrapper/maven-wrapper.properties
vendored
@@ -14,5 +14,7 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
|
||||
distributionSha256Sum=e896b60329a71b719d77bb4388b251a50aebcd73c62f69d510c858ce360afe0f
|
||||
wrapperSha256Sum=e63a53cfb9c4d291ebe3c2b0edacb7622bbc480326beaa5a0456e412f52f066a
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<plugin>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-maven-plugin</artifactId>
|
||||
<version>2.2.8</version>
|
||||
<version>${swagger.version}</version>
|
||||
<configuration>
|
||||
<outputFileName>signal-server-openapi</outputFileName>
|
||||
<outputPath>${project.build.directory}/openapi</outputPath>
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2022 Signal Messenger, LLC
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>TextSecureServer</artifactId>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<version>JGITVER</version>
|
||||
</parent>
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>event-logger</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.google.cloud</groupId>
|
||||
<artifactId>google-cloud-logging</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<!--
|
||||
depends on an outdated version (13.0) for JDK 6 compatibility, but it’s safe to override
|
||||
https://youtrack.jetbrains.com/issue/KT-25047
|
||||
-->
|
||||
<artifactId>annotations</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlinx</groupId>
|
||||
<artifactId>kotlinx-serialization-json</artifactId>
|
||||
<version>${kotlinx-serialization.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
|
||||
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<compilerPlugins>
|
||||
<plugin>kotlinx-serialization</plugin>
|
||||
</compilerPlugins>
|
||||
</configuration>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-serialization</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.google.cloud.tools</groupId>
|
||||
<artifactId>jib-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<!-- we don't want jib to execute on this module -->
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.event
|
||||
|
||||
import java.util.Collections
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
import kotlinx.serialization.modules.subclass
|
||||
|
||||
val module = SerializersModule {
|
||||
polymorphic(Event::class) {
|
||||
subclass(RemoteConfigSetEvent::class)
|
||||
subclass(RemoteConfigDeleteEvent::class)
|
||||
}
|
||||
}
|
||||
val jsonFormat = Json { serializersModule = module }
|
||||
|
||||
sealed interface Event
|
||||
|
||||
@Serializable
|
||||
data class RemoteConfigSetEvent(
|
||||
val identity: String,
|
||||
val name: String,
|
||||
val percentage: Int,
|
||||
val defaultValue: String? = null,
|
||||
val value: String? = null,
|
||||
val hashKey: String? = null,
|
||||
val uuids: Collection<String> = Collections.emptyList(),
|
||||
) : Event
|
||||
|
||||
@Serializable
|
||||
data class RemoteConfigDeleteEvent(
|
||||
val identity: String,
|
||||
val name: String,
|
||||
) : Event
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.event
|
||||
|
||||
import com.google.cloud.logging.LogEntry
|
||||
import com.google.cloud.logging.Logging
|
||||
import com.google.cloud.logging.MonitoredResourceUtil
|
||||
import com.google.cloud.logging.Payload.JsonPayload
|
||||
import com.google.cloud.logging.Severity
|
||||
import com.google.protobuf.Struct
|
||||
import com.google.protobuf.util.JsonFormat
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
interface AdminEventLogger {
|
||||
fun logEvent(event: Event, labels: Map<String, String>?)
|
||||
fun logEvent(event: Event) = logEvent(event, null)
|
||||
}
|
||||
|
||||
class NoOpAdminEventLogger : AdminEventLogger {
|
||||
override fun logEvent(event: Event, labels: Map<String, String>?) {}
|
||||
}
|
||||
|
||||
class GoogleCloudAdminEventLogger(private val logging: Logging, private val projectId: String, private val logName: String) : AdminEventLogger {
|
||||
override fun logEvent(event: Event, labels: Map<String, String>?) {
|
||||
val structBuilder = Struct.newBuilder()
|
||||
JsonFormat.parser().merge(jsonFormat.encodeToString(event), structBuilder)
|
||||
val struct = structBuilder.build()
|
||||
|
||||
val logEntryBuilder = LogEntry.newBuilder(JsonPayload.of(struct))
|
||||
.setLogName(logName)
|
||||
.setSeverity(Severity.NOTICE)
|
||||
.setResource(MonitoredResourceUtil.getResource(projectId, "project"));
|
||||
if (labels != null) {
|
||||
logEntryBuilder.setLabels(labels);
|
||||
}
|
||||
logging.write(listOf(logEntryBuilder.build()))
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.event
|
||||
|
||||
import com.google.cloud.logging.Logging
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.mock
|
||||
|
||||
class GoogleCloudAdminEventLoggerTest {
|
||||
|
||||
@Test
|
||||
fun logEvent() {
|
||||
val logging = mock(Logging::class.java)
|
||||
val logger = GoogleCloudAdminEventLogger(logging, "my-project", "test")
|
||||
|
||||
val event = RemoteConfigDeleteEvent("token", "test")
|
||||
logger.logEvent(event)
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M7</version>
|
||||
<version>3.1.2</version>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>**</exclude>
|
||||
@@ -39,7 +39,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<version>3.1.2</version>
|
||||
<configuration>
|
||||
<additionalClasspathElements>
|
||||
<additionalClasspathElement>${project.basedir}/.libs/software.amazon.awssdk-sso.jar</additionalClasspathElement>
|
||||
|
||||
@@ -17,6 +17,7 @@ import java.net.URL;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
@@ -24,7 +25,6 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executors;
|
||||
import org.apache.commons.lang3.RandomUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
@@ -67,38 +67,8 @@ public final class Operations {
|
||||
}
|
||||
|
||||
public static TestUser newRegisteredUser(final String number) {
|
||||
final byte[] registrationPassword = RandomUtils.nextBytes(32);
|
||||
final String accountPassword = Base64.getEncoder().encodeToString(RandomUtils.nextBytes(32));
|
||||
|
||||
final TestUser user = TestUser.create(number, accountPassword, registrationPassword);
|
||||
final AccountAttributes accountAttributes = user.accountAttributes();
|
||||
|
||||
INTEGRATION_TOOLS.populateRecoveryPassword(number, registrationPassword).join();
|
||||
|
||||
// register account
|
||||
final RegistrationRequest registrationRequest = new RegistrationRequest(
|
||||
null, registrationPassword, accountAttributes, true, false,
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty());
|
||||
|
||||
final AccountIdentityResponse registrationResponse = apiPost("/v1/registration", registrationRequest)
|
||||
.authorized(number, accountPassword)
|
||||
.executeExpectSuccess(AccountIdentityResponse.class);
|
||||
|
||||
user.setAciUuid(registrationResponse.uuid());
|
||||
user.setPniUuid(registrationResponse.pni());
|
||||
|
||||
// upload pre-key
|
||||
final TestUser.PreKeySetPublicView preKeySetPublicView = user.preKeys(Device.MASTER_ID, false);
|
||||
apiPut("/v2/keys", preKeySetPublicView)
|
||||
.authorized(user, Device.MASTER_ID)
|
||||
.executeExpectSuccess();
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public static TestUser newRegisteredUserAtomic(final String number) {
|
||||
final byte[] registrationPassword = RandomUtils.nextBytes(32);
|
||||
final String accountPassword = Base64.getEncoder().encodeToString(RandomUtils.nextBytes(32));
|
||||
final byte[] registrationPassword = randomBytes(32);
|
||||
final String accountPassword = Base64.getEncoder().encodeToString(randomBytes(32));
|
||||
|
||||
final TestUser user = TestUser.create(number, accountPassword, registrationPassword);
|
||||
final AccountAttributes accountAttributes = user.accountAttributes();
|
||||
@@ -113,13 +83,12 @@ public final class Operations {
|
||||
registrationPassword,
|
||||
accountAttributes,
|
||||
true,
|
||||
true,
|
||||
Optional.of(new IdentityKey(aciIdentityKeyPair.getPublicKey())),
|
||||
Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey())),
|
||||
Optional.of(generateSignedECPreKey(1, aciIdentityKeyPair)),
|
||||
Optional.of(generateSignedECPreKey(2, pniIdentityKeyPair)),
|
||||
Optional.of(generateSignedKEMPreKey(3, aciIdentityKeyPair)),
|
||||
Optional.of(generateSignedKEMPreKey(4, pniIdentityKeyPair)),
|
||||
new IdentityKey(aciIdentityKeyPair.getPublicKey()),
|
||||
new IdentityKey(pniIdentityKeyPair.getPublicKey()),
|
||||
generateSignedECPreKey(1, aciIdentityKeyPair),
|
||||
generateSignedECPreKey(2, pniIdentityKeyPair),
|
||||
generateSignedKEMPreKey(3, aciIdentityKeyPair),
|
||||
generateSignedKEMPreKey(4, pniIdentityKeyPair),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
|
||||
@@ -133,6 +102,13 @@ public final class Operations {
|
||||
return user;
|
||||
}
|
||||
|
||||
public record PrescribedVerificationNumber(String number, String verificationCode) {}
|
||||
public static PrescribedVerificationNumber prescribedVerificationNumber() {
|
||||
return new PrescribedVerificationNumber(
|
||||
CONFIG.prescribedRegistrationNumber(),
|
||||
CONFIG.prescribedRegistrationCode());
|
||||
}
|
||||
|
||||
public static void deleteUser(final TestUser user) {
|
||||
apiDelete("/v1/accounts/me").authorized(user).executeExpectSuccess();
|
||||
}
|
||||
@@ -180,6 +156,12 @@ public final class Operations {
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] randomBytes(int numBytes) {
|
||||
final byte[] bytes = new byte[numBytes];
|
||||
new SecureRandom().nextBytes(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static RequestBuilder apiGet(final String endpoint) {
|
||||
return new RequestBuilder(HttpRequest.newBuilder().GET(), endpoint);
|
||||
}
|
||||
@@ -233,10 +215,10 @@ public final class Operations {
|
||||
}
|
||||
|
||||
public RequestBuilder authorized(final TestUser user) {
|
||||
return authorized(user, Device.MASTER_ID);
|
||||
return authorized(user, Device.PRIMARY_ID);
|
||||
}
|
||||
|
||||
public RequestBuilder authorized(final TestUser user, final long deviceId) {
|
||||
public RequestBuilder authorized(final TestUser user, final byte deviceId) {
|
||||
final String username = "%s.%d".formatted(user.aciUuid().toString(), deviceId);
|
||||
return authorized(username, user.accountPassword());
|
||||
}
|
||||
|
||||
@@ -16,13 +16,13 @@ import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||
|
||||
public class TestDevice {
|
||||
|
||||
private final long deviceId;
|
||||
private final byte deviceId;
|
||||
|
||||
private final Map<Integer, Pair<IdentityKeyPair, SignedPreKeyRecord>> signedPreKeys = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
public static TestDevice create(
|
||||
final long deviceId,
|
||||
final byte deviceId,
|
||||
final IdentityKeyPair aciIdentityKeyPair,
|
||||
final IdentityKeyPair pniIdentityKeyPair) {
|
||||
final TestDevice device = new TestDevice(deviceId);
|
||||
@@ -31,11 +31,11 @@ public class TestDevice {
|
||||
return device;
|
||||
}
|
||||
|
||||
public TestDevice(final long deviceId) {
|
||||
public TestDevice(final byte deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public long deviceId() {
|
||||
public byte deviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,17 +9,19 @@ import static java.util.Objects.requireNonNull;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import java.security.SecureRandom;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.apache.commons.lang3.RandomUtils;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||
import org.signal.libsignal.protocol.util.KeyHelper;
|
||||
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
@@ -27,9 +29,11 @@ public class TestUser {
|
||||
|
||||
private final int registrationId;
|
||||
|
||||
private final int pniRegistrationId;
|
||||
|
||||
private final IdentityKeyPair aciIdentityKey;
|
||||
|
||||
private final Map<Long, TestDevice> devices = new ConcurrentHashMap<>();
|
||||
private final Map<Byte, TestDevice> devices = new ConcurrentHashMap<>();
|
||||
|
||||
private final byte[] unidentifiedAccessKey;
|
||||
|
||||
@@ -53,11 +57,14 @@ public class TestUser {
|
||||
final IdentityKeyPair pniIdentityKey = IdentityKeyPair.generate();
|
||||
// registration id
|
||||
final int registrationId = KeyHelper.generateRegistrationId(false);
|
||||
final int pniRegistrationId = KeyHelper.generateRegistrationId(false);
|
||||
// uak
|
||||
final byte[] unidentifiedAccessKey = RandomUtils.nextBytes(16);
|
||||
final byte[] unidentifiedAccessKey = new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH];
|
||||
new SecureRandom().nextBytes(unidentifiedAccessKey);
|
||||
|
||||
return new TestUser(
|
||||
registrationId,
|
||||
pniRegistrationId,
|
||||
aciIdentityKey,
|
||||
phoneNumber,
|
||||
pniIdentityKey,
|
||||
@@ -68,6 +75,7 @@ public class TestUser {
|
||||
|
||||
public TestUser(
|
||||
final int registrationId,
|
||||
final int pniRegistrationId,
|
||||
final IdentityKeyPair aciIdentityKey,
|
||||
final String phoneNumber,
|
||||
final IdentityKeyPair pniIdentityKey,
|
||||
@@ -75,13 +83,14 @@ public class TestUser {
|
||||
final String accountPassword,
|
||||
final byte[] registrationPassword) {
|
||||
this.registrationId = registrationId;
|
||||
this.pniRegistrationId = pniRegistrationId;
|
||||
this.aciIdentityKey = aciIdentityKey;
|
||||
this.phoneNumber = phoneNumber;
|
||||
this.pniIdentityKey = pniIdentityKey;
|
||||
this.unidentifiedAccessKey = unidentifiedAccessKey;
|
||||
this.accountPassword = accountPassword;
|
||||
this.registrationPassword = registrationPassword;
|
||||
devices.put(Device.MASTER_ID, TestDevice.create(Device.MASTER_ID, aciIdentityKey, pniIdentityKey));
|
||||
devices.put(Device.PRIMARY_ID, TestDevice.create(Device.PRIMARY_ID, aciIdentityKey, pniIdentityKey));
|
||||
}
|
||||
|
||||
public int registrationId() {
|
||||
@@ -117,7 +126,7 @@ public class TestUser {
|
||||
}
|
||||
|
||||
public AccountAttributes accountAttributes() {
|
||||
return new AccountAttributes(true, registrationId, "", "", true, new Device.DeviceCapabilities())
|
||||
return new AccountAttributes(true, registrationId, pniRegistrationId, "".getBytes(StandardCharsets.UTF_8), "", true, new Device.DeviceCapabilities(false, false, false, false))
|
||||
.withUnidentifiedAccessKey(unidentifiedAccessKey)
|
||||
.withRecoveryPassword(registrationPassword);
|
||||
}
|
||||
@@ -146,7 +155,7 @@ public class TestUser {
|
||||
this.registrationPassword = registrationPassword;
|
||||
}
|
||||
|
||||
public PreKeySetPublicView preKeys(final long deviceId, final boolean pni) {
|
||||
public PreKeySetPublicView preKeys(final byte deviceId, final boolean pni) {
|
||||
final IdentityKeyPair identity = pni
|
||||
? pniIdentityKey
|
||||
: aciIdentityKey;
|
||||
|
||||
@@ -10,5 +10,7 @@ import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguratio
|
||||
public record Config(String domain,
|
||||
String rootCert,
|
||||
DynamoDbClientConfiguration dynamoDbClientConfiguration,
|
||||
DynamoDbTables dynamoDbTables) {
|
||||
DynamoDbTables dynamoDbTables,
|
||||
String prescribedRegistrationNumber,
|
||||
String prescribedRegistrationCode) {
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ import org.signal.libsignal.usernames.Username;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountIdentifierResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.ConfirmUsernameHashRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.EncryptedUsername;
|
||||
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.UsernameHashResponse;
|
||||
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||
|
||||
public class AccountTest {
|
||||
|
||||
@@ -40,7 +40,7 @@ public class AccountTest {
|
||||
|
||||
@Test
|
||||
public void testCreateAccountAtomic() throws Exception {
|
||||
final TestUser user = Operations.newRegisteredUserAtomic("+19995550201");
|
||||
final TestUser user = Operations.newRegisteredUser("+19995550201");
|
||||
try {
|
||||
final Pair<Integer, AccountIdentityResponse> execute = Operations.apiGet("/v1/accounts/whoami")
|
||||
.authorized(user)
|
||||
@@ -107,7 +107,7 @@ public class AccountTest {
|
||||
final AccountIdentifierResponse accountIdentifierResponse = Operations
|
||||
.apiGet("/v1/accounts/username_hash/" + Base64.getUrlEncoder().encodeToString(reservedHash))
|
||||
.executeExpectSuccess(AccountIdentifierResponse.class);
|
||||
assertEquals(user.aciUuid(), accountIdentifierResponse.uuid());
|
||||
assertEquals(new AciServiceIdentifier(user.aciUuid()), accountIdentifierResponse.uuid());
|
||||
// try authorized
|
||||
Operations
|
||||
.apiGet("/v1/accounts/username_hash/" + Base64.getUrlEncoder().encodeToString(reservedHash))
|
||||
|
||||
@@ -11,8 +11,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
|
||||
@@ -21,24 +20,15 @@ import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
public class MessagingTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
public void testSendMessageUnsealed(final boolean atomicAccountCreation) throws Exception {
|
||||
final TestUser userA;
|
||||
final TestUser userB;
|
||||
|
||||
if (atomicAccountCreation) {
|
||||
userA = Operations.newRegisteredUserAtomic("+19995550102");
|
||||
userB = Operations.newRegisteredUserAtomic("+19995550103");
|
||||
} else {
|
||||
userA = Operations.newRegisteredUser("+19995550104");
|
||||
userB = Operations.newRegisteredUser("+19995550105");
|
||||
}
|
||||
@Test
|
||||
public void testSendMessageUnsealed() {
|
||||
final TestUser userA = Operations.newRegisteredUser("+19995550102");
|
||||
final TestUser userB = Operations.newRegisteredUser("+19995550103");
|
||||
|
||||
try {
|
||||
final byte[] expectedContent = "Hello, World!".getBytes(StandardCharsets.UTF_8);
|
||||
final String contentBase64 = Base64.getEncoder().encodeToString(expectedContent);
|
||||
final IncomingMessage message = new IncomingMessage(1, Device.MASTER_ID, userB.registrationId(), contentBase64);
|
||||
final IncomingMessage message = new IncomingMessage(1, Device.PRIMARY_ID, userB.registrationId(), contentBase64);
|
||||
final IncomingMessageList messages = new IncomingMessageList(List.of(message), false, true, System.currentTimeMillis());
|
||||
|
||||
final Pair<Integer, SendMessageResponse> sendMessage = Operations
|
||||
|
||||
@@ -19,7 +19,10 @@ public class RegistrationTest {
|
||||
public void testRegistration() throws Exception {
|
||||
final UpdateVerificationSessionRequest originalRequest = new UpdateVerificationSessionRequest(
|
||||
"test", UpdateVerificationSessionRequest.PushTokenType.FCM, null, null, null, null);
|
||||
final CreateVerificationSessionRequest input = new CreateVerificationSessionRequest("+19995550102", originalRequest);
|
||||
|
||||
final Operations.PrescribedVerificationNumber params = Operations.prescribedVerificationNumber();
|
||||
final CreateVerificationSessionRequest input = new CreateVerificationSessionRequest(params.number(),
|
||||
originalRequest);
|
||||
|
||||
final VerificationSessionResponse verificationSessionResponse = Operations
|
||||
.apiPost("/v1/verification/session", input)
|
||||
@@ -46,7 +49,8 @@ public class RegistrationTest {
|
||||
.executeExpectSuccess(VerificationSessionResponse.class);
|
||||
|
||||
// verify code
|
||||
final SubmitVerificationCodeRequest submitVerificationCodeRequest = new SubmitVerificationCodeRequest("265402");
|
||||
final SubmitVerificationCodeRequest submitVerificationCodeRequest = new SubmitVerificationCodeRequest(
|
||||
params.verificationCode());
|
||||
final VerificationSessionResponse codeVerified = Operations
|
||||
.apiPut("/v1/verification/session/%s/code".formatted(sessionId), submitVerificationCodeRequest)
|
||||
.executeExpectSuccess(VerificationSessionResponse.class);
|
||||
|
||||
218
mvnw
vendored
218
mvnw
vendored
@@ -19,7 +19,7 @@
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Maven Start Up Batch script
|
||||
# Apache Maven Wrapper startup batch script, version 3.2.0
|
||||
#
|
||||
# Required ENV vars:
|
||||
# ------------------
|
||||
@@ -27,7 +27,6 @@
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# M2_HOME - location of maven2's installed home dir
|
||||
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
# e.g. to debug Maven itself, use
|
||||
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
@@ -54,7 +53,7 @@ fi
|
||||
cygwin=false;
|
||||
darwin=false;
|
||||
mingw=false
|
||||
case "`uname`" in
|
||||
case "$(uname)" in
|
||||
CYGWIN*) cygwin=true ;;
|
||||
MINGW*) mingw=true;;
|
||||
Darwin*) darwin=true
|
||||
@@ -62,9 +61,9 @@ case "`uname`" in
|
||||
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
if [ -x "/usr/libexec/java_home" ]; then
|
||||
export JAVA_HOME="`/usr/libexec/java_home`"
|
||||
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
|
||||
else
|
||||
export JAVA_HOME="/Library/Java/Home"
|
||||
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
@@ -72,68 +71,38 @@ esac
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
if [ -r /etc/gentoo-release ] ; then
|
||||
JAVA_HOME=`java-config --jre-home`
|
||||
JAVA_HOME=$(java-config --jre-home)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$M2_HOME" ] ; then
|
||||
## resolve links - $0 may be a link to maven's home
|
||||
PRG="$0"
|
||||
|
||||
# need this for relative symlinks
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG="`dirname "$PRG"`/$link"
|
||||
fi
|
||||
done
|
||||
|
||||
saveddir=`pwd`
|
||||
|
||||
M2_HOME=`dirname "$PRG"`/..
|
||||
|
||||
# make it fully qualified
|
||||
M2_HOME=`cd "$M2_HOME" && pwd`
|
||||
|
||||
cd "$saveddir"
|
||||
# echo Using m2 at $M2_HOME
|
||||
fi
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
||||
if $cygwin ; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME=`cygpath --unix "$M2_HOME"`
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
|
||||
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
|
||||
fi
|
||||
|
||||
# For Mingw, ensure paths are in UNIX format before anything is touched
|
||||
if $mingw ; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME="`(cd "$M2_HOME"; pwd)`"
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
|
||||
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
|
||||
JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
javaExecutable="`which javac`"
|
||||
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
|
||||
javaExecutable="$(which javac)"
|
||||
if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
|
||||
# readlink(1) is not available as standard on Solaris 10.
|
||||
readLink=`which readlink`
|
||||
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
|
||||
readLink=$(which readlink)
|
||||
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
|
||||
if $darwin ; then
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
|
||||
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
|
||||
else
|
||||
javaExecutable="`readlink -f \"$javaExecutable\"`"
|
||||
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
|
||||
fi
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
|
||||
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
|
||||
JAVA_HOME="$javaHome"
|
||||
export JAVA_HOME
|
||||
fi
|
||||
@@ -149,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
else
|
||||
JAVACMD="`\\unset -f command; \\command -v java`"
|
||||
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -163,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then
|
||||
echo "Warning: JAVA_HOME environment variable is not set."
|
||||
fi
|
||||
|
||||
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
|
||||
|
||||
# traverses directory structure from process work directory to filesystem root
|
||||
# first directory with .mvn subdirectory is considered project base directory
|
||||
find_maven_basedir() {
|
||||
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "Path not specified to find_maven_basedir"
|
||||
@@ -184,96 +150,99 @@ find_maven_basedir() {
|
||||
fi
|
||||
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
|
||||
if [ -d "${wdir}" ]; then
|
||||
wdir=`cd "$wdir/.."; pwd`
|
||||
wdir=$(cd "$wdir/.." || exit 1; pwd)
|
||||
fi
|
||||
# end of workaround
|
||||
done
|
||||
echo "${basedir}"
|
||||
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
|
||||
}
|
||||
|
||||
# concatenates all lines of a file
|
||||
concat_lines() {
|
||||
if [ -f "$1" ]; then
|
||||
echo "$(tr -s '\n' ' ' < "$1")"
|
||||
# Remove \r in case we run on Windows within Git Bash
|
||||
# and check out the repository with auto CRLF management
|
||||
# enabled. Otherwise, we may read lines that are delimited with
|
||||
# \r\n and produce $'-Xarg\r' rather than -Xarg due to word
|
||||
# splitting rules.
|
||||
tr -s '\r\n' ' ' < "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
BASE_DIR=`find_maven_basedir "$(pwd)"`
|
||||
log() {
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
printf '%s\n' "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
|
||||
if [ -z "$BASE_DIR" ]; then
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
|
||||
log "$MAVEN_PROJECTBASEDIR"
|
||||
|
||||
##########################################################################################
|
||||
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
# This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
##########################################################################################
|
||||
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found .mvn/wrapper/maven-wrapper.jar"
|
||||
fi
|
||||
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
|
||||
if [ -r "$wrapperJarPath" ]; then
|
||||
log "Found $wrapperJarPath"
|
||||
else
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
|
||||
fi
|
||||
log "Couldn't find $wrapperJarPath, downloading it ..."
|
||||
|
||||
if [ -n "$MVNW_REPOURL" ]; then
|
||||
jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
|
||||
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
else
|
||||
jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
|
||||
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
fi
|
||||
while IFS="=" read key value; do
|
||||
case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
|
||||
while IFS="=" read -r key value; do
|
||||
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
|
||||
safeValue=$(echo "$value" | tr -d '\r')
|
||||
case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
|
||||
esac
|
||||
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Downloading from: $jarUrl"
|
||||
fi
|
||||
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
|
||||
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
log "Downloading from: $wrapperUrl"
|
||||
|
||||
if $cygwin; then
|
||||
wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
|
||||
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
|
||||
fi
|
||||
|
||||
if command -v wget > /dev/null; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found wget ... using wget"
|
||||
fi
|
||||
log "Found wget ... using wget"
|
||||
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
else
|
||||
wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
elif command -v curl > /dev/null; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found curl ... using curl"
|
||||
fi
|
||||
log "Found curl ... using curl"
|
||||
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
curl -o "$wrapperJarPath" "$jarUrl" -f
|
||||
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||
else
|
||||
curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
|
||||
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
|
||||
else
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Falling back to using Java to download"
|
||||
fi
|
||||
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||
log "Falling back to using Java to download"
|
||||
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
|
||||
# For Cygwin, switch paths to Windows format before running javac
|
||||
if $cygwin; then
|
||||
javaClass=`cygpath --path --windows "$javaClass"`
|
||||
javaSource=$(cygpath --path --windows "$javaSource")
|
||||
javaClass=$(cygpath --path --windows "$javaClass")
|
||||
fi
|
||||
if [ -e "$javaClass" ]; then
|
||||
if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo " - Compiling MavenWrapperDownloader.java ..."
|
||||
fi
|
||||
# Compiling the Java class
|
||||
("$JAVA_HOME/bin/javac" "$javaClass")
|
||||
if [ -e "$javaSource" ]; then
|
||||
if [ ! -e "$javaClass" ]; then
|
||||
log " - Compiling MavenWrapperDownloader.java ..."
|
||||
("$JAVA_HOME/bin/javac" "$javaSource")
|
||||
fi
|
||||
if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
|
||||
# Running the downloader
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo " - Running MavenWrapperDownloader.java ..."
|
||||
fi
|
||||
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
|
||||
if [ -e "$javaClass" ]; then
|
||||
log " - Running MavenWrapperDownloader.java ..."
|
||||
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -282,35 +251,58 @@ fi
|
||||
# End of extension
|
||||
##########################################################################################
|
||||
|
||||
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo $MAVEN_PROJECTBASEDIR
|
||||
# If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||
wrapperSha256Sum=""
|
||||
while IFS="=" read -r key value; do
|
||||
case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
|
||||
esac
|
||||
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
if [ -n "$wrapperSha256Sum" ]; then
|
||||
wrapperSha256Result=false
|
||||
if command -v sha256sum > /dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
|
||||
wrapperSha256Result=true
|
||||
fi
|
||||
elif command -v shasum > /dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
|
||||
wrapperSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
|
||||
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
|
||||
exit 1
|
||||
fi
|
||||
if [ $wrapperSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
|
||||
echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME=`cygpath --path --windows "$M2_HOME"`
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
|
||||
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
|
||||
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
|
||||
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
|
||||
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
|
||||
MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
|
||||
fi
|
||||
|
||||
# Provide a "standardized" way to retrieve the CLI args that will
|
||||
# work with both Windows and non-Windows executions.
|
||||
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
|
||||
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
|
||||
export MAVEN_CMD_LINE_ARGS
|
||||
|
||||
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
# shellcheck disable=SC2086 # safe args
|
||||
exec "$JAVACMD" \
|
||||
$MAVEN_OPTS \
|
||||
$MAVEN_DEBUG_OPTS \
|
||||
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
|
||||
"-Dmaven.home=${M2_HOME}" \
|
||||
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
||||
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
|
||||
|
||||
31
mvnw.cmd
vendored
31
mvnw.cmd
vendored
@@ -18,13 +18,12 @@
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Maven Start Up Batch script
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.2.0
|
||||
@REM
|
||||
@REM Required ENV vars:
|
||||
@REM JAVA_HOME - location of a JDK home dir
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM M2_HOME - location of maven2's installed home dir
|
||||
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
|
||||
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
|
||||
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
@@ -120,10 +119,10 @@ SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
|
||||
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
|
||||
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
|
||||
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
|
||||
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
|
||||
)
|
||||
|
||||
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
@@ -134,11 +133,11 @@ if exist %WRAPPER_JAR% (
|
||||
)
|
||||
) else (
|
||||
if not "%MVNW_REPOURL%" == "" (
|
||||
SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
|
||||
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
)
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Couldn't find %WRAPPER_JAR%, downloading it ...
|
||||
echo Downloading from: %DOWNLOAD_URL%
|
||||
echo Downloading from: %WRAPPER_URL%
|
||||
)
|
||||
|
||||
powershell -Command "&{"^
|
||||
@@ -146,7 +145,7 @@ if exist %WRAPPER_JAR% (
|
||||
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
|
||||
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
|
||||
"}"^
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
|
||||
"}"
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Finished downloading %WRAPPER_JAR%
|
||||
@@ -154,6 +153,24 @@ if exist %WRAPPER_JAR% (
|
||||
)
|
||||
@REM End of extension
|
||||
|
||||
@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||
SET WRAPPER_SHA_256_SUM=""
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
|
||||
)
|
||||
IF NOT %WRAPPER_SHA_256_SUM%=="" (
|
||||
powershell -Command "&{"^
|
||||
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
|
||||
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
|
||||
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
|
||||
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
|
||||
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
|
||||
" exit 1;"^
|
||||
"}"^
|
||||
"}"
|
||||
if ERRORLEVEL 1 goto error
|
||||
)
|
||||
|
||||
@REM Provide a "standardized" way to retrieve the CLI args that will
|
||||
@REM work with both Windows and non-Windows executions.
|
||||
set MAVEN_CMD_LINE_ARGS=%*
|
||||
|
||||
123
pom.xml
123
pom.xml
@@ -31,50 +31,53 @@
|
||||
|
||||
<modules>
|
||||
<module>api-doc</module>
|
||||
<module>event-logger</module>
|
||||
<module>integration-tests</module>
|
||||
<module>service</module>
|
||||
<module>websocket-resources</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<aws.sdk2.version>2.19.8</aws.sdk2.version>
|
||||
<braintree.version>3.19.0</braintree.version>
|
||||
<commons-csv.version>1.9.0</commons-csv.version>
|
||||
<commons-io.version>2.9.0</commons-io.version>
|
||||
<dropwizard.version>2.0.34</dropwizard.version>
|
||||
<aws.sdk2.version>2.21.5</aws.sdk2.version>
|
||||
<braintree.version>3.27.0</braintree.version>
|
||||
<commons-csv.version>1.10.0</commons-csv.version>
|
||||
<commons-io.version>2.14.0</commons-io.version>
|
||||
<dropwizard.version>3.0.4</dropwizard.version>
|
||||
<dropwizard-metrics-datadog.version>1.1.13</dropwizard-metrics-datadog.version>
|
||||
<google-cloud-libraries.version>26.1.3</google-cloud-libraries.version>
|
||||
<grpc.version>1.51.1</grpc.version> <!-- this should be kept in sync with the value from Google’s libraries-bom -->
|
||||
<gson.version>2.9.0</gson.version>
|
||||
<jackson.version>2.13.4</jackson.version>
|
||||
<google-cloud-libraries.version>26.25.0</google-cloud-libraries.version>
|
||||
<grpc.version>1.58.0</grpc.version> <!-- should be kept in sync with the value from Google libraries-bom -->
|
||||
<gson.version>2.10.1</gson.version>
|
||||
<!-- several libraries (AWS, Google Cloud) use Apache http components transitively, and we need to align them -->
|
||||
<httpcore.version>4.4.16</httpcore.version>
|
||||
<httpclient.version>4.5.14</httpclient.version>
|
||||
<jackson.version>2.16.0</jackson.version>
|
||||
<jaxb.version>2.3.1</jaxb.version>
|
||||
<jedis.version>2.9.0</jedis.version>
|
||||
<kotlin.version>1.8.0</kotlin.version>
|
||||
<kotlinx-serialization.version>1.4.1</kotlinx-serialization.version>
|
||||
<lettuce.version>6.2.4.RELEASE</lettuce.version>
|
||||
<libphonenumber.version>8.12.54</libphonenumber.version>
|
||||
<logstash.logback.version>7.2</logstash.logback.version>
|
||||
<junit-pioneer.version>2.1.0</junit-pioneer.version>
|
||||
<jsr305.version>3.0.2</jsr305.version>
|
||||
<kotlin.version>1.9.10</kotlin.version>
|
||||
<kotlinx-serialization.version>1.5.1</kotlinx-serialization.version>
|
||||
<lettuce.version>6.2.6.RELEASE</lettuce.version>
|
||||
<libphonenumber.version>8.13.23</libphonenumber.version>
|
||||
<logstash.logback.version>7.3</logstash.logback.version>
|
||||
<log4j-bom.version>2.21.0</log4j-bom.version>
|
||||
<luajava.version>3.4.0</luajava.version>
|
||||
<micrometer.version>1.10.3</micrometer.version>
|
||||
<mockito.version>4.11.0</mockito.version>
|
||||
<netty.version>4.1.82.Final</netty.version>
|
||||
<opentest4j.version>1.2.0</opentest4j.version>
|
||||
<protobuf.version>3.21.7</protobuf.version>
|
||||
<micrometer.version>1.10.10</micrometer.version>
|
||||
<netty.version>4.1.96.Final</netty.version>
|
||||
<opentest4j.version>1.3.0</opentest4j.version>
|
||||
<protobuf.version>3.24.3</protobuf.version> <!-- should be kept in sync with the value from Google libraries-bom -->
|
||||
<pushy.version>0.15.2</pushy.version>
|
||||
<reactive.grpc.version>1.2.4</reactive.grpc.version>
|
||||
<reactor-bom.version>2022.0.12</reactor-bom.version> <!-- 3.5.x, see https://github.com/reactor/reactor#bom-versioning-scheme -->
|
||||
<resilience4j.version>1.7.0</resilience4j.version>
|
||||
<semver4j.version>3.1.0</semver4j.version>
|
||||
<slf4j.version>1.7.30</slf4j.version>
|
||||
<stripe.version>21.2.0</stripe.version>
|
||||
<slf4j.version>2.0.9</slf4j.version>
|
||||
<stripe.version>23.10.0</stripe.version>
|
||||
<swagger.version>2.2.17</swagger.version>
|
||||
<vavr.version>0.10.4</vavr.version>
|
||||
|
||||
<!-- 17.0.7_7-jre-jammy -->
|
||||
<docker.image.sha256>ddf36656dc8920621fddf4928bdcb4b98c0d0e7bc9672f0cea8115c10ad5cbc6</docker.image.sha256>
|
||||
<!-- 17.0.8_7-jre-jammy -->
|
||||
<docker.image.sha256>b8af44d6a7e0615a7486d7307dd54bba23ff24e3aea14893fd2795e8c436d44e</docker.image.sha256>
|
||||
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
<deploy.skip.s3.upload>false</deploy.skip.s3.upload>
|
||||
</properties>
|
||||
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
@@ -147,7 +150,7 @@
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-bom</artifactId>
|
||||
<version>2022.0.3</version> <!-- 3.5.x, see https://github.com/reactor/reactor#bom-versioning-scheme -->
|
||||
<version>${reactor-bom.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@@ -224,18 +227,6 @@
|
||||
<version>${jaxb.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-inline</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.opentest4j</groupId>
|
||||
<artifactId>opentest4j</artifactId>
|
||||
@@ -253,11 +244,6 @@
|
||||
<version>${slf4j.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
<version>${jedis.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
@@ -266,7 +252,7 @@
|
||||
<dependency>
|
||||
<groupId>org.ow2.asm</groupId>
|
||||
<artifactId>asm</artifactId>
|
||||
<version>9.2</version>
|
||||
<version>9.5</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -279,6 +265,11 @@
|
||||
<artifactId>braintree-java</artifactId>
|
||||
<version>${braintree.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
<version>${jsr305.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
@@ -293,15 +284,25 @@
|
||||
<dependency>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>libsignal-server</artifactId>
|
||||
<version>0.26.0</version>
|
||||
<version>0.37.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-bom</artifactId>
|
||||
<version>2.17.1</version>
|
||||
<version>${log4j-bom.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpcore</artifactId>
|
||||
<version>${httpcore.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>${httpclient.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
@@ -313,9 +314,10 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.tomakehurst</groupId>
|
||||
<artifactId>wiremock-jre8</artifactId>
|
||||
<version>2.35.0</version>
|
||||
<groupId>org.wiremock</groupId>
|
||||
<!-- use standalone until Dropwizard 4 + jakarta.* -->
|
||||
<artifactId>wiremock-standalone</artifactId>
|
||||
<version>3.3.1</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
@@ -331,7 +333,6 @@
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -347,7 +348,7 @@
|
||||
<dependency>
|
||||
<groupId>org.junit-pioneer</groupId>
|
||||
<artifactId>junit-pioneer</artifactId>
|
||||
<version>1.9.1</version>
|
||||
<version>${junit-pioneer.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -389,7 +390,7 @@
|
||||
<plugin>
|
||||
<groupId>com.google.cloud.tools</groupId>
|
||||
<artifactId>jib-maven-plugin</artifactId>
|
||||
<version>3.3.1</version>
|
||||
<version>3.4.0</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
@@ -430,7 +431,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<release>17</release>
|
||||
</configuration>
|
||||
@@ -439,7 +440,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.2.0</version>
|
||||
<version>3.3.0</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
@@ -452,7 +453,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<version>3.1.2</version>
|
||||
<version>3.3.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy</id>
|
||||
@@ -472,7 +473,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<version>3.1.2</version>
|
||||
<configuration>
|
||||
<systemProperties>
|
||||
<property>
|
||||
@@ -486,7 +487,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-enforcer-plugin</artifactId>
|
||||
<version>3.0.0-M3</version>
|
||||
<version>3.3.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
@@ -507,7 +508,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-install-plugin</artifactId>
|
||||
<version>3.0.0-M1</version>
|
||||
<version>3.1.1</version>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
@@ -516,7 +517,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<version>3.0.0-M1</version>
|
||||
<version>3.1.1</version>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
|
||||
@@ -11,6 +11,11 @@ directoryV2.client.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789
|
||||
svr2.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth tokens for Signal users
|
||||
svr2.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth identity tokens for Signal users
|
||||
|
||||
svr3.userAuthenticationTokenSharedSecret: cbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR3 to generate auth tokens for Signal users
|
||||
svr3.userIdTokenSharedSecret: dbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR3 to generate auth identity tokens for Signal users
|
||||
|
||||
tus.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG=
|
||||
|
||||
awsAttachments.accessKey: test
|
||||
awsAttachments.accessSecret: test
|
||||
|
||||
@@ -44,6 +49,8 @@ gcpAttachments.rsaSigningKey: |
|
||||
AAAAAAAA
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
apn.teamId: team-id
|
||||
apn.keyId: key-id
|
||||
apn.signingKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
@@ -65,11 +72,13 @@ hCaptcha.apiKey: unset
|
||||
|
||||
storageService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
|
||||
backupService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
|
||||
zkConfig.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
|
||||
zkConfig-libsignal-0.36.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
|
||||
zkConfig-libsignal-0.37.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
|
||||
|
||||
genericZkConfig.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
|
||||
callingZkConfig.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
|
||||
backupsZkConfig.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
|
||||
|
||||
paymentsService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= # base64-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users
|
||||
paymentsService.fixerApiKey: unset
|
||||
@@ -82,3 +91,7 @@ currentReportingKey.secret: AAAAAAAAAAA=
|
||||
currentReportingKey.salt: AAAAAAAAAAA=
|
||||
|
||||
turn.secret: AAAAAAAAAAA=
|
||||
|
||||
linkDevice.secret: AAAAAAAAAAA=
|
||||
|
||||
tlsKeyStore.password: unset
|
||||
|
||||
@@ -22,8 +22,9 @@ metrics:
|
||||
tags:
|
||||
- "env:staging"
|
||||
- "service:chat"
|
||||
transport:
|
||||
apiKey: secret://datadog.apiKey
|
||||
udpTransport:
|
||||
statsdHost: localhost
|
||||
port: 8125
|
||||
excludesAttributes:
|
||||
- m1_rate
|
||||
- m5_rate
|
||||
@@ -39,30 +40,22 @@ metrics:
|
||||
- ^lettuce\..+$
|
||||
reportOnStop: true
|
||||
|
||||
adminEventLoggingConfiguration:
|
||||
credentials: |
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
secondaryCredentials: |
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
projectId: some-project-id
|
||||
logName: some-log-name
|
||||
|
||||
grpcPort: 8080
|
||||
|
||||
tlsKeyStore:
|
||||
password: secret://tlsKeyStore.password
|
||||
|
||||
stripe:
|
||||
apiKey: secret://stripe.apiKey
|
||||
idempotencyKeyGenerator: secret://stripe.idempotencyKeyGenerator
|
||||
boostDescription: >
|
||||
Example
|
||||
supportedCurrencies:
|
||||
- xts
|
||||
# - ...
|
||||
# - Nth supported currency
|
||||
|
||||
supportedCurrenciesByPaymentMethod:
|
||||
CARD:
|
||||
- usd
|
||||
- eur
|
||||
SEPA_DEBIT:
|
||||
- eur
|
||||
|
||||
braintree:
|
||||
merchantId: unset
|
||||
@@ -73,10 +66,9 @@ braintree:
|
||||
merchantAccounts:
|
||||
# ISO 4217 currency code and its corresponding sub-merchant account
|
||||
'xts': unset
|
||||
supportedCurrencies:
|
||||
- xts
|
||||
# - ...
|
||||
# - Nth supported currency
|
||||
supportedCurrenciesByPaymentMethod:
|
||||
PAYPAL:
|
||||
- usd
|
||||
|
||||
dynamoDbClientConfiguration:
|
||||
region: us-west-2 # AWS Region
|
||||
@@ -87,7 +79,12 @@ dynamoDbTables:
|
||||
phoneNumberTableName: Example_Accounts_PhoneNumbers
|
||||
phoneNumberIdentifierTableName: Example_Accounts_PhoneNumberIdentifiers
|
||||
usernamesTableName: Example_Accounts_Usernames
|
||||
scanPageSize: 100
|
||||
backups:
|
||||
tableName: Example_Backups
|
||||
backupMedia:
|
||||
tableName: Example_BackupMedia
|
||||
clientReleases:
|
||||
tableName: Example_ClientReleases
|
||||
deletedAccounts:
|
||||
tableName: Example_DeletedAccounts
|
||||
deletedAccountsLock:
|
||||
@@ -107,10 +104,9 @@ dynamoDbTables:
|
||||
messages:
|
||||
tableName: Example_Messages
|
||||
expiration: P30D # Duration of time until rows expire
|
||||
pendingAccounts:
|
||||
tableName: Example_PendingAccounts
|
||||
pendingDevices:
|
||||
tableName: Example_PendingDevices
|
||||
onetimeDonations:
|
||||
tableName: Example_OnetimeDonations
|
||||
expiration: P90D
|
||||
phoneNumberIdentifiers:
|
||||
tableName: Example_PhoneNumberIdentifiers
|
||||
profiles:
|
||||
@@ -180,6 +176,34 @@ svr2:
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
svr3:
|
||||
uri: svr3.example.com
|
||||
userAuthenticationTokenSharedSecret: secret://svr3.userAuthenticationTokenSharedSecret
|
||||
userIdTokenSharedSecret: secret://svr3.userIdTokenSharedSecret
|
||||
svrCaCertificates:
|
||||
- |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
|
||||
messageCache: # Redis server configuration for message store cache
|
||||
persistDelayMinutes: 1
|
||||
@@ -202,14 +226,15 @@ gcpAttachments: # GCP Storage configuration
|
||||
pathPrefix:
|
||||
rsaSigningKey: secret://gcpAttachments.rsaSigningKey
|
||||
|
||||
accountDatabaseCrawler:
|
||||
chunkSize: 10 # accounts per run
|
||||
tus:
|
||||
uploadUri: https://example.org/upload
|
||||
userAuthenticationTokenSharedSecret: secret://tus.userAuthenticationTokenSharedSecret
|
||||
|
||||
apn: # Apple Push Notifications configuration
|
||||
sandbox: true
|
||||
bundleId: com.example.textsecuregcm
|
||||
keyId: unset
|
||||
teamId: unset
|
||||
keyId: secret://apn.keyId
|
||||
teamId: secret://apn.teamId
|
||||
signingKey: secret://apn.signingKey
|
||||
|
||||
fcm: # FCM configuration
|
||||
@@ -221,8 +246,34 @@ cdn:
|
||||
bucket: cdn # S3 Bucket name
|
||||
region: us-west-2 # AWS region
|
||||
|
||||
datadog:
|
||||
apiKey: secret://datadog.apiKey
|
||||
clientCdn:
|
||||
attachmentUrls:
|
||||
2: https://cdn2.example.com/attachments/
|
||||
caCertificates:
|
||||
- |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
dogstatsd:
|
||||
environment: dev
|
||||
|
||||
unidentifiedDelivery:
|
||||
@@ -233,11 +284,13 @@ unidentifiedDelivery:
|
||||
recaptcha:
|
||||
projectPath: projects/example
|
||||
credentialConfigurationJson: "{ }" # service account configuration for backend authentication
|
||||
secondaryCredentialConfigurationJson: "{ }" # service account configuration for backend authentication
|
||||
|
||||
hCaptcha:
|
||||
apiKey: secret://hCaptcha.apiKey
|
||||
|
||||
shortCode:
|
||||
baseUrl: https://example.com/shortcodes/
|
||||
|
||||
storageService:
|
||||
uri: storage.example.com
|
||||
userAuthenticationTokenSharedSecret: secret://storageService.userAuthenticationTokenSharedSecret
|
||||
@@ -265,39 +318,15 @@ storageService:
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
backupService:
|
||||
uri: backup.example.com
|
||||
userAuthenticationTokenSharedSecret: secret://backupService.userAuthenticationTokenSharedSecret
|
||||
backupCaCertificates:
|
||||
- |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
zkConfig:
|
||||
serverPublic: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
serverSecret: secret://zkConfig.serverSecret
|
||||
|
||||
genericZkConfig:
|
||||
serverSecret: secret://genericZkConfig.serverSecret
|
||||
callingZkConfig:
|
||||
serverSecret: secret://callingZkConfig.serverSecret
|
||||
|
||||
backupsZkConfig:
|
||||
serverSecret: secret://backupsZkConfig.serverSecret
|
||||
|
||||
appConfig:
|
||||
application: example
|
||||
@@ -305,17 +334,6 @@ appConfig:
|
||||
configuration: example
|
||||
|
||||
remoteConfig:
|
||||
authorizedUsers:
|
||||
- # 1st authorized user
|
||||
- # 2nd authorized user
|
||||
- # ...
|
||||
- # Nth authorized user
|
||||
requiredHostedDomain: example.com
|
||||
audiences:
|
||||
- # 1st audience
|
||||
- # 2nd audience
|
||||
- # ...
|
||||
- # Nth audience
|
||||
globalConfig: # keys and values that are given to clients on GET /v1/config
|
||||
EXAMPLE_KEY: VALUE
|
||||
|
||||
@@ -354,6 +372,7 @@ badges:
|
||||
'1': TEST
|
||||
|
||||
subscription: # configuration for Stripe subscriptions
|
||||
badgeExpiration: P30D
|
||||
badgeGracePeriod: P15D
|
||||
levels:
|
||||
500:
|
||||
@@ -367,6 +386,7 @@ subscription: # configuration for Stripe subscriptions
|
||||
BRAINTREE: plan_example # braintree Plan ID
|
||||
|
||||
oneTimeDonations:
|
||||
sepaMaximumEuros: '10000'
|
||||
boost:
|
||||
level: 1
|
||||
expiration: P90D
|
||||
@@ -395,10 +415,6 @@ registrationService:
|
||||
{
|
||||
"example": "example"
|
||||
}
|
||||
secondaryCredentialConfigurationJson: |
|
||||
{
|
||||
"example": "example"
|
||||
}
|
||||
identityTokenAudience: https://registration.example.com
|
||||
registrationCaCertificate: | # Registration service TLS certificate trust root
|
||||
-----BEGIN CERTIFICATE-----
|
||||
@@ -425,3 +441,9 @@ registrationService:
|
||||
|
||||
turn:
|
||||
secret: secret://turn.secret
|
||||
|
||||
commandStopListener:
|
||||
path: /example/path
|
||||
|
||||
linkDevice:
|
||||
secret: secret://linkDevice.secret
|
||||
|
||||
@@ -10,11 +10,17 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>service</artifactId>
|
||||
|
||||
<properties>
|
||||
<firebase-admin.version>9.2.0</firebase-admin.version>
|
||||
<java-uuid-generator.version>4.3.0</java-uuid-generator.version>
|
||||
<sqlite4java.version>1.0.392</sqlite4java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-jaxrs2</artifactId>
|
||||
<version>2.2.8</version>
|
||||
<version>${swagger.version}</version>
|
||||
<exclusions>
|
||||
<!-- org.yaml:snakeyaml is causing a dependency convergence error -->
|
||||
<exclusion>
|
||||
@@ -36,11 +42,6 @@
|
||||
<artifactId>jakarta.ws.rs-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<artifactId>event-logger</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<artifactId>websocket-resources</artifactId>
|
||||
@@ -159,12 +160,6 @@
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-testing</artifactId>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -189,7 +184,7 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||
<artifactId>websocket-api</artifactId>
|
||||
<artifactId>websocket-jetty-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
@@ -208,7 +203,7 @@
|
||||
<dependency>
|
||||
<groupId>com.google.firebase</groupId>
|
||||
<artifactId>firebase-admin</artifactId>
|
||||
<version>9.1.1</version>
|
||||
<version>${firebase-admin.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -255,7 +250,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-registry-datadog</artifactId>
|
||||
<artifactId>micrometer-registry-statsd</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.coursera</groupId>
|
||||
@@ -291,6 +286,14 @@
|
||||
<artifactId>reactor-grpc-stub</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>apache-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>netty-nio-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>sts</artifactId>
|
||||
@@ -323,11 +326,6 @@
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.lettuce</groupId>
|
||||
<artifactId>lettuce-core</artifactId>
|
||||
@@ -371,12 +369,6 @@
|
||||
<groupId>org.glassfish.jersey.test-framework</groupId>
|
||||
<artifactId>jersey-test-framework-core</artifactId>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
|
||||
@@ -387,17 +379,13 @@
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.almworks.sqlite4java</groupId>
|
||||
<artifactId>sqlite4java</artifactId>
|
||||
<version>1.0.392</version>
|
||||
<version>${sqlite4java.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -434,20 +422,20 @@
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.uuid</groupId>
|
||||
<artifactId>java-uuid-generator</artifactId>
|
||||
<version>4.0.1</version>
|
||||
<version>${java-uuid-generator.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>DynamoDBLocal</artifactId>
|
||||
<version>1.21.1</version>
|
||||
<version>1.23.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.ganadist.sqlite4java</groupId>
|
||||
<artifactId>libsqlite4java-osx-aarch64</artifactId>
|
||||
<version>1.0.392</version>
|
||||
<version>${sqlite4java.version}</version>
|
||||
<type>dylib</type>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
@@ -475,7 +463,14 @@
|
||||
<dependency>
|
||||
<groupId>com.apollographql.apollo3</groupId>
|
||||
<artifactId>apollo-api-jvm</artifactId>
|
||||
<version>3.7.1</version>
|
||||
<version>3.8.2</version>
|
||||
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
@@ -488,7 +483,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.2.4</version>
|
||||
<version>3.5.1</version>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>true</createDependencyReducedPom>
|
||||
<filters>
|
||||
@@ -523,7 +518,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<version>3.6.0</version>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>assembly.xml</descriptor>
|
||||
@@ -543,7 +538,7 @@
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>properties-maven-plugin</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.2.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>read-deploy-configuration</id>
|
||||
@@ -558,28 +553,6 @@
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>com.bazaarvoice.maven.plugins</groupId>
|
||||
<artifactId>s3-upload-maven-plugin</artifactId>
|
||||
<version>2.0.1</version>
|
||||
<configuration>
|
||||
<source>${project.build.directory}/${project.build.finalName}-bin.tar.gz</source>
|
||||
<bucketName>${deploy.bucketName}</bucketName>
|
||||
<region>${deploy.bucketRegion}</region>
|
||||
<destination>${project.build.finalName}-bin.tar.gz</destination>
|
||||
<doNotUpload>${deploy.skip.s3.upload}</doNotUpload>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>deploy-to-s3</id>
|
||||
<phase>deploy</phase>
|
||||
<goals>
|
||||
<goal>s3-upload</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>com.google.cloud.tools</groupId>
|
||||
<artifactId>jib-maven-plugin</artifactId>
|
||||
@@ -665,7 +638,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M7</version>
|
||||
<version>3.1.2</version>
|
||||
<configuration>
|
||||
<!-- work around PATCH not being a supported method on HttpUrlConnection -->
|
||||
<argLine>--add-opens=java.base/java.net=ALL-UNNAMED</argLine>
|
||||
@@ -687,7 +660,7 @@
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>check-all-service-config</id>
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
package org.whispersystems.textsecuregcm;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.dropwizard.Configuration;
|
||||
import io.dropwizard.core.Configuration;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
@@ -14,8 +15,7 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.configuration.AccountDatabaseCrawlerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AdminEventLoggingConfiguration;
|
||||
import org.whispersystems.textsecuregcm.attachments.TusConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AppConfigConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration;
|
||||
@@ -23,15 +23,20 @@ import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguratio
|
||||
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.BraintreeConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ClientCdnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ClientReleaseConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.CommandStopListenerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DirectoryV2Configuration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DogstatsdConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DynamoDbTables;
|
||||
import org.whispersystems.textsecuregcm.configuration.FcmConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.GcpAttachmentsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.GenericZkConfig;
|
||||
import org.whispersystems.textsecuregcm.configuration.HCaptchaConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.LinkDeviceSecretConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.MaxDeviceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.MessageByteLimitCardinalityEstimatorConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.MessageCacheConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.OneTimeDonationConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.PaymentsServiceConfiguration;
|
||||
@@ -41,12 +46,14 @@ import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RegistrationServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RemoteConfigConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ReportMessageConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureBackupServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery3Configuration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ShortCodeExpanderConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SpamFilterConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.StripeConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.TlsKeyStoreConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.TurnSecretConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ZkConfig;
|
||||
@@ -59,7 +66,7 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private AdminEventLoggingConfiguration adminEventLoggingConfiguration;
|
||||
private TlsKeyStoreConfiguration tlsKeyStore;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@@ -99,7 +106,12 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private DatadogConfiguration datadog;
|
||||
private ClientCdnConfiguration clientCdn;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private DogstatsdConfiguration dogstatsd = new DogstatsdConfiguration();
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@@ -125,11 +137,10 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private SecureValueRecovery2Configuration svr2;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private AccountDatabaseCrawlerConfiguration accountDatabaseCrawler;
|
||||
private SecureValueRecovery3Configuration svr3;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@@ -199,12 +210,12 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private SecureStorageServiceConfiguration storageService;
|
||||
private ShortCodeExpanderConfiguration shortCode;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private SecureBackupServiceConfiguration backupService;
|
||||
private SecureStorageServiceConfiguration storageService;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@@ -224,7 +235,12 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private GenericZkConfig genericZkConfig;
|
||||
private GenericZkConfig callingZkConfig;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private GenericZkConfig backupsZkConfig;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@@ -270,15 +286,41 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@JsonProperty
|
||||
private TurnSecretConfiguration turn;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private TusConfiguration tus;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private int grpcPort;
|
||||
|
||||
public AdminEventLoggingConfiguration getAdminEventLoggingConfiguration() {
|
||||
return adminEventLoggingConfiguration;
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private ClientReleaseConfiguration clientRelease = new ClientReleaseConfiguration(Duration.ofHours(4));
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private MessageByteLimitCardinalityEstimatorConfiguration messageByteLimitCardinalityEstimator = new MessageByteLimitCardinalityEstimatorConfiguration(Duration.ofDays(1));
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private CommandStopListenerConfiguration commandStopListener;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private LinkDeviceSecretConfiguration linkDevice;
|
||||
|
||||
public TlsKeyStoreConfiguration getTlsKeyStoreConfiguration() {
|
||||
return tlsKeyStore;
|
||||
}
|
||||
|
||||
|
||||
public StripeConfiguration getStripe() {
|
||||
return stripe;
|
||||
}
|
||||
@@ -303,6 +345,10 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return hCaptcha;
|
||||
}
|
||||
|
||||
public ShortCodeExpanderConfiguration getShortCodeRetrieverConfiguration() {
|
||||
return shortCode;
|
||||
}
|
||||
|
||||
public WebSocketConfiguration getWebSocketConfiguration() {
|
||||
return webSocket;
|
||||
}
|
||||
@@ -327,9 +373,13 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return metricsCluster;
|
||||
}
|
||||
|
||||
|
||||
public SecureValueRecovery2Configuration getSvr2Configuration() {
|
||||
return svr2;
|
||||
}
|
||||
public SecureValueRecovery3Configuration getSvr3Configuration() {
|
||||
return svr3;
|
||||
}
|
||||
|
||||
public DirectoryV2Configuration getDirectoryV2Configuration() {
|
||||
return directoryV2;
|
||||
@@ -339,10 +389,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return storageService;
|
||||
}
|
||||
|
||||
public AccountDatabaseCrawlerConfiguration getAccountDatabaseCrawlerConfiguration() {
|
||||
return accountDatabaseCrawler;
|
||||
}
|
||||
|
||||
public MessageCacheConfiguration getMessageCacheConfiguration() {
|
||||
return messageCache;
|
||||
}
|
||||
@@ -375,8 +421,12 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return cdn;
|
||||
}
|
||||
|
||||
public DatadogConfiguration getDatadogConfiguration() {
|
||||
return datadog;
|
||||
public ClientCdnConfiguration getClientCdn() {
|
||||
return clientCdn;
|
||||
}
|
||||
|
||||
public DogstatsdConfiguration getDatadogConfiguration() {
|
||||
return dogstatsd;
|
||||
}
|
||||
|
||||
public UnidentifiedDeliveryConfiguration getDeliveryCertificate() {
|
||||
@@ -398,10 +448,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return results;
|
||||
}
|
||||
|
||||
public SecureBackupServiceConfiguration getSecureBackupServiceConfiguration() {
|
||||
return backupService;
|
||||
}
|
||||
|
||||
public PaymentsServiceConfiguration getPaymentsServiceConfiguration() {
|
||||
return paymentsService;
|
||||
}
|
||||
@@ -414,8 +460,12 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return zkConfig;
|
||||
}
|
||||
|
||||
public GenericZkConfig getGenericZkConfig() {
|
||||
return genericZkConfig;
|
||||
public GenericZkConfig getCallingZkConfig() {
|
||||
return callingZkConfig;
|
||||
}
|
||||
|
||||
public GenericZkConfig getBackupsZkConfig() {
|
||||
return backupsZkConfig;
|
||||
}
|
||||
|
||||
public RemoteConfigConfiguration getRemoteConfigConfiguration() {
|
||||
@@ -454,8 +504,27 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return turn;
|
||||
}
|
||||
|
||||
public TusConfiguration getTus() {
|
||||
return tus;
|
||||
}
|
||||
|
||||
public int getGrpcPort() {
|
||||
return grpcPort;
|
||||
}
|
||||
|
||||
public ClientReleaseConfiguration getClientReleaseConfiguration() {
|
||||
return clientRelease;
|
||||
}
|
||||
|
||||
public MessageByteLimitCardinalityEstimatorConfiguration getMessageByteLimitCardinalityEstimator() {
|
||||
return messageByteLimitCardinalityEstimator;
|
||||
}
|
||||
|
||||
public CommandStopListenerConfiguration getCommandStopListener() {
|
||||
return commandStopListener;
|
||||
}
|
||||
|
||||
public LinkDeviceSecretConfiguration getLinkDeviceSecretConfiguration() {
|
||||
return linkDevice;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,22 +7,19 @@ package org.whispersystems.textsecuregcm;
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
|
||||
import com.google.api.client.http.apache.v2.ApacheHttpTransport;
|
||||
import com.google.api.client.json.gson.GsonFactory;
|
||||
import com.google.auth.oauth2.GoogleCredentials;
|
||||
import com.google.cloud.logging.LoggingOptions;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Lists;
|
||||
import io.dropwizard.Application;
|
||||
import io.dropwizard.auth.AuthFilter;
|
||||
import io.dropwizard.auth.PolymorphicAuthDynamicFeature;
|
||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||
import io.dropwizard.auth.basic.BasicCredentialAuthFilter;
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
import io.dropwizard.setup.Bootstrap;
|
||||
import io.dropwizard.setup.Environment;
|
||||
import io.dropwizard.core.Application;
|
||||
import io.dropwizard.core.server.DefaultServerFactory;
|
||||
import io.dropwizard.core.setup.Bootstrap;
|
||||
import io.dropwizard.core.setup.Environment;
|
||||
import io.dropwizard.jetty.HttpsConnectorFactory;
|
||||
import io.grpc.ServerBuilder;
|
||||
import io.grpc.ServerInterceptors;
|
||||
import io.lettuce.core.metrics.MicrometerCommandLatencyRecorder;
|
||||
@@ -31,9 +28,7 @@ import io.lettuce.core.resource.ClientResources;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.binder.grpc.MetricCollectingServerInterceptor;
|
||||
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.net.http.HttpClient;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
@@ -51,9 +46,8 @@ import javax.servlet.DispatcherType;
|
||||
import javax.servlet.FilterRegistration;
|
||||
import javax.servlet.ServletRegistration;
|
||||
import org.eclipse.jetty.servlets.CrossOriginFilter;
|
||||
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
||||
import org.glassfish.jersey.server.ServerProperties;
|
||||
import org.signal.event.AdminEventLogger;
|
||||
import org.signal.event.GoogleCloudAdminEventLogger;
|
||||
import org.signal.i18n.HeaderControlledResourceBundleLookup;
|
||||
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
|
||||
import org.signal.libsignal.zkgroup.ServerSecretParams;
|
||||
@@ -63,6 +57,8 @@ import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation;
|
||||
import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.attachments.GcsAttachmentGenerator;
|
||||
import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.BaseAccountAuthenticator;
|
||||
@@ -75,20 +71,28 @@ import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager
|
||||
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.WebsocketRefreshApplicationEventListener;
|
||||
import org.whispersystems.textsecuregcm.auth.grpc.BasicCredentialAuthenticationInterceptor;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupAuthManager;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupManager;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupsDb;
|
||||
import org.whispersystems.textsecuregcm.backup.Cdn3BackupCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.backup.Cdn3RemoteStorageManager;
|
||||
import org.whispersystems.textsecuregcm.badges.ConfiguredProfileBadgeConverter;
|
||||
import org.whispersystems.textsecuregcm.badges.ResourceBundleLevelTranslator;
|
||||
import org.whispersystems.textsecuregcm.captcha.CaptchaChecker;
|
||||
import org.whispersystems.textsecuregcm.captcha.HCaptchaClient;
|
||||
import org.whispersystems.textsecuregcm.captcha.RecaptchaClient;
|
||||
import org.whispersystems.textsecuregcm.captcha.RegistrationCaptchaManager;
|
||||
import org.whispersystems.textsecuregcm.captcha.ShortCodeExpander;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretStore;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretsModule;
|
||||
import org.whispersystems.textsecuregcm.controllers.AccountController;
|
||||
import org.whispersystems.textsecuregcm.controllers.AccountControllerV2;
|
||||
import org.whispersystems.textsecuregcm.controllers.ArchiveController;
|
||||
import org.whispersystems.textsecuregcm.controllers.ArtController;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV2;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV3;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV4;
|
||||
import org.whispersystems.textsecuregcm.controllers.CallLinkController;
|
||||
import org.whispersystems.textsecuregcm.controllers.CertificateController;
|
||||
import org.whispersystems.textsecuregcm.controllers.ChallengeController;
|
||||
@@ -103,28 +107,39 @@ import org.whispersystems.textsecuregcm.controllers.ProfileController;
|
||||
import org.whispersystems.textsecuregcm.controllers.ProvisioningController;
|
||||
import org.whispersystems.textsecuregcm.controllers.RegistrationController;
|
||||
import org.whispersystems.textsecuregcm.controllers.RemoteConfigController;
|
||||
import org.whispersystems.textsecuregcm.controllers.SecureBackupController;
|
||||
import org.whispersystems.textsecuregcm.controllers.SecureStorageController;
|
||||
import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery2Controller;
|
||||
import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery3Controller;
|
||||
import org.whispersystems.textsecuregcm.controllers.StickerController;
|
||||
import org.whispersystems.textsecuregcm.controllers.SubscriptionController;
|
||||
import org.whispersystems.textsecuregcm.controllers.VerificationController;
|
||||
import org.whispersystems.textsecuregcm.currency.CoinMarketCapClient;
|
||||
import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager;
|
||||
import org.whispersystems.textsecuregcm.currency.FixerClient;
|
||||
import org.whispersystems.textsecuregcm.grpc.GrpcServerManagedWrapper;
|
||||
import org.whispersystems.textsecuregcm.grpc.UserAgentInterceptor;
|
||||
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||
import org.whispersystems.textsecuregcm.filters.RemoteDeprecationFilter;
|
||||
import org.whispersystems.textsecuregcm.filters.RequestStatisticsFilter;
|
||||
import org.whispersystems.textsecuregcm.filters.TimestampResponseFilter;
|
||||
import org.whispersystems.textsecuregcm.grpc.KeysGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.AcceptLanguageInterceptor;
|
||||
import org.whispersystems.textsecuregcm.grpc.AccountsAnonymousGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.AccountsGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.ErrorMappingInterceptor;
|
||||
import org.whispersystems.textsecuregcm.grpc.ExternalServiceCredentialsAnonymousGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.ExternalServiceCredentialsGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.GrpcServerManagedWrapper;
|
||||
import org.whispersystems.textsecuregcm.grpc.KeysAnonymousGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.KeysGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.PaymentsGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.ProfileAnonymousGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.ProfileGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.UserAgentInterceptor;
|
||||
import org.whispersystems.textsecuregcm.limits.CardinalityEstimator;
|
||||
import org.whispersystems.textsecuregcm.limits.PushChallengeManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.GrpcStatusRuntimeExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.InvalidWebsocketAddressExceptionMapper;
|
||||
@@ -133,6 +148,7 @@ import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptio
|
||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.ServerRejectedExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.SubscriptionProcessorExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsApplicationEventListener;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.metrics.ReportedMessageMetricsListener;
|
||||
@@ -153,10 +169,10 @@ import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
|
||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
|
||||
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
|
||||
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
|
||||
import org.whispersystems.textsecuregcm.spam.FilterSpam;
|
||||
import org.whispersystems.textsecuregcm.spam.PushChallengeConfigProvider;
|
||||
import org.whispersystems.textsecuregcm.spam.RateLimitChallengeListener;
|
||||
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
|
||||
import org.whispersystems.textsecuregcm.spam.ScoreThresholdProvider;
|
||||
@@ -165,13 +181,15 @@ import org.whispersystems.textsecuregcm.storage.AccountLockManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Accounts;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ChangeNumberManager;
|
||||
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
|
||||
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ClientReleases;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.KeysManager;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesCache;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.OneTimeDonationsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
|
||||
import org.whispersystems.textsecuregcm.storage.Profiles;
|
||||
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
|
||||
@@ -183,11 +201,10 @@ import org.whispersystems.textsecuregcm.storage.RemoteConfigs;
|
||||
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
|
||||
import org.whispersystems.textsecuregcm.storage.VerificationCodeStore;
|
||||
import org.whispersystems.textsecuregcm.storage.VerificationSessionManager;
|
||||
import org.whispersystems.textsecuregcm.storage.VerificationSessions;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.BankMandateTranslator;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.StripeManager;
|
||||
import org.whispersystems.textsecuregcm.util.DynamoDbFromConfig;
|
||||
@@ -201,10 +218,12 @@ import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.workers.AssignUsernameCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.CertificateCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.CheckDynamicConfigurationCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.CrawlAccountsCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.DeleteUserCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.MessagePersisterServiceCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.MigrateSignedECPreKeysCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.ProcessPushNotificationFeedbackCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.RemoveExpiredAccountsCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.RemoveExpiredLinkedDevicesCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.ScheduledApnPushNotificationSenderServiceCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.ServerVersionCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.SetRequestLoggingEnabledTask;
|
||||
@@ -223,6 +242,7 @@ import software.amazon.awssdk.auth.credentials.WebIdentityTokenFileCredentialsPr
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
|
||||
import software.amazon.awssdk.services.s3.S3AsyncClient;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
|
||||
public class WhisperServerService extends Application<WhisperServerConfiguration> {
|
||||
@@ -256,10 +276,12 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
bootstrap.addCommand(new SetUserDiscoverabilityCommand());
|
||||
bootstrap.addCommand(new AssignUsernameCommand());
|
||||
bootstrap.addCommand(new UnlinkDeviceCommand());
|
||||
bootstrap.addCommand(new CrawlAccountsCommand());
|
||||
bootstrap.addCommand(new ScheduledApnPushNotificationSenderServiceCommand());
|
||||
bootstrap.addCommand(new MessagePersisterServiceCommand());
|
||||
bootstrap.addCommand(new MigrateSignedECPreKeysCommand());
|
||||
bootstrap.addCommand(new RemoveExpiredAccountsCommand(Clock.systemUTC()));
|
||||
bootstrap.addCommand(new ProcessPushNotificationFeedbackCommand(Clock.systemUTC()));
|
||||
bootstrap.addCommand(new RemoveExpiredLinkedDevicesCommand());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -276,16 +298,26 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
|
||||
MetricsUtil.configureRegistries(config, environment);
|
||||
|
||||
final boolean useSecondaryCredentialsJson = Optional.ofNullable(
|
||||
System.getenv("SIGNAL_USE_SECONDARY_CREDENTIALS_JSON"))
|
||||
final boolean useRemoteAddress = Optional.ofNullable(
|
||||
System.getenv("SIGNAL_USE_REMOTE_ADDRESS"))
|
||||
.isPresent();
|
||||
|
||||
if (config.getServerFactory() instanceof DefaultServerFactory defaultServerFactory) {
|
||||
defaultServerFactory.getApplicationConnectors()
|
||||
.forEach(connectorFactory -> {
|
||||
if (connectorFactory instanceof HttpsConnectorFactory h) {
|
||||
h.setKeyStorePassword(config.getTlsKeyStoreConfiguration().password().value());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup =
|
||||
new HeaderControlledResourceBundleLookup();
|
||||
ConfiguredProfileBadgeConverter profileBadgeConverter = new ConfiguredProfileBadgeConverter(
|
||||
clock, config.getBadges(), headerControlledResourceBundleLookup);
|
||||
ResourceBundleLevelTranslator resourceBundleLevelTranslator = new ResourceBundleLevelTranslator(
|
||||
headerControlledResourceBundleLookup);
|
||||
BankMandateTranslator bankMandateTranslator = new BankMandateTranslator(headerControlledResourceBundleLookup);
|
||||
|
||||
DynamoDbAsyncClient dynamoDbAsyncClient = DynamoDbFromConfig.asyncClient(config.getDynamoDbClientConfiguration(),
|
||||
AWSSDK_CREDENTIALS_PROVIDER);
|
||||
@@ -293,9 +325,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
DynamoDbClient dynamoDbClient = DynamoDbFromConfig.client(config.getDynamoDbClientConfiguration(),
|
||||
AWSSDK_CREDENTIALS_PROVIDER);
|
||||
|
||||
DeletedAccounts deletedAccounts = new DeletedAccounts(dynamoDbClient,
|
||||
config.getDynamoDbTables().getDeletedAccounts().getTableName());
|
||||
|
||||
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager =
|
||||
new DynamicConfigurationManager<>(config.getAppConfig().getApplication(),
|
||||
config.getAppConfig().getEnvironment(),
|
||||
@@ -306,7 +335,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
Metrics.gaugeCollectionSize(name(getClass(), "messageDeletionQueueSize"), Collections.emptyList(),
|
||||
messageDeletionQueue);
|
||||
ExecutorService messageDeletionAsyncExecutor = environment.lifecycle()
|
||||
.executorService(name(getClass(), "messageDeletionAsyncExecutor-%d")).maxThreads(16)
|
||||
.executorService(name(getClass(), "messageDeletionAsyncExecutor-%d"))
|
||||
.minThreads(2)
|
||||
.maxThreads(2)
|
||||
.allowCoreThreadTimeOut(true)
|
||||
.workQueue(messageDeletionQueue).build();
|
||||
|
||||
Accounts accounts = new Accounts(
|
||||
@@ -316,13 +348,15 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
config.getDynamoDbTables().getAccounts().getPhoneNumberTableName(),
|
||||
config.getDynamoDbTables().getAccounts().getPhoneNumberIdentifierTableName(),
|
||||
config.getDynamoDbTables().getAccounts().getUsernamesTableName(),
|
||||
config.getDynamoDbTables().getAccounts().getScanPageSize());
|
||||
config.getDynamoDbTables().getDeletedAccounts().getTableName());
|
||||
ClientReleases clientReleases = new ClientReleases(dynamoDbAsyncClient,
|
||||
config.getDynamoDbTables().getClientReleases().getTableName());
|
||||
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(dynamoDbClient,
|
||||
config.getDynamoDbTables().getPhoneNumberIdentifiers().getTableName());
|
||||
Profiles profiles = new Profiles(dynamoDbClient, dynamoDbAsyncClient,
|
||||
config.getDynamoDbTables().getProfiles().getTableName());
|
||||
KeysManager keys = new KeysManager(
|
||||
dynamoDbAsyncClient,
|
||||
KeysManager keysManager = new KeysManager(
|
||||
dynamoDbAsyncClient,
|
||||
config.getDynamoDbTables().getEcKeys().getTableName(),
|
||||
config.getDynamoDbTables().getKemKeys().getTableName(),
|
||||
config.getDynamoDbTables().getEcSignedPreKeys().getTableName(),
|
||||
@@ -339,10 +373,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
ReportMessageDynamoDb reportMessageDynamoDb = new ReportMessageDynamoDb(dynamoDbClient,
|
||||
config.getDynamoDbTables().getReportMessage().getTableName(),
|
||||
config.getReportMessageConfiguration().getReportTtl());
|
||||
VerificationCodeStore pendingAccounts = new VerificationCodeStore(dynamoDbClient,
|
||||
config.getDynamoDbTables().getPendingAccounts().getTableName());
|
||||
VerificationCodeStore pendingDevices = new VerificationCodeStore(dynamoDbClient,
|
||||
config.getDynamoDbTables().getPendingDevices().getTableName());
|
||||
RegistrationRecoveryPasswords registrationRecoveryPasswords = new RegistrationRecoveryPasswords(
|
||||
config.getDynamoDbTables().getRegistrationRecovery().getTableName(),
|
||||
config.getDynamoDbTables().getRegistrationRecovery().getExpiration(),
|
||||
@@ -381,9 +411,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
.scheduledExecutorService(name(getClass(), "recurringJob-%d")).threads(6).build();
|
||||
ScheduledExecutorService websocketScheduledExecutor = environment.lifecycle()
|
||||
.scheduledExecutorService(name(getClass(), "websocket-%d")).threads(8).build();
|
||||
ExecutorService keyspaceNotificationDispatchExecutor = environment.lifecycle()
|
||||
.executorService(name(getClass(), "keyspaceNotification-%d")).maxThreads(16)
|
||||
.workQueue(keyspaceNotificationDispatchQueue).build();
|
||||
ExecutorService keyspaceNotificationDispatchExecutor = ExecutorServiceMetrics.monitor(Metrics.globalRegistry,
|
||||
environment.lifecycle()
|
||||
.executorService(name(getClass(), "keyspaceNotification-%d"))
|
||||
.maxThreads(16)
|
||||
.workQueue(keyspaceNotificationDispatchQueue)
|
||||
.build(),
|
||||
MetricsUtil.name(getClass(), "keyspaceNotificationExecutor"),
|
||||
MetricsUtil.PREFIX);
|
||||
ExecutorService apnSenderExecutor = environment.lifecycle().executorService(name(getClass(), "apnSender-%d"))
|
||||
.maxThreads(1).minThreads(1).build();
|
||||
ExecutorService fcmSenderExecutor = environment.lifecycle().executorService(name(getClass(), "fcmSender-%d"))
|
||||
@@ -396,6 +431,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
.scheduledExecutorService(name(getClass(), "secureValueRecoveryServiceRetry-%d")).threads(1).build();
|
||||
ScheduledExecutorService storageServiceRetryExecutor = environment.lifecycle()
|
||||
.scheduledExecutorService(name(getClass(), "storageServiceRetry-%d")).threads(1).build();
|
||||
ScheduledExecutorService hcaptchaRetryExecutor = environment.lifecycle()
|
||||
.scheduledExecutorService(name(getClass(), "hCaptchaRetry-%d")).threads(1).build();
|
||||
ScheduledExecutorService remoteStorageExecutor = environment.lifecycle()
|
||||
.scheduledExecutorService(name(getClass(), "remoteStorageRetry-%d")).threads(1).build();
|
||||
|
||||
Scheduler messageDeliveryScheduler = Schedulers.fromExecutorService(
|
||||
ExecutorServiceMetrics.monitor(Metrics.globalRegistry,
|
||||
@@ -429,26 +468,25 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
.maxThreads(2)
|
||||
.minThreads(2)
|
||||
.build();
|
||||
ExecutorService accountLockExecutor = environment.lifecycle()
|
||||
.executorService(name(getClass(), "accountLock-%d"))
|
||||
.minThreads(8)
|
||||
.maxThreads(8)
|
||||
.build();
|
||||
ExecutorService clientPresenceExecutor = environment.lifecycle()
|
||||
.executorService(name(getClass(), "clientPresence-%d"))
|
||||
.minThreads(8)
|
||||
.maxThreads(8)
|
||||
.build();
|
||||
ScheduledExecutorService subscriptionProcessorRetryExecutor = environment.lifecycle()
|
||||
.scheduledExecutorService(name(getClass(), "subscriptionProcessorRetry-%d")).threads(1).build();
|
||||
|
||||
final AdminEventLogger adminEventLogger = new GoogleCloudAdminEventLogger(
|
||||
LoggingOptions.newBuilder().setProjectId(config.getAdminEventLoggingConfiguration().projectId())
|
||||
.setCredentials(GoogleCredentials.fromStream(new ByteArrayInputStream(
|
||||
useSecondaryCredentialsJson
|
||||
? config.getAdminEventLoggingConfiguration().secondaryCredentials().getBytes(StandardCharsets.UTF_8)
|
||||
: config.getAdminEventLoggingConfiguration().credentials().getBytes(StandardCharsets.UTF_8))))
|
||||
.build().getService(),
|
||||
config.getAdminEventLoggingConfiguration().projectId(),
|
||||
config.getAdminEventLoggingConfiguration().logName());
|
||||
|
||||
StripeManager stripeManager = new StripeManager(config.getStripe().apiKey().value(), subscriptionProcessorExecutor,
|
||||
config.getStripe().idempotencyKeyGenerator().value(), config.getStripe().boostDescription(), config.getStripe()
|
||||
.supportedCurrencies());
|
||||
config.getStripe().idempotencyKeyGenerator().value(), config.getStripe().boostDescription(), config.getStripe().supportedCurrenciesByPaymentMethod());
|
||||
BraintreeManager braintreeManager = new BraintreeManager(config.getBraintree().merchantId(),
|
||||
config.getBraintree().publicKey(), config.getBraintree().privateKey().value(),
|
||||
config.getBraintree().environment(),
|
||||
config.getBraintree().supportedCurrencies(), config.getBraintree().merchantAccounts(),
|
||||
config.getBraintree().supportedCurrenciesByPaymentMethod(), config.getBraintree().merchantAccounts(),
|
||||
config.getBraintree().graphqlUrl(), config.getBraintree().circuitBreaker(), subscriptionProcessorExecutor,
|
||||
subscriptionProcessorRetryExecutor);
|
||||
|
||||
@@ -456,14 +494,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
config.getDirectoryV2Configuration().getDirectoryV2ClientConfiguration());
|
||||
ExternalServiceCredentialsGenerator storageCredentialsGenerator = SecureStorageController.credentialsGenerator(
|
||||
config.getSecureStorageServiceConfiguration());
|
||||
ExternalServiceCredentialsGenerator backupCredentialsGenerator = SecureBackupController.credentialsGenerator(
|
||||
config.getSecureBackupServiceConfiguration());
|
||||
ExternalServiceCredentialsGenerator paymentsCredentialsGenerator = PaymentsController.credentialsGenerator(
|
||||
config.getPaymentsServiceConfiguration());
|
||||
ExternalServiceCredentialsGenerator artCredentialsGenerator = ArtController.credentialsGenerator(
|
||||
config.getArtServiceConfiguration());
|
||||
ExternalServiceCredentialsGenerator svr2CredentialsGenerator = SecureValueRecovery2Controller.credentialsGenerator(
|
||||
config.getSvr2Configuration());
|
||||
config.getSvr2Configuration());
|
||||
ExternalServiceCredentialsGenerator svr3CredentialsGenerator = SecureValueRecovery3Controller.credentialsGenerator(
|
||||
config.getSvr3Configuration());
|
||||
|
||||
dynamicConfigurationManager.start();
|
||||
|
||||
@@ -476,27 +514,24 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
RegistrationServiceClient registrationServiceClient = new RegistrationServiceClient(
|
||||
config.getRegistrationServiceConfiguration().host(),
|
||||
config.getRegistrationServiceConfiguration().port(),
|
||||
useSecondaryCredentialsJson
|
||||
? config.getRegistrationServiceConfiguration().secondaryCredentialConfigurationJson()
|
||||
: config.getRegistrationServiceConfiguration().credentialConfigurationJson(),
|
||||
config.getRegistrationServiceConfiguration().credentialConfigurationJson(),
|
||||
config.getRegistrationServiceConfiguration().identityTokenAudience(),
|
||||
config.getRegistrationServiceConfiguration().registrationCaCertificate(),
|
||||
registrationCallbackExecutor);
|
||||
SecureBackupClient secureBackupClient = new SecureBackupClient(backupCredentialsGenerator,
|
||||
secureValueRecoveryServiceExecutor, secureValueRecoveryServiceRetryExecutor,
|
||||
config.getSecureBackupServiceConfiguration());
|
||||
SecureValueRecovery2Client secureValueRecovery2Client = new SecureValueRecovery2Client(svr2CredentialsGenerator,
|
||||
secureValueRecoveryServiceExecutor, secureValueRecoveryServiceRetryExecutor, config.getSvr2Configuration());
|
||||
SecureStorageClient secureStorageClient = new SecureStorageClient(storageCredentialsGenerator,
|
||||
storageServiceExecutor, storageServiceRetryExecutor, config.getSecureStorageServiceConfiguration());
|
||||
ClientPresenceManager clientPresenceManager = new ClientPresenceManager(clientPresenceCluster, recurringJobExecutor,
|
||||
keyspaceNotificationDispatchExecutor);
|
||||
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
|
||||
StoredVerificationCodeManager pendingDevicesManager = new StoredVerificationCodeManager(pendingDevices);
|
||||
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheCluster);
|
||||
MessagesCache messagesCache = new MessagesCache(messagesCluster, messagesCluster,
|
||||
keyspaceNotificationDispatchExecutor, messageDeliveryScheduler, messageDeletionAsyncExecutor, clock);
|
||||
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster, dynamicConfigurationManager);
|
||||
ClientReleaseManager clientReleaseManager = new ClientReleaseManager(clientReleases,
|
||||
recurringJobExecutor,
|
||||
config.getClientReleaseConfiguration().refreshInterval(),
|
||||
Clock.systemUTC());
|
||||
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster, clientReleaseManager);
|
||||
ReportMessageManager reportMessageManager = new ReportMessageManager(reportMessageDynamoDb, rateLimitersCluster,
|
||||
config.getReportMessageConfiguration().getCounterTtl());
|
||||
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, reportMessageManager,
|
||||
@@ -504,10 +539,11 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
AccountLockManager accountLockManager = new AccountLockManager(dynamoDbClient,
|
||||
config.getDynamoDbTables().getDeletedAccountsLock().getTableName());
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||
accountLockManager, deletedAccounts, keys, messagesManager, profilesManager,
|
||||
pendingAccountsManager, secureStorageClient, secureBackupClient, secureValueRecovery2Client,
|
||||
accountLockManager, keysManager, messagesManager, profilesManager,
|
||||
secureStorageClient, secureValueRecovery2Client,
|
||||
clientPresenceManager,
|
||||
experimentEnrollmentManager, registrationRecoveryPasswordsManager, clock);
|
||||
experimentEnrollmentManager, registrationRecoveryPasswordsManager, accountLockExecutor, clientPresenceExecutor,
|
||||
clock);
|
||||
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
|
||||
APNSender apnSender = new APNSender(apnSenderExecutor, config.getApnConfiguration());
|
||||
FcmSender fcmSender = new FcmSender(fcmSenderExecutor, config.getFcmConfiguration().credentials().value());
|
||||
@@ -525,6 +561,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
config.getDynamoDbTables().getIssuedReceipts().getExpiration(),
|
||||
dynamoDbAsyncClient,
|
||||
config.getDynamoDbTables().getIssuedReceipts().getGenerator());
|
||||
OneTimeDonationsManager oneTimeDonationsManager = new OneTimeDonationsManager(
|
||||
config.getDynamoDbTables().getOnetimeDonations().getTableName(), config.getDynamoDbTables().getOnetimeDonations().getExpiration(), dynamoDbAsyncClient);
|
||||
RedeemedReceiptsManager redeemedReceiptsManager = new RedeemedReceiptsManager(clock,
|
||||
config.getDynamoDbTables().getRedeemedReceipts().getTableName(),
|
||||
dynamoDbAsyncClient,
|
||||
@@ -533,7 +571,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
config.getDynamoDbTables().getSubscriptions().getTableName(), dynamoDbAsyncClient);
|
||||
|
||||
final RegistrationLockVerificationManager registrationLockVerificationManager = new RegistrationLockVerificationManager(
|
||||
accountsManager, clientPresenceManager, backupCredentialsGenerator, svr2CredentialsGenerator, registrationRecoveryPasswordsManager, pushNotificationManager, rateLimiters);
|
||||
accountsManager, clientPresenceManager, svr2CredentialsGenerator, registrationRecoveryPasswordsManager, pushNotificationManager, rateLimiters);
|
||||
final PhoneVerificationTokenManager phoneVerificationTokenManager = new PhoneVerificationTokenManager(
|
||||
registrationServiceClient, registrationRecoveryPasswordsManager);
|
||||
|
||||
@@ -552,17 +590,25 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
final TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager,
|
||||
config.getTurnSecretConfiguration().secret().value());
|
||||
|
||||
final CardinalityEstimator messageByteLimitCardinalityEstimator = new CardinalityEstimator(
|
||||
rateLimitersCluster,
|
||||
"message_byte_limit",
|
||||
config.getMessageByteLimitCardinalityEstimator().period());
|
||||
|
||||
RecaptchaClient recaptchaClient = new RecaptchaClient(
|
||||
config.getRecaptchaConfiguration().projectPath(),
|
||||
useSecondaryCredentialsJson
|
||||
? config.getRecaptchaConfiguration().secondaryCredentialConfigurationJson()
|
||||
: config.getRecaptchaConfiguration().credentialConfigurationJson(),
|
||||
config.getRecaptchaConfiguration().credentialConfigurationJson(),
|
||||
dynamicConfigurationManager);
|
||||
HttpClient hcaptchaHttpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2)
|
||||
HCaptchaClient hCaptchaClient = new HCaptchaClient(
|
||||
config.getHCaptchaConfiguration().getApiKey().value(),
|
||||
hcaptchaRetryExecutor,
|
||||
config.getHCaptchaConfiguration().getCircuitBreaker(),
|
||||
config.getHCaptchaConfiguration().getRetry(),
|
||||
dynamicConfigurationManager);
|
||||
HttpClient shortCodeRetrieverHttpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2)
|
||||
.connectTimeout(Duration.ofSeconds(10)).build();
|
||||
HCaptchaClient hCaptchaClient = new HCaptchaClient(config.getHCaptchaConfiguration().apiKey().value(), hcaptchaHttpClient,
|
||||
dynamicConfigurationManager);
|
||||
CaptchaChecker captchaChecker = new CaptchaChecker(List.of(recaptchaClient, hCaptchaClient));
|
||||
ShortCodeExpander shortCodeRetriever = new ShortCodeExpander(shortCodeRetrieverHttpClient, config.getShortCodeRetrieverConfiguration().baseUrl());
|
||||
CaptchaChecker captchaChecker = new CaptchaChecker(shortCodeRetriever, List.of(recaptchaClient, hCaptchaClient));
|
||||
|
||||
PushChallengeManager pushChallengeManager = new PushChallengeManager(pushNotificationManager,
|
||||
pushChallengeDynamoDb);
|
||||
@@ -575,7 +621,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
FixerClient fixerClient = new FixerClient(currencyClient, config.getPaymentsServiceConfiguration().fixerApiKey().value());
|
||||
CoinMarketCapClient coinMarketCapClient = new CoinMarketCapClient(currencyClient, config.getPaymentsServiceConfiguration().coinMarketCapApiKey().value(), config.getPaymentsServiceConfiguration().coinMarketCapCurrencyIds());
|
||||
CurrencyConversionManager currencyManager = new CurrencyConversionManager(fixerClient, coinMarketCapClient,
|
||||
cacheCluster, config.getPaymentsServiceConfiguration().paymentCurrencies(), Clock.systemUTC());
|
||||
cacheCluster, config.getPaymentsServiceConfiguration().paymentCurrencies(), recurringJobExecutor, Clock.systemUTC());
|
||||
|
||||
environment.lifecycle().manage(apnSender);
|
||||
environment.lifecycle().manage(apnPushNotificationScheduler);
|
||||
@@ -584,6 +630,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
environment.lifecycle().manage(clientPresenceManager);
|
||||
environment.lifecycle().manage(currencyManager);
|
||||
environment.lifecycle().manage(registrationServiceClient);
|
||||
environment.lifecycle().manage(clientReleaseManager);
|
||||
|
||||
final RegistrationCaptchaManager registrationCaptchaManager = new RegistrationCaptchaManager(captchaChecker,
|
||||
rateLimiters, config.getTestDevices(), dynamicConfigurationManager);
|
||||
@@ -596,17 +643,49 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
.credentialsProvider(cdnCredentialsProvider)
|
||||
.region(Region.of(config.getCdnConfiguration().region()))
|
||||
.build();
|
||||
S3AsyncClient asyncCdnS3Client = S3AsyncClient.builder()
|
||||
.credentialsProvider(cdnCredentialsProvider)
|
||||
.region(Region.of(config.getCdnConfiguration().region()))
|
||||
.build();
|
||||
|
||||
final GcsAttachmentGenerator gcsAttachmentGenerator = new GcsAttachmentGenerator(
|
||||
config.getGcpAttachmentsConfiguration().domain(),
|
||||
config.getGcpAttachmentsConfiguration().email(),
|
||||
config.getGcpAttachmentsConfiguration().maxSizeInBytes(),
|
||||
config.getGcpAttachmentsConfiguration().pathPrefix(),
|
||||
config.getGcpAttachmentsConfiguration().rsaSigningKey().value());
|
||||
|
||||
PostPolicyGenerator profileCdnPolicyGenerator = new PostPolicyGenerator(config.getCdnConfiguration().region(),
|
||||
config.getCdnConfiguration().bucket(), config.getCdnConfiguration().accessKey().value());
|
||||
PolicySigner profileCdnPolicySigner = new PolicySigner(config.getCdnConfiguration().accessSecret().value(),
|
||||
config.getCdnConfiguration().region());
|
||||
|
||||
ServerSecretParams zkSecretParams = new ServerSecretParams(config.getZkConfig().serverSecret().value());
|
||||
GenericServerSecretParams genericZkSecretParams = new GenericServerSecretParams(config.getGenericZkConfig().serverSecret().value());
|
||||
GenericServerSecretParams callingGenericZkSecretParams = new GenericServerSecretParams(config.getCallingZkConfig().serverSecret().value());
|
||||
GenericServerSecretParams backupsGenericZkSecretParams = new GenericServerSecretParams(config.getBackupsZkConfig().serverSecret().value());
|
||||
ServerZkProfileOperations zkProfileOperations = new ServerZkProfileOperations(zkSecretParams);
|
||||
ServerZkAuthOperations zkAuthOperations = new ServerZkAuthOperations(zkSecretParams);
|
||||
ServerZkReceiptOperations zkReceiptOperations = new ServerZkReceiptOperations(zkSecretParams);
|
||||
|
||||
Cdn3BackupCredentialGenerator cdn3BackupCredentialGenerator = new Cdn3BackupCredentialGenerator(config.getTus());
|
||||
BackupAuthManager backupAuthManager = new BackupAuthManager(dynamicConfigurationManager, rateLimiters, accountsManager, backupsGenericZkSecretParams, clock);
|
||||
BackupsDb backupsDb = new BackupsDb(
|
||||
dynamoDbAsyncClient,
|
||||
config.getDynamoDbTables().getBackups().getTableName(),
|
||||
config.getDynamoDbTables().getBackupMedia().getTableName(),
|
||||
clock);
|
||||
BackupManager backupManager = new BackupManager(
|
||||
backupsDb,
|
||||
backupsGenericZkSecretParams,
|
||||
cdn3BackupCredentialGenerator,
|
||||
new Cdn3RemoteStorageManager(
|
||||
remoteStorageExecutor,
|
||||
config.getClientCdn().getCircuitBreaker(),
|
||||
config.getClientCdn().getRetry(),
|
||||
config.getClientCdn().getCaCertificates()),
|
||||
config.getClientCdn().getAttachmentUrls(),
|
||||
clock);
|
||||
|
||||
AuthFilter<BasicCredentials, AuthenticatedAccount> accountAuthFilter = new BasicCredentialAuthFilter.Builder<AuthenticatedAccount>().setAuthenticator(
|
||||
accountAuthenticator).buildAuthFilter();
|
||||
AuthFilter<BasicCredentials, DisabledPermittedAuthenticatedAccount> disabledPermittedAccountAuthFilter = new BasicCredentialAuthFilter.Builder<DisabledPermittedAuthenticatedAccount>().setAuthenticator(
|
||||
@@ -616,10 +695,16 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new BasicCredentialAuthenticationInterceptor(new BaseAccountAuthenticator(accountsManager));
|
||||
|
||||
final ServerBuilder<?> grpcServer = ServerBuilder.forPort(config.getGrpcPort())
|
||||
// TODO: specialize metrics with user-agent platform
|
||||
.intercept(new MetricCollectingServerInterceptor(Metrics.globalRegistry))
|
||||
.addService(ServerInterceptors.intercept(new KeysGrpcService(accountsManager, keys, rateLimiters), basicCredentialAuthenticationInterceptor))
|
||||
.addService(new KeysAnonymousGrpcService(accountsManager, keys));
|
||||
.addService(ServerInterceptors.intercept(new AccountsGrpcService(accountsManager, rateLimiters, usernameHashZkProofVerifier, registrationRecoveryPasswordsManager), basicCredentialAuthenticationInterceptor))
|
||||
.addService(new AccountsAnonymousGrpcService(accountsManager, rateLimiters))
|
||||
.addService(ExternalServiceCredentialsGrpcService.createForAllExternalServices(config, rateLimiters))
|
||||
.addService(ExternalServiceCredentialsAnonymousGrpcService.create(accountsManager, config))
|
||||
.addService(ServerInterceptors.intercept(new KeysGrpcService(accountsManager, keysManager, rateLimiters), basicCredentialAuthenticationInterceptor))
|
||||
.addService(new KeysAnonymousGrpcService(accountsManager, keysManager))
|
||||
.addService(new PaymentsGrpcService(currencyManager))
|
||||
.addService(ServerInterceptors.intercept(new ProfileGrpcService(clock, accountsManager, profilesManager, dynamicConfigurationManager,
|
||||
config.getBadges(), asyncCdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, profileBadgeConverter, rateLimiters, zkProfileOperations, config.getCdnConfiguration().bucket()), basicCredentialAuthenticationInterceptor))
|
||||
.addService(new ProfileAnonymousGrpcService(accountsManager, profilesManager, profileBadgeConverter, zkProfileOperations));
|
||||
|
||||
RemoteDeprecationFilter remoteDeprecationFilter = new RemoteDeprecationFilter(dynamicConfigurationManager);
|
||||
environment.servlets()
|
||||
@@ -629,14 +714,19 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
// Note: interceptors run in the reverse order they are added; the remote deprecation filter
|
||||
// depends on the user-agent context so it has to come first here!
|
||||
// http://grpc.github.io/grpc-java/javadoc/io/grpc/ServerBuilder.html#intercept-io.grpc.ServerInterceptor-
|
||||
grpcServer.intercept(remoteDeprecationFilter);
|
||||
grpcServer.intercept(new UserAgentInterceptor());
|
||||
grpcServer
|
||||
// TODO: specialize metrics with user-agent platform
|
||||
.intercept(new MetricCollectingServerInterceptor(Metrics.globalRegistry))
|
||||
.intercept(new ErrorMappingInterceptor())
|
||||
.intercept(new AcceptLanguageInterceptor())
|
||||
.intercept(remoteDeprecationFilter)
|
||||
.intercept(new UserAgentInterceptor());
|
||||
|
||||
environment.lifecycle().manage(new GrpcServerManagedWrapper(grpcServer.build()));
|
||||
|
||||
environment.jersey().register(new RequestStatisticsFilter(TrafficSource.HTTP));
|
||||
environment.jersey().register(MultiRecipientMessageProvider.class);
|
||||
environment.jersey().register(new MetricsApplicationEventListener(TrafficSource.HTTP));
|
||||
environment.jersey().register(new MetricsApplicationEventListener(TrafficSource.HTTP, clientReleaseManager));
|
||||
environment.jersey()
|
||||
.register(new PolymorphicAuthDynamicFeature<>(ImmutableMap.of(AuthenticatedAccount.class, accountAuthFilter,
|
||||
DisabledPermittedAuthenticatedAccount.class, disabledPermittedAccountAuthFilter)));
|
||||
@@ -647,16 +737,16 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
|
||||
///
|
||||
WebSocketEnvironment<AuthenticatedAccount> webSocketEnvironment = new WebSocketEnvironment<>(environment,
|
||||
config.getWebSocketConfiguration(), 90000);
|
||||
config.getWebSocketConfiguration(), Duration.ofMillis(90000));
|
||||
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(accountAuthenticator));
|
||||
webSocketEnvironment.setConnectListener(
|
||||
new AuthenticatedConnectListener(receiptSender, messagesManager, pushNotificationManager,
|
||||
clientPresenceManager, websocketScheduledExecutor, messageDeliveryScheduler, dynamicConfigurationManager));
|
||||
clientPresenceManager, websocketScheduledExecutor, messageDeliveryScheduler, clientReleaseManager));
|
||||
webSocketEnvironment.jersey()
|
||||
.register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
|
||||
webSocketEnvironment.jersey().register(new RequestStatisticsFilter(TrafficSource.WEBSOCKET));
|
||||
webSocketEnvironment.jersey().register(MultiRecipientMessageProvider.class);
|
||||
webSocketEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET));
|
||||
webSocketEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET, clientReleaseManager));
|
||||
webSocketEnvironment.jersey().register(new KeepAliveController(clientPresenceManager));
|
||||
|
||||
// these should be common, but use @Auth DisabledPermittedAccount, which isn’t supported yet on websocket
|
||||
@@ -665,7 +755,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
turnTokenGenerator,
|
||||
registrationRecoveryPasswordsManager, usernameHashZkProofVerifier));
|
||||
|
||||
environment.jersey().register(new KeysController(rateLimiters, keys, accountsManager));
|
||||
environment.jersey().register(new KeysController(rateLimiters, keysManager, accountsManager));
|
||||
|
||||
boolean registeredSpamFilter = false;
|
||||
ReportSpamTokenProvider reportSpamTokenProvider = null;
|
||||
@@ -717,45 +807,49 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new AccountControllerV2(accountsManager, changeNumberManager, phoneVerificationTokenManager,
|
||||
registrationLockVerificationManager, rateLimiters),
|
||||
new ArtController(rateLimiters, artCredentialsGenerator),
|
||||
new AttachmentControllerV2(rateLimiters, config.getAwsAttachmentsConfiguration().accessKey().value(), config.getAwsAttachmentsConfiguration().accessSecret().value(), config.getAwsAttachmentsConfiguration().region(), config.getAwsAttachmentsConfiguration().bucket()),
|
||||
new AttachmentControllerV3(rateLimiters, config.getGcpAttachmentsConfiguration().domain(), config.getGcpAttachmentsConfiguration().email(), config.getGcpAttachmentsConfiguration().maxSizeInBytes(), config.getGcpAttachmentsConfiguration().pathPrefix(), config.getGcpAttachmentsConfiguration().rsaSigningKey().value()),
|
||||
new CallLinkController(rateLimiters, genericZkSecretParams),
|
||||
new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().certificate().value(), config.getDeliveryCertificate().ecPrivateKey(), config.getDeliveryCertificate().expiresDays()), zkAuthOperations, genericZkSecretParams, clock),
|
||||
new ChallengeController(rateLimitChallengeManager),
|
||||
new DeviceController(pendingDevicesManager, accountsManager, messagesManager, keys, rateLimiters, config.getMaxDevices()),
|
||||
new AttachmentControllerV2(rateLimiters, config.getAwsAttachmentsConfiguration().accessKey().value(),
|
||||
config.getAwsAttachmentsConfiguration().accessSecret().value(),
|
||||
config.getAwsAttachmentsConfiguration().region(), config.getAwsAttachmentsConfiguration().bucket()),
|
||||
new AttachmentControllerV3(rateLimiters, gcsAttachmentGenerator),
|
||||
new AttachmentControllerV4(rateLimiters, gcsAttachmentGenerator, new TusAttachmentGenerator(config.getTus()),
|
||||
experimentEnrollmentManager),
|
||||
new ArchiveController(backupAuthManager, backupManager),
|
||||
new CallLinkController(rateLimiters, callingGenericZkSecretParams),
|
||||
new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().certificate().value(),
|
||||
config.getDeliveryCertificate().ecPrivateKey(), config.getDeliveryCertificate().expiresDays()),
|
||||
zkAuthOperations, callingGenericZkSecretParams, clock),
|
||||
new ChallengeController(rateLimitChallengeManager, useRemoteAddress),
|
||||
new DeviceController(config.getLinkDeviceSecretConfiguration().secret().value(), accountsManager,
|
||||
rateLimiters, rateLimitersCluster, config.getMaxDevices(), clock),
|
||||
new DirectoryV2Controller(directoryV2CredentialsGenerator),
|
||||
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
|
||||
ReceiptCredentialPresentation::new),
|
||||
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, deletedAccounts,
|
||||
messagesManager, pushNotificationManager, reportMessageManager, multiRecipientMessageExecutor,
|
||||
messageDeliveryScheduler, reportSpamTokenProvider, dynamicConfigurationManager),
|
||||
new MessageController(rateLimiters, messageByteLimitCardinalityEstimator, messageSender, receiptSender,
|
||||
accountsManager, messagesManager, pushNotificationManager, reportMessageManager,
|
||||
multiRecipientMessageExecutor, messageDeliveryScheduler, reportSpamTokenProvider, clientReleaseManager,
|
||||
dynamicConfigurationManager),
|
||||
new PaymentsController(currencyManager, paymentsCredentialsGenerator),
|
||||
new ProfileController(clock, rateLimiters, accountsManager, profilesManager, dynamicConfigurationManager,
|
||||
profileBadgeConverter, config.getBadges(), cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner,
|
||||
config.getCdnConfiguration().bucket(), zkProfileOperations, batchIdentityCheckExecutor),
|
||||
new ProvisioningController(rateLimiters, provisioningManager),
|
||||
new RegistrationController(accountsManager, phoneVerificationTokenManager, registrationLockVerificationManager,
|
||||
keys, rateLimiters),
|
||||
new RemoteConfigController(remoteConfigsManager, adminEventLogger,
|
||||
config.getRemoteConfigConfiguration().authorizedUsers(),
|
||||
config.getRemoteConfigConfiguration().requiredHostedDomain(),
|
||||
config.getRemoteConfigConfiguration().audiences(),
|
||||
new GoogleIdTokenVerifier.Builder(new ApacheHttpTransport(), new GsonFactory()),
|
||||
config.getRemoteConfigConfiguration().globalConfig()),
|
||||
new SecureBackupController(backupCredentialsGenerator, accountsManager),
|
||||
rateLimiters),
|
||||
new RemoteConfigController(remoteConfigsManager, config.getRemoteConfigConfiguration().globalConfig(), clock),
|
||||
new SecureStorageController(storageCredentialsGenerator),
|
||||
new SecureValueRecovery2Controller(svr2CredentialsGenerator, accountsManager),
|
||||
new SecureValueRecovery3Controller(svr3CredentialsGenerator, accountsManager),
|
||||
new StickerController(rateLimiters, config.getCdnConfiguration().accessKey().value(),
|
||||
config.getCdnConfiguration().accessSecret().value(), config.getCdnConfiguration().region(),
|
||||
config.getCdnConfiguration().bucket()),
|
||||
new VerificationController(registrationServiceClient, new VerificationSessionManager(verificationSessions),
|
||||
pushNotificationManager, registrationCaptchaManager, registrationRecoveryPasswordsManager, rateLimiters,
|
||||
accountsManager, clock)
|
||||
accountsManager, useRemoteAddress, dynamicConfigurationManager, clock)
|
||||
);
|
||||
if (config.getSubscription() != null && config.getOneTimeDonations() != null) {
|
||||
commonControllers.add(new SubscriptionController(clock, config.getSubscription(), config.getOneTimeDonations(),
|
||||
subscriptionManager, stripeManager, braintreeManager, zkReceiptOperations, issuedReceiptsManager, profileBadgeConverter,
|
||||
resourceBundleLevelTranslator));
|
||||
subscriptionManager, stripeManager, braintreeManager, zkReceiptOperations, issuedReceiptsManager, oneTimeDonationsManager,
|
||||
profileBadgeConverter, resourceBundleLevelTranslator, bankMandateTranslator));
|
||||
}
|
||||
|
||||
for (Object controller : commonControllers) {
|
||||
@@ -764,10 +858,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
}
|
||||
|
||||
WebSocketEnvironment<AuthenticatedAccount> provisioningEnvironment = new WebSocketEnvironment<>(environment,
|
||||
webSocketEnvironment.getRequestLog(), 60000);
|
||||
webSocketEnvironment.getRequestLog(), Duration.ofMillis(60000));
|
||||
provisioningEnvironment.jersey().register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
|
||||
provisioningEnvironment.setConnectListener(new ProvisioningConnectListener(provisioningManager));
|
||||
provisioningEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET));
|
||||
provisioningEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET, clientReleaseManager));
|
||||
provisioningEnvironment.jersey().register(new KeepAliveController(clientPresenceManager));
|
||||
|
||||
registerCorsFilter(environment);
|
||||
@@ -778,6 +872,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
webSocketEnvironment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
|
||||
provisioningEnvironment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
|
||||
|
||||
JettyWebSocketServletContainerInitializer.configure(environment.getApplicationContext(), null);
|
||||
|
||||
WebSocketResourceProviderFactory<AuthenticatedAccount> webSocketServlet = new WebSocketResourceProviderFactory<>(
|
||||
webSocketEnvironment, AuthenticatedAccount.class, config.getWebSocketConfiguration());
|
||||
WebSocketResourceProviderFactory<AuthenticatedAccount> provisioningServlet = new WebSocketResourceProviderFactory<>(
|
||||
@@ -803,9 +899,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
private void registerProviders(Environment environment,
|
||||
WebSocketEnvironment<AuthenticatedAccount> webSocketEnvironment,
|
||||
WebSocketEnvironment<AuthenticatedAccount> provisioningEnvironment) {
|
||||
environment.jersey().register(ScoreThresholdProvider.ScoreThresholdFeature.class);
|
||||
webSocketEnvironment.jersey().register(ScoreThresholdProvider.ScoreThresholdFeature.class);
|
||||
provisioningEnvironment.jersey().register(ScoreThresholdProvider.ScoreThresholdFeature.class);
|
||||
List.of(
|
||||
ScoreThresholdProvider.ScoreThresholdFeature.class,
|
||||
PushChallengeConfigProvider.PushChallengeConfigFeature.class)
|
||||
.forEach(feature -> {
|
||||
environment.jersey().register(feature);
|
||||
webSocketEnvironment.jersey().register(feature);
|
||||
provisioningEnvironment.jersey().register(feature);
|
||||
});
|
||||
}
|
||||
|
||||
private void registerExceptionMappers(Environment environment,
|
||||
@@ -815,6 +916,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
List.of(
|
||||
new LoggingUnhandledExceptionMapper(),
|
||||
new CompletionExceptionMapper(),
|
||||
new GrpcStatusRuntimeExceptionMapper(),
|
||||
new IOExceptionMapper(),
|
||||
new RateLimitExceededExceptionMapper(),
|
||||
new InvalidWebsocketAddressExceptionMapper(),
|
||||
@@ -823,6 +925,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new ImpossiblePhoneNumberExceptionMapper(),
|
||||
new NonNormalizedPhoneNumberExceptionMapper(),
|
||||
new RegistrationServiceSenderExceptionMapper(),
|
||||
new SubscriptionProcessorExceptionMapper(),
|
||||
new JsonMappingExceptionMapper()
|
||||
).forEach(exceptionMapper -> {
|
||||
environment.jersey().register(exceptionMapper);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.attachments;
|
||||
import java.util.Map;
|
||||
|
||||
public interface AttachmentGenerator {
|
||||
|
||||
record Descriptor(Map<String, String> headers, String signedUploadLocation) {}
|
||||
|
||||
Descriptor generateAttachment(final String key);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.attachments;
|
||||
|
||||
import org.whispersystems.textsecuregcm.gcp.CanonicalRequest;
|
||||
import org.whispersystems.textsecuregcm.gcp.CanonicalRequestGenerator;
|
||||
import org.whispersystems.textsecuregcm.gcp.CanonicalRequestSigner;
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
public class GcsAttachmentGenerator implements AttachmentGenerator {
|
||||
@Nonnull
|
||||
private final CanonicalRequestGenerator canonicalRequestGenerator;
|
||||
|
||||
@Nonnull
|
||||
private final CanonicalRequestSigner canonicalRequestSigner;
|
||||
|
||||
public GcsAttachmentGenerator(@Nonnull String domain, @Nonnull String email,
|
||||
int maxSizeInBytes, @Nonnull String pathPrefix, @Nonnull String rsaSigningKey)
|
||||
throws IOException, InvalidKeyException, InvalidKeySpecException {
|
||||
this.canonicalRequestGenerator = new CanonicalRequestGenerator(domain, email, maxSizeInBytes, pathPrefix);
|
||||
this.canonicalRequestSigner = new CanonicalRequestSigner(rsaSigningKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Descriptor generateAttachment(final String key) {
|
||||
final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
final CanonicalRequest canonicalRequest = canonicalRequestGenerator.createFor(key, now);
|
||||
return new Descriptor(getHeaderMap(canonicalRequest), getSignedUploadLocation(canonicalRequest));
|
||||
}
|
||||
|
||||
private String getSignedUploadLocation(@Nonnull CanonicalRequest canonicalRequest) {
|
||||
return "https://" + canonicalRequest.getDomain() + canonicalRequest.getResourcePath()
|
||||
+ '?' + canonicalRequest.getCanonicalQuery()
|
||||
+ "&X-Goog-Signature=" + canonicalRequestSigner.sign(canonicalRequest);
|
||||
}
|
||||
|
||||
private static Map<String, String> getHeaderMap(@Nonnull CanonicalRequest canonicalRequest) {
|
||||
return Map.of(
|
||||
"host", canonicalRequest.getDomain(),
|
||||
"x-goog-content-length-range", "1," + canonicalRequest.getMaxSizeInBytes(),
|
||||
"x-goog-resumable", "start");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.attachments;
|
||||
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.util.HeaderUtils;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Clock;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
|
||||
public class TusAttachmentGenerator implements AttachmentGenerator {
|
||||
|
||||
private static final String ATTACHMENTS = "attachments";
|
||||
|
||||
final ExternalServiceCredentialsGenerator credentialsGenerator;
|
||||
final String tusUri;
|
||||
|
||||
public TusAttachmentGenerator(final TusConfiguration cfg) {
|
||||
this.tusUri = cfg.uploadUri();
|
||||
this.credentialsGenerator = credentialsGenerator(Clock.systemUTC(), cfg);
|
||||
}
|
||||
|
||||
private static ExternalServiceCredentialsGenerator credentialsGenerator(final Clock clock, final TusConfiguration cfg) {
|
||||
return ExternalServiceCredentialsGenerator
|
||||
.builder(cfg.userAuthenticationTokenSharedSecret())
|
||||
.prependUsername(false)
|
||||
.withClock(clock)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Descriptor generateAttachment(final String key) {
|
||||
final ExternalServiceCredentials credentials = credentialsGenerator.generateFor(ATTACHMENTS + "/" + key);
|
||||
final String b64Key = Base64.getEncoder().encodeToString(key.getBytes(StandardCharsets.UTF_8));
|
||||
final Map<String, String> headers = Map.of(
|
||||
HttpHeaders.AUTHORIZATION, HeaderUtils.basicAuthHeader(credentials),
|
||||
"Upload-Metadata", String.format("filename %s", b64Key)
|
||||
);
|
||||
return new Descriptor(headers, tusUri + "/" + ATTACHMENTS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.attachments;
|
||||
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
public record TusConfiguration(
|
||||
@ExactlySize(32) SecretBytes userAuthenticationTokenSharedSecret,
|
||||
@NotEmpty String uploadUri
|
||||
){}
|
||||
@@ -47,7 +47,7 @@ public class AuthEnablementRefreshRequirementProvider implements WebsocketRefres
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static Map<Long, Boolean> buildDevicesEnabledMap(final Account account) {
|
||||
static Map<Byte, Boolean> buildDevicesEnabledMap(final Account account) {
|
||||
return account.getDevices().stream().collect(Collectors.toMap(Device::getId, Device::isEnabled));
|
||||
}
|
||||
|
||||
@@ -68,17 +68,17 @@ public class AuthEnablementRefreshRequirementProvider implements WebsocketRefres
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Pair<UUID, Long>> handleRequestFinished(final RequestEvent requestEvent) {
|
||||
public List<Pair<UUID, Byte>> handleRequestFinished(final RequestEvent requestEvent) {
|
||||
// Now that the request is finished, check whether `isEnabled` changed for any of the devices. If the value did
|
||||
// change or if a devices was added or removed, all devices must disconnect and reauthenticate.
|
||||
if (requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED) != null) {
|
||||
|
||||
@SuppressWarnings("unchecked") final Map<Long, Boolean> initialDevicesEnabled =
|
||||
(Map<Long, Boolean>) requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED);
|
||||
@SuppressWarnings("unchecked") final Map<Byte, Boolean> initialDevicesEnabled =
|
||||
(Map<Byte, Boolean>) requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED);
|
||||
|
||||
return accountsManager.getByAccountIdentifier((UUID) requestEvent.getContainerRequest().getProperty(ACCOUNT_UUID)).map(account -> {
|
||||
final Set<Long> deviceIdsToDisplace;
|
||||
final Map<Long, Boolean> currentDevicesEnabled = buildDevicesEnabledMap(account);
|
||||
final Set<Byte> deviceIdsToDisplace;
|
||||
final Map<Byte, Boolean> currentDevicesEnabled = buildDevicesEnabledMap(account);
|
||||
|
||||
if (!initialDevicesEnabled.equals(currentDevicesEnabled)) {
|
||||
deviceIdsToDisplace = new HashSet<>(initialDevicesEnabled.keySet());
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.whispersystems.textsecuregcm.backup.BackupTier;
|
||||
|
||||
public record AuthenticatedBackupUser(byte[] backupId, BackupTier backupTier) {}
|
||||
@@ -32,9 +32,6 @@ public class BaseAccountAuthenticator {
|
||||
private static final String AUTHENTICATION_SUCCEEDED_TAG_NAME = "succeeded";
|
||||
private static final String AUTHENTICATION_FAILURE_REASON_TAG_NAME = "reason";
|
||||
private static final String ENABLED_TAG_NAME = "enabled";
|
||||
private static final String AUTHENTICATION_HAS_STORY_CAPABILITY = "hasStoryCapability";
|
||||
|
||||
private static final String STORY_ADOPTION_COUNTER_NAME = name(BaseAccountAuthenticator.class, "storyAdoption");
|
||||
|
||||
private static final String DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME = name(BaseAccountAuthenticator.class, "daysSinceLastSeen");
|
||||
private static final String IS_PRIMARY_DEVICE_TAG = "isPrimary";
|
||||
@@ -55,18 +52,18 @@ public class BaseAccountAuthenticator {
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
static Pair<String, Long> getIdentifierAndDeviceId(final String basicUsername) {
|
||||
static Pair<String, Byte> getIdentifierAndDeviceId(final String basicUsername) {
|
||||
final String identifier;
|
||||
final long deviceId;
|
||||
final byte deviceId;
|
||||
|
||||
final int deviceIdSeparatorIndex = basicUsername.indexOf(DEVICE_ID_SEPARATOR);
|
||||
|
||||
if (deviceIdSeparatorIndex == -1) {
|
||||
identifier = basicUsername;
|
||||
deviceId = Device.MASTER_ID;
|
||||
deviceId = Device.PRIMARY_ID;
|
||||
} else {
|
||||
identifier = basicUsername.substring(0, deviceIdSeparatorIndex);
|
||||
deviceId = Long.parseLong(basicUsername.substring(deviceIdSeparatorIndex + 1));
|
||||
deviceId = Byte.parseByte(basicUsername.substring(deviceIdSeparatorIndex + 1));
|
||||
}
|
||||
|
||||
return new Pair<>(identifier, deviceId);
|
||||
@@ -75,13 +72,12 @@ public class BaseAccountAuthenticator {
|
||||
public Optional<AuthenticatedAccount> authenticate(BasicCredentials basicCredentials, boolean enabledRequired) {
|
||||
boolean succeeded = false;
|
||||
String failureReason = null;
|
||||
boolean hasStoryCapability = false;
|
||||
|
||||
try {
|
||||
final UUID accountUuid;
|
||||
final long deviceId;
|
||||
final byte deviceId;
|
||||
{
|
||||
final Pair<String, Long> identifierAndDeviceId = getIdentifierAndDeviceId(basicCredentials.getUsername());
|
||||
final Pair<String, Byte> identifierAndDeviceId = getIdentifierAndDeviceId(basicCredentials.getUsername());
|
||||
|
||||
accountUuid = UUID.fromString(identifierAndDeviceId.first());
|
||||
deviceId = identifierAndDeviceId.second();
|
||||
@@ -94,8 +90,6 @@ public class BaseAccountAuthenticator {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
hasStoryCapability = account.map(Account::isStoriesSupported).orElse(false);
|
||||
|
||||
Optional<Device> device = account.get().getDevice(deviceId);
|
||||
|
||||
if (device.isEmpty()) {
|
||||
@@ -119,7 +113,7 @@ public class BaseAccountAuthenticator {
|
||||
} else {
|
||||
Metrics.counter(ENABLED_NOT_REQUIRED_AUTHENTICATION_COUNTER_NAME,
|
||||
ENABLED_TAG_NAME, String.valueOf(device.get().isEnabled() && account.get().isEnabled()),
|
||||
IS_PRIMARY_DEVICE_TAG, String.valueOf(device.get().isMaster()))
|
||||
IS_PRIMARY_DEVICE_TAG, String.valueOf(device.get().isPrimary()))
|
||||
.increment();
|
||||
}
|
||||
|
||||
@@ -150,9 +144,6 @@ public class BaseAccountAuthenticator {
|
||||
}
|
||||
|
||||
Metrics.counter(AUTHENTICATION_COUNTER_NAME, tags).increment();
|
||||
|
||||
Tags storyTags = Tags.of(AUTHENTICATION_HAS_STORY_CAPABILITY, String.valueOf(hasStoryCapability));
|
||||
Metrics.counter(STORY_ADOPTION_COUNTER_NAME, storyTags).increment();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +162,7 @@ public class BaseAccountAuthenticator {
|
||||
// (1) each account will only update last-seen at most once per day
|
||||
// (2) these updates will occur throughout the day rather than all occurring at UTC midnight.
|
||||
if (device.getLastSeen() < todayInMillisWithOffset) {
|
||||
Metrics.summary(DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME, IS_PRIMARY_DEVICE_TAG, String.valueOf(device.isMaster()))
|
||||
Metrics.summary(DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME, IS_PRIMARY_DEVICE_TAG, String.valueOf(device.isPrimary()))
|
||||
.record(Duration.ofMillis(todayInMillisWithOffset - device.getLastSeen()).toDays());
|
||||
|
||||
return accountsManager.updateDeviceLastSeen(account, device, Util.todayInMillis(clock));
|
||||
|
||||
@@ -11,10 +11,10 @@ import org.whispersystems.textsecuregcm.util.Pair;
|
||||
public class BasicAuthorizationHeader {
|
||||
|
||||
private final String username;
|
||||
private final long deviceId;
|
||||
private final byte deviceId;
|
||||
private final String password;
|
||||
|
||||
private BasicAuthorizationHeader(final String username, final long deviceId, final String password) {
|
||||
private BasicAuthorizationHeader(final String username, final byte deviceId, final String password) {
|
||||
this.username = username;
|
||||
this.deviceId = deviceId;
|
||||
this.password = password;
|
||||
@@ -59,9 +59,9 @@ public class BasicAuthorizationHeader {
|
||||
final String usernameComponent = credentials.substring(0, credentialSeparatorIndex);
|
||||
|
||||
final String username;
|
||||
final long deviceId;
|
||||
final byte deviceId;
|
||||
{
|
||||
final Pair<String, Long> identifierAndDeviceId =
|
||||
final Pair<String, Byte> identifierAndDeviceId =
|
||||
BaseAccountAuthenticator.getIdentifierAndDeviceId(usernameComponent);
|
||||
|
||||
username = identifierAndDeviceId.first();
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECPrivateKey;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.SenderCertificate;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.ServerCertificate;
|
||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
@@ -32,11 +33,11 @@ public class CertificateGenerator {
|
||||
|
||||
public byte[] createFor(Account account, Device device, boolean includeE164) throws InvalidKeyException {
|
||||
SenderCertificate.Certificate.Builder builder = SenderCertificate.Certificate.newBuilder()
|
||||
.setSenderDevice(Math.toIntExact(device.getId()))
|
||||
.setExpires(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(expiresDays))
|
||||
.setIdentityKey(ByteString.copyFrom(account.getIdentityKey().serialize()))
|
||||
.setSigner(serverCertificate)
|
||||
.setSenderUuid(account.getUuid().toString());
|
||||
.setSenderDevice(Math.toIntExact(device.getId()))
|
||||
.setExpires(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(expiresDays))
|
||||
.setIdentityKey(ByteString.copyFrom(account.getIdentityKey(IdentityType.ACI).serialize()))
|
||||
.setSigner(serverCertificate)
|
||||
.setSenderUuid(account.getUuid().toString());
|
||||
|
||||
if (includeE164) {
|
||||
builder.setSender(account.getNumber());
|
||||
|
||||
@@ -16,7 +16,7 @@ public class CombinedUnidentifiedSenderAccessKeys {
|
||||
public CombinedUnidentifiedSenderAccessKeys(String header) {
|
||||
try {
|
||||
this.combinedUnidentifiedSenderAccessKeys = Base64.getDecoder().decode(header);
|
||||
if (this.combinedUnidentifiedSenderAccessKeys == null || this.combinedUnidentifiedSenderAccessKeys.length != 16) {
|
||||
if (this.combinedUnidentifiedSenderAccessKeys == null || this.combinedUnidentifiedSenderAccessKeys.length != UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH) {
|
||||
throw new WebApplicationException("Invalid combined unidentified sender access keys", Status.UNAUTHORIZED);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
|
||||
@@ -29,7 +29,7 @@ public class OptionalAccess {
|
||||
verify(requestAccount, accessKey, targetAccount);
|
||||
|
||||
if (!deviceSelector.equals("*")) {
|
||||
long deviceId = Long.parseLong(deviceSelector);
|
||||
byte deviceId = Byte.parseByte(deviceSelector);
|
||||
|
||||
Optional<Device> targetDevice = targetAccount.get().getDevice(deviceId);
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ public class PhoneNumberChangeRefreshRequirementProvider implements WebsocketRef
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Pair<UUID, Long>> handleRequestFinished(final RequestEvent requestEvent) {
|
||||
public List<Pair<UUID, Byte>> handleRequestFinished(final RequestEvent requestEvent) {
|
||||
final String initialNumber = (String) requestEvent.getContainerRequest().getProperty(INITIAL_NUMBER_KEY);
|
||||
|
||||
if (initialNumber != null) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.entities.PhoneVerificationRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure;
|
||||
@@ -30,7 +31,6 @@ import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
public class RegistrationLockVerificationManager {
|
||||
public enum Flow {
|
||||
@@ -54,7 +54,6 @@ public class RegistrationLockVerificationManager {
|
||||
|
||||
private final AccountsManager accounts;
|
||||
private final ClientPresenceManager clientPresenceManager;
|
||||
private final ExternalServiceCredentialsGenerator svr1CredentialGenerator;
|
||||
private final ExternalServiceCredentialsGenerator svr2CredentialGenerator;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
|
||||
@@ -62,14 +61,12 @@ public class RegistrationLockVerificationManager {
|
||||
|
||||
public RegistrationLockVerificationManager(
|
||||
final AccountsManager accounts, final ClientPresenceManager clientPresenceManager,
|
||||
final ExternalServiceCredentialsGenerator svr1CredentialGenerator,
|
||||
final ExternalServiceCredentialsGenerator svr2CredentialGenerator,
|
||||
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
|
||||
final PushNotificationManager pushNotificationManager,
|
||||
final RateLimiters rateLimiters) {
|
||||
this.accounts = accounts;
|
||||
this.clientPresenceManager = clientPresenceManager;
|
||||
this.svr1CredentialGenerator = svr1CredentialGenerator;
|
||||
this.svr2CredentialGenerator = svr2CredentialGenerator;
|
||||
this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
|
||||
this.pushNotificationManager = pushNotificationManager;
|
||||
@@ -109,7 +106,7 @@ public class RegistrationLockVerificationManager {
|
||||
throw new RuntimeException("Unexpected status: " + existingRegistrationLock.getStatus());
|
||||
}
|
||||
|
||||
if (!Util.isEmpty(clientRegistrationLock)) {
|
||||
if (StringUtils.isNotEmpty(clientRegistrationLock)) {
|
||||
rateLimiters.getPinLimiter().validate(account.getNumber());
|
||||
}
|
||||
|
||||
@@ -141,7 +138,6 @@ public class RegistrationLockVerificationManager {
|
||||
// Freezing the existing account credentials will definitively start the reglock timeout.
|
||||
// Until the timeout, the current reglock can still be supplied,
|
||||
// along with phone number verification, to restore access.
|
||||
final ExternalServiceCredentials existingSvr1Credentials = svr1CredentialGenerator.generateForUuid(account.getUuid());
|
||||
final ExternalServiceCredentials existingSvr2Credentials = svr2CredentialGenerator.generateForUuid(account.getUuid());
|
||||
|
||||
final Account updatedAccount;
|
||||
@@ -161,7 +157,7 @@ public class RegistrationLockVerificationManager {
|
||||
registrationRecoveryPasswordsManager.removeForNumber(updatedAccount.getNumber());
|
||||
}
|
||||
|
||||
final List<Long> deviceIds = updatedAccount.getDevices().stream().map(Device::getId).toList();
|
||||
final List<Byte> deviceIds = updatedAccount.getDevices().stream().map(Device::getId).toList();
|
||||
clientPresenceManager.disconnectAllPresences(updatedAccount.getUuid(), deviceIds);
|
||||
|
||||
try {
|
||||
@@ -173,7 +169,6 @@ public class RegistrationLockVerificationManager {
|
||||
|
||||
throw new WebApplicationException(Response.status(FAILURE_HTTP_STATUS)
|
||||
.entity(new RegistrationLockFailure(existingRegistrationLock.getTimeRemaining().toMillis(),
|
||||
existingRegistrationLock.needsFailureCredentials() ? existingSvr1Credentials : null,
|
||||
existingRegistrationLock.needsFailureCredentials() ? existingSvr2Credentials : null))
|
||||
.build());
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class StoredRegistrationLock {
|
||||
@@ -73,7 +73,7 @@ public class StoredRegistrationLock {
|
||||
}
|
||||
|
||||
public boolean verify(@Nullable String clientRegistrationLock) {
|
||||
if (hasLockAndSalt() && Util.nonEmpty(clientRegistrationLock)) {
|
||||
if (hasLockAndSalt() && StringUtils.isNotEmpty(clientRegistrationLock)) {
|
||||
SaltedTokenHash credentials = new SaltedTokenHash(registrationLock.get(), registrationLockSalt.get());
|
||||
return credentials.verify(clientRegistrationLock);
|
||||
} else {
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Duration;
|
||||
import javax.annotation.Nullable;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
public record StoredVerificationCode(@Nullable String code,
|
||||
long timestamp,
|
||||
@Nullable String pushCode,
|
||||
@Nullable byte[] sessionId) {
|
||||
|
||||
public static final Duration EXPIRATION = Duration.ofMinutes(10);
|
||||
|
||||
public boolean isValid(String theirCodeString) {
|
||||
if (Util.isEmpty(code) || Util.isEmpty(theirCodeString)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] ourCode = code.getBytes();
|
||||
byte[] theirCode = theirCodeString.getBytes();
|
||||
|
||||
return MessageDigest.isEqual(ourCode, theirCode);
|
||||
}
|
||||
}
|
||||
@@ -5,30 +5,7 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TurnToken {
|
||||
|
||||
@JsonProperty
|
||||
private String username;
|
||||
|
||||
@JsonProperty
|
||||
private String password;
|
||||
|
||||
@JsonProperty
|
||||
private List<String> urls;
|
||||
|
||||
public TurnToken(String username, String password, List<String> urls) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.urls = urls;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
List<String> getUrls() {
|
||||
return urls;
|
||||
}
|
||||
public record TurnToken(String username, String password, List<String> urls) {
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public class TurnTokenGenerator {
|
||||
|
||||
@@ -39,9 +40,9 @@ public class TurnTokenGenerator {
|
||||
this.turnSecret = turnSecret;
|
||||
}
|
||||
|
||||
public TurnToken generate(final String e164) {
|
||||
public TurnToken generate(final UUID aci) {
|
||||
try {
|
||||
final List<String> urls = urls(e164);
|
||||
final List<String> urls = urls(aci);
|
||||
final Mac mac = Mac.getInstance(ALGORITHM);
|
||||
final long validUntilSeconds = Instant.now().plus(Duration.ofDays(1)).getEpochSecond();
|
||||
final long user = Util.ensureNonNegativeInt(new SecureRandom().nextInt());
|
||||
@@ -56,12 +57,12 @@ public class TurnTokenGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> urls(final String e164) {
|
||||
private List<String> urls(final UUID aci) {
|
||||
final DynamicTurnConfiguration turnConfig = dynamicConfigurationManager.getConfiguration().getTurnConfiguration();
|
||||
|
||||
// Check if number is enrolled to test out specific turn servers
|
||||
final Optional<TurnUriConfiguration> enrolled = turnConfig.getUriConfigs().stream()
|
||||
.filter(config -> config.getEnrolledNumbers().contains(e164))
|
||||
.filter(config -> config.getEnrolledAcis().contains(aci))
|
||||
.findFirst();
|
||||
|
||||
if (enrolled.isPresent()) {
|
||||
|
||||
@@ -9,23 +9,21 @@ import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.Optional;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class UnidentifiedAccessChecksum {
|
||||
|
||||
public static String generateFor(Optional<byte[]> unidentifiedAccessKey) {
|
||||
public static byte[] generateFor(byte[] unidentifiedAccessKey) {
|
||||
try {
|
||||
if (!unidentifiedAccessKey.isPresent()|| unidentifiedAccessKey.get().length != 16) return null;
|
||||
if (unidentifiedAccessKey.length != UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH) {
|
||||
throw new IllegalArgumentException("Invalid UAK length: " + unidentifiedAccessKey.length);
|
||||
}
|
||||
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(unidentifiedAccessKey.get(), "HmacSHA256"));
|
||||
mac.init(new SecretKeySpec(unidentifiedAccessKey, "HmacSHA256"));
|
||||
|
||||
return Base64.getEncoder().encodeToString(mac.doFinal(new byte[32]));
|
||||
return mac.doFinal(new byte[32]);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import java.security.MessageDigest;
|
||||
|
||||
public class UnidentifiedAccessUtil {
|
||||
|
||||
public static final int UNIDENTIFIED_ACCESS_KEY_LENGTH = 16;
|
||||
|
||||
private UnidentifiedAccessUtil() {
|
||||
}
|
||||
|
||||
|
||||
@@ -30,5 +30,5 @@ public interface WebsocketRefreshRequirementProvider {
|
||||
* @return a list of pairs of account UUID/device ID pairs identifying websockets that need to be refreshed as a
|
||||
* result of the observed request
|
||||
*/
|
||||
List<Pair<UUID, Long>> handleRequestFinished(RequestEvent requestEvent);
|
||||
List<Pair<UUID, Byte>> handleRequestFinished(RequestEvent requestEvent);
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@ package org.whispersystems.textsecuregcm.auth.grpc;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record AuthenticatedDevice(UUID accountIdentifier, long deviceId) {
|
||||
public record AuthenticatedDevice(UUID accountIdentifier, byte deviceId) {
|
||||
}
|
||||
|
||||
@@ -9,9 +9,7 @@ import io.grpc.Context;
|
||||
import io.grpc.Status;
|
||||
import java.util.UUID;
|
||||
import javax.annotation.Nullable;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.function.Tuple2;
|
||||
import reactor.util.function.Tuples;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
/**
|
||||
* Provides utility methods for working with authentication in the context of gRPC calls.
|
||||
@@ -19,20 +17,20 @@ import reactor.util.function.Tuples;
|
||||
public class AuthenticationUtil {
|
||||
|
||||
static final Context.Key<UUID> CONTEXT_AUTHENTICATED_ACCOUNT_IDENTIFIER_KEY = Context.key("authenticated-aci");
|
||||
static final Context.Key<Long> CONTEXT_AUTHENTICATED_DEVICE_IDENTIFIER_KEY = Context.key("authenticated-device-id");
|
||||
static final Context.Key<Byte> CONTEXT_AUTHENTICATED_DEVICE_IDENTIFIER_KEY = Context.key("authenticated-device-id");
|
||||
|
||||
/**
|
||||
* Returns the account/device authenticated in the current gRPC context or throws an "unauthenticated" exception if
|
||||
* no authenticated account/device is available.
|
||||
*
|
||||
* @return the account/device authenticated in the current gRPC context
|
||||
* @return the account/device identifier authenticated in the current gRPC context
|
||||
*
|
||||
* @throws io.grpc.StatusRuntimeException with a status of {@code UNAUTHENTICATED} if no authenticated account/device
|
||||
* could be retrieved from the current gRPC context
|
||||
*/
|
||||
public static AuthenticatedDevice requireAuthenticatedDevice() {
|
||||
@Nullable final UUID accountIdentifier = CONTEXT_AUTHENTICATED_ACCOUNT_IDENTIFIER_KEY.get();
|
||||
@Nullable final Long deviceId = CONTEXT_AUTHENTICATED_DEVICE_IDENTIFIER_KEY.get();
|
||||
@Nullable final Byte deviceId = CONTEXT_AUTHENTICATED_DEVICE_IDENTIFIER_KEY.get();
|
||||
|
||||
if (accountIdentifier != null && deviceId != null) {
|
||||
return new AuthenticatedDevice(accountIdentifier, deviceId);
|
||||
@@ -40,4 +38,25 @@ public class AuthenticationUtil {
|
||||
|
||||
throw Status.UNAUTHENTICATED.asRuntimeException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the account/device authenticated in the current gRPC context or throws an "unauthenticated" exception if
|
||||
* no authenticated account/device is available or "permission denied" if the authenticated device is not the primary
|
||||
* device for the account.
|
||||
*
|
||||
* @return the account/device identifier authenticated in the current gRPC context
|
||||
*
|
||||
* @throws io.grpc.StatusRuntimeException with a status of {@code UNAUTHENTICATED} if no authenticated account/device
|
||||
* could be retrieved from the current gRPC context or a status of {@code PERMISSION_DENIED} if the authenticated
|
||||
* device is not the primary device for the authenticated account
|
||||
*/
|
||||
public static AuthenticatedDevice requireAuthenticatedPrimaryDevice() {
|
||||
final AuthenticatedDevice authenticatedDevice = requireAuthenticatedDevice();
|
||||
|
||||
if (authenticatedDevice.deviceId() != Device.PRIMARY_ID) {
|
||||
throw Status.PERMISSION_DENIED.asRuntimeException();
|
||||
}
|
||||
|
||||
return authenticatedDevice;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import java.util.Optional;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.BaseAccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.util.HeaderUtils;
|
||||
|
||||
/**
|
||||
* A basic credential authentication interceptor enforces the presence of a valid username and password on every call.
|
||||
@@ -39,7 +40,7 @@ public class BasicCredentialAuthenticationInterceptor implements ServerIntercept
|
||||
|
||||
@VisibleForTesting
|
||||
static final Metadata.Key<String> BASIC_CREDENTIALS =
|
||||
Metadata.Key.of("x-signal-basic-auth-credentials", Metadata.ASCII_STRING_MARSHALLER);
|
||||
Metadata.Key.of("x-signal-auth", Metadata.ASCII_STRING_MARSHALLER);
|
||||
|
||||
private static final Metadata EMPTY_TRAILERS = new Metadata();
|
||||
|
||||
@@ -48,17 +49,20 @@ public class BasicCredentialAuthenticationInterceptor implements ServerIntercept
|
||||
}
|
||||
|
||||
@Override
|
||||
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(final ServerCall<ReqT, RespT> call,
|
||||
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
|
||||
final ServerCall<ReqT, RespT> call,
|
||||
final Metadata headers,
|
||||
final ServerCallHandler<ReqT, RespT> next) {
|
||||
|
||||
final String credentialString = headers.get(BASIC_CREDENTIALS);
|
||||
final String authHeader = headers.get(BASIC_CREDENTIALS);
|
||||
|
||||
if (StringUtils.isNotBlank(credentialString)) {
|
||||
try {
|
||||
final BasicCredentials credentials = extractBasicCredentials(credentialString);
|
||||
if (StringUtils.isNotBlank(authHeader)) {
|
||||
final Optional<BasicCredentials> maybeCredentials = HeaderUtils.basicCredentialsFromAuthHeader(authHeader);
|
||||
if (maybeCredentials.isEmpty()) {
|
||||
call.close(Status.UNAUTHENTICATED.withDescription("Could not parse credentials"), EMPTY_TRAILERS);
|
||||
} else {
|
||||
final Optional<AuthenticatedAccount> maybeAuthenticatedAccount =
|
||||
baseAccountAuthenticator.authenticate(credentials, false);
|
||||
baseAccountAuthenticator.authenticate(maybeCredentials.get(), false);
|
||||
|
||||
if (maybeAuthenticatedAccount.isPresent()) {
|
||||
final AuthenticatedAccount authenticatedAccount = maybeAuthenticatedAccount.get();
|
||||
@@ -71,8 +75,6 @@ public class BasicCredentialAuthenticationInterceptor implements ServerIntercept
|
||||
} else {
|
||||
call.close(Status.UNAUTHENTICATED.withDescription("Credentials not accepted"), EMPTY_TRAILERS);
|
||||
}
|
||||
} catch (final IllegalArgumentException e) {
|
||||
call.close(Status.UNAUTHENTICATED.withDescription("Could not parse credentials"), EMPTY_TRAILERS);
|
||||
}
|
||||
} else {
|
||||
call.close(Status.UNAUTHENTICATED.withDescription("No credentials provided"), EMPTY_TRAILERS);
|
||||
@@ -80,15 +82,4 @@ public class BasicCredentialAuthenticationInterceptor implements ServerIntercept
|
||||
|
||||
return new ServerCall.Listener<>() {};
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static BasicCredentials extractBasicCredentials(final String credentials) {
|
||||
if (credentials.indexOf(':') < 0) {
|
||||
throw new IllegalArgumentException("Credentials do not include a username and password part");
|
||||
}
|
||||
|
||||
final String[] pieces = credentials.split(":", 2);
|
||||
|
||||
return new BasicCredentials(pieces[0], pieces[1]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import io.grpc.Status;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Stream;
|
||||
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequest;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialResponse;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
/**
|
||||
* Issues ZK backup auth credentials for authenticated accounts
|
||||
* <p>
|
||||
* Authenticated callers can create ZK credentials that contain a blinded backup-id, so that they can later use that
|
||||
* backup id without the verifier learning that the id is associated with this account.
|
||||
* <p>
|
||||
* First use {@link #commitBackupId} to provide a blinded backup-id. This is stored in durable storage. Then the caller
|
||||
* can use {@link #getBackupAuthCredentials} to retrieve credentials that can subsequently be used to make anonymously
|
||||
* authenticated requests against their backup-id.
|
||||
*/
|
||||
public class BackupAuthManager {
|
||||
|
||||
private static final Duration MAX_REDEMPTION_DURATION = Duration.ofDays(7);
|
||||
final static String BACKUP_EXPERIMENT_NAME = "backup";
|
||||
final static String BACKUP_MEDIA_EXPERIMENT_NAME = "backupMedia";
|
||||
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
private final GenericServerSecretParams serverSecretParams;
|
||||
private final Clock clock;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final AccountsManager accountsManager;
|
||||
|
||||
public BackupAuthManager(
|
||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
||||
final RateLimiters rateLimiters,
|
||||
final AccountsManager accountsManager,
|
||||
final GenericServerSecretParams serverSecretParams,
|
||||
final Clock clock) {
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.accountsManager = accountsManager;
|
||||
this.serverSecretParams = serverSecretParams;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a credential request containing a blinded backup-id for future use.
|
||||
*
|
||||
* @param account The account using the backup-id
|
||||
* @param backupAuthCredentialRequest A request containing the blinded backup-id
|
||||
* @return A future that completes when the credentialRequest has been stored
|
||||
* @throws RateLimitExceededException If too many backup-ids have been committed
|
||||
*/
|
||||
public CompletableFuture<Void> commitBackupId(final Account account,
|
||||
final BackupAuthCredentialRequest backupAuthCredentialRequest) throws RateLimitExceededException {
|
||||
if (receiptLevel(account).isEmpty()) {
|
||||
throw Status.PERMISSION_DENIED.withDescription("Backups not allowed on account").asRuntimeException();
|
||||
}
|
||||
|
||||
byte[] serializedRequest = backupAuthCredentialRequest.serialize();
|
||||
byte[] existingRequest = account.getBackupCredentialRequest();
|
||||
if (existingRequest != null && MessageDigest.isEqual(serializedRequest, existingRequest)) {
|
||||
// No need to update or enforce rate limits, this is the credential that the user has already
|
||||
// committed to.
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
rateLimiters.forDescriptor(RateLimiters.For.SET_BACKUP_ID).validate(account.getUuid());
|
||||
|
||||
return this.accountsManager
|
||||
.updateAsync(account, acc -> acc.setBackupCredentialRequest(serializedRequest))
|
||||
.thenRun(Util.NOOP);
|
||||
}
|
||||
|
||||
public record Credential(BackupAuthCredentialResponse credential, Instant redemptionTime) {}
|
||||
|
||||
/**
|
||||
* Create a credential for every day between redemptionStart and redemptionEnd
|
||||
* <p>
|
||||
* This uses a {@link BackupAuthCredentialRequest} previous stored via {@link this#commitBackupId} to generate the
|
||||
* credentials.
|
||||
*
|
||||
* @param account The account to create the credentials for
|
||||
* @param redemptionStart The day (must be truncated to a day boundary) the first credential should be valid
|
||||
* @param redemptionEnd The day (must be truncated to a day boundary) the last credential should be valid
|
||||
* @return Credentials and the day on which they may be redeemed
|
||||
*/
|
||||
public CompletableFuture<List<Credential>> getBackupAuthCredentials(
|
||||
final Account account,
|
||||
final Instant redemptionStart,
|
||||
final Instant redemptionEnd) {
|
||||
|
||||
final long receiptLevel = receiptLevel(account).orElseThrow(
|
||||
() -> Status.PERMISSION_DENIED.withDescription("Backups not allowed on account").asRuntimeException());
|
||||
|
||||
final Instant startOfDay = clock.instant().truncatedTo(ChronoUnit.DAYS);
|
||||
if (redemptionStart.isAfter(redemptionEnd) ||
|
||||
redemptionStart.isBefore(startOfDay) ||
|
||||
redemptionEnd.isAfter(startOfDay.plus(MAX_REDEMPTION_DURATION)) ||
|
||||
!redemptionStart.equals(redemptionStart.truncatedTo(ChronoUnit.DAYS)) ||
|
||||
!redemptionEnd.equals(redemptionEnd.truncatedTo(ChronoUnit.DAYS))) {
|
||||
|
||||
throw Status.INVALID_ARGUMENT.withDescription("invalid redemption window").asRuntimeException();
|
||||
}
|
||||
|
||||
// fetch the blinded backup-id the account should have previously committed to
|
||||
final byte[] committedBytes = account.getBackupCredentialRequest();
|
||||
if (committedBytes == null) {
|
||||
throw Status.NOT_FOUND.withDescription("No blinded backup-id has been added to the account").asRuntimeException();
|
||||
}
|
||||
|
||||
try {
|
||||
// create a credential for every day in the requested period
|
||||
final BackupAuthCredentialRequest credentialReq = new BackupAuthCredentialRequest(committedBytes);
|
||||
return CompletableFuture.completedFuture(Stream
|
||||
.iterate(redemptionStart, curr -> curr.plus(Duration.ofDays(1)))
|
||||
.takeWhile(redemptionTime -> !redemptionTime.isAfter(redemptionEnd))
|
||||
.map(redemption -> new Credential(
|
||||
credentialReq.issueCredential(redemption, receiptLevel, serverSecretParams),
|
||||
redemption))
|
||||
.toList());
|
||||
} catch (InvalidInputException e) {
|
||||
throw Status.INTERNAL
|
||||
.withDescription("Could not deserialize stored request credential")
|
||||
.withCause(e)
|
||||
.asRuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Long> receiptLevel(final Account account) {
|
||||
if (inExperiment(BACKUP_MEDIA_EXPERIMENT_NAME, account)) {
|
||||
return Optional.of(BackupTier.MEDIA.getReceiptLevel());
|
||||
}
|
||||
if (inExperiment(BACKUP_EXPERIMENT_NAME, account)) {
|
||||
return Optional.of(BackupTier.MESSAGES.getReceiptLevel());
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private boolean inExperiment(final String experimentName, final Account account) {
|
||||
return dynamicConfigurationManager.getConfiguration()
|
||||
.getExperimentEnrollmentConfiguration(experimentName)
|
||||
.map(config -> config.getEnrolledUuids().contains(account.getUuid()))
|
||||
.orElse(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import io.grpc.Status;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import java.net.URI;
|
||||
import java.time.Clock;
|
||||
import java.util.Base64;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialPresentation;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||
|
||||
public class BackupManager {
|
||||
private static final Logger logger = LoggerFactory.getLogger(BackupManager.class);
|
||||
|
||||
static final String MESSAGE_BACKUP_NAME = "messageBackup";
|
||||
private static final long MAX_TOTAL_BACKUP_MEDIA_BYTES = 1024L * 1024L * 1024L * 50L;
|
||||
private static final long MAX_MEDIA_OBJECT_SIZE = 1024L * 1024L * 101L;
|
||||
private static final String ZK_AUTHN_COUNTER_NAME = MetricsUtil.name(BackupManager.class, "authentication");
|
||||
private static final String ZK_AUTHZ_FAILURE_COUNTER_NAME = MetricsUtil.name(BackupManager.class,
|
||||
"authorizationFailure");
|
||||
private static final String SUCCESS_TAG_NAME = "success";
|
||||
private static final String FAILURE_REASON_TAG_NAME = "reason";
|
||||
|
||||
private final BackupsDb backupsDb;
|
||||
private final GenericServerSecretParams serverSecretParams;
|
||||
private final Cdn3BackupCredentialGenerator cdn3BackupCredentialGenerator;
|
||||
private final RemoteStorageManager remoteStorageManager;
|
||||
private final Map<Integer, String> attachmentCdnBaseUris;
|
||||
private final Clock clock;
|
||||
|
||||
|
||||
public BackupManager(
|
||||
final BackupsDb backupsDb,
|
||||
final GenericServerSecretParams serverSecretParams,
|
||||
final Cdn3BackupCredentialGenerator cdn3BackupCredentialGenerator,
|
||||
final RemoteStorageManager remoteStorageManager,
|
||||
final Map<Integer, String> attachmentCdnBaseUris,
|
||||
final Clock clock) {
|
||||
this.backupsDb = backupsDb;
|
||||
this.serverSecretParams = serverSecretParams;
|
||||
this.cdn3BackupCredentialGenerator = cdn3BackupCredentialGenerator;
|
||||
this.remoteStorageManager = remoteStorageManager;
|
||||
this.clock = clock;
|
||||
// strip trailing "/" for easier URI construction
|
||||
this.attachmentCdnBaseUris = attachmentCdnBaseUris.entrySet().stream().collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> StringUtils.removeEnd(entry.getValue(), "/")
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the public key for the backup-id.
|
||||
* <p>
|
||||
* Once set, calls {@link BackupManager#authenticateBackupUser} can succeed if the presentation is signed with the
|
||||
* private key corresponding to this public key.
|
||||
*
|
||||
* @param presentation a ZK credential presentation that encodes the backupId
|
||||
* @param signature the signature of the presentation
|
||||
* @param publicKey the public key of a key-pair that the presentation must be signed with
|
||||
*/
|
||||
public CompletableFuture<Void> setPublicKey(
|
||||
final BackupAuthCredentialPresentation presentation,
|
||||
final byte[] signature,
|
||||
final ECPublicKey publicKey) {
|
||||
|
||||
// Note: this is a special case where we can't validate the presentation signature against the stored public key
|
||||
// because we are currently setting it. We check against the provided public key, but we must also verify that
|
||||
// there isn't an existing, different stored public key for the backup-id (verified with a condition expression)
|
||||
final BackupTier backupTier = verifySignatureAndCheckPresentation(presentation, signature, publicKey);
|
||||
if (backupTier.compareTo(BackupTier.MESSAGES) < 0) {
|
||||
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
|
||||
throw Status.PERMISSION_DENIED
|
||||
.withDescription("credential does not support setting public key")
|
||||
.asRuntimeException();
|
||||
}
|
||||
return backupsDb.setPublicKey(presentation.getBackupId(), backupTier, publicKey)
|
||||
.exceptionally(ExceptionUtils.exceptionallyHandler(PublicKeyConflictException.class, ex -> {
|
||||
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
|
||||
SUCCESS_TAG_NAME, String.valueOf(false),
|
||||
FAILURE_REASON_TAG_NAME, "public_key_conflict")
|
||||
.increment();
|
||||
throw Status.UNAUTHENTICATED
|
||||
.withDescription("public key does not match existing public key for the backup-id")
|
||||
.asRuntimeException();
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a form that may be used to upload a backup file for the backupId encoded in the presentation.
|
||||
* <p>
|
||||
* If successful, this also updates the TTL of the backup.
|
||||
*
|
||||
* @param backupUser an already ZK authenticated backup user
|
||||
* @return the upload form
|
||||
*/
|
||||
public CompletableFuture<MessageBackupUploadDescriptor> createMessageBackupUploadDescriptor(
|
||||
final AuthenticatedBackupUser backupUser) {
|
||||
final String encodedBackupId = encodeBackupIdForCdn(backupUser);
|
||||
|
||||
// this could race with concurrent updates, but the only effect would be last-writer-wins on the timestamp
|
||||
return backupsDb
|
||||
.addMessageBackup(backupUser)
|
||||
.thenApply(result -> cdn3BackupCredentialGenerator.generateUpload(encodedBackupId, MESSAGE_BACKUP_NAME));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the last update timestamps for the backupId in the presentation
|
||||
*
|
||||
* @param backupUser an already ZK authenticated backup user
|
||||
*/
|
||||
public CompletableFuture<Void> ttlRefresh(final AuthenticatedBackupUser backupUser) {
|
||||
if (backupUser.backupTier().compareTo(BackupTier.MESSAGES) < 0) {
|
||||
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
|
||||
throw Status.PERMISSION_DENIED
|
||||
.withDescription("credential does not support ttl operation")
|
||||
.asRuntimeException();
|
||||
}
|
||||
// update message backup TTL
|
||||
return backupsDb.ttlRefresh(backupUser);
|
||||
}
|
||||
|
||||
public record BackupInfo(int cdn, String backupSubdir, String messageBackupKey, Optional<Long> mediaUsedSpace) {}
|
||||
|
||||
/**
|
||||
* Retrieve information about the existing backup
|
||||
*
|
||||
* @param backupUser an already ZK authenticated backup user
|
||||
* @return Information about the existing backup
|
||||
*/
|
||||
public CompletableFuture<BackupInfo> backupInfo(final AuthenticatedBackupUser backupUser) {
|
||||
if (backupUser.backupTier().compareTo(BackupTier.MESSAGES) < 0) {
|
||||
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
|
||||
throw Status.PERMISSION_DENIED.withDescription("credential does not support info operation")
|
||||
.asRuntimeException();
|
||||
}
|
||||
return backupsDb.describeBackup(backupUser)
|
||||
.thenApply(backupDescription -> new BackupInfo(
|
||||
backupDescription.cdn(),
|
||||
encodeBackupIdForCdn(backupUser),
|
||||
MESSAGE_BACKUP_NAME,
|
||||
backupDescription.mediaUsedSpace()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is enough capacity to store the requested amount of media
|
||||
*
|
||||
* @param backupUser an already ZK authenticated backup user
|
||||
* @param mediaLength the desired number of media bytes to store
|
||||
* @return true if mediaLength bytes can be stored
|
||||
*/
|
||||
public CompletableFuture<Boolean> canStoreMedia(final AuthenticatedBackupUser backupUser, final long mediaLength) {
|
||||
if (backupUser.backupTier().compareTo(BackupTier.MEDIA) < 0) {
|
||||
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
|
||||
throw Status.PERMISSION_DENIED
|
||||
.withDescription("credential does not support storing media")
|
||||
.asRuntimeException();
|
||||
}
|
||||
return backupsDb.describeBackup(backupUser)
|
||||
.thenApply(info -> info.mediaUsedSpace()
|
||||
.filter(usedSpace -> MAX_TOTAL_BACKUP_MEDIA_BYTES - usedSpace >= mediaLength)
|
||||
.isPresent());
|
||||
}
|
||||
|
||||
public record StorageDescriptor(int cdn, byte[] key) {}
|
||||
|
||||
/**
|
||||
* Copy an encrypted object to the backup cdn, adding a layer of encryption
|
||||
* <p>
|
||||
* Implementation notes: <p> This method guarantees that any object that gets successfully copied to the backup cdn
|
||||
* will also have an entry for the user in the database. <p>
|
||||
* <p>
|
||||
* However, the converse isn't true; there may be entries in the database that have not made it to the cdn. On list,
|
||||
* these entries are checked against the cdn and removed.
|
||||
*
|
||||
* @return A stage that completes successfully with location of the twice-encrypted object on the backup cdn. The
|
||||
* returned CompletionStage can be completed exceptionally with the following exceptions.
|
||||
* <ul>
|
||||
* <li> {@link InvalidLengthException} If the expectedSourceLength does not match the length of the sourceUri </li>
|
||||
* <li> {@link SourceObjectNotFoundException} If the no object at sourceUri is found </li>
|
||||
* <li> {@link java.io.IOException} If there was a generic IO issue </li>
|
||||
* </ul>
|
||||
*/
|
||||
public CompletableFuture<StorageDescriptor> copyToBackup(
|
||||
final AuthenticatedBackupUser backupUser,
|
||||
final int sourceCdn,
|
||||
final String sourceKey,
|
||||
final int sourceLength,
|
||||
final MediaEncryptionParameters encryptionParameters,
|
||||
final byte[] destinationMediaId) {
|
||||
if (backupUser.backupTier().compareTo(BackupTier.MEDIA) < 0) {
|
||||
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
|
||||
throw Status.PERMISSION_DENIED
|
||||
.withDescription("credential does not support storing media")
|
||||
.asRuntimeException();
|
||||
}
|
||||
if (sourceLength > MAX_MEDIA_OBJECT_SIZE) {
|
||||
throw Status.INVALID_ARGUMENT
|
||||
.withDescription("Invalid sourceObject size")
|
||||
.asRuntimeException();
|
||||
}
|
||||
|
||||
final MessageBackupUploadDescriptor dst = cdn3BackupCredentialGenerator.generateUpload(
|
||||
encodeBackupIdForCdn(backupUser),
|
||||
encodeForCdn(destinationMediaId));
|
||||
|
||||
return this.backupsDb
|
||||
// Write the ddb updates before actually updating backing storage
|
||||
.trackMedia(backupUser, destinationMediaId, sourceLength)
|
||||
|
||||
// copy the objects. On a failure, make a best-effort attempt to reverse the ddb transaction. If cleanup fails
|
||||
// the client may be left with some cleanup to do if they don't eventually upload the media id.
|
||||
.thenCompose(ignored -> remoteStorageManager
|
||||
// actually perform the copy
|
||||
.copy(attachmentReadUri(sourceCdn, sourceKey), sourceLength, encryptionParameters, dst)
|
||||
// best effort: on failure, untrack the copied media
|
||||
.exceptionallyCompose(copyError -> backupsDb.untrackMedia(backupUser, destinationMediaId, sourceLength)
|
||||
.thenCompose(ignoredSuccess -> CompletableFuture.failedFuture(copyError))))
|
||||
|
||||
// indicates where the backup was stored
|
||||
.thenApply(ignore -> new StorageDescriptor(dst.cdn(), destinationMediaId));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the URI for an attachment with the specified key
|
||||
*
|
||||
* @param cdn where the attachment is located
|
||||
* @param key the attachment key
|
||||
* @return A {@link URI} where the attachment can be retrieved
|
||||
*/
|
||||
private URI attachmentReadUri(final int cdn, final String key) {
|
||||
final String baseUri = attachmentCdnBaseUris.get(cdn);
|
||||
if (baseUri == null) {
|
||||
throw Status.INVALID_ARGUMENT.withDescription("Unknown cdn " + cdn).asRuntimeException();
|
||||
}
|
||||
return URI.create("%s/%s".formatted(baseUri, key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate credentials that can be used to read from the backup CDN
|
||||
*
|
||||
* @param backupUser an already ZK authenticated backup user
|
||||
* @return A map of headers to include with CDN requests
|
||||
*/
|
||||
public Map<String, String> generateReadAuth(final AuthenticatedBackupUser backupUser) {
|
||||
if (backupUser.backupTier().compareTo(BackupTier.MESSAGES) < 0) {
|
||||
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
|
||||
throw Status.PERMISSION_DENIED
|
||||
.withDescription("credential does not support read auth operation")
|
||||
.asRuntimeException();
|
||||
|
||||
}
|
||||
final String encodedBackupId = encodeBackupIdForCdn(backupUser);
|
||||
return cdn3BackupCredentialGenerator.readHeaders(encodedBackupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the ZK anonymous backup credential's presentation
|
||||
* <p>
|
||||
* This validates:
|
||||
* <li> The presentation was for a credential issued by the server </li>
|
||||
* <li> The credential is in its redemption window </li>
|
||||
* <li> The backup-id matches a previously committed blinded backup-id and server issued receipt level </li>
|
||||
* <li> The signature of the credential matches an existing publicKey associated with this backup-id </li>
|
||||
*
|
||||
* @param presentation A {@link BackupAuthCredentialPresentation}
|
||||
* @param signature An XEd25519 signature of the presentation bytes
|
||||
* @return On authentication success, the authenticated backup-id and backup-tier encoded in the presentation
|
||||
*/
|
||||
public CompletableFuture<AuthenticatedBackupUser> authenticateBackupUser(
|
||||
final BackupAuthCredentialPresentation presentation,
|
||||
final byte[] signature) {
|
||||
return backupsDb
|
||||
.retrievePublicKey(presentation.getBackupId())
|
||||
.thenApply(optionalPublicKey -> {
|
||||
final byte[] publicKeyBytes = optionalPublicKey
|
||||
.orElseThrow(() -> {
|
||||
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
|
||||
SUCCESS_TAG_NAME, String.valueOf(false),
|
||||
FAILURE_REASON_TAG_NAME, "missing_public_key")
|
||||
.increment();
|
||||
return Status.NOT_FOUND.withDescription("Backup not found").asRuntimeException();
|
||||
});
|
||||
try {
|
||||
final ECPublicKey publicKey = new ECPublicKey(publicKeyBytes);
|
||||
return new AuthenticatedBackupUser(
|
||||
presentation.getBackupId(),
|
||||
verifySignatureAndCheckPresentation(presentation, signature, publicKey));
|
||||
} catch (InvalidKeyException e) {
|
||||
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
|
||||
SUCCESS_TAG_NAME, String.valueOf(false),
|
||||
FAILURE_REASON_TAG_NAME, "invalid_public_key")
|
||||
.increment();
|
||||
logger.error("Invalid publicKey for backupId hash {}",
|
||||
HexFormat.of().formatHex(BackupsDb.hashedBackupId(presentation.getBackupId())), e);
|
||||
throw Status.INTERNAL
|
||||
.withCause(e)
|
||||
.withDescription("Could not deserialize stored public key")
|
||||
.asRuntimeException();
|
||||
}
|
||||
})
|
||||
.thenApply(result -> {
|
||||
Metrics.counter(ZK_AUTHN_COUNTER_NAME, SUCCESS_TAG_NAME, String.valueOf(true)).increment();
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Verify the presentation and return the extracted backup tier
|
||||
*
|
||||
* @param presentation A ZK credential presentation that encodes the backupId and the receipt level of the requester
|
||||
* @return The backup tier this presentation supports
|
||||
*/
|
||||
private BackupTier verifySignatureAndCheckPresentation(
|
||||
final BackupAuthCredentialPresentation presentation,
|
||||
final byte[] signature,
|
||||
final ECPublicKey publicKey) {
|
||||
if (!publicKey.verifySignature(presentation.serialize(), signature)) {
|
||||
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
|
||||
SUCCESS_TAG_NAME, String.valueOf(false),
|
||||
FAILURE_REASON_TAG_NAME, "signature_validation")
|
||||
.increment();
|
||||
throw Status.UNAUTHENTICATED
|
||||
.withDescription("backup auth credential presentation signature verification failed")
|
||||
.asRuntimeException();
|
||||
}
|
||||
try {
|
||||
presentation.verify(clock.instant(), serverSecretParams);
|
||||
} catch (VerificationFailedException e) {
|
||||
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
|
||||
SUCCESS_TAG_NAME, String.valueOf(false),
|
||||
FAILURE_REASON_TAG_NAME, "presentation_verification")
|
||||
.increment();
|
||||
throw Status.UNAUTHENTICATED
|
||||
.withDescription("backup auth credential presentation verification failed")
|
||||
.withCause(e)
|
||||
.asRuntimeException();
|
||||
}
|
||||
|
||||
return BackupTier
|
||||
.fromReceiptLevel(presentation.getReceiptLevel())
|
||||
.orElseThrow(() -> {
|
||||
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
|
||||
SUCCESS_TAG_NAME, String.valueOf(false),
|
||||
FAILURE_REASON_TAG_NAME, "invalid_receipt_level")
|
||||
.increment();
|
||||
return Status.PERMISSION_DENIED.withDescription("invalid receipt level").asRuntimeException();
|
||||
});
|
||||
}
|
||||
|
||||
private static String encodeBackupIdForCdn(final AuthenticatedBackupUser backupUser) {
|
||||
return encodeForCdn(BackupsDb.hashedBackupId(backupUser.backupId()));
|
||||
}
|
||||
|
||||
private static String encodeForCdn(final byte[] bytes) {
|
||||
return Base64.getUrlEncoder().encodeToString(bytes);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import java.net.http.HttpRequest;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Flow;
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import org.reactivestreams.FlowAdapters;
|
||||
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public class BackupMediaEncrypter {
|
||||
|
||||
private final Cipher cipher;
|
||||
private final Mac mac;
|
||||
|
||||
public BackupMediaEncrypter(final MediaEncryptionParameters encryptionParameters) {
|
||||
cipher = initializeCipher(encryptionParameters);
|
||||
mac = initializeMac(encryptionParameters);
|
||||
}
|
||||
|
||||
public int outputSize(final int inputSize) {
|
||||
return cipher.getIV().length + cipher.getOutputSize(inputSize) + mac.getMacLength();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform streaming encryption
|
||||
*
|
||||
* @param sourceBody A source of ByteBuffers, typically from an asynchronous HttpResponse
|
||||
* @return A publisher that returns IV + AES/CBC/PKCS5Padding encrypted source + HMAC(IV + encrypted source) suitable
|
||||
* to write with an asynchronous HttpRequest
|
||||
*/
|
||||
public Flow.Publisher<ByteBuffer> encryptBody(Flow.Publisher<List<ByteBuffer>> sourceBody) {
|
||||
|
||||
// Write IV, encrypted payload, mac
|
||||
final Flux<ByteBuffer> encryptedBody = Flux.concat(
|
||||
Mono.fromSupplier(() -> {
|
||||
mac.update(cipher.getIV());
|
||||
return ByteBuffer.wrap(cipher.getIV());
|
||||
}),
|
||||
Flux.from(FlowAdapters.toPublisher(sourceBody))
|
||||
.flatMap(buffers -> Flux.fromIterable(buffers))
|
||||
.concatMap(byteBuffer -> {
|
||||
final byte[] copy = new byte[byteBuffer.remaining()];
|
||||
byteBuffer.get(copy);
|
||||
final byte[] res = cipher.update(copy);
|
||||
if (res == null) {
|
||||
return Mono.empty();
|
||||
} else {
|
||||
mac.update(res);
|
||||
return Mono.just(ByteBuffer.wrap(res));
|
||||
}
|
||||
}),
|
||||
Mono.fromSupplier(() -> {
|
||||
try {
|
||||
final byte[] finalBytes = cipher.doFinal();
|
||||
mac.update(finalBytes);
|
||||
return ByteBuffer.wrap(finalBytes);
|
||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw ExceptionUtils.wrap(e);
|
||||
}
|
||||
}),
|
||||
Mono.fromSupplier(() -> ByteBuffer.wrap(mac.doFinal())));
|
||||
return FlowAdapters.toFlowPublisher(encryptedBody);
|
||||
}
|
||||
|
||||
private static Mac initializeMac(final MediaEncryptionParameters encryptionParameters) {
|
||||
try {
|
||||
final Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(encryptionParameters.hmacSHA256Key());
|
||||
return mac;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Cipher initializeCipher(final MediaEncryptionParameters encryptionParameters) {
|
||||
try {
|
||||
final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
cipher.init(
|
||||
Cipher.ENCRYPT_MODE,
|
||||
encryptionParameters.aesEncryptionKey(),
|
||||
encryptionParameters.iv());
|
||||
return cipher;
|
||||
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidAlgorithmParameterException | InvalidKeyException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public enum BackupTier {
|
||||
NONE(0),
|
||||
MESSAGES(10),
|
||||
MEDIA(20);
|
||||
|
||||
private static Map<Long, BackupTier> LOOKUP = Arrays.stream(BackupTier.values())
|
||||
.collect(Collectors.toMap(BackupTier::getReceiptLevel, Function.identity()));
|
||||
private long receiptLevel;
|
||||
|
||||
private BackupTier(long receiptLevel) {
|
||||
this.receiptLevel = receiptLevel;
|
||||
}
|
||||
|
||||
long getReceiptLevel() {
|
||||
return receiptLevel;
|
||||
}
|
||||
|
||||
static Optional<BackupTier> fromReceiptLevel(long receiptLevel) {
|
||||
return Optional.ofNullable(LOOKUP.get(receiptLevel));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import io.grpc.Status;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Clock;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import software.amazon.awssdk.core.SdkBytes;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
|
||||
import software.amazon.awssdk.services.dynamodb.model.CancellationReason;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
|
||||
import software.amazon.awssdk.services.dynamodb.model.Delete;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.Put;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure;
|
||||
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem;
|
||||
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException;
|
||||
import software.amazon.awssdk.services.dynamodb.model.Update;
|
||||
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
|
||||
|
||||
/**
|
||||
* Tracks backup metadata in a persistent store.
|
||||
*
|
||||
* It's assumed that the caller has already validated that the backupUser being operated on has valid credentials and
|
||||
* possesses the appropriate {@link BackupTier} to perform the current operation.
|
||||
*/
|
||||
public class BackupsDb {
|
||||
private static final Logger logger = LoggerFactory.getLogger(BackupsDb.class);
|
||||
static final int BACKUP_CDN = 3;
|
||||
|
||||
private final DynamoDbAsyncClient dynamoClient;
|
||||
private final String backupTableName;
|
||||
private final String backupMediaTableName;
|
||||
private final Clock clock;
|
||||
|
||||
// The backups table
|
||||
|
||||
// B: 16 bytes that identifies the backup
|
||||
public static final String KEY_BACKUP_ID_HASH = "U";
|
||||
// N: Time in seconds since epoch of the last backup refresh. This timestamp must be periodically updated to avoid
|
||||
// garbage collection of archive objects.
|
||||
public static final String ATTR_LAST_REFRESH = "R";
|
||||
// N: Time in seconds since epoch of the last backup media refresh. This timestamp can only be updated if the client
|
||||
// has BackupTier.MEDIA, and must be periodically updated to avoid garbage collection of media objects.
|
||||
public static final String ATTR_LAST_MEDIA_REFRESH = "MR";
|
||||
// B: A 32 byte public key that should be used to sign the presentation used to authenticate requests against the
|
||||
// backup-id
|
||||
public static final String ATTR_PUBLIC_KEY = "P";
|
||||
// N: Bytes consumed by this backup
|
||||
public static final String ATTR_MEDIA_BYTES_USED = "MB";
|
||||
// N: Number of media objects in the backup
|
||||
public static final String ATTR_MEDIA_COUNT = "MC";
|
||||
// N: The cdn number where the message backup is stored
|
||||
public static final String ATTR_CDN = "CDN";
|
||||
|
||||
// The stored media table (hashedBackupId, mediaId, cdn, objectLength)
|
||||
|
||||
// B: 15-byte mediaId
|
||||
public static final String KEY_MEDIA_ID = "M";
|
||||
// N: The length of the encrypted media object
|
||||
public static final String ATTR_LENGTH = "L";
|
||||
|
||||
public BackupsDb(
|
||||
final DynamoDbAsyncClient dynamoClient,
|
||||
final String backupTableName,
|
||||
final String backupMediaTableName,
|
||||
final Clock clock) {
|
||||
this.dynamoClient = dynamoClient;
|
||||
this.backupTableName = backupTableName;
|
||||
this.backupMediaTableName = backupMediaTableName;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the public key associated with a backupId.
|
||||
*
|
||||
* @param authenticatedBackupId The backup-id bytes that should be associated with the provided public key
|
||||
* @param authenticatedBackupTier The backup tier
|
||||
* @param publicKey The public key to associate with the backup id
|
||||
* @return A stage that completes when the public key has been set. If the backup-id already has a set public key that
|
||||
* does not match, the stage will be completed exceptionally with a {@link PublicKeyConflictException}
|
||||
*/
|
||||
CompletableFuture<Void> setPublicKey(
|
||||
final byte[] authenticatedBackupId,
|
||||
final BackupTier authenticatedBackupTier,
|
||||
final ECPublicKey publicKey) {
|
||||
final byte[] hashedBackupId = hashedBackupId(authenticatedBackupId);
|
||||
return dynamoClient.updateItem(new UpdateBuilder(backupTableName, authenticatedBackupTier, hashedBackupId)
|
||||
.addSetExpression("#publicKey = :publicKey",
|
||||
Map.entry("#publicKey", ATTR_PUBLIC_KEY),
|
||||
Map.entry(":publicKey", AttributeValues.b(publicKey.serialize())))
|
||||
.setRefreshTimes(clock)
|
||||
.withConditionExpression("attribute_not_exists(#publicKey) OR #publicKey = :publicKey")
|
||||
.updateItemBuilder()
|
||||
.build())
|
||||
.exceptionally(throwable -> {
|
||||
// There was already a row for this backup-id and it contained a different publicKey
|
||||
if (ExceptionUtils.unwrap(throwable) instanceof ConditionalCheckFailedException) {
|
||||
throw ExceptionUtils.wrap(new PublicKeyConflictException());
|
||||
}
|
||||
throw ExceptionUtils.wrap(throwable);
|
||||
})
|
||||
.thenRun(Util.NOOP);
|
||||
}
|
||||
|
||||
CompletableFuture<Optional<byte[]>> retrievePublicKey(byte[] backupId) {
|
||||
final byte[] hashedBackupId = hashedBackupId(backupId);
|
||||
return dynamoClient.getItem(GetItemRequest.builder()
|
||||
.tableName(backupTableName)
|
||||
.key(Map.of(KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId)))
|
||||
.consistentRead(true)
|
||||
.projectionExpression("#publicKey")
|
||||
.expressionAttributeNames(Map.of("#publicKey", ATTR_PUBLIC_KEY))
|
||||
.build())
|
||||
.thenApply(response ->
|
||||
AttributeValues.get(response.item(), ATTR_PUBLIC_KEY)
|
||||
.map(AttributeValue::b)
|
||||
.map(SdkBytes::asByteArray));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add media to the backup media table and update the quota in the backup table
|
||||
*
|
||||
* @param backupUser The
|
||||
* @param mediaId The mediaId to add
|
||||
* @param mediaLength The length of the media before encryption (the length of the source media)
|
||||
* @return A stage that completes successfully once the tables are updated. If the media with the provided id has
|
||||
* previously been tracked with a different length, the stage will complete exceptionally with an
|
||||
* {@link InvalidLengthException}
|
||||
*/
|
||||
CompletableFuture<Void> trackMedia(
|
||||
final AuthenticatedBackupUser backupUser,
|
||||
final byte[] mediaId,
|
||||
final int mediaLength) {
|
||||
final byte[] hashedBackupId = hashedBackupId(backupUser);
|
||||
return dynamoClient
|
||||
.transactWriteItems(TransactWriteItemsRequest.builder().transactItems(
|
||||
|
||||
// Add the media to the media table
|
||||
TransactWriteItem.builder().put(Put.builder()
|
||||
.tableName(backupMediaTableName)
|
||||
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
|
||||
.item(Map.of(
|
||||
KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId),
|
||||
KEY_MEDIA_ID, AttributeValues.b(mediaId),
|
||||
ATTR_CDN, AttributeValues.n(BACKUP_CDN),
|
||||
ATTR_LENGTH, AttributeValues.n(mediaLength)))
|
||||
.conditionExpression("attribute_not_exists(#mediaId)")
|
||||
.expressionAttributeNames(Map.of("#mediaId", KEY_MEDIA_ID))
|
||||
.build()).build(),
|
||||
|
||||
// Update the media quota and TTL
|
||||
TransactWriteItem.builder().update(
|
||||
UpdateBuilder.forUser(backupTableName, backupUser)
|
||||
.setRefreshTimes(clock)
|
||||
.incrementMediaBytes(mediaLength)
|
||||
.incrementMediaCount(1)
|
||||
.transactItemBuilder()
|
||||
.build()).build()).build())
|
||||
.exceptionally(throwable -> {
|
||||
if (ExceptionUtils.unwrap(throwable) instanceof TransactionCanceledException txCancelled) {
|
||||
final long oldItemLength = conditionCheckFailed(txCancelled, 0)
|
||||
.flatMap(item -> Optional.ofNullable(item.get(ATTR_LENGTH)))
|
||||
.map(attr -> Long.parseLong(attr.n()))
|
||||
.orElseThrow(() -> ExceptionUtils.wrap(throwable));
|
||||
if (oldItemLength != mediaLength) {
|
||||
throw new CompletionException(
|
||||
new InvalidLengthException("Previously tried to copy media with a different length. "
|
||||
+ "Provided " + mediaLength + " was " + oldItemLength));
|
||||
}
|
||||
// The client already "paid" for this media, can let them through
|
||||
return null;
|
||||
} else {
|
||||
// rethrow original exception
|
||||
throw ExceptionUtils.wrap(throwable);
|
||||
}
|
||||
})
|
||||
.thenRun(Util.NOOP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove media from backup media table and update the quota in the backup table
|
||||
*
|
||||
* @param backupUser The backup user
|
||||
* @param mediaId The mediaId to add
|
||||
* @param mediaLength The length of the media before encryption (the length of the source media)
|
||||
* @return A stage that completes successfully once the tables are updated
|
||||
*/
|
||||
CompletableFuture<Void> untrackMedia(
|
||||
final AuthenticatedBackupUser backupUser,
|
||||
final byte[] mediaId,
|
||||
final int mediaLength) {
|
||||
final byte[] hashedBackupId = hashedBackupId(backupUser);
|
||||
return dynamoClient.transactWriteItems(TransactWriteItemsRequest.builder().transactItems(
|
||||
TransactWriteItem.builder().delete(Delete.builder()
|
||||
.tableName(backupMediaTableName)
|
||||
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
|
||||
.key(Map.of(
|
||||
KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId),
|
||||
KEY_MEDIA_ID, AttributeValues.b(mediaId)
|
||||
))
|
||||
.conditionExpression("#length = :length")
|
||||
.expressionAttributeNames(Map.of("#length", ATTR_LENGTH))
|
||||
.expressionAttributeValues(Map.of(":length", AttributeValues.n(mediaLength)))
|
||||
.build()).build(),
|
||||
|
||||
// Don't update TTLs, since we're just cleaning up media
|
||||
TransactWriteItem.builder().update(UpdateBuilder.forUser(backupTableName, backupUser)
|
||||
.incrementMediaBytes(-mediaLength)
|
||||
.incrementMediaCount(-1)
|
||||
.transactItemBuilder().build()).build()).build())
|
||||
.exceptionally(error -> {
|
||||
logger.warn("failed cleanup after failed copy operation", error);
|
||||
return null;
|
||||
})
|
||||
.thenRun(Util.NOOP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the last update timestamps for the backupId in the presentation
|
||||
*
|
||||
* @param backupUser an already authorized backup user
|
||||
*/
|
||||
CompletableFuture<Void> ttlRefresh(final AuthenticatedBackupUser backupUser) {
|
||||
// update message backup TTL
|
||||
return dynamoClient.updateItem(UpdateBuilder.forUser(backupTableName, backupUser)
|
||||
.setRefreshTimes(clock)
|
||||
.updateItemBuilder()
|
||||
.build())
|
||||
.thenRun(Util.NOOP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track that a backup will be stored for the user
|
||||
* @param backupUser an already authorized backup user
|
||||
*/
|
||||
CompletableFuture<Void> addMessageBackup(final AuthenticatedBackupUser backupUser) {
|
||||
// this could race with concurrent updates, but the only effect would be last-writer-wins on the timestamp
|
||||
return dynamoClient.updateItem(
|
||||
UpdateBuilder.forUser(backupTableName, backupUser)
|
||||
.setRefreshTimes(clock)
|
||||
.setCdn(BACKUP_CDN)
|
||||
.updateItemBuilder()
|
||||
.build())
|
||||
.thenRun(Util.NOOP);
|
||||
}
|
||||
|
||||
|
||||
record BackupDescription(int cdn, Optional<Long> mediaUsedSpace) {}
|
||||
|
||||
/**
|
||||
* Retrieve information about the backup
|
||||
*
|
||||
* @param backupUser an already authorized backup user
|
||||
* @return A {@link BackupDescription} containing the cdn of the message backup and the total number of media space
|
||||
* bytes used by the backup user.
|
||||
*/
|
||||
CompletableFuture<BackupDescription> describeBackup(final AuthenticatedBackupUser backupUser) {
|
||||
return dynamoClient.getItem(GetItemRequest.builder()
|
||||
.tableName(backupTableName)
|
||||
.key(Map.of(KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId(backupUser))))
|
||||
.projectionExpression("#cdn,#bytesUsed")
|
||||
.expressionAttributeNames(Map.of("#cdn", ATTR_CDN, "#bytesUsed", ATTR_MEDIA_BYTES_USED))
|
||||
.consistentRead(true)
|
||||
.build())
|
||||
.thenApply(response -> {
|
||||
if (!response.hasItem()) {
|
||||
throw Status.NOT_FOUND.withDescription("Backup not found").asRuntimeException();
|
||||
}
|
||||
final int cdn = AttributeValues.get(response.item(), ATTR_CDN)
|
||||
.map(AttributeValue::n)
|
||||
.map(Integer::parseInt)
|
||||
.orElseThrow(() -> Status.NOT_FOUND.withDescription("Stored backup not found").asRuntimeException());
|
||||
|
||||
final Optional<Long> mediaUsed = AttributeValues.get(response.item(), ATTR_MEDIA_BYTES_USED)
|
||||
.map(AttributeValue::n)
|
||||
.map(Long::parseLong);
|
||||
|
||||
return new BackupDescription(cdn, mediaUsed);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Build ddb update statements for the backups table
|
||||
*/
|
||||
private static class UpdateBuilder {
|
||||
|
||||
private final List<String> setStatements = new ArrayList<>();
|
||||
private final Map<String, AttributeValue> attrValues = new HashMap<>();
|
||||
private final Map<String, String> attrNames = new HashMap<>();
|
||||
|
||||
private final String tableName;
|
||||
private final BackupTier backupTier;
|
||||
private final byte[] hashedBackupId;
|
||||
private String conditionExpression = null;
|
||||
|
||||
static UpdateBuilder forUser(String tableName, AuthenticatedBackupUser backupUser) {
|
||||
return new UpdateBuilder(tableName, backupUser.backupTier(), hashedBackupId(backupUser));
|
||||
}
|
||||
|
||||
UpdateBuilder(String tableName, BackupTier backupTier, byte[] hashedBackupId) {
|
||||
this.tableName = tableName;
|
||||
this.backupTier = backupTier;
|
||||
this.hashedBackupId = hashedBackupId;
|
||||
}
|
||||
|
||||
private void addAttrValue(Map.Entry<String, AttributeValue> attrValue) {
|
||||
final AttributeValue old = attrValues.put(attrValue.getKey(), attrValue.getValue());
|
||||
if (old != null && !old.equals(attrValue.getValue())) {
|
||||
throw new IllegalArgumentException("duplicate attrValue key used for different values");
|
||||
}
|
||||
}
|
||||
|
||||
private void addAttrName(Map.Entry<String, String> attrName) {
|
||||
final String oldName = attrNames.put(attrName.getKey(), attrName.getValue());
|
||||
if (oldName != null && !oldName.equals(attrName.getValue())) {
|
||||
throw new IllegalArgumentException("duplicate attrName key used for different attribute names");
|
||||
}
|
||||
}
|
||||
|
||||
private void addAttrs(final Map.Entry<String, String> attrName, final Map.Entry<String, AttributeValue> attrValue) {
|
||||
addAttrName(attrName);
|
||||
addAttrValue(attrValue);
|
||||
}
|
||||
|
||||
UpdateBuilder addSetExpression(
|
||||
final String update,
|
||||
final Map.Entry<String, String> attrName,
|
||||
final Map.Entry<String, AttributeValue> attrValue) {
|
||||
setStatements.add(update);
|
||||
addAttrs(attrName, attrValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
UpdateBuilder addSetExpression(final String update) {
|
||||
setStatements.add(update);
|
||||
return this;
|
||||
}
|
||||
|
||||
UpdateBuilder withConditionExpression(final String conditionExpression) {
|
||||
this.conditionExpression = conditionExpression;
|
||||
return this;
|
||||
}
|
||||
|
||||
UpdateBuilder withConditionExpression(
|
||||
final String conditionExpression,
|
||||
final Map.Entry<String, String> attrName,
|
||||
final Map.Entry<String, AttributeValue> attrValue) {
|
||||
this.addAttrs(attrName, attrValue);
|
||||
this.conditionExpression = conditionExpression;
|
||||
return this;
|
||||
}
|
||||
|
||||
UpdateBuilder setCdn(final int cdn) {
|
||||
return addSetExpression(
|
||||
"#cdn = :cdn",
|
||||
Map.entry("#cdn", ATTR_CDN),
|
||||
Map.entry(":cdn", AttributeValues.n(cdn)));
|
||||
}
|
||||
|
||||
UpdateBuilder incrementMediaCount(long delta) {
|
||||
addAttrName(Map.entry("#mediaCount", ATTR_MEDIA_COUNT));
|
||||
addAttrValue(Map.entry(":zero", AttributeValues.n(0)));
|
||||
addAttrValue(Map.entry(":mediaCountDelta", AttributeValues.n(delta)));
|
||||
addSetExpression("#mediaCount = if_not_exists(#mediaCount, :zero) + :mediaCountDelta");
|
||||
return this;
|
||||
}
|
||||
|
||||
UpdateBuilder incrementMediaBytes(long delta) {
|
||||
addAttrName(Map.entry("#mediaBytes", ATTR_MEDIA_BYTES_USED));
|
||||
addAttrValue(Map.entry(":zero", AttributeValues.n(0)));
|
||||
addAttrValue(Map.entry(":mediaBytesDelta", AttributeValues.n(delta)));
|
||||
addSetExpression("#mediaBytes = if_not_exists(#mediaBytes, :zero) + :mediaBytesDelta");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the lastRefresh time as part of the update
|
||||
* <p>
|
||||
* This always updates lastRefreshTime, and updates lastMediaRefreshTime if the backup user has the appropriate
|
||||
* tier
|
||||
*/
|
||||
UpdateBuilder setRefreshTimes(final Clock clock) {
|
||||
final long refreshTimeSecs = clock.instant().getEpochSecond();
|
||||
addSetExpression("#lastRefreshTime = :lastRefreshTime",
|
||||
Map.entry("#lastRefreshTime", ATTR_LAST_REFRESH),
|
||||
Map.entry(":lastRefreshTime", AttributeValues.n(refreshTimeSecs)));
|
||||
|
||||
if (backupTier.compareTo(BackupTier.MEDIA) >= 0) {
|
||||
// update the media time if we have the appropriate tier
|
||||
addSetExpression("#lastMediaRefreshTime = :lastMediaRefreshTime",
|
||||
Map.entry("#lastMediaRefreshTime", ATTR_LAST_MEDIA_REFRESH),
|
||||
Map.entry(":lastMediaRefreshTime", AttributeValues.n(refreshTimeSecs)));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a non-transactional update
|
||||
*
|
||||
* @return An {@link UpdateItemRequest#builder()} that can be used with updateItem
|
||||
*/
|
||||
UpdateItemRequest.Builder updateItemBuilder() {
|
||||
final UpdateItemRequest.Builder bldr = UpdateItemRequest.builder()
|
||||
.tableName(tableName)
|
||||
.key(Map.of(KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId)))
|
||||
.updateExpression("SET %s".formatted(String.join(",", setStatements)))
|
||||
.expressionAttributeNames(attrNames)
|
||||
.expressionAttributeValues(attrValues);
|
||||
if (this.conditionExpression != null) {
|
||||
bldr.conditionExpression(conditionExpression);
|
||||
}
|
||||
return bldr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a transactional update
|
||||
*
|
||||
* @return An {@link Update#builder()} that can be used with transactItem
|
||||
*/
|
||||
Update.Builder transactItemBuilder() {
|
||||
final Update.Builder bldr = Update.builder()
|
||||
.tableName(tableName)
|
||||
.key(Map.of(KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId)))
|
||||
.updateExpression("SET %s".formatted(String.join(",", setStatements)))
|
||||
.expressionAttributeNames(attrNames)
|
||||
.expressionAttributeValues(attrValues);
|
||||
if (this.conditionExpression != null) {
|
||||
bldr.conditionExpression(conditionExpression);
|
||||
}
|
||||
return bldr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static byte[] hashedBackupId(final AuthenticatedBackupUser backupId) {
|
||||
return hashedBackupId(backupId.backupId());
|
||||
}
|
||||
|
||||
static byte[] hashedBackupId(final byte[] backupId) {
|
||||
try {
|
||||
return Arrays.copyOf(MessageDigest.getInstance("SHA-256").digest(backupId), 16);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a DynamoDb error indicates a condition check failed error, and return the value of the item failed to
|
||||
* update.
|
||||
*
|
||||
* @param e The error returned by {@link DynamoDbAsyncClient#transactWriteItems} attempt
|
||||
* @param itemIndex The index of the item in the transaction that had a condition expression
|
||||
* @return The remote value of the item that failed to update, or empty if the error was not a condition check failure
|
||||
*/
|
||||
private static Optional<Map<String, AttributeValue>> conditionCheckFailed(TransactionCanceledException e,
|
||||
int itemIndex) {
|
||||
if (!e.hasCancellationReasons()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (e.cancellationReasons().size() < itemIndex + 1) {
|
||||
return Optional.empty();
|
||||
}
|
||||
final CancellationReason reason = e.cancellationReasons().get(itemIndex);
|
||||
if (!"ConditionalCheckFailed".equals(reason.code()) || !reason.hasItem()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(reason.item());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.whispersystems.textsecuregcm.attachments.TusConfiguration;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.util.HeaderUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Clock;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
|
||||
public class Cdn3BackupCredentialGenerator {
|
||||
|
||||
public static final String CDN_PATH = "backups";
|
||||
public static final int BACKUP_CDN = 3;
|
||||
|
||||
private static String READ_PERMISSION = "read";
|
||||
private static String WRITE_PERMISSION = "write";
|
||||
private static String PERMISSION_SEPARATOR = "$";
|
||||
|
||||
// Write entities will be of the form 'write$backups/<string>
|
||||
private static final String WRITE_ENTITY_PREFIX = String.format("%s%s%s/", WRITE_PERMISSION, PERMISSION_SEPARATOR,
|
||||
CDN_PATH);
|
||||
// Read entities will be of the form 'read$backups/<string>
|
||||
private static final String READ_ENTITY_PREFIX = String.format("%s%s%s/", READ_PERMISSION, PERMISSION_SEPARATOR,
|
||||
CDN_PATH);
|
||||
|
||||
private final ExternalServiceCredentialsGenerator credentialsGenerator;
|
||||
private final String tusUri;
|
||||
|
||||
public Cdn3BackupCredentialGenerator(final TusConfiguration cfg) {
|
||||
this.tusUri = cfg.uploadUri();
|
||||
this.credentialsGenerator = credentialsGenerator(Clock.systemUTC(), cfg);
|
||||
}
|
||||
|
||||
private static ExternalServiceCredentialsGenerator credentialsGenerator(final Clock clock,
|
||||
final TusConfiguration cfg) {
|
||||
return ExternalServiceCredentialsGenerator
|
||||
.builder(cfg.userAuthenticationTokenSharedSecret())
|
||||
.prependUsername(false)
|
||||
.withClock(clock)
|
||||
.build();
|
||||
}
|
||||
|
||||
public MessageBackupUploadDescriptor generateUpload(final String hashedBackupId, final String objectName) {
|
||||
if (hashedBackupId.isBlank() || objectName.isBlank()) {
|
||||
throw new IllegalArgumentException("Upload descriptors must have non-empty keys");
|
||||
}
|
||||
final String key = "%s/%s".formatted(hashedBackupId, objectName);
|
||||
final String entity = WRITE_ENTITY_PREFIX + key;
|
||||
final ExternalServiceCredentials credentials = credentialsGenerator.generateFor(entity);
|
||||
final String b64Key = Base64.getEncoder().encodeToString(key.getBytes(StandardCharsets.UTF_8));
|
||||
final Map<String, String> headers = Map.of(
|
||||
HttpHeaders.AUTHORIZATION, HeaderUtils.basicAuthHeader(credentials),
|
||||
"Upload-Metadata", String.format("filename %s", b64Key));
|
||||
|
||||
return new MessageBackupUploadDescriptor(
|
||||
BACKUP_CDN,
|
||||
key,
|
||||
headers,
|
||||
tusUri + "/" + CDN_PATH);
|
||||
}
|
||||
|
||||
public Map<String, String> readHeaders(final String hashedBackupId) {
|
||||
if (hashedBackupId.isBlank()) {
|
||||
throw new IllegalArgumentException("Backup subdir name must be non-empty");
|
||||
}
|
||||
final ExternalServiceCredentials credentials = credentialsGenerator.generateFor(
|
||||
READ_ENTITY_PREFIX + hashedBackupId);
|
||||
return Map.of(HttpHeaders.AUTHORIZATION, HeaderUtils.basicAuthHeader(credentials));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.stream.Stream;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RetryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
|
||||
|
||||
public class Cdn3RemoteStorageManager implements RemoteStorageManager {
|
||||
|
||||
private final FaultTolerantHttpClient httpClient;
|
||||
|
||||
public Cdn3RemoteStorageManager(
|
||||
final ScheduledExecutorService retryExecutor,
|
||||
final CircuitBreakerConfiguration circuitBreakerConfiguration,
|
||||
final RetryConfiguration retryConfiguration,
|
||||
final List<String> caCertificates) throws CertificateException {
|
||||
this.httpClient = FaultTolerantHttpClient.newBuilder()
|
||||
.withName("cdn3-remote-storage")
|
||||
.withCircuitBreaker(circuitBreakerConfiguration)
|
||||
.withExecutor(Executors.newCachedThreadPool())
|
||||
.withRetryExecutor(retryExecutor)
|
||||
.withRetry(retryConfiguration)
|
||||
.withConnectTimeout(Duration.ofSeconds(10))
|
||||
.withVersion(HttpClient.Version.HTTP_2)
|
||||
.withTrustedServerCertificates(caCertificates.toArray(new String[0]))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int cdnNumber() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<Void> copy(
|
||||
final URI sourceUri,
|
||||
final int expectedSourceLength,
|
||||
final MediaEncryptionParameters encryptionParameters,
|
||||
final MessageBackupUploadDescriptor uploadDescriptor) {
|
||||
|
||||
if (uploadDescriptor.cdn() != cdnNumber()) {
|
||||
throw new IllegalArgumentException("Cdn3RemoteStorageManager can only copy to cdn3");
|
||||
}
|
||||
|
||||
final BackupMediaEncrypter encrypter = new BackupMediaEncrypter(encryptionParameters);
|
||||
|
||||
final HttpRequest request = HttpRequest.newBuilder().GET().uri(sourceUri).build();
|
||||
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofPublisher()).thenCompose(response -> {
|
||||
if (response.statusCode() == Response.Status.NOT_FOUND.getStatusCode()) {
|
||||
throw new CompletionException(new SourceObjectNotFoundException());
|
||||
} else if (response.statusCode() != Response.Status.OK.getStatusCode()) {
|
||||
throw new CompletionException(new IOException("error reading from source: " + response.statusCode()));
|
||||
}
|
||||
|
||||
final int actualSourceLength = Math.toIntExact(response.headers().firstValueAsLong("Content-Length")
|
||||
.orElseThrow(() -> new CompletionException(new IOException("upstream missing Content-Length"))));
|
||||
|
||||
if (actualSourceLength != expectedSourceLength) {
|
||||
throw new CompletionException(
|
||||
new InvalidLengthException("Provided sourceLength " + expectedSourceLength + " was " + actualSourceLength));
|
||||
}
|
||||
|
||||
final int expectedEncryptedLength = encrypter.outputSize(actualSourceLength);
|
||||
final HttpRequest.BodyPublisher encryptedBody = HttpRequest.BodyPublishers.fromPublisher(
|
||||
encrypter.encryptBody(response.body()), expectedEncryptedLength);
|
||||
|
||||
final String[] headers = Stream.concat(
|
||||
uploadDescriptor.headers().entrySet()
|
||||
.stream()
|
||||
.flatMap(e -> Stream.of(e.getKey(), e.getValue())),
|
||||
Stream.of("Upload-Length", Integer.toString(expectedEncryptedLength), "Tus-Resumable", "1.0.0"))
|
||||
.toArray(String[]::new);
|
||||
|
||||
final HttpRequest put = HttpRequest.newBuilder()
|
||||
.uri(URI.create(uploadDescriptor.signedUploadLocation()))
|
||||
.headers(headers)
|
||||
.POST(encryptedBody)
|
||||
.build();
|
||||
|
||||
return httpClient.sendAsync(put, HttpResponse.BodyHandlers.discarding());
|
||||
})
|
||||
.thenAccept(response -> {
|
||||
if (response.statusCode() != Response.Status.CREATED.getStatusCode() &&
|
||||
response.statusCode() != Response.Status.OK.getStatusCode()) {
|
||||
throw new CompletionException(new IOException("Failed to copy object: " + response.statusCode()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class InvalidLengthException extends IOException {
|
||||
|
||||
public InvalidLengthException(String s) {
|
||||
super(s);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public record MediaEncryptionParameters(
|
||||
SecretKeySpec aesEncryptionKey,
|
||||
SecretKeySpec hmacSHA256Key,
|
||||
IvParameterSpec iv) {
|
||||
|
||||
public MediaEncryptionParameters(byte[] encryptionKey, byte[] macKey, byte[] iv) {
|
||||
this(
|
||||
new SecretKeySpec(encryptionKey, "AES"),
|
||||
new SecretKeySpec(macKey, "HmacSHA256"),
|
||||
new IvParameterSpec(iv));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public record MessageBackupUploadDescriptor(
|
||||
int cdn,
|
||||
String key,
|
||||
Map<String, String> headers,
|
||||
String signedUploadLocation) {}
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class PublicKeyConflictException extends IOException {
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
|
||||
/**
|
||||
* Handles management operations over a external cdn storage system.
|
||||
*/
|
||||
public interface RemoteStorageManager {
|
||||
|
||||
/**
|
||||
* @return The cdn number that this RemoteStorageManager manages
|
||||
*/
|
||||
int cdnNumber();
|
||||
|
||||
/**
|
||||
* Copy and the object from a remote source into the backup, adding an additional layer of encryption
|
||||
*
|
||||
* @param sourceUri The location of the object to copy
|
||||
* @param expectedSourceLength The length of the source object, should match the content-length of the object returned
|
||||
* from the sourceUri.
|
||||
* @param encryptionParameters The encryption keys that should be used to apply an additional layer of encryption to
|
||||
* the object
|
||||
* @param uploadDescriptor The destination, which must be in the cdn returned by {@link #cdnNumber()}
|
||||
* @return A stage that completes successfully when the source has been successfully re-encrypted and copied into
|
||||
* uploadDescriptor. The returned CompletionStage can be completed exceptionally with the following exceptions.
|
||||
* <ul>
|
||||
* <li> {@link InvalidLengthException} If the expectedSourceLength does not match the length of the sourceUri </li>
|
||||
* <li> {@link SourceObjectNotFoundException} If the no object at sourceUri is found </li>
|
||||
* <li> {@link java.io.IOException} If there was a generic IO issue </li>
|
||||
* </ul>
|
||||
*/
|
||||
CompletionStage<Void> copy(
|
||||
URI sourceUri,
|
||||
int expectedSourceLength,
|
||||
MediaEncryptionParameters encryptionParameters,
|
||||
MessageBackupUploadDescriptor uploadDescriptor);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class SourceObjectNotFoundException extends IOException {
|
||||
}
|
||||
@@ -29,9 +29,15 @@ public class CaptchaChecker {
|
||||
@VisibleForTesting
|
||||
static final String SEPARATOR = ".";
|
||||
|
||||
private static final String SHORT_SUFFIX = "-short";
|
||||
|
||||
private final ShortCodeExpander shortCodeExpander;
|
||||
private final Map<String, CaptchaClient> captchaClientMap;
|
||||
|
||||
public CaptchaChecker(final List<CaptchaClient> captchaClients) {
|
||||
public CaptchaChecker(
|
||||
final ShortCodeExpander shortCodeRetriever,
|
||||
final List<CaptchaClient> captchaClients) {
|
||||
this.shortCodeExpander = shortCodeRetriever;
|
||||
this.captchaClientMap = captchaClients.stream()
|
||||
.collect(Collectors.toMap(CaptchaClient::scheme, Function.identity()));
|
||||
}
|
||||
@@ -63,9 +69,17 @@ public class CaptchaChecker {
|
||||
final String prefix = parts[0];
|
||||
final String siteKey = parts[1].toLowerCase(Locale.ROOT).strip();
|
||||
final String action = parts[2];
|
||||
final String token = parts[3];
|
||||
String token = parts[3];
|
||||
|
||||
final CaptchaClient client = this.captchaClientMap.get(prefix);
|
||||
String provider = prefix;
|
||||
if (prefix.endsWith(SHORT_SUFFIX)) {
|
||||
// This is a "short" solution that points to the actual solution. We need to fetch the
|
||||
// full solution before proceeding
|
||||
provider = prefix.substring(0, prefix.length() - SHORT_SUFFIX.length());
|
||||
token = shortCodeExpander.retrieve(token).orElseThrow(() -> new BadRequestException("invalid shortcode"));
|
||||
}
|
||||
|
||||
final CaptchaClient client = this.captchaClientMap.get(provider);
|
||||
if (client == null) {
|
||||
throw new BadRequestException("invalid captcha scheme");
|
||||
}
|
||||
@@ -92,7 +106,7 @@ public class CaptchaChecker {
|
||||
Metrics.counter(ASSESSMENTS_COUNTER_NAME,
|
||||
"action", action,
|
||||
"score", result.getScoreString(),
|
||||
"provider", prefix)
|
||||
"provider", provider)
|
||||
.increment();
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.captcha;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
@@ -16,15 +17,25 @@ import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RetryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicCaptchaConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
public class HCaptchaClient implements CaptchaClient {
|
||||
@@ -34,16 +45,36 @@ public class HCaptchaClient implements CaptchaClient {
|
||||
private static final String ASSESSMENT_REASON_COUNTER_NAME = name(HCaptchaClient.class, "assessmentReason");
|
||||
private static final String INVALID_REASON_COUNTER_NAME = name(HCaptchaClient.class, "invalidReason");
|
||||
private final String apiKey;
|
||||
private final HttpClient client;
|
||||
private final FaultTolerantHttpClient client;
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
|
||||
@VisibleForTesting
|
||||
HCaptchaClient(final String apiKey,
|
||||
final FaultTolerantHttpClient faultTolerantHttpClient,
|
||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
|
||||
this.apiKey = apiKey;
|
||||
this.client = faultTolerantHttpClient;
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
}
|
||||
|
||||
public HCaptchaClient(
|
||||
final String apiKey,
|
||||
final HttpClient client,
|
||||
final ScheduledExecutorService retryExecutor,
|
||||
final CircuitBreakerConfiguration circuitBreakerConfiguration,
|
||||
final RetryConfiguration retryConfiguration,
|
||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
|
||||
this.apiKey = apiKey;
|
||||
this.client = client;
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
this(apiKey,
|
||||
FaultTolerantHttpClient.newBuilder()
|
||||
.withName("hcaptcha")
|
||||
.withCircuitBreaker(circuitBreakerConfiguration)
|
||||
.withExecutor(Executors.newCachedThreadPool())
|
||||
.withRetryExecutor(retryExecutor)
|
||||
.withRetry(retryConfiguration)
|
||||
.withRetryOnException(ex -> ex instanceof IOException)
|
||||
.withConnectTimeout(Duration.ofSeconds(10))
|
||||
.withVersion(HttpClient.Version.HTTP_2)
|
||||
.build(),
|
||||
dynamicConfigurationManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,11 +113,12 @@ public class HCaptchaClient implements CaptchaClient {
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response;
|
||||
final HttpResponse<String> response;
|
||||
try {
|
||||
response = this.client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
} catch (InterruptedException e) {
|
||||
throw new IOException(e);
|
||||
response = this.client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join();
|
||||
} catch (CompletionException e) {
|
||||
logger.warn("failed to make http request to hCaptcha: {}", e.getMessage());
|
||||
throw new IOException(ExceptionUtils.unwrap(e));
|
||||
}
|
||||
|
||||
if (response.statusCode() != Response.Status.OK.getStatusCode()) {
|
||||
|
||||
@@ -58,50 +58,4 @@ public class RegistrationCaptchaManager {
|
||||
? Optional.of(captchaChecker.verify(Action.REGISTRATION, captcha.get(), sourceHost))
|
||||
: Optional.empty();
|
||||
}
|
||||
|
||||
public boolean requiresCaptcha(final String number, final String forwardedFor, String sourceHost,
|
||||
final boolean pushChallengeMatch) {
|
||||
if (testDevices.contains(number)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!pushChallengeMatch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final String countryCode = Util.getCountryCode(number);
|
||||
final String region = Util.getRegion(number);
|
||||
|
||||
DynamicCaptchaConfiguration captchaConfig = dynamicConfigurationManager.getConfiguration()
|
||||
.getCaptchaConfiguration();
|
||||
|
||||
boolean countryFiltered = captchaConfig.getSignupCountryCodes().contains(countryCode) ||
|
||||
captchaConfig.getSignupRegions().contains(region);
|
||||
|
||||
try {
|
||||
rateLimiters.getSmsVoiceIpLimiter().validate(sourceHost);
|
||||
} catch (RateLimitExceededException e) {
|
||||
logger.info("Rate limit exceeded: {}, {} ({})", number, sourceHost, forwardedFor);
|
||||
rateLimitedHostMeter.mark();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
rateLimiters.getSmsVoicePrefixLimiter().validate(Util.getNumberPrefix(number));
|
||||
} catch (RateLimitExceededException e) {
|
||||
logger.info("Prefix rate limit exceeded: {}, {} ({})", number, sourceHost, forwardedFor);
|
||||
rateLimitedPrefixMeter.mark();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (countryFiltered) {
|
||||
countryFilteredHostMeter.mark();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.captcha;
|
||||
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import org.apache.http.HttpStatus;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
public class ShortCodeExpander {
|
||||
private static final String EXPAND_COUNTER_NAME = name(ShortCodeExpander.class, "expand");
|
||||
|
||||
private final HttpClient client;
|
||||
private final URI shortenerHost;
|
||||
|
||||
public ShortCodeExpander(final HttpClient client, final String shortenerHost) {
|
||||
this.client = client;
|
||||
this.shortenerHost = URI.create(shortenerHost);
|
||||
}
|
||||
|
||||
public Optional<String> retrieve(final String shortCode) throws IOException {
|
||||
final URI uri = shortenerHost.resolve(shortCode);
|
||||
final HttpRequest request = HttpRequest.newBuilder().uri(uri).GET().build();
|
||||
|
||||
try {
|
||||
final HttpResponse<String> response = this.client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
Metrics.counter(EXPAND_COUNTER_NAME, "responseCode", Integer.toString(response.statusCode())).increment();
|
||||
return switch (response.statusCode()) {
|
||||
case HttpStatus.SC_OK -> Optional.of(response.body());
|
||||
case HttpStatus.SC_NOT_FOUND -> Optional.empty();
|
||||
default -> throw new IOException("Failed to look up shortcode");
|
||||
};
|
||||
} catch (InterruptedException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class AccountDatabaseCrawlerConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private int chunkSize = 1000;
|
||||
|
||||
public int getChunkSize() {
|
||||
return chunkSize;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,22 +10,19 @@ public class AccountsTableConfiguration extends Table {
|
||||
private final String phoneNumberTableName;
|
||||
private final String phoneNumberIdentifierTableName;
|
||||
private final String usernamesTableName;
|
||||
private final int scanPageSize;
|
||||
|
||||
@JsonCreator
|
||||
public AccountsTableConfiguration(
|
||||
@JsonProperty("tableName") final String tableName,
|
||||
@JsonProperty("phoneNumberTableName") final String phoneNumberTableName,
|
||||
@JsonProperty("phoneNumberIdentifierTableName") final String phoneNumberIdentifierTableName,
|
||||
@JsonProperty("usernamesTableName") final String usernamesTableName,
|
||||
@JsonProperty("scanPageSize") final int scanPageSize) {
|
||||
@JsonProperty("usernamesTableName") final String usernamesTableName) {
|
||||
|
||||
super(tableName);
|
||||
|
||||
this.phoneNumberTableName = phoneNumberTableName;
|
||||
this.phoneNumberIdentifierTableName = phoneNumberIdentifierTableName;
|
||||
this.usernamesTableName = usernamesTableName;
|
||||
this.scanPageSize = scanPageSize;
|
||||
}
|
||||
|
||||
@NotBlank
|
||||
@@ -42,8 +39,4 @@ public class AccountsTableConfiguration extends Table {
|
||||
public String getUsernamesTableName() {
|
||||
return usernamesTableName;
|
||||
}
|
||||
|
||||
public int getScanPageSize() {
|
||||
return scanPageSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
public record AdminEventLoggingConfiguration(
|
||||
@NotBlank String credentials,
|
||||
@NotBlank String secondaryCredentials,
|
||||
@NotEmpty String projectId,
|
||||
@NotEmpty String logName) {
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import javax.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
||||
|
||||
|
||||
public record ApnConfiguration(@NotBlank String teamId,
|
||||
@NotBlank String keyId,
|
||||
public record ApnConfiguration(@NotNull SecretString teamId,
|
||||
@NotNull SecretString keyId,
|
||||
@NotNull SecretString signingKey,
|
||||
@NotBlank String bundleId,
|
||||
boolean sandbox) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod;
|
||||
|
||||
/**
|
||||
* @param merchantId the Braintree merchant ID
|
||||
@@ -27,7 +28,7 @@ public record BraintreeConfiguration(@NotBlank String merchantId,
|
||||
@NotBlank String publicKey,
|
||||
@NotNull SecretString privateKey,
|
||||
@NotBlank String environment,
|
||||
@NotEmpty Set<@NotBlank String> supportedCurrencies,
|
||||
@Valid @NotEmpty Map<PaymentMethod, Set<@NotBlank String>> supportedCurrenciesByPaymentMethod,
|
||||
@NotBlank String graphqlUrl,
|
||||
@NotEmpty Map<String, String> merchantAccounts,
|
||||
@NotNull
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Configuration used to interact with a cdn via HTTP
|
||||
*/
|
||||
public class ClientCdnConfiguration {
|
||||
|
||||
/**
|
||||
* Map from cdn number to the base url for attachments.
|
||||
* <p>
|
||||
* For example, if an attachment with the id 'abc' can be retrieved from cdn 2 at https://example.org/attachments/abc,
|
||||
* the attachment url for 2 should https://example.org/attachments
|
||||
*/
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
Map<Integer, @NotBlank String> attachmentUrls;
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@NotEmpty List<@NotBlank String> caCertificates = new ArrayList<>();
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
RetryConfiguration retry = new RetryConfiguration();
|
||||
|
||||
public List<String> getCaCertificates() {
|
||||
return caCertificates;
|
||||
}
|
||||
|
||||
public CircuitBreakerConfiguration getCircuitBreaker() {
|
||||
return circuitBreaker;
|
||||
}
|
||||
|
||||
public RetryConfiguration getRetry() {
|
||||
return retry;
|
||||
}
|
||||
|
||||
public Map<Integer, String> getAttachmentUrls() {
|
||||
return attachmentUrls;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.Duration;
|
||||
|
||||
public record ClientReleaseConfiguration(@NotNull Duration refreshInterval) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public record CommandStopListenerConfiguration(@NotNull String path) {
|
||||
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import io.dropwizard.db.DataSourceFactory;
|
||||
|
||||
public class DatabaseConfiguration extends DataSourceFactory {
|
||||
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration();
|
||||
|
||||
public CircuitBreakerConfiguration getCircuitBreakerConfiguration() {
|
||||
return circuitBreaker;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,18 +6,13 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.micrometer.datadog.DatadogConfig;
|
||||
import io.micrometer.statsd.StatsdConfig;
|
||||
import io.micrometer.statsd.StatsdFlavor;
|
||||
import java.time.Duration;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
||||
|
||||
public class DatadogConfiguration implements DatadogConfig {
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private SecretString apiKey;
|
||||
public class DogstatsdConfiguration implements StatsdConfig {
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@@ -27,15 +22,6 @@ public class DatadogConfiguration implements DatadogConfig {
|
||||
@NotBlank
|
||||
private String environment;
|
||||
|
||||
@JsonProperty
|
||||
@Min(1)
|
||||
private int batchSize = 5_000;
|
||||
|
||||
@Override
|
||||
public String apiKey() {
|
||||
return apiKey.value();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration step() {
|
||||
return step;
|
||||
@@ -46,17 +32,13 @@ public class DatadogConfiguration implements DatadogConfig {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int batchSize() {
|
||||
return batchSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String hostTag() {
|
||||
return "host";
|
||||
public StatsdFlavor flavor() {
|
||||
return StatsdFlavor.DATADOG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get(final String key) {
|
||||
// We have no Micrometer key/value pairs to report, so always return `null`
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -5,37 +5,27 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.time.Duration;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Positive;
|
||||
|
||||
public class DynamoDbClientConfiguration {
|
||||
public record DynamoDbClientConfiguration(@NotBlank String region,
|
||||
@NotNull Duration clientExecutionTimeout,
|
||||
@NotNull Duration clientRequestTimeout,
|
||||
@Positive int maxConnections) {
|
||||
|
||||
private final String region;
|
||||
private final Duration clientExecutionTimeout;
|
||||
private final Duration clientRequestTimeout;
|
||||
public DynamoDbClientConfiguration {
|
||||
if (clientExecutionTimeout == null) {
|
||||
clientExecutionTimeout = Duration.ofSeconds(30);
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public DynamoDbClientConfiguration(
|
||||
@JsonProperty("region") final String region,
|
||||
@JsonProperty("clientExcecutionTimeout") final Duration clientExecutionTimeout,
|
||||
@JsonProperty("clientRequestTimeout") final Duration clientRequestTimeout) {
|
||||
this.region = region;
|
||||
this.clientExecutionTimeout = clientExecutionTimeout != null ? clientExecutionTimeout : Duration.ofSeconds(30);
|
||||
this.clientRequestTimeout = clientRequestTimeout != null ? clientRequestTimeout : Duration.ofSeconds(10);
|
||||
}
|
||||
if (clientRequestTimeout == null) {
|
||||
clientRequestTimeout = Duration.ofSeconds(10);
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
public String getRegion() {
|
||||
return region;
|
||||
}
|
||||
|
||||
public Duration getClientExecutionTimeout() {
|
||||
return clientExecutionTimeout;
|
||||
}
|
||||
|
||||
public Duration getClientRequestTimeout() {
|
||||
return clientRequestTimeout;
|
||||
if (maxConnections == 0) {
|
||||
maxConnections = 50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ public class DynamoDbTables {
|
||||
}
|
||||
|
||||
private final AccountsTableConfiguration accounts;
|
||||
|
||||
private final Table backups;
|
||||
private final Table backupMedia;
|
||||
private final Table clientReleases;
|
||||
private final Table deletedAccounts;
|
||||
private final Table deletedAccountsLock;
|
||||
private final IssuedReceiptsTableConfiguration issuedReceipts;
|
||||
@@ -55,8 +59,7 @@ public class DynamoDbTables {
|
||||
private final Table kemKeys;
|
||||
private final Table kemLastResortKeys;
|
||||
private final TableWithExpiration messages;
|
||||
private final Table pendingAccounts;
|
||||
private final Table pendingDevices;
|
||||
private final TableWithExpiration onetimeDonations;
|
||||
private final Table phoneNumberIdentifiers;
|
||||
private final Table profiles;
|
||||
private final Table pushChallenge;
|
||||
@@ -69,6 +72,9 @@ public class DynamoDbTables {
|
||||
|
||||
public DynamoDbTables(
|
||||
@JsonProperty("accounts") final AccountsTableConfiguration accounts,
|
||||
@JsonProperty("backups") final Table backups,
|
||||
@JsonProperty("backupMedia") final Table backupMedia,
|
||||
@JsonProperty("clientReleases") final Table clientReleases,
|
||||
@JsonProperty("deletedAccounts") final Table deletedAccounts,
|
||||
@JsonProperty("deletedAccountsLock") final Table deletedAccountsLock,
|
||||
@JsonProperty("issuedReceipts") final IssuedReceiptsTableConfiguration issuedReceipts,
|
||||
@@ -77,8 +83,7 @@ public class DynamoDbTables {
|
||||
@JsonProperty("pqKeys") final Table kemKeys,
|
||||
@JsonProperty("pqLastResortKeys") final Table kemLastResortKeys,
|
||||
@JsonProperty("messages") final TableWithExpiration messages,
|
||||
@JsonProperty("pendingAccounts") final Table pendingAccounts,
|
||||
@JsonProperty("pendingDevices") final Table pendingDevices,
|
||||
@JsonProperty("onetimeDonations") final TableWithExpiration onetimeDonations,
|
||||
@JsonProperty("phoneNumberIdentifiers") final Table phoneNumberIdentifiers,
|
||||
@JsonProperty("profiles") final Table profiles,
|
||||
@JsonProperty("pushChallenge") final Table pushChallenge,
|
||||
@@ -90,6 +95,9 @@ public class DynamoDbTables {
|
||||
@JsonProperty("verificationSessions") final Table verificationSessions) {
|
||||
|
||||
this.accounts = accounts;
|
||||
this.backups = backups;
|
||||
this.backupMedia = backupMedia;
|
||||
this.clientReleases = clientReleases;
|
||||
this.deletedAccounts = deletedAccounts;
|
||||
this.deletedAccountsLock = deletedAccountsLock;
|
||||
this.issuedReceipts = issuedReceipts;
|
||||
@@ -98,8 +106,7 @@ public class DynamoDbTables {
|
||||
this.kemKeys = kemKeys;
|
||||
this.kemLastResortKeys = kemLastResortKeys;
|
||||
this.messages = messages;
|
||||
this.pendingAccounts = pendingAccounts;
|
||||
this.pendingDevices = pendingDevices;
|
||||
this.onetimeDonations = onetimeDonations;
|
||||
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
|
||||
this.profiles = profiles;
|
||||
this.pushChallenge = pushChallenge;
|
||||
@@ -117,6 +124,24 @@ public class DynamoDbTables {
|
||||
return accounts;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public Table getBackups() {
|
||||
return backups;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public Table getBackupMedia() {
|
||||
return backupMedia;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public Table getClientReleases() {
|
||||
return clientReleases;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public Table getDeletedAccounts() {
|
||||
@@ -167,14 +192,8 @@ public class DynamoDbTables {
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public Table getPendingAccounts() {
|
||||
return pendingAccounts;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public Table getPendingDevices() {
|
||||
return pendingDevices;
|
||||
public TableWithExpiration getOnetimeDonations() {
|
||||
return onetimeDonations;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import io.dropwizard.util.Strings;
|
||||
import io.dropwizard.validation.ValidationMethod;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
||||
|
||||
public record GcpAttachmentsConfiguration(@NotBlank String domain,
|
||||
@@ -20,6 +20,6 @@ public record GcpAttachmentsConfiguration(@NotBlank String domain,
|
||||
@SuppressWarnings("unused")
|
||||
@ValidationMethod(message = "pathPrefix must be empty or start with /")
|
||||
public boolean isPathPrefixValid() {
|
||||
return Strings.isNullOrEmpty(pathPrefix) || pathPrefix.startsWith("/");
|
||||
return StringUtils.isEmpty(pathPrefix) || pathPrefix.startsWith("/");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class GraphiteConfiguration {
|
||||
@JsonProperty
|
||||
private String host;
|
||||
|
||||
@JsonProperty
|
||||
private int port;
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return host != null && port != 0;
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,35 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
||||
|
||||
public record HCaptchaConfiguration(@NotNull SecretString apiKey) {
|
||||
public class HCaptchaConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
SecretString apiKey;
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
RetryConfiguration retry = new RetryConfiguration();
|
||||
|
||||
|
||||
public SecretString getApiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
public CircuitBreakerConfiguration getCircuitBreaker() {
|
||||
return circuitBreaker;
|
||||
}
|
||||
|
||||
public RetryConfiguration getRetry() {
|
||||
return retry;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
|
||||
|
||||
public record LinkDeviceSecretConfiguration(SecretBytes secret) {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.Duration;
|
||||
|
||||
public record MessageByteLimitCardinalityEstimatorConfiguration(@NotNull Duration period) {}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import javax.validation.Valid;
|
||||
@@ -18,7 +19,8 @@ import javax.validation.constraints.Positive;
|
||||
*/
|
||||
public record OneTimeDonationConfiguration(@Valid ExpiringLevelConfiguration boost,
|
||||
@Valid ExpiringLevelConfiguration gift,
|
||||
Map<String, @Valid OneTimeDonationCurrencyConfiguration> currencies) {
|
||||
Map<String, @Valid OneTimeDonationCurrencyConfiguration> currencies,
|
||||
BigDecimal sepaMaximumEuros) {
|
||||
|
||||
/**
|
||||
* @param badge the numeric donation level ID
|
||||
|
||||
@@ -7,7 +7,6 @@ package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
public record RecaptchaConfiguration(@NotEmpty String projectPath, @NotEmpty String credentialConfigurationJson,
|
||||
@NotEmpty String secondaryCredentialConfigurationJson) {
|
||||
public record RecaptchaConfiguration(@NotEmpty String projectPath, @NotEmpty String credentialConfigurationJson) {
|
||||
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import javax.validation.constraints.NotBlank;
|
||||
public record RegistrationServiceConfiguration(@NotBlank String host,
|
||||
int port,
|
||||
@NotBlank String credentialConfigurationJson,
|
||||
@NotBlank String secondaryCredentialConfigurationJson,
|
||||
@NotBlank String identityTokenAudience,
|
||||
@NotBlank String registrationCaCertificate) {
|
||||
}
|
||||
|
||||
@@ -5,15 +5,9 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public record RemoteConfigConfiguration(@NotNull Set<String> authorizedUsers,
|
||||
@NotNull String requiredHostedDomain,
|
||||
@NotNull @NotEmpty List<String> audiences,
|
||||
@NotNull Map<String, String> globalConfig) {
|
||||
public record RemoteConfigConfiguration(@NotNull Map<String, String> globalConfig) {
|
||||
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.util.List;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
|
||||
|
||||
public class SecureBackupServiceConfiguration {
|
||||
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private SecretBytes userAuthenticationTokenSharedSecret;
|
||||
|
||||
@NotBlank
|
||||
@JsonProperty
|
||||
private String uri;
|
||||
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
private List<@NotBlank String> backupCaCertificates;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration();
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private RetryConfiguration retry = new RetryConfiguration();
|
||||
|
||||
public SecretBytes userAuthenticationTokenSharedSecret() {
|
||||
return userAuthenticationTokenSharedSecret;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setUri(final String uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public String getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setBackupCaCertificates(final List<String> backupCaCertificates) {
|
||||
this.backupCaCertificates = backupCaCertificates;
|
||||
}
|
||||
|
||||
public List<String> getBackupCaCertificates() {
|
||||
return backupCaCertificates;
|
||||
}
|
||||
|
||||
public CircuitBreakerConfiguration getCircuitBreakerConfiguration() {
|
||||
return circuitBreaker;
|
||||
}
|
||||
|
||||
public RetryConfiguration getRetryConfiguration() {
|
||||
return retry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
public record SecureValueRecovery3Configuration(
|
||||
@NotBlank String uri,
|
||||
@ExactlySize(32) SecretBytes userAuthenticationTokenSharedSecret,
|
||||
@ExactlySize(32) SecretBytes userIdTokenSharedSecret,
|
||||
@NotEmpty List<@NotBlank String> svrCaCertificates,
|
||||
@NotNull @Valid CircuitBreakerConfiguration circuitBreaker,
|
||||
@NotNull @Valid RetryConfiguration retry) {
|
||||
|
||||
public SecureValueRecovery3Configuration {
|
||||
if (circuitBreaker == null) {
|
||||
circuitBreaker = new CircuitBreakerConfiguration();
|
||||
}
|
||||
|
||||
if (retry == null) {
|
||||
retry = new RetryConfiguration();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
public record ShortCodeExpanderConfiguration(String baseUrl) {
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.List;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
public class SqsConfiguration {
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
private String accessKey;
|
||||
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
private String accessSecret;
|
||||
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
private List<String> queueUrls;
|
||||
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
private String region = "us-east-1";
|
||||
|
||||
public String getAccessKey() {
|
||||
return accessKey;
|
||||
}
|
||||
|
||||
public String getAccessSecret() {
|
||||
return accessSecret;
|
||||
}
|
||||
|
||||
public List<String> getQueueUrls() {
|
||||
return queueUrls;
|
||||
}
|
||||
|
||||
public String getRegion() {
|
||||
return region;
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,18 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod;
|
||||
|
||||
public record StripeConfiguration(@NotNull SecretString apiKey,
|
||||
@NotNull SecretBytes idempotencyKeyGenerator,
|
||||
@NotBlank String boostDescription,
|
||||
@NotEmpty Set<@NotBlank String> supportedCurrencies) {
|
||||
@Valid @NotEmpty Map<PaymentMethod, Set<@NotBlank String>> supportedCurrenciesByPaymentMethod) {
|
||||
}
|
||||
|
||||
@@ -20,13 +20,16 @@ import javax.validation.constraints.NotNull;
|
||||
public class SubscriptionConfiguration {
|
||||
|
||||
private final Duration badgeGracePeriod;
|
||||
private final Duration badgeExpiration;
|
||||
private final Map<Long, SubscriptionLevelConfiguration> levels;
|
||||
|
||||
@JsonCreator
|
||||
public SubscriptionConfiguration(
|
||||
@JsonProperty("badgeGracePeriod") @Valid Duration badgeGracePeriod,
|
||||
@JsonProperty("badgeExpiration") @Valid Duration badgeExpiration,
|
||||
@JsonProperty("levels") @Valid Map<@NotNull @Min(1) Long, @NotNull @Valid SubscriptionLevelConfiguration> levels) {
|
||||
this.badgeGracePeriod = badgeGracePeriod;
|
||||
this.badgeExpiration = badgeExpiration;
|
||||
this.levels = levels;
|
||||
}
|
||||
|
||||
@@ -34,6 +37,11 @@ public class SubscriptionConfiguration {
|
||||
return badgeGracePeriod;
|
||||
}
|
||||
|
||||
// This is the badge expiration time starting from when a payment successfully completes
|
||||
public Duration getBadgeExpiration() {
|
||||
return badgeExpiration;
|
||||
}
|
||||
|
||||
public Map<Long, SubscriptionLevelConfiguration> getLevels() {
|
||||
return levels;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public record TlsKeyStoreConfiguration(@NotNull SecretString password) {
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import javax.validation.constraints.NotNull;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class TurnUriConfiguration {
|
||||
@JsonProperty
|
||||
@@ -22,7 +23,8 @@ public class TurnUriConfiguration {
|
||||
/**
|
||||
* Enrolled numbers will always get this uri list
|
||||
*/
|
||||
private Set<String> enrolledNumbers = Collections.emptySet();
|
||||
@JsonProperty
|
||||
private Set<UUID> enrolledAcis = Collections.emptySet();
|
||||
|
||||
public List<String> getUris() {
|
||||
return uris;
|
||||
@@ -32,7 +34,7 @@ public class TurnUriConfiguration {
|
||||
return weight;
|
||||
}
|
||||
|
||||
public Set<String> getEnrolledNumbers() {
|
||||
return Collections.unmodifiableSet(enrolledNumbers);
|
||||
public Set<UUID> getEnrolledAcis() {
|
||||
return Collections.unmodifiableSet(enrolledAcis);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,36 +43,10 @@ public class DynamicCaptchaConfiguration {
|
||||
@NotNull
|
||||
private Map<Action, BigDecimal> scoreFloorByAction = Collections.emptyMap();
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private Set<String> signupCountryCodes = Collections.emptySet();
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private Set<String> signupRegions = Collections.emptySet();
|
||||
|
||||
public BigDecimal getScoreFloor() {
|
||||
return scoreFloor;
|
||||
}
|
||||
|
||||
public Set<String> getSignupCountryCodes() {
|
||||
return signupCountryCodes;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setSignupCountryCodes(Set<String> numbers) {
|
||||
this.signupCountryCodes = numbers;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setSignupRegions(final Set<String> signupRegions) {
|
||||
this.signupRegions = signupRegions;
|
||||
}
|
||||
|
||||
public Set<String> getSignupRegions() {
|
||||
return signupRegions;
|
||||
}
|
||||
|
||||
public boolean isAllowHCaptcha() {
|
||||
return allowHCaptcha;
|
||||
}
|
||||
|
||||
@@ -39,10 +39,6 @@ public class DynamicConfiguration {
|
||||
@Valid
|
||||
private DynamicCaptchaConfiguration captcha = new DynamicCaptchaConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private DynamicPushLatencyConfiguration pushLatency = new DynamicPushLatencyConfiguration(Collections.emptyMap());
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private DynamicTurnConfiguration turn = new DynamicTurnConfiguration();
|
||||
@@ -61,11 +57,12 @@ public class DynamicConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
DynamicDeliveryLatencyConfiguration deliveryLatency = new DynamicDeliveryLatencyConfiguration(Collections.emptyMap());
|
||||
DynamicInboundMessageByteLimitConfiguration inboundMessageByteLimit = new DynamicInboundMessageByteLimitConfiguration(true);
|
||||
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
DynamicInboundMessageByteLimitConfiguration inboundMessageByteLimit = new DynamicInboundMessageByteLimitConfiguration(true);
|
||||
DynamicRegistrationConfiguration registrationConfiguration = new DynamicRegistrationConfiguration(false);
|
||||
|
||||
public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(
|
||||
final String experimentName) {
|
||||
@@ -93,10 +90,6 @@ public class DynamicConfiguration {
|
||||
return captcha;
|
||||
}
|
||||
|
||||
public DynamicPushLatencyConfiguration getPushLatencyConfiguration() {
|
||||
return pushLatency;
|
||||
}
|
||||
|
||||
public DynamicTurnConfiguration getTurnConfiguration() {
|
||||
return turn;
|
||||
}
|
||||
@@ -113,11 +106,11 @@ public class DynamicConfiguration {
|
||||
return ecPreKeyMigration;
|
||||
}
|
||||
|
||||
public DynamicDeliveryLatencyConfiguration getDeliveryLatencyConfiguration() {
|
||||
return deliveryLatency;
|
||||
}
|
||||
|
||||
public DynamicInboundMessageByteLimitConfiguration getInboundMessageByteLimitConfiguration() {
|
||||
return inboundMessageByteLimit;
|
||||
}
|
||||
|
||||
public DynamicRegistrationConfiguration getRegistrationConfiguration() {
|
||||
return registrationConfiguration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public record DynamicDeliveryLatencyConfiguration(Map<ClientPlatform, Set<Semver>> instrumentedVersions) {
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public record DynamicPushLatencyConfiguration(Map<ClientPlatform, Set<Semver>> instrumentedVersions) {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
public record DynamicRegistrationConfiguration(boolean squashDeclinedAttemptErrors) {}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user