diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75d892a8..23e19225 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ '21', '23' ] + java: [ '21', '24' ] steps: - uses: actions/checkout@v4 @@ -26,11 +26,22 @@ jobs: distribution: 'zulu' java-version: ${{ matrix.java }} - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v4 with: dependency-graph: generate-and-submit + - name: Install asciidoc + run: sudo apt update && sudo apt --no-install-recommends install -y asciidoc-base - name: Build with Gradle run: ./gradlew --no-daemon build + - name: Build man page + run: | + cd man + make install + - name: Add man page to archive + run: | + version=$(tar tf build/distributions/signal-cli-*.tar | head -n1 | sed 's|signal-cli-\([^/]*\)/.*|\1|') + echo $version + tar --transform="flags=r;s|man|signal-cli-${version}/man|" -rf build/distributions/signal-cli-${version}.tar man/man{1,5} - name: Compress archive run: gzip -n -9 build/distributions/signal-cli-*.tar - name: Archive production artifacts @@ -58,3 +69,28 @@ jobs: with: name: signal-cli-native path: build/native/nativeCompile/signal-cli + + build-client: + strategy: + matrix: + os: + - ubuntu + - macos + - windows + runs-on: ${{ matrix.os }}-latest + defaults: + run: + working-directory: ./client + steps: + - uses: actions/checkout@v4 + - name: Install rust + run: rustup default stable + - name: Build client + run: cargo build --release --verbose + - name: Archive production artifacts + uses: actions/upload-artifact@v4 + with: + name: signal-cli-client-${{ matrix.os }} + path: | + client/target/release/signal-cli-client + client/target/release/signal-cli-client.exe diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ff13469a..f778268a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,7 +35,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 # Override language selection by uncommenting this and choosing your languages # with: # languages: go, javascript, csharp, python, cpp, java @@ -43,7 +43,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -57,4 +57,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1f4027d..b7ce03f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -182,7 +182,7 @@ jobs: tar xf ./"${ARCHIVE_DIR}"/*.tar.gz rm -r signal-cli-archive-* signal-cli-native mkdir -p build/install/ - mv ./signal-cli-*/ build/install/signal-cli + mv ./signal-cli-"${GITHUB_REF_NAME#v}"/ build/install/signal-cli - name: Build Image id: build_image diff --git a/CHANGELOG.md b/CHANGELOG.md index f49b8b81..74d0bdc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,120 @@ # Changelog +## [Unreleased] + +## [0.13.18] - 2025-07-16 + +Requires libsignal-client version 0.76.3. + +### Added + +- Added `--view-once` parameter to send command to send view once images + +### Fixed + +- Handle rate limit exception correctly when querying usernames + +### Improved + +- Shut down when dbus daemon connection goes away unexpectedly +- In daemon mode, exit immediately if account check fails at startup +- Improve behavior when sending to devices that have no available prekeys + +## [0.13.17] - 2025-06-28 + +Requires libsignal-client version 0.76.0. + +### Fixed + +- Fix issue when loading an older inactive group +- Close attachment input streams after upload +- Fix storage sync behavior with unhandled fields + +### Changed + +- Improve behavior when pin data doesn't exist on the server + +## [0.13.16] - 2025-06-07 + +Requires libsignal-client version 0.73.2. + +### Changed + +- Ensure every sent message gets a unique timestamp + +## [0.13.15] - 2025-05-08 + +Requires libsignal-client version 0.70.0. + +### Fixed + +- Fix native access warning with Java 24 +- Fix storage sync loop due to old removed e164 field + +### Changed + +- Increased compatibility of native build with older/virtual CPUs + +## [0.13.14] - 2025-04-06 + +Requires libsignal-client version 0.68.1. + +### Fixed + +- Fix pre key import from old data files + +### Changed + +- Use websocket connection instead of HTTP for more requests +- Improve handling of messages with decryption error + +## [0.13.13] - 2025-02-28 + +Requires libsignal-client version 0.66.2. + +### Added +- Allow setting nickname and note with `updateContact` command + +### Fixed +- Fix syncing nickname, note and expiration timer +- Fix check for registered users with a proxy +- Improve handling of storage records not yet supported by signal-cli +- Fix contact sync for networks requiring proxy + +## [0.13.12] - 2025-01-18 + +Requires libsignal-client version 0.65.2. + +### Fixed + +- Fix sync of contact nick name +- Fix incorrectly marking recipients as unregistered after sync +- Fix cause of database deadlock (Thanks @dukhaSlayer) +- Fix parsing of account query param in events http endpoint + +### Changed + +- Enable sqlite WAL journal\_mode for improved performance + +## [0.13.11] - 2024-12-26 + +Requires libsignal-client version 0.64.0. + +### Fixed +- Fix issue with receiving messages that have an invalid destination + +## [0.13.10] - 2024-11-30 + +Requires libsignal-client version 0.62.0. + +### Fixed + +- Fix receiving some unusual contact sync messages +- Fix receiving expiration timer updates + +### Improved +- Add support for new storage encryption scheme + ## [0.13.9] - 2024-10-28 ### Fixed diff --git a/README.md b/README.md index 03455d12..c7228c09 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ For this use-case, it has a daemon mode with JSON-RPC interface ([man page](http and D-BUS interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)) . For the JSON-RPC interface there's also a simple [example client](https://github.com/AsamK/signal-cli/tree/master/client), written in Rust. +signal-cli needs to be kept up-to-date to keep up with Signal-Server changes. +The official Signal clients expire after three months and then the Signal-Server can make incompatible changes. +So signal-cli releases older than three months may not work correctly. + ## Installation You can [build signal-cli](#building) yourself or use @@ -55,8 +59,15 @@ of all country codes.) signal-cli -a ACCOUNT register - You can register Signal using a landline number. In this case you can skip SMS verification process and jump directly - to the voice call verification by adding the `--voice` switch at the end of above register command. + You can register Signal using a landline number. In this case, you need to follow the procedure below: + * Attempt a SMS verification process first (`signal-cli -a ACCOUNT register`) + * You will get an error `400 (InvalidTransportModeException)`, this is normal + * Wait 60 seconds + * Attempt a voice call verification by adding the `--voice` switch and wait for the call: + + ```sh + signal-cli -a ACCOUNT register --voice + ``` Registering may require solving a CAPTCHA challenge: [Registration with captcha](https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha) @@ -72,6 +83,12 @@ of all country codes.) signal-cli -a ACCOUNT send -m "This is a message" RECIPIENT ``` +* Send a message to a username, usernames need to be prefixed with `u:` + + ```sh + signal-cli -a ACCOUNT send -m "This is a message" u:USERNAME.000 + ``` + * Pipe the message content from another process. uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT diff --git a/build.gradle.kts b/build.gradle.kts index 8e74a5bf..ea69d97e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,10 +3,13 @@ plugins { application eclipse `check-lib-versions` - id("org.graalvm.buildtools.native") version "0.10.3" + id("org.graalvm.buildtools.native") version "0.10.6" } -version = "0.13.9" +allprojects { + group = "org.asamk" + version = "0.13.19-SNAPSHOT" +} java { sourceCompatibility = JavaVersion.VERSION_21 @@ -21,6 +24,7 @@ java { application { mainClass.set("org.asamk.signal.Main") + applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED") } graalvmNative { @@ -29,6 +33,7 @@ graalvmNative { buildArgs.add("--install-exit-handlers") buildArgs.add("-Dfile.encoding=UTF-8") buildArgs.add("-J-Dfile.encoding=UTF-8") + buildArgs.add("-march=compatibility") resources.autodetect() configurationFileDirectories.from(file("graalvm-config-dir")) if (System.getenv("GRAALVM_HOME") == null) { @@ -43,7 +48,41 @@ graalvmNative { } } +val artifactType = Attribute.of("artifactType", String::class.java) +val minified = Attribute.of("minified", Boolean::class.javaObjectType) dependencies { + attributesSchema { + attribute(minified) + } + artifactTypes.getByName("jar") { + attributes.attribute(minified, false) + } +} + +configurations.runtimeClasspath.configure { + attributes { + attribute(minified, true) + } +} +val excludePatterns = mapOf( + "libsignal-client" to setOf( + "libsignal_jni_testing_amd64.so", + "signal_jni_testing_amd64.dll", + "libsignal_jni_testing_amd64.dylib", + "libsignal_jni_testing_aarch64.dylib", + ) +) + +dependencies { + registerTransform(JarFileExcluder::class) { + from.attribute(minified, false).attribute(artifactType, "jar") + to.attribute(minified, true).attribute(artifactType, "jar") + + parameters { + excludeFilesByArtifact = excludePatterns + } + } + implementation(libs.bouncycastle) implementation(libs.jackson.databind) implementation(libs.argparse4j) @@ -51,7 +90,7 @@ dependencies { implementation(libs.slf4j.api) implementation(libs.slf4j.jul) implementation(libs.logback) - implementation(project(":lib")) + implementation(project(":libsignal-cli")) } configurations { @@ -75,12 +114,13 @@ tasks.withType { attributes( "Implementation-Title" to project.name, "Implementation-Version" to project.version, - "Main-Class" to application.mainClass.get() + "Main-Class" to application.mainClass.get(), + "Enable-Native-Access" to "ALL-UNNAMED", ) } } -task("fatJar", type = Jar::class) { +tasks.register("fatJar", type = Jar::class) { archiveBaseName.set("${project.name}-fat") exclude( "META-INF/*.SF", @@ -89,9 +129,11 @@ task("fatJar", type = Jar::class) { "META-INF/NOTICE*", "META-INF/LICENSE*", "META-INF/INDEX.LIST", - "**/module-info.class" + "**/module-info.class", ) duplicatesStrategy = DuplicatesStrategy.WARN - from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) + doFirst { + from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) + } with(tasks.jar.get()) } diff --git a/buildSrc/src/main/kotlin/CheckLibVersionsPlugin.kt b/buildSrc/src/main/kotlin/CheckLibVersionsPlugin.kt index eb74acb2..ae8ae4de 100644 --- a/buildSrc/src/main/kotlin/CheckLibVersionsPlugin.kt +++ b/buildSrc/src/main/kotlin/CheckLibVersionsPlugin.kt @@ -1,12 +1,10 @@ @file:Suppress("DEPRECATION") -import groovy.util.XmlSlurper -import groovy.util.slurpersupport.GPathResult -import org.codehaus.groovy.runtime.ResourceGroovyMethods import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.artifacts.Dependency +import javax.xml.parsers.DocumentBuilderFactory class CheckLibVersionsPlugin : Plugin { override fun apply(project: Project) { @@ -28,10 +26,10 @@ class CheckLibVersionsPlugin : Plugin { val name = dependency.name val metaDataUrl = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml" try { - val url = ResourceGroovyMethods.toURL(metaDataUrl) - val metaDataText = ResourceGroovyMethods.getText(url) - val metadata = XmlSlurper().parseText(metaDataText) - val newest = (metadata.getProperty("versioning") as GPathResult).getProperty("latest") + val dbf = DocumentBuilderFactory.newInstance() + val db = dbf.newDocumentBuilder() + val doc = db.parse(metaDataUrl); + val newest = doc.getElementsByTagName("latest").item(0).textContent if (version != newest.toString()) { println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}") } diff --git a/buildSrc/src/main/kotlin/ExcludeFileFromJar.kt b/buildSrc/src/main/kotlin/ExcludeFileFromJar.kt new file mode 100644 index 00000000..25862cc9 --- /dev/null +++ b/buildSrc/src/main/kotlin/ExcludeFileFromJar.kt @@ -0,0 +1,53 @@ +import org.gradle.api.artifacts.transform.* +import org.gradle.api.file.FileSystemLocation +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream + +@CacheableTransform +abstract class JarFileExcluder : TransformAction { + interface Parameters : TransformParameters { + @get:Input + var excludeFilesByArtifact: Map> + } + + @get:PathSensitive(PathSensitivity.NAME_ONLY) + @get:InputArtifact + abstract val inputArtifact: Provider + + override + fun transform(outputs: TransformOutputs) { + val fileName = inputArtifact.get().asFile.name + for (entry in parameters.excludeFilesByArtifact) { + if (fileName.startsWith(entry.key)) { + val nameWithoutExtension = fileName.substring(0, fileName.lastIndexOf(".")) + excludeFiles(inputArtifact.get().asFile, entry.value, outputs.file("${nameWithoutExtension}.jar")) + return + } + } + outputs.file(inputArtifact) + } + + private fun excludeFiles(artifact: File, excludeFiles: Set, jarFile: File) { + ZipInputStream(FileInputStream(artifact)).use { input -> + ZipOutputStream(FileOutputStream(jarFile)).use { output -> + var entry = input.nextEntry + while (entry != null) { + if (!excludeFiles.contains(entry.name)) { + output.putNextEntry(entry) + input.copyTo(output) + output.closeEntry() + } + + entry = input.nextEntry + } + } + } + } +} diff --git a/client/Cargo.lock b/client/Cargo.lock index 5118fb2a..01f28e0c 100644 --- a/client/Cargo.lock +++ b/client/Cargo.lock @@ -1,27 +1,27 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -34,49 +34,50 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell_polyfill", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.87" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", @@ -89,25 +90,19 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "autocfg" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" - [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -118,21 +113,21 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "bytes" -version = "1.7.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.1.18" +version = "1.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" +checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" dependencies = [ "shlex", ] @@ -145,15 +140,15 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "clap" -version = "4.5.17" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" dependencies = [ "clap_builder", "clap_derive", @@ -161,9 +156,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.17" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" dependencies = [ "anstream", "anstyle", @@ -174,9 +169,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" dependencies = [ "heck", "proc-macro2", @@ -186,15 +181,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "combine" @@ -208,9 +203,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -223,19 +218,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "equivalent" -version = "1.0.1" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -255,24 +261,24 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -281,15 +287,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" @@ -299,9 +305,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-macro", @@ -314,9 +320,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", @@ -325,15 +331,15 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "h2" -version = "0.4.6" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" dependencies = [ "atomic-waker", "bytes", @@ -350,9 +356,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" [[package]] name = "heck" @@ -360,17 +366,11 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "http" -version = "1.1.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -389,12 +389,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "pin-project-lite", @@ -402,15 +402,15 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.4.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", @@ -428,11 +428,10 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.3" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", "http", "hyper", "hyper-util", @@ -446,44 +445,153 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.7" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" dependencies = [ "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", "hyper", + "libc", "pin-project-lite", "socket2", "tokio", - "tower", "tower-service", "tracing", ] [[package]] -name = "idna" -version = "0.5.0" +name = "icu_collections" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] name = "indexmap" -version = "2.5.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown", ] +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -492,22 +600,24 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jni" -version = "0.19.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", + "cfg-if", "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.69", "walkdir", + "windows-sys 0.45.0", ] [[package]] @@ -518,9 +628,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jsonrpsee" -version = "0.24.3" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec465b607a36dc5dd45d48b7689bc83f679f66a3ac6b6b21cc787a11e0f8685" +checksum = "1fba77a59c4c644fd48732367624d1bcf6f409f9c9a286fbc71d2f1fc0b2ea16" dependencies = [ "jsonrpsee-core", "jsonrpsee-http-client", @@ -531,9 +641,9 @@ dependencies = [ [[package]] name = "jsonrpsee-core" -version = "0.24.3" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e942c55635fbf5dc421938b8558a8141c7e773720640f4f1dbe1f4164ca4e221" +checksum = "693c93cbb7db25f4108ed121304b671a36002c2db67dff2ee4391a688c738547" dependencies = [ "async-trait", "bytes", @@ -547,19 +657,19 @@ dependencies = [ "rustc-hash", "serde", "serde_json", - "thiserror", + "thiserror 2.0.12", "tokio", "tokio-stream", + "tower", "tracing", ] [[package]] name = "jsonrpsee-http-client" -version = "0.24.3" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33774602df12b68a2310b38a535733c477ca4a498751739f89fe8dbbb62ec4c" +checksum = "6962d2bd295f75e97dd328891e58fce166894b974c1f7ce2e7597f02eeceb791" dependencies = [ - "async-trait", "base64", "http-body", "hyper", @@ -571,18 +681,17 @@ dependencies = [ "rustls-platform-verifier", "serde", "serde_json", - "thiserror", + "thiserror 2.0.12", "tokio", "tower", - "tracing", "url", ] [[package]] name = "jsonrpsee-proc-macros" -version = "0.24.3" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b07a2daf52077ab1b197aea69a5c990c060143835bf04c77070e98903791715" +checksum = "2fa4f5daed39f982a1bb9d15449a28347490ad42b212f8eaa2a2a344a0dce9e9" dependencies = [ "heck", "proc-macro-crate", @@ -593,109 +702,92 @@ dependencies = [ [[package]] name = "jsonrpsee-types" -version = "0.24.3" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b67d6e008164f027afbc2e7bb79662650158d26df200040282d2aa1cbb093b" +checksum = "66df7256371c45621b3b7d2fb23aea923d577616b9c0e9c0b950a6ea5c2be0ca" dependencies = [ "http", "serde", "serde_json", - "thiserror", + "thiserror 2.0.12", ] [[package]] name = "libc" -version = "0.2.158" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "log" -version = "0.4.22" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "miniz_oxide" -version = "0.7.4" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ - "adler", + "adler2", ] [[package]] name = "mio" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ - "hermit-abi", "libc", "wasi", - "windows-sys 0.52.0", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", + "windows-sys 0.59.0", ] [[package]] name = "object" -version = "0.36.4" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "percent-encoding" @@ -705,18 +797,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", @@ -725,9 +817,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -736,77 +828,85 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "proc-macro-crate" -version = "3.2.0" +name = "potential_utf" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.37" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", "getrandom", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustc-hash" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "0.38.36" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f55e80d50763938498dd5ebb18647174e0c76dc38c5505294bb224624f30f36" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.23.12" +version = "0.23.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" dependencies = [ "log", "once_cell", @@ -819,38 +919,30 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ "openssl-probe", - "rustls-pemfile", "rustls-pki-types", "schannel", "security-framework", ] [[package]] -name = "rustls-pemfile" -version = "2.1.3" +name = "rustls-pki-types" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ - "base64", - "rustls-pki-types", + "zeroize", ] -[[package]] -name = "rustls-pki-types" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" - [[package]] name = "rustls-platform-verifier" -version = "0.3.4" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afbb878bdfdf63a336a5e63561b1835e7a8c91524f51621db870169eac84b490" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" dependencies = [ "core-foundation", "core-foundation-sys", @@ -863,8 +955,8 @@ dependencies = [ "rustls-webpki", "security-framework", "security-framework-sys", - "webpki-roots", - "winapi", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", ] [[package]] @@ -875,9 +967,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.102.7" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "ring", "rustls-pki-types", @@ -886,9 +978,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -901,32 +993,31 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.24" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "security-framework" -version = "2.11.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ "bitflags", "core-foundation", "core-foundation-sys", "libc", - "num-bigint", "security-framework-sys", ] [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -934,18 +1025,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.210" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -954,9 +1045,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -979,44 +1070,40 @@ dependencies = [ "clap", "futures-util", "jsonrpsee", - "log", "serde", "serde_json", - "thiserror", + "thiserror 2.0.12", "tokio", "tokio-util", ] [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] -name = "spin" -version = "0.9.8" +name = "stable_deref_trait" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "strsim" @@ -1032,9 +1119,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.77" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -1042,29 +1129,16 @@ dependencies = [ ] [[package]] -name = "terminal_size" -version = "0.3.0" +name = "sync_wrapper" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" -dependencies = [ - "rustix", - "windows-sys 0.48.0", -] +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] -name = "thiserror" -version = "1.0.63" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -1072,31 +1146,78 @@ dependencies = [ ] [[package]] -name = "tinyvec" -version = "1.8.0" +name = "terminal_size" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ - "tinyvec_macros", + "rustix", + "windows-sys 0.59.0", ] [[package]] -name = "tinyvec_macros" -version = "0.1.1" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] [[package]] name = "tokio" -version = "1.40.0" +version = "1.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "pin-project-lite", + "slab", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -1104,9 +1225,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", @@ -1115,20 +1236,19 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ "rustls", - "rustls-pki-types", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -1137,9 +1257,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -1150,15 +1270,15 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "toml_datetime", @@ -1167,18 +1287,16 @@ dependencies = [ [[package]] name = "tower" -version = "0.4.13" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "pin-project", "pin-project-lite", - "tokio", + "sync_wrapper", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -1195,11 +1313,10 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ - "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1207,9 +1324,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", @@ -1218,9 +1335,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", ] @@ -1231,26 +1348,11 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" - [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-normalization" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" -dependencies = [ - "tinyvec", -] +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "untrusted" @@ -1260,15 +1362,21 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1296,35 +1404,28 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "webpki-roots" -version = "0.26.5" +name = "webpki-root-certs" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.1", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86138b15b2b7d561bc4469e77027b8dd005a43dc502e9031d1f5afc8ce1f280e" dependencies = [ "rustls-pki-types", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - [[package]] name = "winapi-util" version = "0.1.9" @@ -1334,19 +1435,13 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.42.2", ] [[package]] @@ -1368,18 +1463,27 @@ dependencies = [ ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows-sys" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-targets 0.53.2", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -1391,7 +1495,7 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", @@ -1399,10 +1503,26 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" +name = "windows-targets" +version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" @@ -1411,10 +1531,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" +name = "windows_aarch64_gnullvm" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" @@ -1423,10 +1549,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "windows_i686_gnu" -version = "0.48.5" +name = "windows_aarch64_msvc" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" @@ -1434,6 +1566,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" @@ -1441,10 +1579,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_i686_msvc" -version = "0.48.5" +name = "windows_i686_gnullvm" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" @@ -1453,10 +1597,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" +name = "windows_i686_msvc" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" @@ -1465,10 +1615,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" +name = "windows_x86_64_gnu" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" @@ -1477,10 +1633,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" +name = "windows_x86_64_gnullvm" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" @@ -1489,16 +1651,106 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "winnow" -version = "0.6.18" +name = "windows_x86_64_msvc" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/client/Cargo.toml b/client/Cargo.toml index b60f2041..aff9ede4 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -1,18 +1,17 @@ [package] name = "signal-cli-client" version = "0.0.1" -edition = "2021" +edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] anyhow = "1" clap = { version = "4", features = ["cargo", "derive", "wrap_help"] } -log = "0.4" serde = "1" serde_json = "1" tokio = { version = "1", features = ["rt", "macros", "net", "rt-multi-thread"] } -jsonrpsee = { version = "0.24", features = [ +jsonrpsee = { version = "0.25", features = [ "macros", "async-client", "http-client", @@ -20,4 +19,4 @@ jsonrpsee = { version = "0.24", features = [ bytes = "1" tokio-util = "0.7" futures-util = "0.3" -thiserror = "1" +thiserror = "2" diff --git a/client/src/cli.rs b/client/src/cli.rs index a64ab511..7fa5d1c5 100644 --- a/client/src/cli.rs +++ b/client/src/cli.rs @@ -15,6 +15,7 @@ pub struct Cli { pub json_rpc_tcp: Option>, /// UNIX socket address and port of signal-cli daemon + #[cfg(unix)] #[arg(long, conflicts_with = "json_rpc_tcp")] pub json_rpc_socket: Option>, @@ -84,6 +85,8 @@ pub enum CliCommands { }, GetUserStatus { recipient: Vec, + #[arg(long)] + username: Vec, }, JoinGroup { #[arg(long)] @@ -176,6 +179,9 @@ pub enum CliCommands { #[arg(short = 'a', long)] attachment: Vec, + #[arg(long)] + view_once: bool, + #[arg(long)] mention: Vec, @@ -413,7 +419,7 @@ pub enum CliCommands { #[arg(long = "about-emoji")] about_emoji: Option, - #[arg(long = "mobile-coin-address")] + #[arg(long = "mobile-coin-address", visible_alias = "mobilecoin-address")] mobile_coin_address: Option, #[arg(long)] diff --git a/client/src/jsonrpc.rs b/client/src/jsonrpc.rs index b085cde5..66ef9d9d 100644 --- a/client/src/jsonrpc.rs +++ b/client/src/jsonrpc.rs @@ -70,6 +70,7 @@ pub trait Rpc { &self, account: Option, recipients: Vec, + usernames: Vec, ) -> Result; #[method(name = "joinGroup", param_kind = map)] @@ -182,6 +183,7 @@ pub trait Rpc { endSession: bool, message: String, attachments: Vec, + viewOnce: bool, mentions: Vec, textStyle: Vec, quoteTimestamp: Option, @@ -190,10 +192,10 @@ pub trait Rpc { quoteMention: Vec, quoteTextStyle: Vec, quoteAttachment: Vec, - preview_url: Option, - preview_title: Option, - preview_description: Option, - preview_image: Option, + previewUrl: Option, + previewTitle: Option, + previewDescription: Option, + previewImage: Option, sticker: Option, storyTimestamp: Option, storyAuthor: Option, @@ -409,6 +411,7 @@ pub async fn connect_tcp( Ok(ClientBuilder::default().build_with_tokio(sender, receiver)) } +#[cfg(unix)] pub async fn connect_unix( socket_path: impl AsRef, ) -> Result { @@ -417,6 +420,6 @@ pub async fn connect_unix( Ok(ClientBuilder::default().build_with_tokio(sender, receiver)) } -pub async fn connect_http(uri: &str) -> Result { +pub async fn connect_http(uri: &str) -> Result, Error> { HttpClientBuilder::default().build(uri) } diff --git a/client/src/main.rs b/client/src/main.rs index 35c1ab22..ac12331d 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -60,8 +60,13 @@ async fn handle_command( .delete_local_account_data(cli.account, ignore_registered) .await } - CliCommands::GetUserStatus { recipient } => { - client.get_user_status(cli.account, recipient).await + CliCommands::GetUserStatus { + recipient, + username, + } => { + client + .get_user_status(cli.account, recipient, username) + .await } CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await, CliCommands::Link { name } => { @@ -70,7 +75,7 @@ async fn handle_command( .await .map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))? .device_link_uri; - println!("{}", url); + println!("{url}"); client.finish_link(url, name).await } CliCommands::ListAccounts => client.list_accounts().await, @@ -139,6 +144,7 @@ async fn handle_command( end_session, message, attachment, + view_once, mention, text_style, quote_timestamp, @@ -165,6 +171,7 @@ async fn handle_command( end_session, message.unwrap_or_default(), attachment, + view_once, mention, text_style, quote_timestamp, @@ -477,23 +484,30 @@ async fn connect(cli: Cli) -> Result { handle_command(cli, client).await } else { - let socket_path = cli - .json_rpc_socket - .clone() - .unwrap_or(None) - .or_else(|| { - std::env::var_os("XDG_RUNTIME_DIR").map(|runtime_dir| { - PathBuf::from(runtime_dir) - .join(DEFAULT_SOCKET_SUFFIX) - .into() + #[cfg(windows)] + { + Err(RpcError::Custom("Invalid socket".into())) + } + #[cfg(unix)] + { + let socket_path = cli + .json_rpc_socket + .clone() + .unwrap_or(None) + .or_else(|| { + std::env::var_os("XDG_RUNTIME_DIR").map(|runtime_dir| { + PathBuf::from(runtime_dir) + .join(DEFAULT_SOCKET_SUFFIX) + .into() + }) }) - }) - .unwrap_or_else(|| ("/run".to_owned() + DEFAULT_SOCKET_SUFFIX).into()); - let client = jsonrpc::connect_unix(socket_path) - .await - .map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?; + .unwrap_or_else(|| ("/run".to_owned() + DEFAULT_SOCKET_SUFFIX).into()); + let client = jsonrpc::connect_unix(socket_path) + .await + .map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?; - handle_command(cli, client).await + handle_command(cli, client).await + } } } diff --git a/client/src/transports/mod.rs b/client/src/transports/mod.rs index 04f4390f..ed1963a0 100644 --- a/client/src/transports/mod.rs +++ b/client/src/transports/mod.rs @@ -1,10 +1,8 @@ use futures_util::{stream::StreamExt, Sink, SinkExt, Stream}; -use jsonrpsee::core::{ - async_trait, - client::{ReceivedMessage, TransportReceiverT, TransportSenderT}, -}; +use jsonrpsee::core::client::{ReceivedMessage, TransportReceiverT, TransportSenderT}; use thiserror::Error; +#[cfg(unix)] pub mod ipc; mod stream_codec; pub mod tcp; @@ -21,7 +19,6 @@ struct Sender> { inner: T, } -#[async_trait] impl + Unpin + 'static> TransportSenderT for Sender { @@ -31,7 +28,7 @@ impl + Unpin + 'static> T self.inner .send(body) .await - .map_err(|e| Errors::Other(format!("{:?}", e)))?; + .map_err(|e| Errors::Other(format!("{e:?}")))?; Ok(()) } @@ -39,7 +36,7 @@ impl + Unpin + 'static> T self.inner .close() .await - .map_err(|e| Errors::Other(format!("{:?}", e)))?; + .map_err(|e| Errors::Other(format!("{e:?}")))?; Ok(()) } } @@ -48,7 +45,6 @@ struct Receiver { inner: T, } -#[async_trait] impl> + Unpin + 'static> TransportReceiverT for Receiver { @@ -58,7 +54,7 @@ impl> + Unpin + 'static> match self.inner.next().await { None => Err(Errors::Closed), Some(Ok(msg)) => Ok(ReceivedMessage::Text(msg)), - Some(Err(e)) => Err(Errors::Other(format!("{:?}", e))), + Some(Err(e)) => Err(Errors::Other(format!("{e:?}"))), } } } diff --git a/client/src/transports/stream_codec.rs b/client/src/transports/stream_codec.rs index 6f77306f..e46233cb 100644 --- a/client/src/transports/stream_codec.rs +++ b/client/src/transports/stream_codec.rs @@ -41,7 +41,7 @@ impl Decoder for StreamCodec { match str::from_utf8(line.as_ref()) { Ok(s) => Ok(Some(s.to_string())), - Err(_) => Err(io::Error::new(io::ErrorKind::Other, "invalid UTF-8")), + Err(_) => Err(io::Error::other("invalid UTF-8")), } } else { Ok(None) diff --git a/data/org.asamk.SignalCli.metainfo.xml b/data/org.asamk.SignalCli.metainfo.xml index 46921033..c6c07411 100644 --- a/data/org.asamk.SignalCli.metainfo.xml +++ b/data/org.asamk.SignalCli.metainfo.xml @@ -45,6 +45,33 @@ intense + + https://github.com/AsamK/signal-cli/releases/tag/v0.13.18 + + + https://github.com/AsamK/signal-cli/releases/tag/v0.13.17 + + + https://github.com/AsamK/signal-cli/releases/tag/v0.13.16 + + + https://github.com/AsamK/signal-cli/releases/tag/v0.13.15 + + + https://github.com/AsamK/signal-cli/releases/tag/v0.13.14 + + + https://github.com/AsamK/signal-cli/releases/tag/v0.13.13 + + + https://github.com/AsamK/signal-cli/releases/tag/v0.13.12 + + + https://github.com/AsamK/signal-cli/releases/tag/v0.13.11 + + + https://github.com/AsamK/signal-cli/releases/tag/v0.13.10 + https://github.com/AsamK/signal-cli/releases/tag/v0.13.9 diff --git a/graalvm-config-dir/jni-config.json b/graalvm-config-dir/jni-config.json index 381dca9e..523a6c03 100644 --- a/graalvm-config-dir/jni-config.json +++ b/graalvm-config-dir/jni-config.json @@ -27,6 +27,10 @@ { "name":"java.lang.ClassNotFoundException" }, +{ + "name":"java.lang.Enum", + "methods":[{"name":"ordinal","parameterTypes":[] }] +}, { "name":"java.lang.IllegalArgumentException", "methods":[{"name":"","parameterTypes":["java.lang.String"] }] @@ -48,9 +52,13 @@ { "name":"java.lang.String" }, +{ + "name":"java.lang.Thread", + "methods":[{"name":"currentThread","parameterTypes":[] }, {"name":"getStackTrace","parameterTypes":[] }] +}, { "name":"java.lang.Throwable", - "methods":[{"name":"getMessage","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }] + "methods":[{"name":"getMessage","parameterTypes":[] }, {"name":"setStackTrace","parameterTypes":["java.lang.StackTraceElement[]"] }, {"name":"toString","parameterTypes":[] }] }, { "name":"java.lang.UnsatisfiedLinkError", @@ -88,7 +96,11 @@ }, { "name":"org.signal.libsignal.internal.CompletableFuture", - "methods":[{"name":"","parameterTypes":[] }, {"name":"complete","parameterTypes":["java.lang.Object"] }] + "methods":[{"name":"","parameterTypes":[] }, {"name":"complete","parameterTypes":["java.lang.Object"] }, {"name":"completeExceptionally","parameterTypes":["java.lang.Throwable"] }, {"name":"setCancellationId","parameterTypes":["long"] }] +}, +{ + "name":"org.signal.libsignal.internal.NativeHandleGuard$SimpleOwner", + "methods":[{"name":"unsafeNativeHandleWithoutGuard","parameterTypes":[] }] }, { "name":"org.signal.libsignal.net.CdsiLookupResponse", @@ -110,6 +122,14 @@ { "name":"org.signal.libsignal.net.ChatService$ResponseAndDebugInfo" }, +{ + "name":"org.signal.libsignal.net.NetworkException", + "methods":[{"name":"","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"org.signal.libsignal.net.RetryLaterException", + "methods":[{"name":"","parameterTypes":["long"] }] +}, { "name":"org.signal.libsignal.protocol.DuplicateMessageException", "methods":[{"name":"","parameterTypes":["java.lang.String"] }] @@ -187,6 +207,9 @@ "name":"org.signal.libsignal.protocol.state.IdentityKeyStore$Direction", "fields":[{"name":"RECEIVING"}, {"name":"SENDING"}] }, +{ + "name":"org.signal.libsignal.protocol.state.IdentityKeyStore$IdentityChange" +}, { "name":"org.signal.libsignal.protocol.state.KyberPreKeyRecord", "fields":[{"name":"unsafeHandle"}] @@ -228,6 +251,10 @@ "name":"org.signal.libsignal.usernames.CannotBeEmptyException", "methods":[{"name":"","parameterTypes":["java.lang.String"] }] }, +{ + "name":"org.signal.libsignal.usernames.DiscriminatorCannotBeZeroException", + "methods":[{"name":"","parameterTypes":["java.lang.String"] }] +}, { "name":"org.signal.libsignal.usernames.MissingSeparatorException", "methods":[{"name":"","parameterTypes":["java.lang.String"] }] diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index 8a80f9c7..0bfd1cdf 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -39,9 +39,24 @@ { "name":"[Ljava.sql.Statement;" }, +{ + "name":"[Lorg.asamk.signal.commands.ListStickerPacksCommand$JsonStickerPack$JsonSticker;" +}, { "name":"[Lorg.asamk.signal.json.JsonAttachment;" }, +{ + "name":"[Lorg.asamk.signal.json.JsonCallMessage$IceUpdate;" +}, +{ + "name":"[Lorg.asamk.signal.json.JsonContactAddress;" +}, +{ + "name":"[Lorg.asamk.signal.json.JsonContactEmail;" +}, +{ + "name":"[Lorg.asamk.signal.json.JsonContactPhone;" +}, { "name":"[Lorg.asamk.signal.json.JsonMention;" }, @@ -51,6 +66,9 @@ { "name":"[Lorg.asamk.signal.json.JsonQuotedAttachment;" }, +{ + "name":"[Lorg.asamk.signal.json.JsonSharedContact;" +}, { "name":"[Lorg.asamk.signal.json.JsonSyncReadMessage;" }, @@ -60,6 +78,9 @@ { "name":"[Lorg.asamk.signal.manager.storage.accounts.AccountsStorage$Account;" }, +{ + "name":"[Lorg.asamk.signal.manager.storage.stickerPacks.JsonStickerPack$JsonSticker;" +}, { "name":"[Lorg.whispersystems.signalservice.api.groupsv2.TemporalCredential;" }, @@ -124,6 +145,13 @@ "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl", "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"com.squareup.wire.Message", + "methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }] +}, +{ + "name":"com.squareup.wire.ProtoAdapter" +}, { "name":"com.squareup.wire.internal.ImmutableList", "allDeclaredFields":true, @@ -209,9 +237,14 @@ { "name":"java.io.FilePermission" }, +{ + "name":"java.io.OutputStream" +}, { "name":"java.io.Serializable", - "allDeclaredMethods":true + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredClasses":true }, { "name":"java.lang.Boolean", @@ -426,6 +459,12 @@ "allDeclaredFields":true, "queryAllDeclaredMethods":true }, +{ + "name":"java.util.ImmutableCollections$List12", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, { "name":"java.util.ImmutableCollections$ListN", "allDeclaredFields":true, @@ -577,6 +616,9 @@ { "name":"kotlin.String" }, +{ + "name":"kotlin.Unit" +}, { "name":"kotlin.collections.AbstractCollection", "allDeclaredFields":true, @@ -629,6 +671,13 @@ { "name":"long[]" }, +{ + "name":"okhttp3.internal.connection.RealConnectionPool", + "fields":[{"name":"addressStates"}] +}, +{ + "name":"okio.BufferedSink" +}, { "name":"okio.ByteString" }, @@ -990,7 +1039,7 @@ "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true, - "methods":[{"name":"display","parameterTypes":[] }, {"name":"family","parameterTypes":[] }, {"name":"given","parameterTypes":[] }, {"name":"middle","parameterTypes":[] }, {"name":"prefix","parameterTypes":[] }, {"name":"suffix","parameterTypes":[] }] + "methods":[{"name":"display","parameterTypes":[] }, {"name":"family","parameterTypes":[] }, {"name":"given","parameterTypes":[] }, {"name":"middle","parameterTypes":[] }, {"name":"nickname","parameterTypes":[] }, {"name":"prefix","parameterTypes":[] }, {"name":"suffix","parameterTypes":[] }] }, { "name":"org.asamk.signal.json.JsonContactPhone", @@ -1025,7 +1074,7 @@ "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true, - "methods":[{"name":"groupId","parameterTypes":[] }, {"name":"type","parameterTypes":[] }] + "methods":[{"name":"groupId","parameterTypes":[] }, {"name":"groupName","parameterTypes":[] }, {"name":"revision","parameterTypes":[] }, {"name":"type","parameterTypes":[] }] }, { "name":"org.asamk.signal.json.JsonMention", @@ -1247,7 +1296,7 @@ "allDeclaredFields":true, "queryAllDeclaredMethods":true, "queryAllDeclaredConstructors":true, - "methods":[{"name":"","parameterTypes":["int","long","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"","parameterTypes":["int","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"","parameterTypes":["int","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"aciAccountData","parameterTypes":[] }, {"name":"deviceId","parameterTypes":[] }, {"name":"encryptedDeviceName","parameterTypes":[] }, {"name":"isMultiDevice","parameterTypes":[] }, {"name":"number","parameterTypes":[] }, {"name":"password","parameterTypes":[] }, {"name":"pinMasterKey","parameterTypes":[] }, {"name":"pniAccountData","parameterTypes":[] }, {"name":"profileKey","parameterTypes":[] }, {"name":"registered","parameterTypes":[] }, {"name":"registrationLockPin","parameterTypes":[] }, {"name":"serviceEnvironment","parameterTypes":[] }, {"name":"storageKey","parameterTypes":[] }, {"name":"timestamp","parameterTypes":[] }, {"name":"username","parameterTypes":[] }, {"name":"usernameLinkEntropy","parameterTypes":[] }, {"name":"usernameLinkServerId","parameterTypes":[] }, {"name":"version","parameterTypes":[] }] + "methods":[{"name":"","parameterTypes":["int","long","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"","parameterTypes":["int","long","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"","parameterTypes":["int","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"","parameterTypes":["int","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"accountEntropyPool","parameterTypes":[] }, {"name":"aciAccountData","parameterTypes":[] }, {"name":"deviceId","parameterTypes":[] }, {"name":"encryptedDeviceName","parameterTypes":[] }, {"name":"isMultiDevice","parameterTypes":[] }, {"name":"mediaRootBackupKey","parameterTypes":[] }, {"name":"number","parameterTypes":[] }, {"name":"password","parameterTypes":[] }, {"name":"pinMasterKey","parameterTypes":[] }, {"name":"pniAccountData","parameterTypes":[] }, {"name":"profileKey","parameterTypes":[] }, {"name":"registered","parameterTypes":[] }, {"name":"registrationLockPin","parameterTypes":[] }, {"name":"serviceEnvironment","parameterTypes":[] }, {"name":"storageKey","parameterTypes":[] }, {"name":"timestamp","parameterTypes":[] }, {"name":"username","parameterTypes":[] }, {"name":"usernameLinkEntropy","parameterTypes":[] }, {"name":"usernameLinkServerId","parameterTypes":[] }, {"name":"version","parameterTypes":[] }] }, { "name":"org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData", @@ -1364,6 +1413,12 @@ "name":"org.asamk.signal.manager.storage.profiles.LegacyProfileStore$ProfileStoreDeserializer", "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"org.asamk.signal.manager.storage.profiles.LegacySignalProfile", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, { "name":"org.asamk.signal.manager.storage.profiles.LegacySignalProfileEntry", "allDeclaredFields":true, @@ -1499,6 +1554,10 @@ "name":"org.bouncycastle.jcajce.provider.asymmetric.COMPOSITE$Mappings", "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.CONTEXT$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, { "name":"org.bouncycastle.jcajce.provider.asymmetric.CompositeSignatures$Mappings", "methods":[{"name":"","parameterTypes":[] }] @@ -1559,14 +1618,30 @@ "name":"org.bouncycastle.jcajce.provider.asymmetric.LMS$Mappings", "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.MLDSA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.MLKEM$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, { "name":"org.bouncycastle.jcajce.provider.asymmetric.NTRU$Mappings", "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.NoSig$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, { "name":"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings", "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.SLHDSA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, { "name":"org.bouncycastle.jcajce.provider.asymmetric.SPHINCSPlus$Mappings", "methods":[{"name":"","parameterTypes":[] }] @@ -1979,7 +2054,10 @@ "name":"org.signal.libsignal.protocol.IdentityKey" }, { - "name":"org.signal.libsignal.protocol.ServiceId" + "name":"org.signal.libsignal.protocol.ServiceId", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true }, { "name":"org.signal.libsignal.protocol.SignalProtocolAddress" @@ -2241,7 +2319,7 @@ "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true, - "methods":[{"name":"getAnnouncementGroup","parameterTypes":[] }, {"name":"getChangeNumber","parameterTypes":[] }, {"name":"getDeleteSync","parameterTypes":[] }, {"name":"getGiftBadges","parameterTypes":[] }, {"name":"getPaymentActivation","parameterTypes":[] }, {"name":"getPni","parameterTypes":[] }, {"name":"getSenderKey","parameterTypes":[] }, {"name":"getStorage","parameterTypes":[] }, {"name":"getStories","parameterTypes":[] }, {"name":"getVersionedExpirationTimer","parameterTypes":[] }] + "methods":[{"name":"getAnnouncementGroup","parameterTypes":[] }, {"name":"getAttachmentBackfill","parameterTypes":[] }, {"name":"getChangeNumber","parameterTypes":[] }, {"name":"getDeleteSync","parameterTypes":[] }, {"name":"getGiftBadges","parameterTypes":[] }, {"name":"getPaymentActivation","parameterTypes":[] }, {"name":"getPni","parameterTypes":[] }, {"name":"getSenderKey","parameterTypes":[] }, {"name":"getStorage","parameterTypes":[] }, {"name":"getStorageServiceEncryptionV2","parameterTypes":[] }, {"name":"getStories","parameterTypes":[] }, {"name":"getVersionedExpirationTimer","parameterTypes":[] }] }, { "name":"org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest", @@ -2268,6 +2346,20 @@ { "name":"org.whispersystems.signalservice.api.groupsv2.TemporalCredential[]" }, +{ + "name":"org.whispersystems.signalservice.api.keys.OneTimePreKeyCounts", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"","parameterTypes":["java.lang.String","java.lang.String","int","kotlin.jvm.internal.DefaultConstructorMarker"] }] +}, { "name":"org.whispersystems.signalservice.api.messages.calls.HangupMessage", "allDeclaredFields":true, @@ -2328,7 +2420,14 @@ "name":"org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true + "allDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":["java.lang.String","byte[]","byte[]","byte[]","byte[]","byte[]","boolean","boolean","byte[]","java.util.List"] }, {"name":"getAbout","parameterTypes":[] }, {"name":"getAboutEmoji","parameterTypes":[] }, {"name":"getAvatar","parameterTypes":[] }, {"name":"getBadgeIds","parameterTypes":[] }, {"name":"getCommitment","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPaymentAddress","parameterTypes":[] }, {"name":"getPhoneNumberSharing","parameterTypes":[] }, {"name":"getSameAvatar","parameterTypes":[] }, {"name":"getVersion","parameterTypes":[] }] +}, +{ + "name":"org.whispersystems.signalservice.api.provisioning.ProvisioningMessage", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true }, { "name":"org.whispersystems.signalservice.api.push.ServiceId", @@ -2373,6 +2472,12 @@ "queryAllDeclaredConstructors":true, "methods":[{"name":"","parameterTypes":["java.lang.String","java.lang.String"] }] }, +{ + "name":"org.whispersystems.signalservice.api.ratelimit.SubmitRecaptchaChallengePayload", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, { "name":"org.whispersystems.signalservice.api.storage.StorageAuthResponse", "allDeclaredFields":true, @@ -2851,7 +2956,28 @@ "allDeclaredFields":true, "queryAllDeclaredMethods":true, "queryAllDeclaredConstructors":true, - "methods":[{"name":"","parameterTypes":[] }] + "methods":[{"name":"","parameterTypes":[] }, {"name":"","parameterTypes":["java.lang.String","java.lang.String","java.lang.String","java.lang.String","org.whispersystems.signalservice.internal.push.WhoAmIResponse$Entitlements"] }, {"name":"","parameterTypes":["java.lang.String","java.lang.String","java.lang.String","java.lang.String","org.whispersystems.signalservice.internal.push.WhoAmIResponse$Entitlements","int","kotlin.jvm.internal.DefaultConstructorMarker"] }] +}, +{ + "name":"org.whispersystems.signalservice.internal.push.WhoAmIResponse$BackupEntitlement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":["java.lang.Long","java.lang.Long"] }] +}, +{ + "name":"org.whispersystems.signalservice.internal.push.WhoAmIResponse$BadgeEntitlement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":["java.lang.String","java.lang.Boolean","java.lang.Long"] }, {"name":"","parameterTypes":["java.lang.String","java.lang.Boolean","java.lang.Long","int","kotlin.jvm.internal.DefaultConstructorMarker"] }] +}, +{ + "name":"org.whispersystems.signalservice.internal.push.WhoAmIResponse$Entitlements", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":["java.util.List","org.whispersystems.signalservice.internal.push.WhoAmIResponse$BackupEntitlement"] }, {"name":"","parameterTypes":["java.util.List","org.whispersystems.signalservice.internal.push.WhoAmIResponse$BackupEntitlement","int","kotlin.jvm.internal.DefaultConstructorMarker"] }] }, { "name":"org.whispersystems.signalservice.internal.serialize.protos.AddressProto", @@ -2875,7 +3001,26 @@ }, { "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord", - "allDeclaredFields":true + "allDeclaredFields":true, + "methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }] +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$BackupTierHistory" +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$Builder" +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$Companion" +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$IAPSubscriberData" +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$NotificationProfileManualOverride" +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$PhoneNumberSharingMode" }, { "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$PinnedConversation", @@ -2889,9 +3034,22 @@ "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$UsernameLink", "allDeclaredFields":true }, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.AvatarColor" +}, { "name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord", - "allDeclaredFields":true + "allDeclaredFields":true, + "methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }] +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$Builder" +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$Companion" +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$IdentityState" }, { "name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$Name", @@ -2899,11 +3057,30 @@ }, { "name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record", - "allDeclaredFields":true + "allDeclaredFields":true, + "fields":[{"name":"archived"}, {"name":"blocked"}, {"name":"id"}, {"name":"markedUnread"}, {"name":"mutedUntilTimestamp"}, {"name":"whitelisted"}], + "methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }] +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record$Builder" +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record$Companion" }, { "name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record", - "allDeclaredFields":true + "allDeclaredFields":true, + "fields":[{"name":"archived"}, {"name":"avatarColor"}, {"name":"blocked"}, {"name":"dontNotifyForMentionsIfMuted"}, {"name":"hideStory"}, {"name":"markedUnread"}, {"name":"masterKey"}, {"name":"mutedUntilTimestamp"}, {"name":"storySendMode"}, {"name":"whitelisted"}], + "methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }] +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record$Builder" +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record$Companion" +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record$StorySendMode" }, { "name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord", @@ -2913,6 +3090,9 @@ "name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord$Identifier", "fields":[{"name":"raw_"}, {"name":"type_"}] }, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.OptionalBool" +}, { "name":"org.whispersystems.signalservice.internal.storage.protos.Payments", "allDeclaredFields":true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..fec44a43 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,17 @@ +[versions] +slf4j = "2.0.17" + +[libraries] +bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.81" +jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.19.1" +argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0" +dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0" +slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } +slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" } +logback = "ch.qos.logback:logback-classic:1.5.18" + +signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_127" +sqlite = "org.xerial:sqlite-jdbc:3.50.2.0" +hikari = "com.zaxxer:HikariCP:6.3.0" +junit-jupiter = "org.junit.jupiter:junit-jupiter:5.13.2" +junit-launcher = "org.junit.platform:junit-platform-launcher:1.13.2" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b95..8bdaf60c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df97d72b..2a84e188 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f5feea6d..ef07e016 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright Š 2015-2021 the original authors. +# Copyright Š 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -115,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -206,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. @@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9b42019c..5eed7ee8 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,11 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 4b086994..ccb661e1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -1,5 +1,7 @@ package org.asamk.signal.manager; +import com.google.i18n.phonenumbers.PhoneNumberUtil; + import org.asamk.signal.manager.api.AlreadyReceivingException; import org.asamk.signal.manager.api.AttachmentInvalidException; import org.asamk.signal.manager.api.CaptchaRejectedException; @@ -28,6 +30,7 @@ import org.asamk.signal.manager.api.NotAGroupMemberException; import org.asamk.signal.manager.api.NotPrimaryDeviceException; import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.PendingAdminApprovalException; +import org.asamk.signal.manager.api.PinLockMissingException; import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.ReceiveConfig; @@ -49,7 +52,6 @@ import org.asamk.signal.manager.api.UsernameStatus; import org.asamk.signal.manager.api.VerificationMethodNotAvailableException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import java.io.Closeable; import java.io.File; @@ -65,7 +67,7 @@ import java.util.Set; public interface Manager extends Closeable { static boolean isValidNumber(final String e164Number, final String countryCode) { - return PhoneNumberFormatter.isValidNumber(e164Number, countryCode); + return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode); } static boolean isSignalClientAvailable() { @@ -94,7 +96,7 @@ public interface Manager extends Closeable { */ Map getUserStatus(Set numbers) throws IOException, RateLimitException; - Map getUsernameStatus(Set usernames); + Map getUsernameStatus(Set usernames) throws IOException; void updateAccountAttributes( String deviceName, @@ -130,19 +132,24 @@ public interface Manager extends Closeable { void deleteUsername() throws IOException; void startChangeNumber( - String newNumber, boolean voiceVerification, String captcha + String newNumber, + boolean voiceVerification, + String captcha ) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, NotPrimaryDeviceException, VerificationMethodNotAvailableException; void finishChangeNumber( - String newNumber, String verificationCode, String pin - ) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException; + String newNumber, + String verificationCode, + String pin + ) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException; void unregister() throws IOException; void deleteAccount() throws IOException; void submitRateLimitRecaptchaChallenge( - String challenge, String captcha + String challenge, + String captcha ) throws IOException, CaptchaRejectedException; List getLinkedDevices() throws IOException; @@ -156,17 +163,21 @@ public interface Manager extends Closeable { List getGroups(); SendGroupMessageResults quitGroup( - GroupId groupId, Set groupAdmins + GroupId groupId, + Set groupAdmins ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException; void deleteGroup(GroupId groupId) throws IOException; Pair createGroup( - String name, Set members, String avatarFile + String name, + Set members, + String avatarFile ) throws IOException, AttachmentInvalidException, UnregisteredRecipientException; SendGroupMessageResults updateGroup( - final GroupId groupId, final UpdateGroup updateGroup + final GroupId groupId, + final UpdateGroup updateGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException; Pair joinGroup( @@ -174,27 +185,29 @@ public interface Manager extends Closeable { ) throws IOException, InactiveGroupLinkException, PendingAdminApprovalException; SendMessageResults sendTypingMessage( - TypingAction action, Set recipients + TypingAction action, + Set recipients ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; - SendMessageResults sendReadReceipt( - RecipientIdentifier.Single sender, List messageIds - ); + SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List messageIds); - SendMessageResults sendViewedReceipt( - RecipientIdentifier.Single sender, List messageIds - ); + SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List messageIds); SendMessageResults sendMessage( - Message message, Set recipients, boolean notifySelf + Message message, + Set recipients, + boolean notifySelf ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException; SendMessageResults sendEditMessage( - Message message, Set recipients, long editTargetTimestamp + Message message, + Set recipients, + long editTargetTimestamp ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException; SendMessageResults sendRemoteDeleteMessage( - long targetSentTimestamp, Set recipients + long targetSentTimestamp, + Set recipients ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; SendMessageResults sendMessageReaction( @@ -207,13 +220,16 @@ public interface Manager extends Closeable { ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException; SendMessageResults sendPaymentNotificationMessage( - byte[] receipt, String note, RecipientIdentifier.Single recipient + byte[] receipt, + String note, + RecipientIdentifier.Single recipient ) throws IOException; SendMessageResults sendEndSessionMessage(Set recipients) throws IOException; SendMessageResults sendMessageRequestResponse( - MessageEnvelope.Sync.MessageRequestResponse.Type type, Set recipientIdentifiers + MessageEnvelope.Sync.MessageRequestResponse.Type type, + Set recipientIdentifiers ); void hideRecipient(RecipientIdentifier.Single recipient); @@ -223,22 +239,30 @@ public interface Manager extends Closeable { void deleteContact(RecipientIdentifier.Single recipient); void setContactName( - RecipientIdentifier.Single recipient, String givenName, final String familyName + final RecipientIdentifier.Single recipient, + final String givenName, + final String familyName, + final String nickGivenName, + final String nickFamilyName, + final String note ) throws NotPrimaryDeviceException, UnregisteredRecipientException; void setContactsBlocked( - Collection recipient, boolean blocked + Collection recipient, + boolean blocked ) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException; void setGroupsBlocked( - Collection groupId, boolean blocked + Collection groupId, + boolean blocked ) throws GroupNotFoundException, IOException, NotPrimaryDeviceException; /** * Change the expiration timer for a contact */ void setExpirationTimer( - RecipientIdentifier.Single recipient, int messageExpirationTimer + RecipientIdentifier.Single recipient, + int messageExpirationTimer ) throws IOException, UnregisteredRecipientException; /** @@ -277,7 +301,9 @@ public interface Manager extends Closeable { * Receive new messages from server, returns if no new message arrive in a timespan of timeout. */ void receiveMessages( - Optional timeout, Optional maxMessages, ReceiveMessageHandler handler + Optional timeout, + Optional maxMessages, + ReceiveMessageHandler handler ) throws IOException, AlreadyReceivingException; void stopReceiveMessages(); @@ -309,7 +335,8 @@ public interface Manager extends Closeable { * @param recipient account of the identity */ boolean trustIdentityVerified( - RecipientIdentifier.Single recipient, IdentityVerificationCode verificationCode + RecipientIdentifier.Single recipient, + IdentityVerificationCode verificationCode ) throws UnregisteredRecipientException; /** diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java index 5660bb13..5f4f4463 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -3,6 +3,7 @@ package org.asamk.signal.manager; import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; +import org.asamk.signal.manager.api.PinLockMissingException; import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.VerificationMethodNotAvailableException; @@ -13,12 +14,15 @@ import java.io.IOException; public interface RegistrationManager extends Closeable { void register( - boolean voiceVerification, String captcha, final boolean forceRegister + boolean voiceVerification, + String captcha, + final boolean forceRegister ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException; void verifyAccount( - String verificationCode, String pin - ) throws IOException, PinLockedException, IncorrectPinException; + String verificationCode, + String pin + ) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException; void deleteLocalAccountData() throws IOException; diff --git a/lib/src/main/java/org/asamk/signal/manager/SignalAccountFiles.java b/lib/src/main/java/org/asamk/signal/manager/SignalAccountFiles.java index 03c44013..cd0b5a21 100644 --- a/lib/src/main/java/org/asamk/signal/manager/SignalAccountFiles.java +++ b/lib/src/main/java/org/asamk/signal/manager/SignalAccountFiles.java @@ -2,6 +2,7 @@ package org.asamk.signal.manager; import org.asamk.signal.manager.api.AccountCheckException; import org.asamk.signal.manager.api.NotRegisteredException; +import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.ServiceEnvironment; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; @@ -63,19 +64,28 @@ public class SignalAccountFiles { return accountsStore.getAllNumbers(); } - public MultiAccountManager initMultiAccountManager() throws IOException { - final var managers = accountsStore.getAllAccounts().parallelStream().map(a -> { + public MultiAccountManager initMultiAccountManager() throws IOException, AccountCheckException { + final var managerPairs = accountsStore.getAllAccounts().parallelStream().map(a -> { try { - return initManager(a.number(), a.path()); - } catch (NotRegisteredException | IOException | AccountCheckException e) { + return new Pair(initManager(a.number(), a.path()), null); + } catch (NotRegisteredException e) { logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName()); return null; - } catch (Throwable e) { + } catch (AccountCheckException | IOException e) { logger.error("Failed to load {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName()); - throw e; + return new Pair(null, e); } }).filter(Objects::nonNull).toList(); + for (final var pair : managerPairs) { + if (pair.second() instanceof IOException e) { + throw e; + } else if (pair.second() instanceof AccountCheckException e) { + throw e; + } + } + + final var managers = managerPairs.stream().map(Pair::first).toList(); return new MultiAccountManagerImpl(managers, this); } @@ -85,7 +95,8 @@ public class SignalAccountFiles { } private Manager initManager( - String number, String accountPath + String number, + String accountPath ) throws IOException, NotRegisteredException, AccountCheckException { if (accountPath == null) { throw new NotRegisteredException(); @@ -152,7 +163,8 @@ public class SignalAccountFiles { } public RegistrationManager initRegistrationManager( - String number, Consumer newManagerListener + String number, + Consumer newManagerListener ) throws IOException { final var accountPath = accountsStore.getPathByNumber(number); if (accountPath == null || !SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) { diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/RenewSessionAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/RenewSessionAction.java index 40117246..35058940 100644 --- a/lib/src/main/java/org/asamk/signal/manager/actions/RenewSessionAction.java +++ b/lib/src/main/java/org/asamk/signal/manager/actions/RenewSessionAction.java @@ -19,9 +19,7 @@ public class RenewSessionAction implements HandleAction { @Override public void execute(Context context) throws Throwable { context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId); - if (!recipientId.equals(context.getAccount().getSelfRecipientId())) { - context.getSendHelper().sendNullMessage(recipientId); - } + context.getSendHelper().sendNullMessage(recipientId); } @Override diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/ResendMessageAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/ResendMessageAction.java index 9f399dd8..a8f67fd6 100644 --- a/lib/src/main/java/org/asamk/signal/manager/actions/ResendMessageAction.java +++ b/lib/src/main/java/org/asamk/signal/manager/actions/ResendMessageAction.java @@ -13,7 +13,9 @@ public class ResendMessageAction implements HandleAction { private final MessageSendLogEntry messageSendLogEntry; public ResendMessageAction( - final RecipientId recipientId, final long timestamp, final MessageSendLogEntry messageSendLogEntry + final RecipientId recipientId, + final long timestamp, + final MessageSendLogEntry messageSendLogEntry ) { this.recipientId = recipientId; this.timestamp = timestamp; diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendReceiptAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendReceiptAction.java index c08438b5..6a5cfd59 100644 --- a/lib/src/main/java/org/asamk/signal/manager/actions/SendReceiptAction.java +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendReceiptAction.java @@ -15,7 +15,9 @@ public class SendReceiptAction implements HandleAction { private final List timestamps = new ArrayList<>(); public SendReceiptAction( - final RecipientId recipientId, final SignalServiceReceiptMessage.Type type, final long timestamp + final RecipientId recipientId, + final SignalServiceReceiptMessage.Type type, + final long timestamp ) { this.recipientId = recipientId; this.type = type; diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java index 865f062f..96a94128 100644 --- a/lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java @@ -7,7 +7,6 @@ import org.signal.libsignal.metadata.ProtocolException; import org.signal.libsignal.protocol.message.CiphertextMessage; import org.signal.libsignal.protocol.message.DecryptionErrorMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; -import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.internal.push.Envelope; import java.util.Optional; @@ -15,29 +14,21 @@ import java.util.Optional; public class SendRetryMessageRequestAction implements HandleAction { private final RecipientId recipientId; - private final ServiceId serviceId; private final ProtocolException protocolException; private final SignalServiceEnvelope envelope; - private final ServiceId accountId; public SendRetryMessageRequestAction( final RecipientId recipientId, - final ServiceId serviceId, final ProtocolException protocolException, - final SignalServiceEnvelope envelope, - final ServiceId accountId + final SignalServiceEnvelope envelope ) { this.recipientId = recipientId; - this.serviceId = serviceId; this.protocolException = protocolException; this.envelope = envelope; - this.accountId = accountId; } @Override public void execute(Context context) throws Throwable { - context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId); - int senderDevice = protocolException.getSenderDevice(); Optional groupId = protocolException.getGroupId().isPresent() ? Optional.of(GroupId.unknownVersion( protocolException.getGroupId().get())) : Optional.empty(); diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Contact.java b/lib/src/main/java/org/asamk/signal/manager/api/Contact.java index 91cf82df..3eef0117 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/Contact.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/Contact.java @@ -49,8 +49,12 @@ public record Contact( builder.givenName = copy.givenName(); builder.familyName = copy.familyName(); builder.nickName = copy.nickName(); + builder.nickNameGivenName = copy.nickNameGivenName(); + builder.nickNameFamilyName = copy.nickNameFamilyName(); + builder.note = copy.note(); builder.color = copy.color(); builder.messageExpirationTime = copy.messageExpirationTime(); + builder.messageExpirationTimeVersion = copy.messageExpirationTimeVersion(); builder.muteUntil = copy.muteUntil(); builder.hideStory = copy.hideStory(); builder.isBlocked = copy.isBlocked(); diff --git a/lib/src/main/java/org/asamk/signal/manager/api/DeviceLinkUrl.java b/lib/src/main/java/org/asamk/signal/manager/api/DeviceLinkUrl.java index 54271dac..2bb63507 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/DeviceLinkUrl.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/DeviceLinkUrl.java @@ -2,7 +2,6 @@ package org.asamk.signal.manager.api; import org.asamk.signal.manager.util.Utils; import org.signal.libsignal.protocol.InvalidKeyException; -import org.signal.libsignal.protocol.ecc.Curve; import org.signal.libsignal.protocol.ecc.ECPublicKey; import java.net.URI; @@ -37,7 +36,7 @@ public record DeviceLinkUrl(String deviceIdentifier, ECPublicKey deviceKey) { } ECPublicKey deviceKey; try { - deviceKey = Curve.decodePoint(publicKeyBytes, 0); + deviceKey = new ECPublicKey(publicKeyBytes); } catch (InvalidKeyException e) { throw new InvalidDeviceLinkException("Invalid device link", e); } diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Group.java b/lib/src/main/java/org/asamk/signal/manager/api/Group.java index 5e7e36f8..4d1fdaa1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/Group.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/Group.java @@ -27,7 +27,9 @@ public record Group( ) { public static Group from( - final GroupInfo groupInfo, final RecipientAddressResolver recipientStore, final RecipientId selfRecipientId + final GroupInfo groupInfo, + final RecipientAddressResolver recipientStore, + final RecipientId selfRecipientId ) { return new Group(groupInfo.getGroupId(), groupInfo.getTitle(), diff --git a/lib/src/main/java/org/asamk/signal/manager/api/InvalidNumberException.java b/lib/src/main/java/org/asamk/signal/manager/api/InvalidNumberException.java index ba71ef2e..14f37966 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/InvalidNumberException.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/InvalidNumberException.java @@ -2,6 +2,10 @@ package org.asamk.signal.manager.api; public class InvalidNumberException extends Exception { + public InvalidNumberException(String message) { + super(message); + } + InvalidNumberException(String message, Throwable e) { super(message, e); } diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Message.java b/lib/src/main/java/org/asamk/signal/manager/api/Message.java index 48277e6d..9b372451 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/Message.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/Message.java @@ -6,6 +6,7 @@ import java.util.Optional; public record Message( String messageText, List attachments, + boolean viewOnce, List mentions, Optional quote, Optional sticker, diff --git a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java index aefaa800..d8438f5d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java @@ -338,7 +338,8 @@ public record MessageEnvelope( } static Attachment from( - SignalServiceDataMessage.Quote.QuotedAttachment a, final AttachmentFileProvider fileProvider + SignalServiceDataMessage.Quote.QuotedAttachment a, + final AttachmentFileProvider fileProvider ) { return new Attachment(Optional.empty(), Optional.empty(), @@ -510,9 +511,7 @@ public record MessageEnvelope( public record Preview(String title, String description, long date, String url, Optional image) { - static Preview from( - SignalServicePreview preview, final AttachmentFileProvider fileProvider - ) { + static Preview from(SignalServicePreview preview, final AttachmentFileProvider fileProvider) { return new Preview(preview.getTitle(), preview.getDescription(), preview.getDate(), @@ -612,11 +611,12 @@ public record MessageEnvelope( RecipientResolver recipientResolver, RecipientAddressResolver addressResolver ) { - return new Blocked(blockedListMessage.getAddresses() - .stream() - .map(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(d)) - .toApiRecipientAddress()) - .toList(), blockedListMessage.getGroupIds().stream().map(GroupId::unknownVersion).toList()); + return new Blocked(blockedListMessage.individuals.stream() + .map(d -> new RecipientAddress(d.getAci() == null ? null : d.getAci().toString(), + null, + d.getE164(), + null)) + .toList(), blockedListMessage.groupIds.stream().map(GroupId::unknownVersion).toList()); } } @@ -832,9 +832,7 @@ public record MessageEnvelope( Optional textAttachment ) { - public static Story from( - SignalServiceStoryMessage storyMessage, final AttachmentFileProvider fileProvider - ) { + public static Story from(SignalServiceStoryMessage storyMessage, final AttachmentFileProvider fileProvider) { return new Story(storyMessage.getAllowsReplies().orElse(false), storyMessage.getGroupContext().map(c -> GroupUtils.getGroupIdV2(c.getMasterKey())), storyMessage.getFileAttachment().map(f -> Data.Attachment.from(f, fileProvider)), @@ -852,7 +850,8 @@ public record MessageEnvelope( ) { static TextAttachment from( - SignalServiceTextAttachment textAttachment, final AttachmentFileProvider fileProvider + SignalServiceTextAttachment textAttachment, + final AttachmentFileProvider fileProvider ) { return new TextAttachment(textAttachment.getText(), textAttachment.getStyle().map(Style::from), diff --git a/lib/src/main/java/org/asamk/signal/manager/api/PinLockMissingException.java b/lib/src/main/java/org/asamk/signal/manager/api/PinLockMissingException.java new file mode 100644 index 00000000..7887d45f --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/PinLockMissingException.java @@ -0,0 +1,3 @@ +package org.asamk.signal.manager.api; + +public class PinLockMissingException extends Exception {} diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Profile.java b/lib/src/main/java/org/asamk/signal/manager/api/Profile.java index 5b1ecf3a..acbaac11 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/Profile.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/Profile.java @@ -161,7 +161,8 @@ public class Profile { } public enum Capability { - storage; + storage, + storageServiceEncryptionV2Capability; public static Capability valueOfOrNull(String value) { try { diff --git a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java index 794672a4..b1a3ad61 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java @@ -1,8 +1,8 @@ package org.asamk.signal.manager.api; +import org.asamk.signal.manager.util.PhoneNumberFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.UuidUtil; import java.util.UUID; @@ -24,24 +24,28 @@ public sealed interface RecipientIdentifier { sealed interface Single extends RecipientIdentifier { static Single fromString(String identifier, String localNumber) throws InvalidNumberException { - try { - if (UuidUtil.isUuid(identifier)) { - return new Uuid(UUID.fromString(identifier)); - } - - if (identifier.startsWith("u:")) { - return new Username(identifier.substring(2)); - } - - final var normalizedNumber = PhoneNumberFormatter.formatNumber(identifier, localNumber); - if (!normalizedNumber.equals(identifier)) { - final Logger logger = LoggerFactory.getLogger(RecipientIdentifier.class); - logger.debug("Normalized number {} to {}.", identifier, normalizedNumber); - } - return new Number(normalizedNumber); - } catch (org.whispersystems.signalservice.api.util.InvalidNumberException e) { - throw new InvalidNumberException(e.getMessage(), e); + if (UuidUtil.isUuid(identifier)) { + return new Uuid(UUID.fromString(identifier)); } + + if (identifier.startsWith("PNI:")) { + final var pni = identifier.substring(4); + if (!UuidUtil.isUuid(pni)) { + throw new InvalidNumberException("Invalid PNI"); + } + return new Pni(UUID.fromString(pni)); + } + + if (identifier.startsWith("u:")) { + return new Username(identifier.substring(2)); + } + + final var normalizedNumber = PhoneNumberFormatter.formatNumber(identifier, localNumber); + if (!normalizedNumber.equals(identifier)) { + final Logger logger = LoggerFactory.getLogger(RecipientIdentifier.class); + logger.debug("Normalized number {} to {}.", identifier, normalizedNumber); + } + return new Number(normalizedNumber); } static Single fromAddress(RecipientAddress address) { @@ -50,7 +54,7 @@ public sealed interface RecipientIdentifier { } else if (address.aci().isPresent()) { return new Uuid(UUID.fromString(address.aci().get())); } else if (address.pni().isPresent()) { - return new Pni(address.pni().get()); + return new Pni(UUID.fromString(address.pni().get().substring(4))); } else if (address.username().isPresent()) { return new Username(address.username().get()); } @@ -73,16 +77,16 @@ public sealed interface RecipientIdentifier { } } - record Pni(String pni) implements Single { + record Pni(UUID pni) implements Single { @Override public String getIdentifier() { - return pni; + return "PNI:" + pni.toString(); } @Override public RecipientAddress toPartialRecipientAddress() { - return new RecipientAddress(null, pni, null, null); + return new RecipientAddress(null, getIdentifier(), null, null); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java index f7859dae..c76da017 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java @@ -2,9 +2,9 @@ package org.asamk.signal.manager.config; import org.signal.libsignal.net.Network.Environment; import org.signal.libsignal.protocol.InvalidKeyException; -import org.signal.libsignal.protocol.ecc.Curve; import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.whispersystems.signalservice.api.push.TrustStore; +import org.whispersystems.signalservice.internal.configuration.HttpProxy; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl; import org.whispersystems.signalservice.internal.configuration.SignalProxy; @@ -28,8 +28,9 @@ class LiveConfig { private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder() .decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"); private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57"; - private static final String SVR2_MRENCLAVE = "9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570"; - private static final String SVR2_LEGACY_MRENCLAVE = "a6622ad4656e1abcd0bc0ff17c229477747d2ded0495c4ebee7ed35c1789fa97"; + private static final String SVR2_MRENCLAVE_LEGACY_LEGACY = "9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570"; + private static final String SVR2_MRENCLAVE_LEGACY = "093be9ea32405e85ae28dbb48eb668aebeb7dbe29517b9b86ad4bec4dfe0e6a6"; + private static final String SVR2_MRENCLAVE = "29cd63c87bea751e3bfd0fbd401279192e2e5c99948b4ee9437eafc4968355fb"; private static final String URL = "https://chat.signal.org"; private static final String CDN_URL = "https://cdn.signal.org"; @@ -42,6 +43,7 @@ class LiveConfig { private static final Optional dns = Optional.empty(); private static final Optional proxy = Optional.empty(); + private static final Optional systemProxy = Optional.empty(); private static final byte[] zkGroupServerPublicParams = Base64.getDecoder() .decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw=="); @@ -51,7 +53,7 @@ class LiveConfig { private static final byte[] backupServerPublicParams = Base64.getDecoder() .decode("AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O"); - private static Environment LIBSIGNAL_NET_ENV = Environment.PRODUCTION; + private static final Environment LIBSIGNAL_NET_ENV = Environment.PRODUCTION; static SignalServiceConfiguration createDefaultServiceConfiguration( final List interceptors @@ -69,14 +71,16 @@ class LiveConfig { interceptors, dns, proxy, + systemProxy, zkGroupServerPublicParams, genericServerPublicParams, - backupServerPublicParams); + backupServerPublicParams, + false); } static ECPublicKey getUnidentifiedSenderTrustRoot() { try { - return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0); + return new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT); } catch (InvalidKeyException e) { throw new AssertionError(e); } @@ -88,7 +92,7 @@ class LiveConfig { createDefaultServiceConfiguration(interceptors), getUnidentifiedSenderTrustRoot(), CDSI_MRENCLAVE, - List.of(SVR2_MRENCLAVE, SVR2_LEGACY_MRENCLAVE)); + List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_LEGACY_LEGACY)); } private LiveConfig() { diff --git a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java index 7ee80815..587fc89b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java @@ -20,7 +20,7 @@ public class ServiceConfig { public static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; public static final long MAX_ENVELOPE_SIZE = 0; - public static final int MAX_MESSAGE_BODY_SIZE = 2000; + public static final int MAX_MESSAGE_SIZE_BYTES = 2000; public static final long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024; public static final boolean AUTOMATIC_NETWORK_RETRY = true; public static final int GROUP_MAX_SIZE = 1001; @@ -29,11 +29,14 @@ public class ServiceConfig { public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) { final var deleteSync = !isPrimaryDevice; - return new AccountAttributes.Capabilities(true, deleteSync, true); + final var storageEncryptionV2 = !isPrimaryDevice; + final var attachmentBackfill = !isPrimaryDevice; + return new AccountAttributes.Capabilities(true, deleteSync, true, storageEncryptionV2, attachmentBackfill); } public static ServiceEnvironmentConfig getServiceEnvironmentConfig( - ServiceEnvironment serviceEnvironment, String userAgent + ServiceEnvironment serviceEnvironment, + String userAgent ) { final Interceptor userAgentInterceptor = chain -> chain.proceed(chain.request() .newBuilder() diff --git a/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java index a3212c07..e25fe21b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java @@ -2,9 +2,9 @@ package org.asamk.signal.manager.config; import org.signal.libsignal.net.Network; import org.signal.libsignal.protocol.InvalidKeyException; -import org.signal.libsignal.protocol.ecc.Curve; import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.whispersystems.signalservice.api.push.TrustStore; +import org.whispersystems.signalservice.internal.configuration.HttpProxy; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl; import org.whispersystems.signalservice.internal.configuration.SignalProxy; @@ -28,8 +28,9 @@ class StagingConfig { private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder() .decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx"); private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57"; - private static final String SVR2_MRENCLAVE = "38e01eff4fe357dc0b0e8ef7a44b4abc5489fbccba3a78780f3872c277f62bf3"; - private static final String SVR2_LEGACY_MRENCLAVE = "acb1973aa0bbbd14b3b4e06f145497d948fd4a98efc500fcce363b3b743ec482"; + private static final String SVR2_MRENCLAVE_LEGACY_LEGACY = "38e01eff4fe357dc0b0e8ef7a44b4abc5489fbccba3a78780f3872c277f62bf3"; + private static final String SVR2_MRENCLAVE_LEGACY = "2e8cefe6e3f389d8426adb24e9b7fb7adf10902c96f06f7bbcee36277711ed91"; + private static final String SVR2_MRENCLAVE = "a75542d82da9f6914a1e31f8a7407053b99cc99a0e7291d8fbd394253e19b036"; private static final String URL = "https://chat.staging.signal.org"; private static final String CDN_URL = "https://cdn-staging.signal.org"; @@ -42,6 +43,7 @@ class StagingConfig { private static final Optional dns = Optional.empty(); private static final Optional proxy = Optional.empty(); + private static final Optional systemProxy = Optional.empty(); private static final byte[] zkGroupServerPublicParams = Base64.getDecoder() .decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ=="); @@ -51,7 +53,7 @@ class StagingConfig { private static final byte[] backupServerPublicParams = Base64.getDecoder() .decode("AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8"); - private static Network.Environment LIBSIGNAL_NET_ENV = Network.Environment.STAGING; + private static final Network.Environment LIBSIGNAL_NET_ENV = Network.Environment.STAGING; static SignalServiceConfiguration createDefaultServiceConfiguration( final List interceptors @@ -69,14 +71,16 @@ class StagingConfig { interceptors, dns, proxy, + systemProxy, zkGroupServerPublicParams, genericServerPublicParams, - backupServerPublicParams); + backupServerPublicParams, + false); } static ECPublicKey getUnidentifiedSenderTrustRoot() { try { - return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0); + return new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT); } catch (InvalidKeyException e) { throw new AssertionError(e); } @@ -88,7 +92,7 @@ class StagingConfig { createDefaultServiceConfiguration(interceptors), getUnidentifiedSenderTrustRoot(), CDSI_MRENCLAVE, - List.of(SVR2_MRENCLAVE, SVR2_LEGACY_MRENCLAVE)); + List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_LEGACY_LEGACY)); } private StagingConfig() { diff --git a/lib/src/main/java/org/asamk/signal/manager/groups/GroupUtils.java b/lib/src/main/java/org/asamk/signal/manager/groups/GroupUtils.java index 76352d1a..ae686c36 100644 --- a/lib/src/main/java/org/asamk/signal/manager/groups/GroupUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/groups/GroupUtils.java @@ -18,7 +18,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; public class GroupUtils { public static void setGroupContext( - final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo groupInfo + final SignalServiceDataMessage.Builder messageBuilder, + final GroupInfo groupInfo ) { if (groupInfo instanceof GroupInfoV1) { var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java index 474a2822..de3e2402 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java @@ -3,8 +3,8 @@ package org.asamk.signal.manager.helper; import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.DeviceLinkUrl; import org.asamk.signal.manager.api.IncorrectPinException; -import org.asamk.signal.manager.api.InvalidDeviceLinkException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; +import org.asamk.signal.manager.api.PinLockMissingException; import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.VerificationMethodNotAvailableException; @@ -27,11 +27,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse; import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.PNI; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignedPreKeyEntity; +import org.whispersystems.signalservice.api.push.UsernameLinkComponents; import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; @@ -50,12 +52,13 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Objects; -import java.util.Optional; +import java.util.UUID; import java.util.concurrent.TimeUnit; import okio.ByteString; import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID; +import static org.asamk.signal.manager.util.Utils.handleResponseException; import static org.whispersystems.signalservice.internal.util.Util.isEmpty; public class AccountHelper { @@ -101,9 +104,9 @@ public class AccountHelper { checkWhoAmiI(); } if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) { - context.getSyncHelper().requestSyncPniIdentity(); + throw new IOException("Missing PNI identity key, relinking required"); } - if (account.getPreviousStorageVersion() < 4 + if (account.getPreviousStorageVersion() < 10 && account.isPrimaryDevice() && account.getRegistrationLockPin() != null) { migrateRegistrationPin(); @@ -165,7 +168,9 @@ public class AccountHelper { } public void startChangeNumber( - String newNumber, boolean voiceVerification, String captcha + String newNumber, + boolean voiceVerification, + String captcha ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException { final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword()); final var registrationApi = accountManager.getRegistrationApi(); @@ -178,8 +183,10 @@ public class AccountHelper { } public void finishChangeNumber( - String newNumber, String verificationCode, String pin - ) throws IncorrectPinException, PinLockedException, IOException { + String newNumber, + String verificationCode, + String pin + ) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException { for (var attempts = 0; attempts < 5; attempts++) { try { finishChangeNumberInternal(newNumber, verificationCode, pin); @@ -196,8 +203,10 @@ public class AccountHelper { } private void finishChangeNumberInternal( - String newNumber, String verificationCode, String pin - ) throws IncorrectPinException, PinLockedException, IOException { + String newNumber, + String verificationCode, + String pin + ) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException { final var pniIdentity = KeyUtils.generateIdentityKeyPair(); final var encryptedDeviceMessages = new ArrayList(); final var devicePniSignedPreKeys = new HashMap(); @@ -282,13 +291,13 @@ public class AccountHelper { context.getPinHelper(), (sessionId1, verificationCode1, registrationLock) -> { final var registrationApi = dependencies.getRegistrationApi(); + final var accountApi = dependencies.getAccountApi(); try { - Utils.handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1)); + handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1)); } catch (AlreadyVerifiedException e) { // Already verified so can continue changing number } - return Utils.handleResponseException(registrationApi.changeNumber(new ChangePhoneNumberRequest( - sessionId1, + return handleResponseException(accountApi.changeNumber(new ChangePhoneNumberRequest(sessionId1, null, newNumber, registrationLock, @@ -308,9 +317,7 @@ public class AccountHelper { handlePniChangeNumberMessage(selfChangeNumber, updatePni); } - public void handlePniChangeNumberMessage( - final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni - ) { + public void handlePniChangeNumberMessage(final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni) { if (pniChangeNumber.identityKeyPair != null && pniChangeNumber.registrationId != null && pniChangeNumber.signedPreKey != null) { @@ -374,7 +381,7 @@ public class AccountHelper { candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash())); } - final var response = dependencies.getAccountManager().reserveUsername(candidateHashes); + final var response = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes)); final var hashIndex = candidateHashes.indexOf(response.getUsernameHash()); if (hashIndex == -1) { logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes."); @@ -384,7 +391,7 @@ public class AccountHelper { logger.debug("[reserveUsername] Successfully reserved username."); final var username = candidates.get(hashIndex); - final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username); + final var linkComponents = confirmUsernameAndCreateNewLink(username); account.setUsername(username.getUsername()); account.setUsernameLink(linkComponents); account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress()); @@ -392,6 +399,40 @@ public class AccountHelper { logger.debug("[confirmUsername] Successfully confirmed username."); } + public UsernameLinkComponents createUsernameLink(Username username) throws IOException { + try { + Username.UsernameLink link = username.generateLink(); + return handleResponseException(dependencies.getAccountApi().createUsernameLink(link)); + } catch (BaseUsernameException e) { + throw new AssertionError(e); + } + } + + private UsernameLinkComponents confirmUsernameAndCreateNewLink(Username username) throws IOException { + try { + Username.UsernameLink link = username.generateLink(); + UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link)); + + return new UsernameLinkComponents(link.getEntropy(), serverId); + } catch (BaseUsernameException e) { + throw new AssertionError(e); + } + } + + private UsernameLinkComponents reclaimUsernameAndLink( + Username username, + UsernameLinkComponents linkComponents + ) throws IOException { + try { + Username.UsernameLink link = username.generateLink(linkComponents.getEntropy()); + UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link)); + + return new UsernameLinkComponents(link.getEntropy(), serverId); + } catch (BaseUsernameException e) { + throw new AssertionError(e); + } + } + public void refreshCurrentUsername() throws IOException, BaseUsernameException { final var localUsername = account.getUsername(); if (localUsername == null) { @@ -434,14 +475,14 @@ public class AccountHelper { final var usernameLink = account.getUsernameLink(); if (usernameLink == null) { - dependencies.getAccountManager() - .reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash()))); + handleResponseException(dependencies.getAccountApi() + .reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash())))); logger.debug("[reserveUsername] Successfully reserved existing username."); - final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username); + final var linkComponents = confirmUsernameAndCreateNewLink(username); account.setUsernameLink(linkComponents); logger.debug("[confirmUsername] Successfully confirmed existing username."); } else { - final var linkComponents = dependencies.getAccountManager().reclaimUsernameAndLink(username, usernameLink); + final var linkComponents = reclaimUsernameAndLink(username, usernameLink); account.setUsernameLink(linkComponents); logger.debug("[confirmUsername] Successfully reclaimed existing username and link."); } @@ -451,7 +492,7 @@ public class AccountHelper { private void tryToSetUsernameLink(Username username) { for (var i = 1; i < 4; i++) { try { - final var linkComponents = dependencies.getAccountManager().createUsernameLink(username); + final var linkComponents = createUsernameLink(username); account.setUsernameLink(linkComponents); break; } catch (IOException e) { @@ -461,9 +502,8 @@ public class AccountHelper { } public void deleteUsername() throws IOException { - dependencies.getAccountManager().deleteUsernameLink(); + handleResponseException(dependencies.getAccountApi().deleteUsername()); account.setUsernameLink(null); - dependencies.getAccountManager().deleteUsername(); account.setUsername(null); logger.debug("[deleteUsername] Successfully deleted the username."); } @@ -475,36 +515,39 @@ public class AccountHelper { } public void updateAccountAttributes() throws IOException { - dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null)); + handleResponseException(dependencies.getAccountApi().setAccountAttributes(account.getAccountAttributes(null))); } - public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, InvalidDeviceLinkException, org.asamk.signal.manager.api.DeviceLimitExceededException { - String verificationCode; + public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, org.asamk.signal.manager.api.DeviceLimitExceededException { + final var linkDeviceApi = dependencies.getLinkDeviceApi(); + final LinkedDeviceVerificationCodeResponse verificationCode; try { - verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); + verificationCode = handleResponseException(linkDeviceApi.getDeviceVerificationCode()); } catch (DeviceLimitExceededException e) { throw new org.asamk.signal.manager.api.DeviceLimitExceededException("Too many linked devices", e); } - try { - dependencies.getAccountManager() - .addDevice(deviceLinkInfo.deviceIdentifier(), - deviceLinkInfo.deviceKey(), - account.getAciIdentityKeyPair(), - account.getPniIdentityKeyPair(), - account.getProfileKey(), - account.getOrCreatePinMasterKey(), - verificationCode); - } catch (InvalidKeyException e) { - throw new InvalidDeviceLinkException("Invalid device link", e); - } + handleResponseException(dependencies.getLinkDeviceApi() + .linkDevice(account.getNumber(), + account.getAci(), + account.getPni(), + deviceLinkInfo.deviceIdentifier(), + deviceLinkInfo.deviceKey(), + account.getAciIdentityKeyPair(), + account.getPniIdentityKeyPair(), + account.getProfileKey(), + account.getOrCreateAccountEntropyPool(), + account.getOrCreatePinMasterKey(), + account.getOrCreateMediaRootBackupKey(), + verificationCode.getVerificationCode(), + null)); account.setMultiDevice(true); context.getJobExecutor().enqueueJob(new SyncStorageJob()); } public void removeLinkedDevices(int deviceId) throws IOException { - dependencies.getAccountManager().removeDevice(deviceId); - var devices = dependencies.getAccountManager().getDevices(); + handleResponseException(dependencies.getLinkDeviceApi().removeDevice(deviceId)); + var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices()); account.setMultiDevice(devices.size() > 1); } @@ -512,14 +555,16 @@ public class AccountHelper { var masterKey = account.getOrCreatePinMasterKey(); context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey); - dependencies.getAccountManager().enableRegistrationLock(masterKey); + handleResponseException(dependencies.getAccountApi() + .enableRegistrationLock(masterKey.deriveRegistrationLock())); } public void setRegistrationPin(String pin) throws IOException { var masterKey = account.getOrCreatePinMasterKey(); context.getPinHelper().setRegistrationLockPin(pin, masterKey); - dependencies.getAccountManager().enableRegistrationLock(masterKey); + handleResponseException(dependencies.getAccountApi() + .enableRegistrationLock(masterKey.deriveRegistrationLock())); account.setRegistrationLockPin(pin); updateAccountAttributes(); @@ -528,7 +573,7 @@ public class AccountHelper { public void removeRegistrationPin() throws IOException { // Remove KBS Pin context.getPinHelper().removeRegistrationLockPin(); - dependencies.getAccountManager().disableRegistrationLock(); + handleResponseException(dependencies.getAccountApi().disableRegistrationLock()); account.setRegistrationLockPin(null); } @@ -537,7 +582,7 @@ public class AccountHelper { // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. // If this is the primary device, other users can't send messages to this number anymore. // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore. - dependencies.getAccountManager().setGcmId(Optional.empty()); + handleResponseException(dependencies.getAccountApi().clearFcmToken()); account.setRegistered(false); unregisteredListener.call(); @@ -551,7 +596,7 @@ public class AccountHelper { } account.setRegistrationLockPin(null); - dependencies.getAccountManager().deleteAccount(); + handleResponseException(dependencies.getAccountApi().deleteAccount()); account.setRegistered(false); unregisteredListener.call(); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java index 3005325a..08526c72 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java @@ -9,6 +9,7 @@ import org.asamk.signal.manager.util.IOUtils; import org.signal.libsignal.protocol.InvalidMessageException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; @@ -44,14 +45,20 @@ public class AttachmentHelper { } public List uploadAttachments(final List attachments) throws AttachmentInvalidException, IOException { - var attachmentStreams = createAttachmentStreams(attachments); + final var attachmentStreams = createAttachmentStreams(attachments); - // Upload attachments here, so we only upload once even for multiple recipients - var attachmentPointers = new ArrayList(attachmentStreams.size()); - for (var attachmentStream : attachmentStreams) { - attachmentPointers.add(uploadAttachment(attachmentStream)); + try { + // Upload attachments here, so we only upload once even for multiple recipients + final var attachmentPointers = new ArrayList(attachmentStreams.size()); + for (final var attachmentStream : attachmentStreams) { + attachmentPointers.add(uploadAttachment(attachmentStream)); + } + return attachmentPointers; + } finally { + for (final var attachmentStream : attachmentStreams) { + attachmentStream.close(); + } } - return attachmentPointers; } private List createAttachmentStreams(List attachments) throws AttachmentInvalidException, IOException { @@ -104,9 +111,7 @@ public class AttachmentHelper { retrieveAttachment(attachment, input -> IOUtils.copyStream(input, outputStream)); } - public void retrieveAttachment( - SignalServiceAttachment attachment, AttachmentHandler consumer - ) throws IOException { + public void retrieveAttachment(SignalServiceAttachment attachment, AttachmentHandler consumer) throws IOException { if (attachment.isStream()) { var input = attachment.asStream().getInputStream(); // don't close input stream here, it might be reused later (e.g. with contact sync messages ...) @@ -131,11 +136,18 @@ public class AttachmentHelper { } private InputStream retrieveAttachmentAsStream( - SignalServiceAttachmentPointer pointer, File tmpFile + SignalServiceAttachmentPointer pointer, + File tmpFile ) throws IOException { + if (pointer.getDigest().isEmpty()) { + throw new IOException("Attachment pointer has no digest."); + } try { return dependencies.getMessageReceiver() - .retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); + .retrieveAttachment(pointer, + tmpFile, + ServiceConfig.MAX_ATTACHMENT_SIZE, + AttachmentCipherInputStream.IntegrityCheck.forEncryptedDigest(pointer.getDigest().get())); } catch (MissingConfigurationException | InvalidMessageException e) { throw new IOException(e); } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java index 56ef89ed..8461fbe0 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java @@ -17,7 +17,14 @@ public class ContactHelper { return sourceContact != null && sourceContact.isBlocked(); } - public void setContactName(final RecipientId recipientId, final String givenName, final String familyName) { + public void setContactName( + final RecipientId recipientId, + final String givenName, + final String familyName, + final String nickGivenName, + final String nickFamilyName, + final String note + ) { var contact = account.getContactStore().getContact(recipientId); final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); builder.withIsHidden(false); @@ -27,6 +34,15 @@ public class ContactHelper { if (familyName != null) { builder.withFamilyName(familyName); } + if (nickGivenName != null) { + builder.withNickNameGivenName(nickGivenName); + } + if (nickFamilyName != null) { + builder.withNickNameFamilyName(nickFamilyName); + } + if (note != null) { + builder.withNote(note); + } account.getContactStore().storeContact(recipientId, builder.build()); } @@ -49,7 +65,9 @@ public class ContactHelper { } public void setExpirationTimer( - RecipientId recipientId, int messageExpirationTimer, int messageExpirationTimerVersion + RecipientId recipientId, + int messageExpirationTimer, + int messageExpirationTimerVersion ) { var contact = account.getContactStore().getContact(recipientId); if (contact != null && ( diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index ad31f35b..682bd996 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -118,7 +118,9 @@ public class GroupHelper { } public GroupInfoV2 getOrMigrateGroup( - final GroupMasterKey groupMasterKey, final int revision, final byte[] signedGroupChange + final GroupMasterKey groupMasterKey, + final int revision, + final byte[] signedGroupChange ) { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); @@ -166,7 +168,8 @@ public class GroupHelper { } private DecryptedGroup handleDecryptedGroupResponse( - GroupInfoV2 groupInfoV2, final DecryptedGroupResponse decryptedGroupResponse + GroupInfoV2 groupInfoV2, + final DecryptedGroupResponse decryptedGroupResponse ) { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); ReceivedGroupSendEndorsements groupSendEndorsements = dependencies.getGroupsV2Operations() @@ -181,7 +184,8 @@ public class GroupHelper { } private GroupChange handleGroupChangeResponse( - final GroupInfoV2 groupInfoV2, final GroupChangeResponse groupChangeResponse + final GroupInfoV2 groupInfoV2, + final GroupChangeResponse groupChangeResponse ) { ReceivedGroupSendEndorsements groupSendEndorsements = dependencies.getGroupsV2Operations() .forGroup(GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey())) @@ -195,7 +199,9 @@ public class GroupHelper { } public Pair createGroup( - String name, Set members, String avatarFile + String name, + Set members, + String avatarFile ) throws IOException, AttachmentInvalidException { final var selfRecipientId = account.getSelfRecipientId(); if (members != null && members.contains(selfRecipientId)) { @@ -363,7 +369,8 @@ public class GroupHelper { } public SendGroupMessageResults quitGroup( - final GroupId groupId, final Set newAdmins + final GroupId groupId, + final Set newAdmins ) throws IOException, LastGroupAdminException, NotAGroupMemberException, GroupNotFoundException { var group = getGroupForUpdating(groupId); if (group instanceof GroupInfoV1) { @@ -396,9 +403,7 @@ public class GroupHelper { context.getJobExecutor().enqueueJob(new SyncStorageJob()); } - public SendGroupMessageResults sendGroupInfoRequest( - GroupIdV1 groupId, RecipientId recipientId - ) throws IOException { + public SendGroupMessageResults sendGroupInfoRequest(GroupIdV1 groupId, RecipientId recipientId) throws IOException { var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize()); var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build()); @@ -408,7 +413,8 @@ public class GroupHelper { } public SendGroupMessageResults sendGroupInfoMessage( - GroupIdV1 groupId, RecipientId recipientId + GroupIdV1 groupId, + RecipientId recipientId ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { GroupInfoV1 g; var group = getGroupForUpdating(groupId); @@ -480,7 +486,9 @@ public class GroupHelper { } private void retrieveGroupV2Avatar( - GroupSecretParams groupSecretParams, String cdnKey, OutputStream outputStream + GroupSecretParams groupSecretParams, + String cdnKey, + OutputStream outputStream ) throws IOException { var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams); @@ -543,6 +551,9 @@ public class GroupHelper { while (true) { final var page = context.getGroupV2Helper() .getDecryptedGroupHistoryPage(groupSecretParams, fromRevision, sendEndorsementsExpirationMs); + if (page == null) { + break; + } page.getChangeLogs() .stream() .map(DecryptedGroupChangeLog::getChange) @@ -583,7 +594,10 @@ public class GroupHelper { } private SendGroupMessageResults updateGroupV1( - final GroupInfoV1 gv1, final String name, final Set members, final byte[] avatarFile + final GroupInfoV1 gv1, + final String name, + final Set members, + final byte[] avatarFile ) throws IOException, AttachmentInvalidException { updateGroupV1Details(gv1, name, members, avatarFile); @@ -596,7 +610,10 @@ public class GroupHelper { } private void updateGroupV1Details( - final GroupInfoV1 g, final String name, final Collection members, final byte[] avatarFile + final GroupInfoV1 g, + final String name, + final Collection members, + final byte[] avatarFile ) throws IOException { if (name != null) { g.name = name; @@ -615,7 +632,8 @@ public class GroupHelper { * Change the expiration timer for a group */ private void setExpirationTimer( - GroupInfoV1 groupInfoV1, int messageExpirationTimer + GroupInfoV1 groupInfoV1, + int messageExpirationTimer ) throws NotAGroupMemberException, GroupNotFoundException, IOException, GroupSendingNotAllowedException { groupInfoV1.messageExpirationTime = messageExpirationTimer; account.getGroupStore().updateGroup(groupInfoV1); @@ -828,7 +846,8 @@ public class GroupHelper { } private SendGroupMessageResults quitGroupV2( - final GroupInfoV2 groupInfoV2, final Set newAdmins + final GroupInfoV2 groupInfoV2, + final Set newAdmins ) throws LastGroupAdminException, IOException { final var currentAdmins = groupInfoV2.getAdminMembers(); newAdmins.removeAll(currentAdmins); @@ -882,7 +901,9 @@ public class GroupHelper { } private SendGroupMessageResults sendUpdateGroupV2Message( - GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange + GroupInfoV2 group, + DecryptedGroup newDecryptedGroup, + GroupChange groupChange ) throws IOException { final var selfRecipientId = account.getSelfRecipientId(); final var members = group.getMembersIncludingPendingWithout(selfRecipientId); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index 6c6acd65..8eb66843 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -28,6 +28,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.signal.storageservice.protos.groups.local.DecryptedPendingMember; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.groupsv2.DecryptChangeVerificationMode; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupResponse; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.groupsv2.GroupCandidate; @@ -43,6 +44,7 @@ import org.whispersystems.signalservice.api.push.ServiceId.PNI; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException; import java.io.IOException; import java.util.ArrayList; @@ -82,7 +84,7 @@ class GroupV2Helper { final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams); return dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupsV2AuthorizationString); } catch (NonSuccessfulResponseCodeException e) { - if (e.getCode() == 403) { + if (e.code == 403) { throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null); } logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage()); @@ -94,7 +96,8 @@ class GroupV2Helper { } DecryptedGroupJoinInfo getDecryptedGroupJoinInfo( - GroupMasterKey groupMasterKey, GroupLinkPassword password + GroupMasterKey groupMasterKey, + GroupLinkPassword password ) throws IOException, GroupLinkNotActiveException { var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); @@ -105,7 +108,9 @@ class GroupV2Helper { } GroupHistoryPage getDecryptedGroupHistoryPage( - final GroupSecretParams groupSecretParams, int fromRevision, long sendEndorsementsExpirationMs + final GroupSecretParams groupSecretParams, + int fromRevision, + long sendEndorsementsExpirationMs ) throws NotAGroupMemberException { try { final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams); @@ -115,8 +120,10 @@ class GroupV2Helper { groupsV2AuthorizationString, false, sendEndorsementsExpirationMs); + } catch (NotInGroupException e) { + throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null); } catch (NonSuccessfulResponseCodeException e) { - if (e.getCode() == 403) { + if (e.code == 403) { throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null); } logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage()); @@ -138,9 +145,7 @@ class GroupV2Helper { return partialDecryptedGroup.revision; } - Pair createGroup( - String name, Set members, byte[] avatarFile - ) { + Pair createGroup(String name, Set members, byte[] avatarFile) { final var newGroup = buildNewGroup(name, members, avatarFile); if (newGroup == null) { return null; @@ -170,9 +175,7 @@ class GroupV2Helper { return new Pair<>(g, response); } - private GroupsV2Operations.NewGroup buildNewGroup( - String name, Set members, byte[] avatar - ) { + private GroupsV2Operations.NewGroup buildNewGroup(String name, Set members, byte[] avatar) { final var profileKeyCredential = context.getProfileHelper() .getExpiringProfileKeyCredential(context.getAccount().getSelfRecipientId()); if (profileKeyCredential == null) { @@ -202,7 +205,10 @@ class GroupV2Helper { } Pair updateGroup( - GroupInfoV2 groupInfoV2, String name, String description, byte[] avatarFile + GroupInfoV2 groupInfoV2, + String name, + String description, + byte[] avatarFile ) throws IOException { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams); @@ -225,7 +231,8 @@ class GroupV2Helper { } Pair addMembers( - GroupInfoV2 groupInfoV2, Set newMembers + GroupInfoV2 groupInfoV2, + Set newMembers ) throws IOException { GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); @@ -251,7 +258,8 @@ class GroupV2Helper { } Pair leaveGroup( - GroupInfoV2 groupInfoV2, Set membersToMakeAdmin + GroupInfoV2 groupInfoV2, + Set membersToMakeAdmin ) throws IOException { var pendingMembersList = groupInfoV2.getGroup().pendingMembers; final var selfAci = getSelfAci(); @@ -271,7 +279,8 @@ class GroupV2Helper { } Pair removeMembers( - GroupInfoV2 groupInfoV2, Set members + GroupInfoV2 groupInfoV2, + Set members ) throws IOException { final var memberUuids = members.stream() .map(context.getRecipientHelper()::resolveSignalServiceAddress) @@ -283,7 +292,8 @@ class GroupV2Helper { } Pair approveJoinRequestMembers( - GroupInfoV2 groupInfoV2, Set members + GroupInfoV2 groupInfoV2, + Set members ) throws IOException { final var memberUuids = members.stream() .map(context.getRecipientHelper()::resolveSignalServiceAddress) @@ -294,7 +304,8 @@ class GroupV2Helper { } Pair refuseJoinRequestMembers( - GroupInfoV2 groupInfoV2, Set members + GroupInfoV2 groupInfoV2, + Set members ) throws IOException { final var memberUuids = members.stream() .map(context.getRecipientHelper()::resolveSignalServiceAddress) @@ -304,7 +315,8 @@ class GroupV2Helper { } Pair revokeInvitedMembers( - GroupInfoV2 groupInfoV2, Set members + GroupInfoV2 groupInfoV2, + Set members ) throws IOException { var pendingMembersList = groupInfoV2.getGroup().pendingMembers; final var memberUuids = members.stream() @@ -318,7 +330,8 @@ class GroupV2Helper { } Pair banMembers( - GroupInfoV2 groupInfoV2, Set block + GroupInfoV2 groupInfoV2, + Set block ) throws IOException { GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); @@ -336,7 +349,8 @@ class GroupV2Helper { } Pair unbanMembers( - GroupInfoV2 groupInfoV2, Set block + GroupInfoV2 groupInfoV2, + Set block ) throws IOException { GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); @@ -359,7 +373,8 @@ class GroupV2Helper { } Pair setGroupLinkState( - GroupInfoV2 groupInfoV2, GroupLinkState state + GroupInfoV2 groupInfoV2, + GroupLinkState state ) throws IOException { final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); @@ -374,7 +389,8 @@ class GroupV2Helper { } Pair setEditDetailsPermission( - GroupInfoV2 groupInfoV2, GroupPermission permission + GroupInfoV2 groupInfoV2, + GroupPermission permission ) throws IOException { final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); @@ -384,7 +400,8 @@ class GroupV2Helper { } Pair setAddMemberPermission( - GroupInfoV2 groupInfoV2, GroupPermission permission + GroupInfoV2 groupInfoV2, + GroupPermission permission ) throws IOException { final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); @@ -468,7 +485,9 @@ class GroupV2Helper { } Pair setMemberAdmin( - GroupInfoV2 groupInfoV2, RecipientId recipientId, boolean admin + GroupInfoV2 groupInfoV2, + RecipientId recipientId, + boolean admin ) throws IOException { final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId); @@ -482,7 +501,8 @@ class GroupV2Helper { } Pair setMessageExpirationTimer( - GroupInfoV2 groupInfoV2, int messageExpirationTimer + GroupInfoV2 groupInfoV2, + int messageExpirationTimer ) throws IOException { final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final var change = groupOperations.createModifyGroupTimerChange(messageExpirationTimer); @@ -490,7 +510,8 @@ class GroupV2Helper { } Pair setIsAnnouncementGroup( - GroupInfoV2 groupInfoV2, boolean isAnnouncementGroup + GroupInfoV2 groupInfoV2, + boolean isAnnouncementGroup ) throws IOException { final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final var change = groupOperations.createAnnouncementGroupChange(isAnnouncementGroup); @@ -518,7 +539,8 @@ class GroupV2Helper { } private Pair revokeInvites( - GroupInfoV2 groupInfoV2, Set pendingMembers + GroupInfoV2 groupInfoV2, + Set pendingMembers ) throws IOException { final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final var uuidCipherTexts = pendingMembers.stream().map(member -> { @@ -532,28 +554,32 @@ class GroupV2Helper { } private Pair approveJoinRequest( - GroupInfoV2 groupInfoV2, Set uuids + GroupInfoV2 groupInfoV2, + Set uuids ) throws IOException { final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); return commitChange(groupInfoV2, groupOperations.createApproveGroupJoinRequest(uuids)); } private Pair refuseJoinRequest( - GroupInfoV2 groupInfoV2, Set serviceIds + GroupInfoV2 groupInfoV2, + Set serviceIds ) throws IOException { final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); return commitChange(groupInfoV2, groupOperations.createRefuseGroupJoinRequest(serviceIds, false, List.of())); } private Pair ejectMembers( - GroupInfoV2 groupInfoV2, Set members + GroupInfoV2 groupInfoV2, + Set members ) throws IOException { final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(members, false, List.of())); } private Pair commitChange( - GroupInfoV2 groupInfoV2, GroupChange.Actions.Builder change + GroupInfoV2 groupInfoV2, + GroupChange.Actions.Builder change ) throws IOException { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); final var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams); @@ -630,11 +656,13 @@ class GroupV2Helper { DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) { if (signedGroupChange != null) { - var groupOperations = dependencies.getGroupsV2Operations() - .forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey)); + final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); + final var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams); + final var groupId = groupSecretParams.getPublicParams().getGroupIdentifier(); try { - return groupOperations.decryptChange(GroupChange.ADAPTER.decode(signedGroupChange), true).orElse(null); + return groupOperations.decryptChange(GroupChange.ADAPTER.decode(signedGroupChange), + DecryptChangeVerificationMode.verify(groupId)).orElse(null); } catch (VerificationFailedException | InvalidGroupStateException | IOException e) { return null; } @@ -676,7 +704,8 @@ class GroupV2Helper { } private GroupsV2AuthorizationString getAuthorizationString( - final GroupSecretParams groupSecretParams, final long todaySeconds + final GroupSecretParams groupSecretParams, + final long todaySeconds ) throws VerificationFailedException { var authCredentialResponse = groupApiCredentials.get(todaySeconds); final var aci = getSelfAci(); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java index cc71e15c..82d44fde 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java @@ -66,9 +66,7 @@ public class IdentityHelper { return fingerprint == null ? null : fingerprint.getScannableFingerprint(); } - private Fingerprint computeSafetyNumberFingerprint( - final ServiceId serviceId, final IdentityKey theirIdentityKey - ) { + private Fingerprint computeSafetyNumberFingerprint(final ServiceId serviceId, final IdentityKey theirIdentityKey) { if (!serviceId.isUnknown()) { return Utils.computeSafetyNumberForUuid(account.getAci(), account.getAciIdentityKeyPair().getPublicKey(), @@ -89,7 +87,9 @@ public class IdentityHelper { } private boolean trustIdentity( - RecipientId recipientId, BiFunction verifier, TrustLevel trustLevel + RecipientId recipientId, + BiFunction verifier, + TrustLevel trustLevel ) { final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId); final var serviceId = address.serviceId().orElse(null); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 294b17f6..dd9cb38f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -31,6 +31,7 @@ import org.asamk.signal.manager.internal.SignalDependencies; import org.asamk.signal.manager.jobs.RetrieveStickerPackJob; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfoV1; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.stickers.StickerPack; import org.signal.libsignal.metadata.ProtocolInvalidKeyException; @@ -40,6 +41,7 @@ import org.signal.libsignal.metadata.ProtocolNoSessionException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.signal.libsignal.metadata.SelfSendException; import org.signal.libsignal.protocol.InvalidMessageException; +import org.signal.libsignal.protocol.UsePqRatchet; import org.signal.libsignal.protocol.groups.GroupSessionBuilder; import org.signal.libsignal.protocol.message.DecryptionErrorMessage; import org.signal.libsignal.zkgroup.InvalidInputException; @@ -104,7 +106,7 @@ public final class IncomingMessageHandler { try { final var cipherResult = dependencies.getCipher(destination == null || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) - .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); + .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp(), UsePqRatchet.NO); content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp()); if (content == null) { return new Pair<>(List.of(), null); @@ -142,7 +144,7 @@ public final class IncomingMessageHandler { try { final var cipherResult = dependencies.getCipher(destination == null || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) - .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); + .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp(), UsePqRatchet.NO); content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp()); if (content == null) { return new Pair<>(List.of(), null); @@ -156,6 +158,9 @@ public final class IncomingMessageHandler { } catch (ProtocolInvalidKeyIdException | ProtocolInvalidKeyException | ProtocolNoSessionException | ProtocolInvalidMessageException e) { logger.debug("Failed to decrypt incoming message", e); + if (e instanceof ProtocolInvalidKeyIdException) { + actions.add(RefreshPreKeysAction.create()); + } final var sender = account.getRecipientResolver().resolveRecipient(e.getSender()); if (context.getContactHelper().isContactBlocked(sender)) { logger.debug("Received invalid message from blocked contact, ignoring."); @@ -164,12 +169,11 @@ public final class IncomingMessageHandler { if (serviceId != null) { final var isSelf = sender.equals(account.getSelfRecipientId()) && e.getSenderDevice() == account.getDeviceId(); + logger.debug("Received invalid message, queuing renew session action."); + actions.add(new RenewSessionAction(sender, serviceId, destination)); if (!isSelf) { logger.debug("Received invalid message, requesting message resend."); - actions.add(new SendRetryMessageRequestAction(sender, serviceId, e, envelope, destination)); - } else { - logger.debug("Received invalid message, queuing renew session action."); - actions.add(new RenewSessionAction(sender, serviceId, destination)); + actions.add(new SendRetryMessageRequestAction(sender, e, envelope)); } } else { logger.debug("Received invalid message from invalid sender: {}", e.getSender()); @@ -190,7 +194,9 @@ public final class IncomingMessageHandler { } private SignalServiceContent validate( - Envelope envelope, SignalServiceCipherResult cipherResult, long serverDeliveredTimestamp + Envelope envelope, + SignalServiceCipherResult cipherResult, + long serverDeliveredTimestamp ) throws ProtocolInvalidKeyException, ProtocolInvalidMessageException, UnsupportedDataMessageException, InvalidMessageStructureException { final var content = cipherResult.getContent(); final var envelopeMetadata = cipherResult.getMetadata(); @@ -280,7 +286,9 @@ public final class IncomingMessageHandler { } public List handleMessage( - SignalServiceEnvelope envelope, SignalServiceContent content, ReceiveConfig receiveConfig + SignalServiceEnvelope envelope, + SignalServiceContent content, + ReceiveConfig receiveConfig ) { var actions = new ArrayList(); final var senderDeviceAddress = getSender(envelope, content); @@ -381,7 +389,8 @@ public final class IncomingMessageHandler { } private boolean handlePniSignatureMessage( - final SignalServicePniSignatureMessage message, final SignalServiceAddress senderAddress + final SignalServicePniSignatureMessage message, + final SignalServiceAddress senderAddress ) { final var aci = senderAddress.getServiceId(); final var aciIdentity = account.getIdentityKeyStore().getIdentityInfo(aci); @@ -520,12 +529,12 @@ public final class IncomingMessageHandler { } if (syncMessage.getBlockedList().isPresent()) { final var blockedListMessage = syncMessage.getBlockedList().get(); - for (var address : blockedListMessage.getAddresses()) { - context.getContactHelper() - .setContactBlocked(account.getRecipientResolver().resolveRecipient(address), true); + for (var individual : blockedListMessage.individuals) { + final var address = new RecipientAddress(individual.getAci(), individual.getE164()); + final var recipientId = account.getRecipientResolver().resolveRecipient(address); + context.getContactHelper().setContactBlocked(recipientId, true); } - for (var groupId : blockedListMessage.getGroupIds() - .stream() + for (var groupId : blockedListMessage.groupIds.stream() .map(GroupId::unknownVersion) .collect(Collectors.toSet())) { try { @@ -580,14 +589,22 @@ public final class IncomingMessageHandler { } if (syncMessage.getKeys().isPresent()) { final var keysMessage = syncMessage.getKeys().get(); - if (keysMessage.getStorageService().isPresent()) { - final var storageKey = keysMessage.getStorageService().get(); + if (keysMessage.getAccountEntropyPool() != null) { + final var aep = keysMessage.getAccountEntropyPool(); + account.setAccountEntropyPool(aep); + actions.add(SyncStorageDataAction.create()); + } else if (keysMessage.getMaster() != null) { + final var masterKey = keysMessage.getMaster(); + account.setMasterKey(masterKey); + actions.add(SyncStorageDataAction.create()); + } else if (keysMessage.getStorageService() != null) { + final var storageKey = keysMessage.getStorageService(); account.setStorageKey(storageKey); actions.add(SyncStorageDataAction.create()); } - if (keysMessage.getMaster().isPresent()) { - final var masterKey = keysMessage.getMaster().get(); - account.setMasterKey(masterKey); + if (keysMessage.getMediaRootBackupKey() != null) { + final var mrb = keysMessage.getMediaRootBackupKey(); + account.setMediaRootBackupKey(mrb); actions.add(SyncStorageDataAction.create()); } } @@ -865,7 +882,9 @@ public final class IncomingMessageHandler { } private List handleSignalServiceStoryMessage( - SignalServiceStoryMessage message, RecipientId source, boolean ignoreAttachments + SignalServiceStoryMessage message, + RecipientId source, + boolean ignoreAttachments ) { var actions = new ArrayList(); if (message.getGroupContext().isPresent()) { @@ -946,7 +965,7 @@ public final class IncomingMessageHandler { private DeviceAddress getDestination(SignalServiceEnvelope envelope) { final var destination = envelope.getDestinationServiceId(); - if (destination == null) { + if (destination == null || destination.isUnknown()) { return new DeviceAddress(account.getSelfRecipientId(), account.getAci(), account.getDeviceId()); } return new DeviceAddress(account.getRecipientResolver().resolveRecipient(destination), diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/PinHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/PinHelper.java index 7669b383..40878647 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/PinHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/PinHelper.java @@ -21,9 +21,7 @@ public class PinHelper { this.secureValueRecoveries = secureValueRecoveries; } - public void setRegistrationLockPin( - String pin, MasterKey masterKey - ) throws IOException { + public void setRegistrationLockPin(String pin, MasterKey masterKey) throws IOException { IOException exception = null; for (final var secureValueRecovery : secureValueRecoveries) { try { @@ -82,14 +80,19 @@ public class PinHelper { } public SecureValueRecovery.RestoreResponse.Success getRegistrationLockData( - String pin, LockedException lockedException + String pin, + LockedException lockedException ) throws IOException, IncorrectPinException { var svr2Credentials = lockedException.getSvr2Credentials(); if (svr2Credentials != null) { IOException exception = null; for (final var secureValueRecovery : secureValueRecoveries) { try { - return getRegistrationLockData(secureValueRecovery, svr2Credentials, pin); + final var lockData = getRegistrationLockData(secureValueRecovery, svr2Credentials, pin); + if (lockData == null) { + continue; + } + return lockData; } catch (IOException e) { exception = e; } @@ -103,7 +106,9 @@ public class PinHelper { } public SecureValueRecovery.RestoreResponse.Success getRegistrationLockData( - SecureValueRecovery secureValueRecovery, AuthCredentials authCredentials, String pin + SecureValueRecovery secureValueRecovery, + AuthCredentials authCredentials, + String pin ) throws IOException, IncorrectPinException { final var restoreResponse = secureValueRecovery.restoreDataPreRegistration(authCredentials, null, pin); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java index 864ef656..bf7ad580 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java @@ -11,17 +11,19 @@ import org.signal.libsignal.protocol.state.PreKeyRecord; import org.signal.libsignal.protocol.state.SignedPreKeyRecord; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.NetworkResultUtil; import org.whispersystems.signalservice.api.account.PreKeyUpload; +import org.whispersystems.signalservice.api.keys.OneTimePreKeyCounts; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; -import org.whispersystems.signalservice.internal.push.OneTimePreKeyCounts; import java.io.IOException; import java.util.List; import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_STALE_AGE; import static org.asamk.signal.manager.config.ServiceConfig.SIGNED_PREKEY_ROTATE_AGE; +import static org.asamk.signal.manager.util.Utils.handleResponseException; public class PreKeyHelper { @@ -30,9 +32,7 @@ public class PreKeyHelper { private final SignalAccount account; private final SignalDependencies dependencies; - public PreKeyHelper( - final SignalAccount account, final SignalDependencies dependencies - ) { + public PreKeyHelper(final SignalAccount account, final SignalDependencies dependencies) { this.account = account; this.dependencies = dependencies; } @@ -79,11 +79,12 @@ public class PreKeyHelper { } private boolean refreshPreKeysIfNecessary( - final ServiceIdType serviceIdType, final IdentityKeyPair identityKeyPair + final ServiceIdType serviceIdType, + final IdentityKeyPair identityKeyPair ) throws IOException { OneTimePreKeyCounts preKeyCounts; try { - preKeyCounts = dependencies.getAccountManager().getPreKeyCounts(serviceIdType); + preKeyCounts = handleResponseException(dependencies.getKeysApi().getAvailablePreKeyCounts(serviceIdType)); } catch (AuthorizationFailedException e) { logger.debug("Failed to get pre key count, ignoring: " + e.getClass().getSimpleName()); preKeyCounts = new OneTimePreKeyCounts(0, 0); @@ -144,7 +145,7 @@ public class PreKeyHelper { kyberPreKeyRecords); var needsReset = false; try { - dependencies.getAccountManager().setPreKeys(preKeyUpload); + NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeys(preKeyUpload)); try { if (preKeyRecords != null) { account.addPreKeys(serviceIdType, preKeyRecords); @@ -173,7 +174,7 @@ public class PreKeyHelper { // This can happen when the primary device has changed phone number logger.warn("Failed to updated pre keys: {}", e.getMessage()); } catch (NonSuccessfulResponseCodeException e) { - if (serviceIdType != ServiceIdType.PNI || e.getCode() != 422) { + if (serviceIdType != ServiceIdType.PNI || e.code != 422) { throw e; } logger.warn("Failed to set PNI pre keys, ignoring for now. Account needs to be reregistered to fix this."); @@ -221,7 +222,8 @@ public class PreKeyHelper { } private List generateKyberPreKeys( - ServiceIdType serviceIdType, final IdentityKeyPair identityKeyPair + ServiceIdType serviceIdType, + final IdentityKeyPair identityKeyPair ) { final var accountData = account.getAccountData(serviceIdType); final var offset = accountData.getPreKeyMetadata().getNextKyberPreKeyId(); @@ -246,7 +248,9 @@ public class PreKeyHelper { } private KyberPreKeyRecord generateLastResortKyberPreKey( - ServiceIdType serviceIdType, IdentityKeyPair identityKeyPair, final int offset + ServiceIdType serviceIdType, + IdentityKeyPair identityKeyPair, + final int offset ) { final var accountData = account.getAccountData(serviceIdType); final var signedPreKeyId = accountData.getPreKeyMetadata().getNextKyberPreKeyId() + offset; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index 03d277f9..af16ed1d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -23,6 +23,7 @@ import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.NetworkResultUtil; import org.whispersystems.signalservice.api.crypto.SealedSenderAccess; import org.whispersystems.signalservice.api.profiles.AvatarUploadParams; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; @@ -196,9 +197,10 @@ public final class ProfileHelper { : avatar == null ? AvatarUploadParams.unchanged(true) : AvatarUploadParams.unchanged(false); final var paymentsAddress = Optional.ofNullable(newProfile.getMobileCoinAddress()) .map(address -> PaymentUtils.signPaymentsAddress(address, - account.getAciIdentityKeyPair().getPrivateKey())); + account.getAciIdentityKeyPair().getPrivateKey())) + .orElse(null); logger.debug("Uploading new profile"); - final var avatarPath = dependencies.getAccountManager() + final var avatarPath = NetworkResultUtil.toSetProfileLegacy(dependencies.getProfileApi() .setVersionedProfile(account.getAci(), account.getProfileKey(), newProfile.getInternalServiceName(), @@ -208,9 +210,9 @@ public final class ProfileHelper { avatarUploadParams, List.of(/* TODO implement support for badges */), account.getConfigurationStore().getPhoneNumberSharingMode() - == PhoneNumberSharingMode.EVERYBODY); + == PhoneNumberSharingMode.EVERYBODY)); if (!avatarUploadParams.keepTheSame) { - builder.withAvatarUrlPath(avatarPath.orElse(null)); + builder.withAvatarUrlPath(avatarPath); } newProfile = builder.build(); } @@ -271,7 +273,9 @@ public final class ProfileHelper { } private Profile decryptProfileAndDownloadAvatar( - final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile + final RecipientId recipientId, + final ProfileKey profileKey, + final SignalServiceProfile encryptedProfile ) { final var avatarPath = encryptedProfile.getAvatar(); downloadProfileAvatar(recipientId, avatarPath, profileKey); @@ -280,7 +284,9 @@ public final class ProfileHelper { } public void downloadProfileAvatar( - final RecipientId recipientId, final String avatarPath, final ProfileKey profileKey + final RecipientId recipientId, + final String avatarPath, + final ProfileKey profileKey ) { var profile = account.getProfileStore().getProfile(recipientId); if (profile == null || !Objects.equals(avatarPath, profile.getAvatarUrlPath())) { @@ -308,7 +314,8 @@ public final class ProfileHelper { } private Single retrieveProfile( - RecipientId recipientId, SignalServiceProfile.RequestType requestType + RecipientId recipientId, + SignalServiceProfile.RequestType requestType ) { var unidentifiedAccess = getUnidentifiedAccess(recipientId); var profileKey = Optional.ofNullable(account.getProfileStore().getProfileKey(recipientId)); @@ -331,13 +338,6 @@ public final class ProfileHelper { final var profile = account.getProfileStore().getProfile(recipientId); - if (recipientId.equals(account.getSelfRecipientId())) { - final var isUnrestricted = encryptedProfile.isUnrestrictedUnidentifiedAccess(); - if (account.isUnrestrictedUnidentifiedAccess() != isUnrestricted) { - account.setUnrestrictedUnidentifiedAccess(isUnrestricted); - } - } - Profile newProfile = null; if (profileKey.isPresent()) { logger.trace("Decrypting profile"); @@ -353,6 +353,18 @@ public final class ProfileHelper { .build(); } + if (recipientId.equals(account.getSelfRecipientId())) { + final var isUnrestricted = encryptedProfile.isUnrestrictedUnidentifiedAccess(); + if (account.isUnrestrictedUnidentifiedAccess() != isUnrestricted) { + account.setUnrestrictedUnidentifiedAccess(isUnrestricted); + } + if (account.isPrimaryDevice() && profile != null && newProfile.getCapabilities() + .contains(Profile.Capability.storageServiceEncryptionV2Capability) && !profile.getCapabilities() + .contains(Profile.Capability.storageServiceEncryptionV2Capability)) { + context.getJobExecutor().enqueueJob(new SyncStorageJob(true)); + } + } + try { logger.trace("Storing identity"); final var identityKey = new IdentityKey(Base64.getDecoder().decode(encryptedProfile.getIdentityKey())); @@ -408,9 +420,7 @@ public final class ProfileHelper { }); } - private void downloadProfileAvatar( - RecipientAddress address, String avatarPath, ProfileKey profileKey - ) { + private void downloadProfileAvatar(RecipientAddress address, String avatarPath, ProfileKey profileKey) { if (avatarPath == null) { try { context.getAvatarStore().deleteProfileAvatar(address); @@ -430,7 +440,9 @@ public final class ProfileHelper { } private void retrieveProfileAvatar( - String avatarPath, ProfileKey profileKey, OutputStream outputStream + String avatarPath, + ProfileKey profileKey, + OutputStream outputStream ) throws IOException { var tmpFile = IOUtils.createTempFile(); try (var input = dependencies.getMessageReceiver() diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java index baa0d583..71f69081 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java @@ -11,10 +11,10 @@ import org.asamk.signal.manager.storage.messageCache.CachedMessage; import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.SignalWebSocket; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId.ACI; +import org.whispersystems.signalservice.api.websocket.SignalWebSocket; import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState; import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException; @@ -28,7 +28,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.TimeoutException; -import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.schedulers.Schedulers; public class ReceiveHelper { @@ -83,7 +82,10 @@ public class ReceiveHelper { } public void receiveMessages( - Duration timeout, boolean returnOnTimeout, Integer maxMessages, Manager.ReceiveMessageHandler handler + Duration timeout, + boolean returnOnTimeout, + Integer maxMessages, + Manager.ReceiveMessageHandler handler ) throws IOException { account.setNeedsToRetryFailedMessages(true); hasCaughtUpWithOldMessages = false; @@ -91,14 +93,14 @@ public class ReceiveHelper { // Use a Map here because java Set doesn't have a get method ... Map queuedActions = new HashMap<>(); - final var signalWebSocket = dependencies.getSignalWebSocket(); - final var webSocketStateDisposable = Observable.merge(signalWebSocket.getUnidentifiedWebSocketState(), - signalWebSocket.getWebSocketState()) + final var signalWebSocket = dependencies.getAuthenticatedSignalWebSocket(); + final var webSocketStateDisposable = signalWebSocket.getState() .subscribeOn(Schedulers.computation()) .observeOn(Schedulers.computation()) .distinctUntilChanged() .subscribe(this::onWebSocketStateChange); signalWebSocket.connect(); + signalWebSocket.registerKeepAliveToken("receive"); try { receiveMessagesInternal(signalWebSocket, timeout, returnOnTimeout, maxMessages, handler, queuedActions); @@ -106,6 +108,7 @@ public class ReceiveHelper { hasCaughtUpWithOldMessages = false; handleQueuedActions(queuedActions.keySet()); queuedActions.clear(); + signalWebSocket.removeKeepAliveToken("receive"); signalWebSocket.disconnect(); webSocketStateDisposable.dispose(); shouldStop = false; @@ -113,7 +116,7 @@ public class ReceiveHelper { } private void receiveMessagesInternal( - final SignalWebSocket signalWebSocket, + final SignalWebSocket.AuthenticatedWebSocket signalWebSocket, Duration timeout, boolean returnOnTimeout, Integer maxMessages, @@ -264,7 +267,8 @@ public class ReceiveHelper { } private List retryFailedReceivedMessage( - final Manager.ReceiveMessageHandler handler, final CachedMessage cachedMessage + final Manager.ReceiveMessageHandler handler, + final CachedMessage cachedMessage ) { var envelope = cachedMessage.loadEnvelope(); if (envelope == null) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java index 24b3eb5f..a73bb741 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java @@ -10,13 +10,14 @@ import org.signal.libsignal.usernames.BaseUsernameException; import org.signal.libsignal.usernames.Username; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.cds.CdsiV2Service; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.PNI; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException; import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException; -import org.whispersystems.signalservice.api.services.CdsiV2Service; +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import java.io.IOException; import java.util.Collection; @@ -25,8 +26,10 @@ import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.UUID; import static org.asamk.signal.manager.config.ServiceConfig.MAXIMUM_ONE_OFF_REQUEST_SIZE; +import static org.asamk.signal.manager.util.Utils.handleResponseException; public class RecipientHelper { @@ -66,7 +69,7 @@ public class RecipientHelper { .toSignalServiceAddress(); } - public Set resolveRecipients(Collection recipients) throws UnregisteredRecipientException { + public Set resolveRecipients(Collection recipients) throws UnregisteredRecipientException, IOException { final var recipientIds = new HashSet(recipients.size()); for (var number : recipients) { final var recipientId = resolveRecipient(number); @@ -76,12 +79,11 @@ public class RecipientHelper { } public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredRecipientException { - if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) { - return account.getRecipientResolver().resolveRecipient(ACI.from(uuidRecipient.uuid())); - } else if (recipient instanceof RecipientIdentifier.Pni pniRecipient) { - return account.getRecipientResolver().resolveRecipient(PNI.parseOrThrow(pniRecipient.pni())); - } else if (recipient instanceof RecipientIdentifier.Number numberRecipient) { - final var number = numberRecipient.number(); + if (recipient instanceof RecipientIdentifier.Uuid(UUID uuid)) { + return account.getRecipientResolver().resolveRecipient(ACI.from(uuid)); + } else if (recipient instanceof RecipientIdentifier.Pni(UUID pni)) { + return account.getRecipientResolver().resolveRecipient(PNI.from(pni)); + } else if (recipient instanceof RecipientIdentifier.Number(String number)) { return account.getRecipientStore().resolveRecipientByNumber(number, () -> { try { return getRegisteredUserByNumber(number); @@ -89,16 +91,20 @@ public class RecipientHelper { return null; } }); - } else if (recipient instanceof RecipientIdentifier.Username usernameRecipient) { - var username = usernameRecipient.username(); - return resolveRecipientByUsernameOrLink(username, false); + } else if (recipient instanceof RecipientIdentifier.Username(String username)) { + try { + return resolveRecipientByUsernameOrLink(username, false); + } catch (Exception e) { + return null; + } } throw new AssertionError("Unexpected RecipientIdentifier: " + recipient); } public RecipientId resolveRecipientByUsernameOrLink( - String username, boolean forceRefresh - ) throws UnregisteredRecipientException { + String username, + boolean forceRefresh + ) throws UnregisteredRecipientException, IOException { final Username finalUsername; try { finalUsername = getUsernameFromUsernameOrLink(username); @@ -107,18 +113,22 @@ public class RecipientHelper { } if (forceRefresh) { try { - final var aci = dependencies.getAccountManager().getAciByUsername(finalUsername); + final var aci = handleResponseException(dependencies.getUsernameApi().getAciByUsername(finalUsername)); return account.getRecipientStore().resolveRecipientTrusted(aci, finalUsername.getUsername()); - } catch (IOException e) { - throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null, - null, - null, - username)); + } catch (NonSuccessfulResponseCodeException e) { + if (e.code == 404) { + throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null, + null, + null, + username)); + } + logger.debug("Failed to get uuid for username: {}", username, e); + throw e; } } return account.getRecipientStore().resolveRecipientByUsername(finalUsername.getUsername(), () -> { try { - return dependencies.getAccountManager().getAciByUsername(finalUsername); + return handleResponseException(dependencies.getUsernameApi().getAciByUsername(finalUsername)); } catch (Exception e) { return null; } @@ -129,8 +139,8 @@ public class RecipientHelper { try { final var usernameLinkUrl = UsernameLinkUrl.fromUri(username); final var components = usernameLinkUrl.getComponents(); - final var encryptedUsername = dependencies.getAccountManager() - .getEncryptedUsernameFromLinkServerId(components.getServerId()); + final var encryptedUsername = handleResponseException(dependencies.getUsernameApi() + .getEncryptedUsernameFromLinkServerId(components.getServerId())); final var link = new Username.UsernameLink(components.getEntropy(), encryptedUsername); return Username.fromLink(link); @@ -143,8 +153,8 @@ public class RecipientHelper { try { return Optional.of(resolveRecipient(recipient)); } catch (UnregisteredRecipientException e) { - if (recipient instanceof RecipientIdentifier.Number r) { - return account.getRecipientStore().resolveRecipientByNumberOptional(r.number()); + if (recipient instanceof RecipientIdentifier.Number(String number)) { + return account.getRecipientStore().resolveRecipientByNumberOptional(number); } else { return Optional.empty(); } @@ -180,7 +190,8 @@ public class RecipientHelper { } private Map getRegisteredUsers( - final Set numbers, final boolean isPartialRefresh + final Set numbers, + final boolean isPartialRefresh ) throws IOException { Map registeredUsers = getRegisteredUsersV2(numbers, isPartialRefresh); @@ -211,7 +222,8 @@ public class RecipientHelper { } private Map getRegisteredUsersV2( - final Set numbers, boolean isPartialRefresh + final Set numbers, + boolean isPartialRefresh ) throws IOException { final var previousNumbers = isPartialRefresh ? Set.of() : account.getCdsiStore().getAllNumbers(); final var newNumbers = new HashSet<>(numbers) {{ @@ -231,12 +243,11 @@ public class RecipientHelper { final CdsiV2Service.Response response; try { - response = dependencies.getAccountManager() - .getRegisteredUsersWithCdsi(token.isEmpty() ? Set.of() : previousNumbers, + response = handleResponseException(dependencies.getCdsApi() + .getRegisteredUsers(token.isEmpty() ? Set.of() : previousNumbers, newNumbers, account.getRecipientStore().getServiceIdToProfileKeyMap(), token, - dependencies.getServiceEnvironmentConfig().cdsiMrenclave(), null, dependencies.getLibSignalNetwork(), newToken -> { @@ -254,7 +265,7 @@ public class RecipientHelper { account.setCdsiToken(newToken); account.setLastRecipientsRefresh(System.currentTimeMillis()); } - }); + })); } catch (CdsiInvalidTokenException | CdsiInvalidArgumentException e) { account.setCdsiToken(null); account.getCdsiStore().clearAll(); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index 0622b4a8..2fb55003 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -125,7 +125,8 @@ public class SendHelper { } public SendMessageResult sendReceiptMessage( - final SignalServiceReceiptMessage receiptMessage, final RecipientId recipientId + final SignalServiceReceiptMessage receiptMessage, + final RecipientId recipientId ) { final var messageSendLogStore = account.getMessageSendLogStore(); final var result = handleSendMessage(recipientId, @@ -157,7 +158,9 @@ public class SendHelper { } public SendMessageResult sendRetryReceipt( - DecryptionErrorMessage errorMessage, RecipientId recipientId, Optional groupId + DecryptionErrorMessage errorMessage, + RecipientId recipientId, + Optional groupId ) { logger.debug("Sending retry receipt for {} to {}, device: {}", errorMessage.getTimestamp(), @@ -183,7 +186,8 @@ public class SendHelper { } public SendMessageResult sendSelfMessage( - SignalServiceDataMessage.Builder messageBuilder, Optional editTargetTimestamp + SignalServiceDataMessage.Builder messageBuilder, + Optional editTargetTimestamp ) { final var recipientId = account.getSelfRecipientId(); final var contact = account.getContactStore().getContact(recipientId); @@ -214,9 +218,7 @@ public class SendHelper { } } - public SendMessageResult sendTypingMessage( - SignalServiceTypingMessage message, RecipientId recipientId - ) { + public SendMessageResult sendTypingMessage(SignalServiceTypingMessage message, RecipientId recipientId) { final var result = handleSendMessage(recipientId, (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendTyping(List.of( address), List.of(unidentifiedAccess), message, null).getFirst()); @@ -225,7 +227,8 @@ public class SendHelper { } public List sendGroupTypingMessage( - SignalServiceTypingMessage message, GroupId groupId + SignalServiceTypingMessage message, + GroupId groupId ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { final var g = getGroupForSending(groupId); if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) { @@ -238,7 +241,9 @@ public class SendHelper { } public SendMessageResult resendMessage( - final RecipientId recipientId, final long timestamp, final MessageSendLogEntry messageSendLogEntry + final RecipientId recipientId, + final long timestamp, + final MessageSendLogEntry messageSendLogEntry ) { logger.trace("Resending message {} to {}", timestamp, recipientId); if (messageSendLogEntry.groupId().isEmpty()) { @@ -552,7 +557,9 @@ public class SendHelper { } private List sendGroupMessageInternalWithLegacy( - final LegacySenderHandler sender, final Set recipientIds, final boolean isRecipientUpdate + final LegacySenderHandler sender, + final Set recipientIds, + final boolean isRecipientUpdate ) throws IOException { final var recipientIdList = new ArrayList<>(recipientIds); final var addresses = recipientIdList.stream() @@ -644,7 +651,9 @@ public class SendHelper { } private SendMessageResult sendMessage( - SignalServiceDataMessage message, RecipientId recipientId, Optional editTargetTimestamp + SignalServiceDataMessage message, + RecipientId recipientId, + Optional editTargetTimestamp ) { final var messageSendLogStore = account.getMessageSendLogStore(); final var urgent = true; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StickerHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StickerHelper.java index 4a0d25fe..b6d90ff9 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/StickerHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/StickerHelper.java @@ -30,7 +30,9 @@ public class StickerHelper { } public StickerPack addOrUpdateStickerPack( - final StickerPackId stickerPackId, final byte[] stickerPackKey, final boolean installed + final StickerPackId stickerPackId, + final byte[] stickerPackKey, + final boolean installed ) { final var sticker = account.getStickerStore().getStickerPack(stickerPackId); if (sticker != null) { @@ -50,7 +52,8 @@ public class StickerHelper { } public JsonStickerPack getOrRetrieveStickerPack( - StickerPackId packId, byte[] packKey + StickerPackId packId, + byte[] packKey ) throws InvalidStickerException { try { retrieveStickerPack(packId, packKey); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java index c18e8df3..14db7c13 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java @@ -2,6 +2,7 @@ package org.asamk.signal.manager.helper; import org.asamk.signal.manager.api.GroupIdV1; import org.asamk.signal.manager.api.GroupIdV2; +import org.asamk.signal.manager.api.Profile; import org.asamk.signal.manager.internal.SignalDependencies; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.recipients.RecipientId; @@ -17,11 +18,17 @@ import org.signal.core.util.SetUtil; import org.signal.libsignal.protocol.InvalidKeyException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.storage.RecordIkm; import org.whispersystems.signalservice.api.storage.SignalStorageManifest; import org.whispersystems.signalservice.api.storage.SignalStorageRecord; import org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.api.storage.StorageKey; +import org.whispersystems.signalservice.api.storage.StorageRecordConvertersKt; +import org.whispersystems.signalservice.api.storage.StorageServiceRepository; +import org.whispersystems.signalservice.api.storage.StorageServiceRepository.ManifestIfDifferentVersionResult; +import org.whispersystems.signalservice.api.storage.StorageServiceRepository.WriteStorageRecordsResult; import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord; +import org.whispersystems.signalservice.internal.storage.protos.StorageRecord; import java.io.IOException; import java.sql.Connection; @@ -32,9 +39,10 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.stream.Collectors; +import static org.asamk.signal.manager.util.Utils.handleResponseException; + public class StorageHelper { private static final Logger logger = LoggerFactory.getLogger(StorageHelper.class); @@ -54,7 +62,7 @@ public class StorageHelper { } public void syncDataWithStorage() throws IOException { - final var storageKey = account.getOrCreateStorageKey(); + var storageKey = account.getOrCreateStorageKey(); if (storageKey == null) { if (!account.isPrimaryDevice()) { logger.debug("Storage key unknown, requesting from primary device."); @@ -65,52 +73,76 @@ public class StorageHelper { logger.trace("Reading manifest from remote storage"); final var localManifestVersion = account.getStorageManifestVersion(); - final var localManifest = account.getStorageManifest().orElse(SignalStorageManifest.EMPTY); - SignalStorageManifest remoteManifest; - try { - remoteManifest = dependencies.getAccountManager() - .getStorageManifestIfDifferentVersion(storageKey, localManifestVersion) - .orElse(localManifest); - } catch (InvalidKeyException e) { - logger.warn("Manifest couldn't be decrypted."); - if (account.isPrimaryDevice()) { - try { - forcePushToStorage(storageKey); - } catch (RetryLaterException rle) { - // TODO retry later - return; - } - } - return; - } - - logger.trace("Manifest versions: local {}, remote {}", localManifestVersion, remoteManifest.getVersion()); + final var localManifest = account.getStorageManifest().orElse(SignalStorageManifest.Companion.getEMPTY()); + final var storageServiceRepository = dependencies.getStorageServiceRepository(); + final var result = storageServiceRepository.getStorageManifestIfDifferentVersion(storageKey, + localManifestVersion); var needsForcePush = false; - if (remoteManifest.getVersion() > localManifestVersion) { - logger.trace("Remote version was newer, reading records."); - needsForcePush = readDataFromStorage(storageKey, localManifest, remoteManifest); - } else if (remoteManifest.getVersion() < localManifest.getVersion()) { - logger.debug("Remote storage manifest version was older. User might have switched accounts."); - } - logger.trace("Done reading data from remote storage"); + final var remoteManifest = switch (result) { + case ManifestIfDifferentVersionResult.DifferentVersion diff -> { + final var manifest = diff.getManifest(); + storeManifestLocally(manifest); + yield manifest; + } + case ManifestIfDifferentVersionResult.DecryptionError ignore -> { + logger.warn("Manifest couldn't be decrypted."); + if (account.isPrimaryDevice()) { + needsForcePush = true; + } else { + context.getSyncHelper().requestSyncKeys(); + } + yield null; + } + case ManifestIfDifferentVersionResult.SameVersion ignored -> localManifest; + case ManifestIfDifferentVersionResult.NetworkError e -> throw e.getException(); + case ManifestIfDifferentVersionResult.StatusCodeError e -> throw e.getException(); + default -> throw new RuntimeException("Unhandled ManifestIfDifferentVersionResult type"); + }; - if (localManifest != remoteManifest) { - storeManifestLocally(remoteManifest); - } + if (remoteManifest != null) { + logger.trace("Manifest versions: local {}, remote {}", localManifestVersion, remoteManifest.version); - readRecordsWithPreviouslyUnknownTypes(storageKey); + if (remoteManifest.version > localManifestVersion) { + logger.trace("Remote version was newer, reading records."); + needsForcePush = readDataFromStorage(storageKey, localManifest, remoteManifest); + } else if (remoteManifest.version < localManifest.version) { + logger.debug("Remote storage manifest version was older. User might have switched accounts."); + } + logger.trace("Done reading data from remote storage"); + + readRecordsWithPreviouslyUnknownTypes(storageKey, remoteManifest); + } logger.trace("Adding missing storageIds to local data"); account.getRecipientStore().setMissingStorageIds(); account.getGroupStore().setMissingStorageIds(); var needsMultiDeviceSync = false; - try { - needsMultiDeviceSync = writeToStorage(storageKey, remoteManifest, needsForcePush); - } catch (RetryLaterException e) { - // TODO retry later - return; + + if (account.needsStorageKeyMigration()) { + logger.debug("Storage needs force push due to new account entropy pool"); + // Set new aep and reset previous master key and storage key + account.setAccountEntropyPool(account.getOrCreateAccountEntropyPool()); + storageKey = account.getOrCreateStorageKey(); + context.getSyncHelper().sendKeysMessage(); + needsForcePush = true; + } else if (remoteManifest == null) { + if (account.isPrimaryDevice()) { + needsForcePush = true; + } + } else if (remoteManifest.recordIkm == null && account.getSelfRecipientProfile() + .getCapabilities() + .contains(Profile.Capability.storageServiceEncryptionV2Capability)) { + logger.debug("The SSRE2 capability is supported, but no recordIkm is set! Force pushing."); + needsForcePush = true; + } else { + try { + needsMultiDeviceSync = writeToStorage(storageKey, remoteManifest, needsForcePush); + } catch (RetryLaterException e) { + // TODO retry later + return; + } } if (needsForcePush) { @@ -131,6 +163,23 @@ public class StorageHelper { logger.debug("Done syncing data with remote storage"); } + public void forcePushToStorage() throws IOException { + if (!account.isPrimaryDevice()) { + return; + } + + final var storageKey = account.getOrCreateStorageKey(); + if (storageKey == null) { + return; + } + + try { + forcePushToStorage(storageKey); + } catch (RetryLaterException e) { + // TODO retry later + } + } + private boolean readDataFromStorage( final StorageKey storageKey, final SignalStorageManifest localManifest, @@ -140,36 +189,37 @@ public class StorageHelper { try (final var connection = account.getAccountDatabase().getConnection()) { connection.setAutoCommit(false); - var idDifference = findIdDifference(remoteManifest.getStorageIds(), localManifest.getStorageIds()); + var idDifference = findIdDifference(remoteManifest.storageIds, localManifest.storageIds); if (idDifference.hasTypeMismatches() && account.isPrimaryDevice()) { logger.debug("Found type mismatches in the ID sets! Scheduling a force push after this sync completes."); needsForcePush = true; } - logger.debug("Pre-Merge ID Difference :: " + idDifference); - - if (!idDifference.localOnlyIds().isEmpty()) { - final var updated = account.getRecipientStore() - .removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection, idDifference.localOnlyIds()); - - if (updated > 0) { - logger.warn( - "Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.", - updated); - } - } + logger.debug("Pre-Merge ID Difference :: {}", idDifference); if (!idDifference.isEmpty()) { - final var remoteOnlyRecords = getSignalStorageRecords(storageKey, idDifference.remoteOnlyIds()); + final var remoteOnlyRecords = getSignalStorageRecords(storageKey, + remoteManifest, + idDifference.remoteOnlyIds()); if (remoteOnlyRecords.size() != idDifference.remoteOnlyIds().size()) { - logger.debug("Could not find all remote-only records! Requested: " - + idDifference.remoteOnlyIds() - .size() - + ", Found: " - + remoteOnlyRecords.size() - + ". These stragglers should naturally get deleted during the sync."); + logger.debug( + "Could not find all remote-only records! Requested: {}, Found: {}. These stragglers should naturally get deleted during the sync.", + idDifference.remoteOnlyIds().size(), + remoteOnlyRecords.size()); + } + + if (!idDifference.localOnlyIds().isEmpty()) { + final var updated = account.getRecipientStore() + .removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection, + idDifference.localOnlyIds()); + + if (updated > 0) { + logger.warn( + "Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.", + updated); + } } final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords); @@ -194,18 +244,21 @@ public class StorageHelper { return needsForcePush; } - private void readRecordsWithPreviouslyUnknownTypes(final StorageKey storageKey) throws IOException { + private void readRecordsWithPreviouslyUnknownTypes( + final StorageKey storageKey, + final SignalStorageManifest remoteManifest + ) throws IOException { try (final var connection = account.getAccountDatabase().getConnection()) { connection.setAutoCommit(false); final var knownUnknownIds = account.getUnknownStorageIdStore() .getUnknownStorageIds(connection, KNOWN_TYPES); if (!knownUnknownIds.isEmpty()) { - logger.debug("We have " + knownUnknownIds.size() + " unknown records that we can now process."); + logger.debug("We have {} unknown records that we can now process.", knownUnknownIds.size()); - final var remote = getSignalStorageRecords(storageKey, knownUnknownIds); + final var remote = getSignalStorageRecords(storageKey, remoteManifest, knownUnknownIds); - logger.debug("Found " + remote.size() + " of the known-unknowns remotely."); + logger.debug("Found {} of the known-unknowns remotely.", remote.size()); processKnownRecords(connection, remote); account.getUnknownStorageIdStore() @@ -218,22 +271,37 @@ public class StorageHelper { } private boolean writeToStorage( - final StorageKey storageKey, final SignalStorageManifest remoteManifest, final boolean needsForcePush + final StorageKey storageKey, + final SignalStorageManifest remoteManifest, + final boolean needsForcePush ) throws IOException, RetryLaterException { final WriteOperationResult remoteWriteOperation; try (final var connection = account.getAccountDatabase().getConnection()) { connection.setAutoCommit(false); - final var localStorageIds = getAllLocalStorageIds(connection); - final var idDifference = findIdDifference(remoteManifest.getStorageIds(), localStorageIds); - logger.debug("ID Difference :: " + idDifference); + var localStorageIds = getAllLocalStorageIds(connection); + var idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds); + logger.debug("ID Difference :: {}", idDifference); + + final var unknownOnlyLocal = idDifference.localOnlyIds() + .stream() + .filter(id -> !KNOWN_TYPES.contains(id.getType())) + .toList(); + + if (!unknownOnlyLocal.isEmpty()) { + logger.debug("Storage ids with unknown type: {} to delete", unknownOnlyLocal.size()); + account.getUnknownStorageIdStore().deleteUnknownStorageIds(connection, unknownOnlyLocal); + localStorageIds = getAllLocalStorageIds(connection); + idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds); + } final var remoteDeletes = idDifference.remoteOnlyIds().stream().map(StorageId::getRaw).toList(); final var remoteInserts = buildLocalStorageRecords(connection, idDifference.localOnlyIds()); // TODO check if local storage record proto matches remote, then reset to remote storage_id - remoteWriteOperation = new WriteOperationResult(new SignalStorageManifest(remoteManifest.getVersion() + 1, + remoteWriteOperation = new WriteOperationResult(new SignalStorageManifest(remoteManifest.version + 1, account.getDeviceId(), + remoteManifest.recordIkm, localStorageIds), remoteInserts, remoteDeletes); connection.commit(); @@ -242,39 +310,37 @@ public class StorageHelper { } if (remoteWriteOperation.isEmpty()) { - logger.debug("No remote writes needed. Still at version: " + remoteManifest.getVersion()); + logger.debug("No remote writes needed. Still at version: {}", remoteManifest.version); return false; } logger.debug("We have something to write remotely."); - logger.debug("WriteOperationResult :: " + remoteWriteOperation); + logger.debug("WriteOperationResult :: {}", remoteWriteOperation); StorageSyncValidations.validate(remoteWriteOperation, remoteManifest, needsForcePush, account.getSelfRecipientAddress()); - final Optional conflict; - try { - conflict = dependencies.getAccountManager() - .writeStorageRecords(storageKey, - remoteWriteOperation.manifest(), - remoteWriteOperation.inserts(), - remoteWriteOperation.deletes()); - } catch (InvalidKeyException e) { - logger.warn("Failed to decrypt conflicting storage manifest: {}", e.getMessage()); - throw new IOException(e); + final var result = dependencies.getStorageServiceRepository() + .writeStorageRecords(storageKey, + remoteWriteOperation.manifest(), + remoteWriteOperation.inserts(), + remoteWriteOperation.deletes()); + switch (result) { + case WriteStorageRecordsResult.ConflictError ignored -> { + logger.debug("Hit a conflict when trying to resolve the conflict! Retrying."); + throw new RetryLaterException(); + } + case WriteStorageRecordsResult.NetworkError networkError -> throw networkError.getException(); + case WriteStorageRecordsResult.StatusCodeError statusCodeError -> throw statusCodeError.getException(); + case WriteStorageRecordsResult.Success ignored -> { + logger.debug("Saved new manifest. Now at version: {}", remoteWriteOperation.manifest().version); + storeManifestLocally(remoteWriteOperation.manifest()); + return true; + } + default -> throw new IllegalStateException("Unexpected value: " + result); } - - if (conflict.isPresent()) { - logger.debug("Hit a conflict when trying to resolve the conflict! Retrying."); - throw new RetryLaterException(); - } - - logger.debug("Saved new manifest. Now at version: " + remoteWriteOperation.manifest().getVersion()); - storeManifestLocally(remoteWriteOperation.manifest()); - - return true; } private void forcePushToStorage( @@ -282,7 +348,8 @@ public class StorageHelper { ) throws IOException, RetryLaterException { logger.debug("Force pushing local state to remote storage"); - final var currentVersion = dependencies.getAccountManager().getStorageManifestVersion(); + final var currentVersion = handleResponseException(dependencies.getStorageServiceRepository() + .getManifestVersion()); final var newVersion = currentVersion + 1; final var newStorageRecords = new ArrayList(); final Map newContactStorageIds; @@ -298,17 +365,19 @@ public class StorageHelper { final var storageId = newContactStorageIds.get(recipientId); if (storageId.getType() == ManifestRecord.Identifier.Type.ACCOUNT.getValue()) { final var recipient = account.getRecipientStore().getRecipient(connection, recipientId); - final var accountRecord = StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(), + final var accountRecord = StorageSyncModels.localToRemoteRecord(connection, + account.getConfigurationStore(), recipient, - account.getUsernameLink(), - storageId.getRaw()); - newStorageRecords.add(accountRecord); + account.getUsernameLink()); + newStorageRecords.add(new SignalStorageRecord(storageId, + new StorageRecord.Builder().account(accountRecord).build())); } else { final var recipient = account.getRecipientStore().getRecipient(connection, recipientId); final var address = recipient.getAddress().getIdentifier(); final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, address); - final var record = StorageSyncModels.localToRemoteRecord(recipient, identity, storageId.getRaw()); - newStorageRecords.add(record); + final var record = StorageSyncModels.localToRemoteRecord(recipient, identity); + newStorageRecords.add(new SignalStorageRecord(storageId, + new StorageRecord.Builder().contact(record).build())); } } @@ -317,8 +386,9 @@ public class StorageHelper { for (final var groupId : groupV1Ids) { final var storageId = newGroupV1StorageIds.get(groupId); final var group = account.getGroupStore().getGroup(connection, groupId); - final var record = StorageSyncModels.localToRemoteRecord(group, storageId.getRaw()); - newStorageRecords.add(record); + final var record = StorageSyncModels.localToRemoteRecord(group); + newStorageRecords.add(new SignalStorageRecord(storageId, + new StorageRecord.Builder().groupV1(record).build())); } final var groupV2Ids = account.getGroupStore().getGroupV2Ids(connection); @@ -326,8 +396,9 @@ public class StorageHelper { for (final var groupId : groupV2Ids) { final var storageId = newGroupV2StorageIds.get(groupId); final var group = account.getGroupStore().getGroup(connection, groupId); - final var record = StorageSyncModels.localToRemoteRecord(group, storageId.getRaw()); - newStorageRecords.add(record); + final var record = StorageSyncModels.localToRemoteRecord(group); + newStorageRecords.add(new SignalStorageRecord(storageId, + new StorageRecord.Builder().groupV2(record).build())); } connection.commit(); @@ -336,34 +407,46 @@ public class StorageHelper { } final var newStorageIds = newStorageRecords.stream().map(SignalStorageRecord::getId).toList(); - final var manifest = new SignalStorageManifest(newVersion, account.getDeviceId(), newStorageIds); + final RecordIkm recordIkm; + if (account.getSelfRecipientProfile() + .getCapabilities() + .contains(Profile.Capability.storageServiceEncryptionV2Capability)) { + logger.debug("Generating and including a new recordIkm."); + recordIkm = RecordIkm.Companion.generate(); + } else { + logger.debug("SSRE2 not yet supported. Not including recordIkm."); + recordIkm = null; + } + + final var manifest = new SignalStorageManifest(newVersion, account.getDeviceId(), recordIkm, newStorageIds); StorageSyncValidations.validateForcePush(manifest, newStorageRecords, account.getSelfRecipientAddress()); - final Optional conflict; - try { - if (newVersion > 1) { - logger.trace("Force-pushing data. Inserting {} IDs.", newStorageRecords.size()); - conflict = dependencies.getAccountManager() - .resetStorageRecords(storageServiceKey, manifest, newStorageRecords); - } else { - logger.trace("First version, normal push. Inserting {} IDs.", newStorageRecords.size()); - conflict = dependencies.getAccountManager() - .writeStorageRecords(storageServiceKey, manifest, newStorageRecords, Collections.emptyList()); + final WriteStorageRecordsResult result; + if (newVersion > 1) { + logger.trace("Force-pushing data. Inserting {} IDs.", newStorageRecords.size()); + result = dependencies.getStorageServiceRepository() + .resetAndWriteStorageRecords(storageServiceKey, manifest, newStorageRecords); + } else { + logger.trace("First version, normal push. Inserting {} IDs.", newStorageRecords.size()); + result = dependencies.getStorageServiceRepository() + .writeStorageRecords(storageServiceKey, manifest, newStorageRecords, Collections.emptyList()); + } + + switch (result) { + case WriteStorageRecordsResult.ConflictError ignored -> { + logger.debug("Hit a conflict. Trying again."); + throw new RetryLaterException(); } - } catch (InvalidKeyException e) { - logger.debug("Hit an invalid key exception, which likely indicates a conflict.", e); - throw new RetryLaterException(); + case WriteStorageRecordsResult.NetworkError networkError -> throw networkError.getException(); + case WriteStorageRecordsResult.StatusCodeError statusCodeError -> throw statusCodeError.getException(); + case WriteStorageRecordsResult.Success ignored -> { + logger.debug("Force push succeeded. Updating local manifest version to: {}", manifest.version); + storeManifestLocally(manifest); + } + default -> throw new IllegalStateException("Unexpected value: " + result); } - if (conflict.isPresent()) { - logger.debug("Hit a conflict. Trying again."); - throw new RetryLaterException(); - } - - logger.debug("Force push succeeded. Updating local manifest version to: " + manifest.getVersion()); - storeManifestLocally(manifest); - try (final var connection = account.getAccountDatabase().getConnection()) { connection.setAutoCommit(false); account.getRecipientStore().updateStorageIds(connection, newContactStorageIds); @@ -403,21 +486,35 @@ public class StorageHelper { private void storeManifestLocally( final SignalStorageManifest remoteManifest ) { - account.setStorageManifestVersion(remoteManifest.getVersion()); + account.setStorageManifestVersion(remoteManifest.version); account.setStorageManifest(remoteManifest); } private List getSignalStorageRecords( - final StorageKey storageKey, final List storageIds + final StorageKey storageKey, + final SignalStorageManifest manifest, + final List storageIds ) throws IOException { - List records; - try { - records = dependencies.getAccountManager().readStorageRecords(storageKey, storageIds); - } catch (InvalidKeyException e) { - logger.warn("Failed to read storage records, ignoring."); - return List.of(); - } - return records; + final var result = dependencies.getStorageServiceRepository() + .readStorageRecords(storageKey, manifest.recordIkm, storageIds); + return switch (result) { + case StorageServiceRepository.StorageRecordResult.DecryptionError decryptionError -> { + if (decryptionError.getException() instanceof InvalidKeyException) { + logger.warn("Failed to read storage records, ignoring."); + yield List.of(); + } else if (decryptionError.getException() instanceof IOException ioe) { + throw ioe; + } else { + throw new IOException(decryptionError.getException()); + } + } + case StorageServiceRepository.StorageRecordResult.NetworkError networkError -> + throw networkError.getException(); + case StorageServiceRepository.StorageRecordResult.StatusCodeError statusCodeError -> + throw statusCodeError.getException(); + case StorageServiceRepository.StorageRecordResult.Success success -> success.getRecords(); + default -> throw new IllegalStateException("Unexpected value: " + result); + }; } private List getAllLocalStorageIds(final Connection connection) throws SQLException { @@ -430,45 +527,52 @@ public class StorageHelper { } private List buildLocalStorageRecords( - final Connection connection, final List storageIds + final Connection connection, + final List storageIds ) throws SQLException { - final var records = new ArrayList(); + final var records = new ArrayList(storageIds.size()); for (final var storageId : storageIds) { final var record = buildLocalStorageRecord(connection, storageId); - if (record != null) { - records.add(record); - } + records.add(record); } return records; } private SignalStorageRecord buildLocalStorageRecord( - Connection connection, StorageId storageId + Connection connection, + StorageId storageId ) throws SQLException { return switch (ManifestRecord.Identifier.Type.fromValue(storageId.getType())) { case ManifestRecord.Identifier.Type.CONTACT -> { final var recipient = account.getRecipientStore().getRecipient(connection, storageId); final var address = recipient.getAddress().getIdentifier(); final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, address); - yield StorageSyncModels.localToRemoteRecord(recipient, identity, storageId.getRaw()); + final var record = StorageSyncModels.localToRemoteRecord(recipient, identity); + yield new SignalStorageRecord(storageId, new StorageRecord.Builder().contact(record).build()); } case ManifestRecord.Identifier.Type.GROUPV1 -> { final var groupV1 = account.getGroupStore().getGroupV1(connection, storageId); - yield StorageSyncModels.localToRemoteRecord(groupV1, storageId.getRaw()); + final var record = StorageSyncModels.localToRemoteRecord(groupV1); + yield new SignalStorageRecord(storageId, new StorageRecord.Builder().groupV1(record).build()); } case ManifestRecord.Identifier.Type.GROUPV2 -> { final var groupV2 = account.getGroupStore().getGroupV2(connection, storageId); - yield StorageSyncModels.localToRemoteRecord(groupV2, storageId.getRaw()); + final var record = StorageSyncModels.localToRemoteRecord(groupV2); + yield new SignalStorageRecord(storageId, new StorageRecord.Builder().groupV2(record).build()); } case ManifestRecord.Identifier.Type.ACCOUNT -> { final var selfRecipient = account.getRecipientStore() .getRecipient(connection, account.getSelfRecipientId()); - yield StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(), + + final var record = StorageSyncModels.localToRemoteRecord(connection, + account.getConfigurationStore(), selfRecipient, - account.getUsernameLink(), - storageId.getRaw()); + account.getUsernameLink()); + yield new SignalStorageRecord(storageId, new StorageRecord.Builder().account(record).build()); + } + case null, default -> { + throw new AssertionError("Got unknown local storage record type: " + storageId); } - case null, default -> throw new AssertionError("Got unknown local storage record type: " + storageId); }; } @@ -484,7 +588,8 @@ public class StorageHelper { * exclusive to the local data set. */ private static IdDifferenceResult findIdDifference( - Collection remoteIds, Collection localIds + Collection remoteIds, + Collection localIds ) { final var base64Encoder = Base64.getEncoder(); final var remoteByRawId = remoteIds.stream() @@ -502,7 +607,7 @@ public class StorageHelper { final var remote = remoteByRawId.get(rawId); final var local = localByRawId.get(rawId); - if (remote.getType() != local.getType() && local.getType() != 0) { + if (remote.getType() != local.getType() && KNOWN_TYPES.contains(local.getType())) { remoteOnlyRawIds.remove(rawId); localOnlyRawIds.remove(rawId); hasTypeMismatch = true; @@ -520,7 +625,8 @@ public class StorageHelper { } private List processKnownRecords( - final Connection connection, List records + final Connection connection, + List records ) throws SQLException { final var unknownRecords = new ArrayList(); @@ -530,13 +636,24 @@ public class StorageHelper { final var groupV2RecordProcessor = new GroupV2RecordProcessor(account, connection); for (final var record : records) { - logger.debug("Reading record of type {}", record.getType()); - switch (ManifestRecord.Identifier.Type.fromValue(record.getType())) { - case ACCOUNT -> accountRecordProcessor.process(record.getAccount().get()); - case GROUPV1 -> groupV1RecordProcessor.process(record.getGroupV1().get()); - case GROUPV2 -> groupV2RecordProcessor.process(record.getGroupV2().get()); - case CONTACT -> contactRecordProcessor.process(record.getContact().get()); - case null, default -> unknownRecords.add(record.getId()); + if (record.getProto().account != null) { + logger.debug("Reading record {} of type account", record.getId()); + accountRecordProcessor.process(StorageRecordConvertersKt.toSignalAccountRecord(record.getProto().account, + record.getId())); + } else if (record.getProto().groupV1 != null) { + logger.debug("Reading record {} of type groupV1", record.getId()); + groupV1RecordProcessor.process(StorageRecordConvertersKt.toSignalGroupV1Record(record.getProto().groupV1, + record.getId())); + } else if (record.getProto().groupV2 != null) { + logger.debug("Reading record {} of type groupV2", record.getId()); + groupV2RecordProcessor.process(StorageRecordConvertersKt.toSignalGroupV2Record(record.getProto().groupV2, + record.getId())); + } else if (record.getProto().contact != null) { + logger.debug("Reading record {} of type contact", record.getId()); + contactRecordProcessor.process(StorageRecordConvertersKt.toSignalContactRecord(record.getProto().contact, + record.getId())); + } else { + unknownRecords.add(record.getId()); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java index 56665fda..108ba1a4 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -70,17 +70,12 @@ public class SyncHelper { requestSyncData(SyncMessage.Request.Type.BLOCKED); requestSyncData(SyncMessage.Request.Type.CONFIGURATION); requestSyncKeys(); - requestSyncPniIdentity(); } public void requestSyncKeys() { requestSyncData(SyncMessage.Request.Type.KEYS); } - public void requestSyncPniIdentity() { - requestSyncData(SyncMessage.Request.Type.PNI_IDENTITY); - } - public SendMessageResult sendSyncFetchProfileMessage() { return context.getSendHelper() .sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)); @@ -165,7 +160,7 @@ public class SyncHelper { final var contact = contactPair.second(); final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId); - final var deviceContact = getDeviceContact(address, recipientId, contact); + final var deviceContact = getDeviceContact(address, contact); out.write(deviceContact); deviceContact.getAvatar().ifPresent(a -> { try { @@ -180,7 +175,7 @@ public class SyncHelper { final var address = account.getSelfRecipientAddress(); final var recipientId = account.getSelfRecipientId(); final var contact = account.getContactStore().getContact(recipientId); - final var deviceContact = getDeviceContact(address, recipientId, contact); + final var deviceContact = getDeviceContact(address, contact); out.write(deviceContact); deviceContact.getAvatar().ifPresent(a -> { try { @@ -216,39 +211,25 @@ public class SyncHelper { } @NotNull - private DeviceContact getDeviceContact( - final RecipientAddress address, final RecipientId recipientId, final Contact contact - ) throws IOException { - var currentIdentity = address.serviceId().isEmpty() - ? null - : account.getIdentityKeyStore().getIdentityInfo(address.serviceId().get()); - VerifiedMessage verifiedMessage = null; - if (currentIdentity != null) { - verifiedMessage = new VerifiedMessage(address.toSignalServiceAddress(), - currentIdentity.getIdentityKey(), - currentIdentity.getTrustLevel().toVerifiedState(), - currentIdentity.getDateAddedTimestamp()); - } - - var profileKey = account.getProfileStore().getProfileKey(recipientId); + private DeviceContact getDeviceContact(final RecipientAddress address, final Contact contact) throws IOException { return new DeviceContact(address.aci(), address.number(), Optional.ofNullable(contact == null ? null : contact.getName()), createContactAvatarAttachment(address), - Optional.ofNullable(contact == null ? null : contact.color()), - Optional.ofNullable(verifiedMessage), - Optional.ofNullable(profileKey), Optional.ofNullable(contact == null ? null : contact.messageExpirationTime()), Optional.ofNullable(contact == null ? null : contact.messageExpirationTimeVersion()), - Optional.empty(), - contact != null && contact.isArchived()); + Optional.empty()); } public SendMessageResult sendBlockedList() { - var addresses = new ArrayList(); + var addresses = new ArrayList(); for (var record : account.getContactStore().getContacts()) { if (record.second().isBlocked()) { - addresses.add(context.getRecipientHelper().resolveSignalServiceAddress(record.first())); + final var address = account.getRecipientAddressResolver().resolveRecipientAddress(record.first()); + if (address.aci().isPresent() || address.number().isPresent()) { + addresses.add(new BlockedListMessage.Individual(address.aci().orElse(null), + address.number().orElse(null))); + } } } var groupIds = new ArrayList(); @@ -262,7 +243,9 @@ public class SyncHelper { } public SendMessageResult sendVerifiedMessage( - SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel + SignalServiceAddress destination, + IdentityKey identityKey, + TrustLevel trustLevel ) { var verifiedMessage = new VerifiedMessage(destination, identityKey, @@ -272,13 +255,16 @@ public class SyncHelper { } public SendMessageResult sendKeysMessage() { - var keysMessage = new KeysMessage(Optional.ofNullable(account.getOrCreateStorageKey()), - Optional.ofNullable(account.getOrCreatePinMasterKey())); + var keysMessage = new KeysMessage(account.getOrCreateStorageKey(), + account.getOrCreatePinMasterKey(), + account.getOrCreateAccountEntropyPool(), + account.getOrCreateMediaRootBackupKey()); return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage)); } public SendMessageResult sendStickerOperationsMessage( - List installStickers, List removeStickers + List installStickers, + List removeStickers ) { var installStickerMessages = installStickers.stream().map(s -> getStickerPackOperationMessage(s, true)); var removeStickerMessages = removeStickers.stream().map(s -> getStickerPackOperationMessage(s, false)); @@ -288,7 +274,8 @@ public class SyncHelper { } private static StickerPackOperationMessage getStickerPackOperationMessage( - final StickerPack s, final boolean installed + final StickerPack s, + final boolean installed ) { return new StickerPackOperationMessage(s.packId().serialize(), s.packKey(), @@ -354,7 +341,7 @@ public class SyncHelper { c = s.read(); } catch (IOException e) { if (e.getMessage() != null && e.getMessage().contains("Missing contact address!")) { - logger.warn("Sync contacts contained invalid contact, ignoring: {}", e.getMessage()); + logger.debug("Sync contacts contained invalid contact, ignoring: {}", e.getMessage()); continue; } else { throw e; @@ -364,9 +351,6 @@ public class SyncHelper { break; } final var address = new RecipientAddress(c.getAci(), Optional.empty(), c.getE164(), Optional.empty()); - if (address.matches(account.getSelfRecipientAddress()) && c.getProfileKey().isPresent()) { - account.setProfileKey(c.getProfileKey().get()); - } final var recipientId = account.getRecipientTrustedResolver().resolveRecipientTrusted(address); var contact = account.getContactStore().getContact(recipientId); final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); @@ -378,19 +362,6 @@ public class SyncHelper { builder.withGivenName(c.getName().get()); builder.withFamilyName(null); } - if (c.getColor().isPresent()) { - builder.withColor(c.getColor().get()); - } - if (c.getProfileKey().isPresent()) { - account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get()); - } - if (c.getVerified().isPresent()) { - final var verifiedMessage = c.getVerified().get(); - account.getIdentityKeyStore() - .setIdentityTrustLevel(verifiedMessage.getDestination().getServiceId(), - verifiedMessage.getIdentityKey(), - TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); - } if (c.getExpirationTimer().isPresent()) { if (c.getExpirationTimerVersion().isPresent() && ( contact == null || c.getExpirationTimerVersion().get() > contact.messageExpirationTimeVersion() @@ -399,13 +370,12 @@ public class SyncHelper { builder.withMessageExpirationTimeVersion(c.getExpirationTimerVersion().get()); } else { logger.debug( - "[ContactSync] {} was synced with an old expiration timer. Ignoring. Received: {} Current: ${}", + "[ContactSync] {} was synced with an old expiration timer. Ignoring. Received: {} Current: {}", recipientId, c.getExpirationTimerVersion(), contact == null ? 1 : contact.messageExpirationTimeVersion()); } } - builder.withIsArchived(c.isArchived()); account.getContactStore().storeContact(recipientId, builder.build()); if (c.getAvatar().isPresent()) { @@ -414,15 +384,14 @@ public class SyncHelper { } } - public SendMessageResult sendMessageRequestResponse( - final MessageRequestResponse.Type type, final GroupId groupId - ) { + public SendMessageResult sendMessageRequestResponse(final MessageRequestResponse.Type type, final GroupId groupId) { final var response = MessageRequestResponseMessage.forGroup(groupId.serialize(), localToRemoteType(type)); return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response)); } public SendMessageResult sendMessageRequestResponse( - final MessageRequestResponse.Type type, final RecipientId recipientId + final MessageRequestResponse.Type type, + final RecipientId recipientId ) { final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId); if (address.serviceId().isEmpty()) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java index 3899179c..cd2719a1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java @@ -18,6 +18,8 @@ import java.io.IOException; import java.util.List; import java.util.concurrent.TimeUnit; +import static org.asamk.signal.manager.util.Utils.handleResponseException; + public class UnidentifiedAccessHelper { private static final Logger logger = LoggerFactory.getLogger(UnidentifiedAccessHelper.class); @@ -109,7 +111,8 @@ public class UnidentifiedAccessHelper { return privacySenderCertificate.getSerialized(); } try { - final var certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy(); + final var certificate = handleResponseException(dependencies.getCertificateApi() + .getSenderCertificateForPhoneNumberPrivacy()); privacySenderCertificate = new SenderCertificate(certificate); return certificate; } catch (IOException | InvalidCertificateException e) { @@ -125,7 +128,7 @@ public class UnidentifiedAccessHelper { return senderCertificate.getSerialized(); } try { - final var certificate = dependencies.getAccountManager().getSenderCertificate(); + final var certificate = handleResponseException(dependencies.getCertificateApi().getSenderCertificate()); this.senderCertificate = new SenderCertificate(certificate); return certificate; } catch (IOException | InvalidCertificateException e) { @@ -158,7 +161,8 @@ public class UnidentifiedAccessHelper { } private static byte[] getTargetUnidentifiedAccessKey( - final Profile targetProfile, final ProfileKey theirProfileKey + final Profile targetProfile, + final ProfileKey theirProfileKey ) { return switch (targetProfile.getUnidentifiedAccessMode()) { case ENABLED -> theirProfileKey == null ? null : UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey); diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java index 27bda583..726d0aaf 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java @@ -35,6 +35,7 @@ import org.asamk.signal.manager.api.IdentityVerificationCode; import org.asamk.signal.manager.api.InactiveGroupLinkException; import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.InvalidDeviceLinkException; +import org.asamk.signal.manager.api.InvalidNumberException; import org.asamk.signal.manager.api.InvalidStickerException; import org.asamk.signal.manager.api.InvalidUsernameException; import org.asamk.signal.manager.api.LastGroupAdminException; @@ -47,6 +48,7 @@ import org.asamk.signal.manager.api.NotPrimaryDeviceException; import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.PendingAdminApprovalException; import org.asamk.signal.manager.api.PhoneNumberSharingMode; +import org.asamk.signal.manager.api.PinLockMissingException; import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.Profile; import org.asamk.signal.manager.api.RateLimitException; @@ -68,7 +70,6 @@ import org.asamk.signal.manager.api.UserStatus; import org.asamk.signal.manager.api.UsernameLinkUrl; import org.asamk.signal.manager.api.UsernameStatus; import org.asamk.signal.manager.api.VerificationMethodNotAvailableException; -import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.helper.AccountFileUpdater; import org.asamk.signal.manager.helper.Context; @@ -88,12 +89,12 @@ import org.asamk.signal.manager.storage.stickers.StickerPack; import org.asamk.signal.manager.util.AttachmentUtils; import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.MimeUtils; +import org.asamk.signal.manager.util.PhoneNumberFormatter; import org.asamk.signal.manager.util.StickerUtils; import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.usernames.BaseUsernameException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServicePreview; @@ -107,8 +108,6 @@ import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhauste import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException; import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; -import org.whispersystems.signalservice.api.util.InvalidNumberException; -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.Util; @@ -133,13 +132,18 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.schedulers.Schedulers; +import okio.Utf8; + +import static org.asamk.signal.manager.config.ServiceConfig.MAX_MESSAGE_SIZE_BYTES; +import static org.asamk.signal.manager.util.Utils.handleResponseException; +import static org.signal.core.util.StringExtensionsKt.splitByByteLength; public class ManagerImpl implements Manager { @@ -158,6 +162,7 @@ public class ManagerImpl implements Manager { private final List closedListeners = new ArrayList<>(); private final List addressChangedListeners = new ArrayList<>(); private final CompositeDisposable disposable = new CompositeDisposable(); + private final AtomicLong lastMessageTimestamp = new AtomicLong(); public ManagerImpl( SignalAccount account, @@ -168,15 +173,7 @@ public class ManagerImpl implements Manager { ) { this.account = account; - final var sessionLock = new SignalSessionLock() { - private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); - - @Override - public Lock acquire() { - LEGACY_LOCK.lock(); - return LEGACY_LOCK::unlock; - } - }; + final var sessionLock = new ReentrantSignalSessionLock(); this.dependencies = new SignalDependencies(serviceEnvironmentConfig, userAgent, account.getCredentialsProvider(), @@ -288,7 +285,7 @@ public class ManagerImpl implements Manager { } @Override - public Map getUsernameStatus(Set usernames) { + public Map getUsernameStatus(Set usernames) throws IOException { final var registeredUsers = new HashMap(); for (final var username : usernames) { try { @@ -417,7 +414,9 @@ public class ManagerImpl implements Manager { @Override public void startChangeNumber( - String newNumber, boolean voiceVerification, String captcha + String newNumber, + boolean voiceVerification, + String captcha ) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, NotPrimaryDeviceException, VerificationMethodNotAvailableException { if (!account.isPrimaryDevice()) { throw new NotPrimaryDeviceException(); @@ -427,8 +426,10 @@ public class ManagerImpl implements Manager { @Override public void finishChangeNumber( - String newNumber, String verificationCode, String pin - ) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException { + String newNumber, + String verificationCode, + String pin + ) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException { if (!account.isPrimaryDevice()) { throw new NotPrimaryDeviceException(); } @@ -447,12 +448,13 @@ public class ManagerImpl implements Manager { @Override public void submitRateLimitRecaptchaChallenge( - String challenge, String captcha + String challenge, + String captcha ) throws IOException, CaptchaRejectedException { - captcha = captcha == null ? null : captcha.replace("signalcaptcha://", ""); + captcha = captcha == null ? "" : captcha.replace("signalcaptcha://", ""); try { - dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha); + handleResponseException(dependencies.getRateLimitChallengeApi().submitCaptchaChallenge(challenge, captcha)); } catch (org.whispersystems.signalservice.internal.push.exceptions.CaptchaRejectedException ignored) { throw new CaptchaRejectedException(); } @@ -460,7 +462,7 @@ public class ManagerImpl implements Manager { @Override public List getLinkedDevices() throws IOException { - var devices = dependencies.getAccountManager().getDevices(); + var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices()); account.setMultiDevice(devices.size() > 1); var identityKey = account.getAciIdentityKeyPair().getPrivateKey(); return devices.stream().map(d -> { @@ -527,7 +529,8 @@ public class ManagerImpl implements Manager { @Override public SendGroupMessageResults quitGroup( - GroupId groupId, Set groupAdmins + GroupId groupId, + Set groupAdmins ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException { final var newAdmins = context.getRecipientHelper().resolveRecipients(groupAdmins); return context.getGroupHelper().quitGroup(groupId, newAdmins); @@ -545,7 +548,9 @@ public class ManagerImpl implements Manager { @Override public Pair createGroup( - String name, Set members, String avatarFile + String name, + Set members, + String avatarFile ) throws IOException, AttachmentInvalidException, UnregisteredRecipientException { return context.getGroupHelper() .createGroup(name, @@ -555,7 +560,8 @@ public class ManagerImpl implements Manager { @Override public SendGroupMessageResults updateGroup( - final GroupId groupId, final UpdateGroup updateGroup + final GroupId groupId, + final UpdateGroup updateGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException { return context.getGroupHelper() .updateGroup(groupId, @@ -595,8 +601,28 @@ public class ManagerImpl implements Manager { return context.getGroupHelper().joinGroup(inviteLinkUrl); } + private long getNextMessageTimestamp() { + while (true) { + final var last = lastMessageTimestamp.get(); + final var timestamp = System.currentTimeMillis(); + if (last == timestamp) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + continue; + } + if (lastMessageTimestamp.compareAndSet(last, timestamp)) { + return timestamp; + } + } + } + private SendMessageResults sendMessage( - SignalServiceDataMessage.Builder messageBuilder, Set recipients, boolean notifySelf + SignalServiceDataMessage.Builder messageBuilder, + Set recipients, + boolean notifySelf ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { return sendMessage(messageBuilder, recipients, notifySelf, Optional.empty()); } @@ -608,7 +634,7 @@ public class ManagerImpl implements Manager { Optional editTargetTimestamp ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { var results = new HashMap>(); - long timestamp = System.currentTimeMillis(); + long timestamp = getNextMessageTimestamp(); messageBuilder.withTimestamp(timestamp); for (final var recipient : recipients) { if (recipient instanceof RecipientIdentifier.NoteToSelf || ( @@ -644,10 +670,11 @@ public class ManagerImpl implements Manager { } private SendMessageResults sendTypingMessage( - SignalServiceTypingMessage.Action action, Set recipients + SignalServiceTypingMessage.Action action, + Set recipients ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { var results = new HashMap>(); - final var timestamp = System.currentTimeMillis(); + final var timestamp = getNextMessageTimestamp(); for (var recipient : recipients) { if (recipient instanceof RecipientIdentifier.Single single) { final var message = new SignalServiceTypingMessage(action, timestamp, Optional.empty()); @@ -671,16 +698,15 @@ public class ManagerImpl implements Manager { @Override public SendMessageResults sendTypingMessage( - TypingAction action, Set recipients + TypingAction action, + Set recipients ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { return sendTypingMessage(action.toSignalService(), recipients); } @Override - public SendMessageResults sendReadReceipt( - RecipientIdentifier.Single sender, List messageIds - ) { - final var timestamp = System.currentTimeMillis(); + public SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List messageIds) { + final var timestamp = getNextMessageTimestamp(); var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, messageIds, timestamp); @@ -689,10 +715,8 @@ public class ManagerImpl implements Manager { } @Override - public SendMessageResults sendViewedReceipt( - RecipientIdentifier.Single sender, List messageIds - ) { - final var timestamp = System.currentTimeMillis(); + public SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List messageIds) { + final var timestamp = getNextMessageTimestamp(); var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, messageIds, timestamp); @@ -724,7 +748,9 @@ public class ManagerImpl implements Manager { @Override public SendMessageResults sendMessage( - Message message, Set recipients, boolean notifySelf + Message message, + Set recipients, + boolean notifySelf ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException { final var selfProfile = context.getProfileHelper().getSelfProfile(); if (selfProfile == null || selfProfile.getDisplayName().isEmpty()) { @@ -738,7 +764,9 @@ public class ManagerImpl implements Manager { @Override public SendMessageResults sendEditMessage( - Message message, Set recipients, long editTargetTimestamp + Message message, + Set recipients, + long editTargetTimestamp ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException { final var messageBuilder = SignalServiceDataMessage.newBuilder(); applyMessage(messageBuilder, message); @@ -746,20 +774,28 @@ public class ManagerImpl implements Manager { } private void applyMessage( - final SignalServiceDataMessage.Builder messageBuilder, final Message message + final SignalServiceDataMessage.Builder messageBuilder, + final Message message ) throws AttachmentInvalidException, IOException, UnregisteredRecipientException, InvalidStickerException { final var additionalAttachments = new ArrayList(); - if (message.messageText().length() > ServiceConfig.MAX_MESSAGE_BODY_SIZE) { - final var messageBytes = message.messageText().getBytes(StandardCharsets.UTF_8); - final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec(); - final var streamDetails = new StreamDetails(new ByteArrayInputStream(messageBytes), - MimeUtils.LONG_TEXT, - messageBytes.length); - final var textAttachment = AttachmentUtils.createAttachmentStream(streamDetails, - Optional.empty(), - uploadSpec); - messageBuilder.withBody(message.messageText().substring(0, ServiceConfig.MAX_MESSAGE_BODY_SIZE)); - additionalAttachments.add(context.getAttachmentHelper().uploadAttachment(textAttachment)); + if (Utf8.size(message.messageText()) > MAX_MESSAGE_SIZE_BYTES) { + final var result = splitByByteLength(message.messageText(), MAX_MESSAGE_SIZE_BYTES); + final var trimmed = result.getFirst(); + final var remainder = result.getSecond(); + if (remainder != null) { + final var messageBytes = message.messageText().getBytes(StandardCharsets.UTF_8); + final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec(); + final var streamDetails = new StreamDetails(new ByteArrayInputStream(messageBytes), + MimeUtils.LONG_TEXT, + messageBytes.length); + final var textAttachment = AttachmentUtils.createAttachmentStream(streamDetails, + Optional.empty(), + uploadSpec); + messageBuilder.withBody(trimmed); + additionalAttachments.add(context.getAttachmentHelper().uploadAttachment(textAttachment)); + } else { + messageBuilder.withBody(message.messageText()); + } } else { messageBuilder.withBody(message.messageText()); } @@ -774,6 +810,7 @@ public class ManagerImpl implements Manager { } else if (!additionalAttachments.isEmpty()) { messageBuilder.withAttachments(additionalAttachments); } + messageBuilder.withViewOnce(message.viewOnce()); if (!message.mentions().isEmpty()) { messageBuilder.withMentions(resolveMentions(message.mentions())); } @@ -863,7 +900,8 @@ public class ManagerImpl implements Manager { @Override public SendMessageResults sendRemoteDeleteMessage( - long targetSentTimestamp, Set recipients + long targetSentTimestamp, + Set recipients ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete); @@ -873,7 +911,7 @@ public class ManagerImpl implements Manager { .deleteEntryForRecipientNonGroup(targetSentTimestamp, ACI.from(u.uuid())); } else if (recipient instanceof RecipientIdentifier.Pni pni) { account.getMessageSendLogStore() - .deleteEntryForRecipientNonGroup(targetSentTimestamp, PNI.parseOrThrow(pni.pni())); + .deleteEntryForRecipientNonGroup(targetSentTimestamp, PNI.from(pni.pni())); } else if (recipient instanceof RecipientIdentifier.Single r) { try { final var recipientId = context.getRecipientHelper().resolveRecipient(r); @@ -915,7 +953,9 @@ public class ManagerImpl implements Manager { @Override public SendMessageResults sendPaymentNotificationMessage( - byte[] receipt, String note, RecipientIdentifier.Single recipient + byte[] receipt, + String note, + RecipientIdentifier.Single recipient ) throws IOException { final var paymentNotification = new SignalServiceDataMessage.PaymentNotification(receipt, note); final var payment = new SignalServiceDataMessage.Payment(paymentNotification, null); @@ -958,7 +998,8 @@ public class ManagerImpl implements Manager { @Override public SendMessageResults sendMessageRequestResponse( - final MessageRequestResponse.Type type, final Set recipients + final MessageRequestResponse.Type type, + final Set recipients ) { var results = new HashMap>(); for (final var recipient : recipients) { @@ -1021,19 +1062,30 @@ public class ManagerImpl implements Manager { @Override public void setContactName( - RecipientIdentifier.Single recipient, String givenName, final String familyName + final RecipientIdentifier.Single recipient, + final String givenName, + final String familyName, + final String nickGivenName, + final String nickFamilyName, + final String note ) throws NotPrimaryDeviceException, UnregisteredRecipientException { if (!account.isPrimaryDevice()) { throw new NotPrimaryDeviceException(); } context.getContactHelper() - .setContactName(context.getRecipientHelper().resolveRecipient(recipient), givenName, familyName); + .setContactName(context.getRecipientHelper().resolveRecipient(recipient), + givenName, + familyName, + nickGivenName, + nickFamilyName, + note); syncRemoteStorage(); } @Override public void setContactsBlocked( - Collection recipients, boolean blocked + Collection recipients, + boolean blocked ) throws IOException, UnregisteredRecipientException { if (recipients.isEmpty()) { return; @@ -1067,7 +1119,8 @@ public class ManagerImpl implements Manager { @Override public void setGroupsBlocked( - final Collection groupIds, final boolean blocked + final Collection groupIds, + final boolean blocked ) throws GroupNotFoundException, IOException { if (groupIds.isEmpty()) { return; @@ -1093,7 +1146,8 @@ public class ManagerImpl implements Manager { @Override public void setExpirationTimer( - RecipientIdentifier.Single recipient, int messageExpirationTimer + RecipientIdentifier.Single recipient, + int messageExpirationTimer ) throws IOException, UnregisteredRecipientException { var recipientId = context.getRecipientHelper().resolveRecipient(recipient); context.getContactHelper().setExpirationTimer(recipientId, messageExpirationTimer); @@ -1255,7 +1309,9 @@ public class ManagerImpl implements Manager { @Override public void receiveMessages( - Optional timeout, Optional maxMessages, ReceiveMessageHandler handler + Optional timeout, + Optional maxMessages, + ReceiveMessageHandler handler ) throws IOException, AlreadyReceivingException { receiveMessages(timeout.orElse(Duration.ofMinutes(1)), timeout.isPresent(), maxMessages.orElse(null), handler); } @@ -1275,7 +1331,10 @@ public class ManagerImpl implements Manager { } private void receiveMessages( - Duration timeout, boolean returnOnTimeout, Integer maxMessages, ReceiveMessageHandler handler + Duration timeout, + boolean returnOnTimeout, + Integer maxMessages, + ReceiveMessageHandler handler ) throws IOException, AlreadyReceivingException { synchronized (messageHandlers) { if (isReceiving()) { @@ -1431,7 +1490,8 @@ public class ManagerImpl implements Manager { @Override public boolean trustIdentityVerified( - RecipientIdentifier.Single recipient, IdentityVerificationCode verificationCode + RecipientIdentifier.Single recipient, + IdentityVerificationCode verificationCode ) throws UnregisteredRecipientException { return switch (verificationCode) { case IdentityVerificationCode.Fingerprint fingerprint -> trustIdentity(recipient, @@ -1450,7 +1510,8 @@ public class ManagerImpl implements Manager { } private boolean trustIdentity( - RecipientIdentifier.Single recipient, Function trustMethod + RecipientIdentifier.Single recipient, + Function trustMethod ) throws UnregisteredRecipientException { final var recipientId = context.getRecipientHelper().resolveRecipient(recipient); final var updated = trustMethod.apply(recipientId); @@ -1546,7 +1607,8 @@ public class ManagerImpl implements Manager { context.close(); executor.close(); - dependencies.getSignalWebSocket().disconnect(); + dependencies.getAuthenticatedSignalWebSocket().disconnect(); + dependencies.getUnauthenticatedSignalWebSocket().disconnect(); dependencies.getPushServiceSocket().close(); disposable.dispose(); diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/ProvisioningManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/ProvisioningManagerImpl.java index 2204b743..eeca842a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/ProvisioningManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/ProvisioningManagerImpl.java @@ -29,12 +29,10 @@ import org.asamk.signal.manager.util.KeyUtils; import org.signal.libsignal.protocol.IdentityKeyPair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.SignalServiceAccountManager; -import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; -import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; +import org.whispersystems.signalservice.api.registration.ProvisioningApi; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.internal.push.ProvisioningSocket; import org.whispersystems.signalservice.internal.push.PushServiceSocket; @@ -58,7 +56,7 @@ public class ProvisioningManagerImpl implements ProvisioningManager { private final Consumer newManagerListener; private final AccountsStore accountsStore; - private final SignalServiceAccountManager accountManager; + private final ProvisioningApi provisioningApi; private final IdentityKeyPair tempIdentityKey; private final String password; @@ -77,8 +75,6 @@ public class ProvisioningManagerImpl implements ProvisioningManager { tempIdentityKey = KeyUtils.generateIdentityKeyPair(); password = KeyUtils.createPassword(); - final var clientZkOperations = ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration()); - final var groupsV2Operations = new GroupsV2Operations(clientZkOperations, ServiceConfig.GROUP_MAX_SIZE); final var credentialsProvider = new DynamicCredentialsProvider(null, null, null, @@ -87,23 +83,22 @@ public class ProvisioningManagerImpl implements ProvisioningManager { final var pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(), credentialsProvider, userAgent, - clientZkOperations.getProfileOperations(), ServiceConfig.AUTOMATIC_NETWORK_RETRY); - accountManager = new SignalServiceAccountManager(pushServiceSocket, - new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(), userAgent), - groupsV2Operations); + final var provisioningSocket = new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(), + userAgent); + this.provisioningApi = new ProvisioningApi(pushServiceSocket, provisioningSocket, credentialsProvider); } @Override public URI getDeviceLinkUri() throws TimeoutException, IOException { - var deviceUuid = accountManager.getNewDeviceUuid(); + var deviceUuid = provisioningApi.getNewDeviceUuid(); return new DeviceLinkUrl(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri(); } @Override public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException { - var ret = accountManager.getNewDeviceRegistration(tempIdentityKey); + var ret = provisioningApi.getNewDeviceRegistration(tempIdentityKey); var number = ret.getNumber(); var aci = ret.getAci(); var pni = ret.getPni(); @@ -150,7 +145,9 @@ public class ProvisioningManagerImpl implements ProvisioningManager { ret.getAciIdentity(), ret.getPniIdentity(), profileKey, - ret.getMasterKey()); + ret.getMasterKey(), + ret.getAccountEntropyPool(), + ret.getMediaRootBackupKey()); account.getConfigurationStore().setReadReceipts(ret.isReadReceipts()); @@ -158,7 +155,7 @@ public class ProvisioningManagerImpl implements ProvisioningManager { final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI)); logger.debug("Finishing new device registration"); - var deviceId = accountManager.finishNewDeviceRegistration(ret.getProvisioningCode(), + var deviceId = provisioningApi.finishNewDeviceRegistration(ret.getProvisioningCode(), account.getAccountAttributes(null), aciPreKeys, pniPreKeys); diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/ReentrantSignalSessionLock.java b/lib/src/main/java/org/asamk/signal/manager/internal/ReentrantSignalSessionLock.java new file mode 100644 index 00000000..78304469 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/internal/ReentrantSignalSessionLock.java @@ -0,0 +1,16 @@ +package org.asamk.signal.manager.internal; + +import org.whispersystems.signalservice.api.SignalSessionLock; + +import java.util.concurrent.locks.ReentrantLock; + +class ReentrantSignalSessionLock implements SignalSessionLock { + + private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); + + @Override + public Lock acquire() { + LEGACY_LOCK.lock(); + return LEGACY_LOCK::unlock; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java index 543d9a99..72c4b63f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java @@ -21,6 +21,7 @@ import org.asamk.signal.manager.RegistrationManager; import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; +import org.asamk.signal.manager.api.PinLockMissingException; import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.UpdateProfile; @@ -32,14 +33,11 @@ import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.NumberVerificationUtils; -import org.asamk.signal.manager.util.Utils; import org.signal.libsignal.usernames.BaseUsernameException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.account.PreKeyCollection; -import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; -import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.PNI; @@ -48,13 +46,13 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; import org.whispersystems.signalservice.api.svr.SecureValueRecovery; -import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import java.io.IOException; import java.util.function.Consumer; import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType; +import static org.asamk.signal.manager.util.Utils.handleResponseException; public class RegistrationManagerImpl implements RegistrationManager { @@ -105,7 +103,9 @@ public class RegistrationManagerImpl implements RegistrationManager { @Override public void register( - boolean voiceVerification, String captcha, final boolean forceRegister + boolean voiceVerification, + String captcha, + final boolean forceRegister ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException { if (account.isRegistered() && account.getServiceEnvironment() != null @@ -130,12 +130,15 @@ public class RegistrationManagerImpl implements RegistrationManager { } final var registrationApi = unauthenticatedAccountManager.getRegistrationApi(); + logger.trace("Creating verification session"); String sessionId = NumberVerificationUtils.handleVerificationSession(registrationApi, account.getSessionId(account.getNumber()), id -> account.setSessionId(account.getNumber(), id), voiceVerification, captcha); + logger.trace("Requesting verification code"); NumberVerificationUtils.requestVerificationCode(registrationApi, sessionId, voiceVerification); + logger.debug("Successfully requested verification code"); account.setRegistered(false); } catch (DeprecatedVersionException e) { logger.debug("Signal-Server returned deprecated version exception", e); @@ -145,8 +148,9 @@ public class RegistrationManagerImpl implements RegistrationManager { @Override public void verifyAccount( - String verificationCode, String pin - ) throws IOException, PinLockedException, IncorrectPinException { + String verificationCode, + String pin + ) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException { if (account.isRegistered()) { throw new IOException("Account is already registered"); } @@ -196,7 +200,7 @@ public class RegistrationManagerImpl implements RegistrationManager { final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI)); final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI)); final var registrationApi = unauthenticatedAccountManager.getRegistrationApi(); - final var response = Utils.handleResponseException(registrationApi.registerAccount(null, + final var response = handleResponseException(registrationApi.registerAccount(null, recoveryPassword, account.getAccountAttributes(null), aciPreKeys, @@ -218,8 +222,14 @@ public class RegistrationManagerImpl implements RegistrationManager { private boolean attemptReactivateAccount() { try { - final var accountManager = createAuthenticatedSignalServiceAccountManager(); - accountManager.setAccountAttributes(account.getAccountAttributes(null)); + final var dependencies = new SignalDependencies(serviceEnvironmentConfig, + userAgent, + account.getCredentialsProvider(), + account.getSignalServiceDataStore(), + null, + new ReentrantSignalSessionLock()); + handleResponseException(dependencies.getAccountApi() + .setAccountAttributes(account.getAccountAttributes(null))); account.setRegistered(true); logger.info("Reactivated existing account, verify is not necessary."); if (newManagerListener != null) { @@ -238,17 +248,6 @@ public class RegistrationManagerImpl implements RegistrationManager { return false; } - private SignalServiceAccountManager createAuthenticatedSignalServiceAccountManager() { - final var clientZkOperations = ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration()); - final var pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(), - account.getCredentialsProvider(), - userAgent, - clientZkOperations.getProfileOperations(), - ServiceConfig.AUTOMATIC_NETWORK_RETRY); - final var groupsV2Operations = new GroupsV2Operations(clientZkOperations, ServiceConfig.GROUP_MAX_SIZE); - return new SignalServiceAccountManager(pushServiceSocket, null, groupsV2Operations); - } - private VerifyAccountResponse verifyAccountWithCode( final String sessionId, final String verificationCode, @@ -258,11 +257,11 @@ public class RegistrationManagerImpl implements RegistrationManager { ) throws IOException { final var registrationApi = unauthenticatedAccountManager.getRegistrationApi(); try { - Utils.handleResponseException(registrationApi.verifyAccount(sessionId, verificationCode)); + handleResponseException(registrationApi.verifyAccount(sessionId, verificationCode)); } catch (AlreadyVerifiedException e) { // Already verified so can continue registering } - return Utils.handleResponseException(registrationApi.registerAccount(sessionId, + return handleResponseException(registrationApi.registerAccount(sessionId, null, account.getAccountAttributes(registrationLock), aciPreKeys, diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java index 7ee2b39e..0bc895cb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java @@ -2,39 +2,58 @@ package org.asamk.signal.manager.internal; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; +import org.asamk.signal.manager.util.Utils; import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.signal.libsignal.net.Network; +import org.signal.libsignal.protocol.UsePqRatchet; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceDataStore; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalSessionLock; -import org.whispersystems.signalservice.api.SignalWebSocket; +import org.whispersystems.signalservice.api.account.AccountApi; +import org.whispersystems.signalservice.api.attachment.AttachmentApi; +import org.whispersystems.signalservice.api.cds.CdsApi; +import org.whispersystems.signalservice.api.certificate.CertificateApi; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; +import org.whispersystems.signalservice.api.keys.KeysApi; +import org.whispersystems.signalservice.api.link.LinkDeviceApi; +import org.whispersystems.signalservice.api.message.MessageApi; +import org.whispersystems.signalservice.api.profiles.ProfileApi; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi; import org.whispersystems.signalservice.api.registration.RegistrationApi; import org.whispersystems.signalservice.api.services.ProfileService; +import org.whispersystems.signalservice.api.storage.StorageServiceApi; +import org.whispersystems.signalservice.api.storage.StorageServiceRepository; import org.whispersystems.signalservice.api.svr.SecureValueRecovery; +import org.whispersystems.signalservice.api.username.UsernameApi; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; -import org.whispersystems.signalservice.api.websocket.WebSocketFactory; -import org.whispersystems.signalservice.internal.push.ProvisioningSocket; +import org.whispersystems.signalservice.api.websocket.SignalWebSocket; import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection; -import org.whispersystems.signalservice.internal.websocket.WebSocketConnection; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; import java.util.function.Supplier; public class SignalDependencies { + private static final Logger logger = LoggerFactory.getLogger(SignalDependencies.class); + private final Object LOCK = new Object(); private final ServiceEnvironmentConfig serviceEnvironmentConfig; @@ -47,20 +66,31 @@ public class SignalDependencies { private boolean allowStories = true; private SignalServiceAccountManager accountManager; + private AccountApi accountApi; + private RateLimitChallengeApi rateLimitChallengeApi; + private CdsApi cdsApi; + private UsernameApi usernameApi; private GroupsV2Api groupsV2Api; private RegistrationApi registrationApi; + private LinkDeviceApi linkDeviceApi; + private StorageServiceApi storageServiceApi; + private CertificateApi certificateApi; + private AttachmentApi attachmentApi; + private MessageApi messageApi; + private KeysApi keysApi; private GroupsV2Operations groupsV2Operations; private ClientZkOperations clientZkOperations; private PushServiceSocket pushServiceSocket; - private ProvisioningSocket provisioningSocket; private Network libSignalNetwork; - private SignalWebSocket signalWebSocket; + private SignalWebSocket.AuthenticatedWebSocket authenticatedSignalWebSocket; + private SignalWebSocket.UnauthenticatedWebSocket unauthenticatedSignalWebSocket; private SignalServiceMessageReceiver messageReceiver; private SignalServiceMessageSender messageSender; private List secureValueRecovery; private ProfileService profileService; + private ProfileApi profileApi; SignalDependencies( final ServiceEnvironmentConfig serviceEnvironmentConfig, @@ -90,7 +120,12 @@ public class SignalDependencies { this.registrationApi = null; this.secureValueRecovery = null; } - getSignalWebSocket().forceNewWebSockets(); + if (this.authenticatedSignalWebSocket != null) { + this.authenticatedSignalWebSocket.forceNewWebSocket(); + } + if (this.unauthenticatedSignalWebSocket != null) { + this.unauthenticatedSignalWebSocket.forceNewWebSocket(); + } } /** @@ -113,25 +148,45 @@ public class SignalDependencies { () -> pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(), credentialsProvider, userAgent, - getClientZkProfileOperations(), ServiceConfig.AUTOMATIC_NETWORK_RETRY)); } - public ProvisioningSocket getProvisioningSocket() { - return getOrCreate(() -> provisioningSocket, - () -> provisioningSocket = new ProvisioningSocket(getServiceEnvironmentConfig().signalServiceConfiguration(), - userAgent)); + public Network getLibSignalNetwork() { + return getOrCreate(() -> libSignalNetwork, () -> { + libSignalNetwork = new Network(serviceEnvironmentConfig.netEnvironment(), userAgent); + setSignalNetworkProxy(libSignalNetwork); + }); } - public Network getLibSignalNetwork() { - return getOrCreate(() -> libSignalNetwork, - () -> libSignalNetwork = new Network(serviceEnvironmentConfig.netEnvironment(), userAgent)); + private void setSignalNetworkProxy(Network libSignalNetwork) { + final var proxy = Utils.getHttpsProxy(); + if (proxy.address() instanceof InetSocketAddress addr) { + switch (proxy.type()) { + case Proxy.Type.DIRECT -> { + } + case Proxy.Type.HTTP -> { + try { + libSignalNetwork.setProxy("http", addr.getHostName(), addr.getPort(), null, null); + } catch (IOException e) { + logger.warn("Failed to set http proxy", e); + } + } + case Proxy.Type.SOCKS -> { + try { + libSignalNetwork.setProxy("socks", addr.getHostName(), addr.getPort(), null, null); + } catch (IOException e) { + logger.warn("Failed to set socks proxy", e); + } + } + } + } } public SignalServiceAccountManager getAccountManager() { return getOrCreate(() -> accountManager, - () -> accountManager = new SignalServiceAccountManager(getPushServiceSocket(), - getProvisioningSocket(), + () -> accountManager = new SignalServiceAccountManager(getAuthenticatedSignalWebSocket(), + getAccountApi(), + getPushServiceSocket(), getGroupsV2Operations())); } @@ -147,6 +202,23 @@ public class SignalDependencies { ServiceConfig.GROUP_MAX_SIZE); } + public AccountApi getAccountApi() { + return getOrCreate(() -> accountApi, () -> accountApi = new AccountApi(getAuthenticatedSignalWebSocket())); + } + + public RateLimitChallengeApi getRateLimitChallengeApi() { + return getOrCreate(() -> rateLimitChallengeApi, + () -> rateLimitChallengeApi = new RateLimitChallengeApi(getAuthenticatedSignalWebSocket())); + } + + public CdsApi getCdsApi() { + return getOrCreate(() -> cdsApi, () -> cdsApi = new CdsApi(getAuthenticatedSignalWebSocket())); + } + + public UsernameApi getUsernameApi() { + return getOrCreate(() -> usernameApi, () -> usernameApi = new UsernameApi(getUnauthenticatedSignalWebSocket())); + } + public GroupsV2Api getGroupsV2Api() { return getOrCreate(() -> groupsV2Api, () -> groupsV2Api = getAccountManager().getGroupsV2Api()); } @@ -155,6 +227,42 @@ public class SignalDependencies { return getOrCreate(() -> registrationApi, () -> registrationApi = getAccountManager().getRegistrationApi()); } + public LinkDeviceApi getLinkDeviceApi() { + return getOrCreate(() -> linkDeviceApi, + () -> linkDeviceApi = new LinkDeviceApi(getAuthenticatedSignalWebSocket())); + } + + private StorageServiceApi getStorageServiceApi() { + return getOrCreate(() -> storageServiceApi, + () -> storageServiceApi = new StorageServiceApi(getAuthenticatedSignalWebSocket(), + getPushServiceSocket())); + } + + public StorageServiceRepository getStorageServiceRepository() { + return new StorageServiceRepository(getStorageServiceApi()); + } + + public CertificateApi getCertificateApi() { + return getOrCreate(() -> certificateApi, + () -> certificateApi = new CertificateApi(getAuthenticatedSignalWebSocket())); + } + + public AttachmentApi getAttachmentApi() { + return getOrCreate(() -> attachmentApi, + () -> attachmentApi = new AttachmentApi(getAuthenticatedSignalWebSocket(), getPushServiceSocket())); + } + + public MessageApi getMessageApi() { + return getOrCreate(() -> messageApi, + () -> messageApi = new MessageApi(getAuthenticatedSignalWebSocket(), + getUnauthenticatedSignalWebSocket())); + } + + public KeysApi getKeysApi() { + return getOrCreate(() -> keysApi, + () -> keysApi = new KeysApi(getAuthenticatedSignalWebSocket(), getUnauthenticatedSignalWebSocket())); + } + public GroupsV2Operations getGroupsV2Operations() { return getOrCreate(() -> groupsV2Operations, () -> groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration()), @@ -171,33 +279,35 @@ public class SignalDependencies { return clientZkOperations.getProfileOperations(); } - public SignalWebSocket getSignalWebSocket() { - return getOrCreate(() -> signalWebSocket, () -> { + public SignalWebSocket.AuthenticatedWebSocket getAuthenticatedSignalWebSocket() { + return getOrCreate(() -> authenticatedSignalWebSocket, () -> { final var timer = new UptimeSleepTimer(); final var healthMonitor = new SignalWebSocketHealthMonitor(timer); - final var webSocketFactory = new WebSocketFactory() { - @Override - public WebSocketConnection createWebSocket() { - return new OkHttpWebSocketConnection("normal", - serviceEnvironmentConfig.signalServiceConfiguration(), - Optional.of(credentialsProvider), - userAgent, - healthMonitor, - allowStories); - } - @Override - public WebSocketConnection createUnidentifiedWebSocket() { - return new OkHttpWebSocketConnection("unidentified", - serviceEnvironmentConfig.signalServiceConfiguration(), - Optional.empty(), - userAgent, - healthMonitor, - allowStories); - } - }; - signalWebSocket = new SignalWebSocket(webSocketFactory); - healthMonitor.monitor(signalWebSocket); + authenticatedSignalWebSocket = new SignalWebSocket.AuthenticatedWebSocket(() -> new OkHttpWebSocketConnection( + "normal", + serviceEnvironmentConfig.signalServiceConfiguration(), + Optional.of(credentialsProvider), + userAgent, + healthMonitor, + allowStories), () -> true, timer, TimeUnit.SECONDS.toMillis(10)); + healthMonitor.monitor(authenticatedSignalWebSocket); + }); + } + + public SignalWebSocket.UnauthenticatedWebSocket getUnauthenticatedSignalWebSocket() { + return getOrCreate(() -> unauthenticatedSignalWebSocket, () -> { + final var timer = new UptimeSleepTimer(); + final var healthMonitor = new SignalWebSocketHealthMonitor(timer); + + unauthenticatedSignalWebSocket = new SignalWebSocket.UnauthenticatedWebSocket(() -> new OkHttpWebSocketConnection( + "unidentified", + serviceEnvironmentConfig.signalServiceConfiguration(), + Optional.empty(), + userAgent, + healthMonitor, + allowStories), () -> true, timer, TimeUnit.SECONDS.toMillis(10)); + healthMonitor.monitor(unauthenticatedSignalWebSocket); }); } @@ -211,10 +321,14 @@ public class SignalDependencies { () -> messageSender = new SignalServiceMessageSender(getPushServiceSocket(), dataStore, sessionLock, - getSignalWebSocket(), + getAttachmentApi(), + getMessageApi(), + getKeysApi(), Optional.empty(), executor, - ServiceConfig.MAX_ENVELOPE_SIZE)); + ServiceConfig.MAX_ENVELOPE_SIZE, + () -> true, + UsePqRatchet.NO)); } public List getSecureValueRecovery() { @@ -225,11 +339,19 @@ public class SignalDependencies { .toList()); } + public ProfileApi getProfileApi() { + return getOrCreate(() -> profileApi, + () -> profileApi = new ProfileApi(getAuthenticatedSignalWebSocket(), + getUnauthenticatedSignalWebSocket(), + getPushServiceSocket(), + getClientZkProfileOperations())); + } + public ProfileService getProfileService() { return getOrCreate(() -> profileService, () -> profileService = new ProfileService(getClientZkProfileOperations(), - getMessageReceiver(), - getSignalWebSocket())); + getAuthenticatedSignalWebSocket(), + getUnauthenticatedSignalWebSocket())); } public SignalServiceCipher getCipher(ServiceIdType serviceIdType) { diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java index 0fb1585a..7d927ddd 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java @@ -2,195 +2,157 @@ package org.asamk.signal.manager.internal; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.SignalWebSocket; import org.whispersystems.signalservice.api.util.Preconditions; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.websocket.HealthMonitor; +import org.whispersystems.signalservice.api.websocket.SignalWebSocket; import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState; import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection; -import java.util.Arrays; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import io.reactivex.rxjava3.schedulers.Schedulers; +import kotlin.Unit; -/** - * Monitors the health of the identified and unidentified WebSockets. If either one appears to be - * unhealthy, will trigger restarting both. - *

- * The monitor is also responsible for sending heartbeats/keep-alive messages to prevent - * timeouts. - */ final class SignalWebSocketHealthMonitor implements HealthMonitor { private static final Logger logger = LoggerFactory.getLogger(SignalWebSocketHealthMonitor.class); + /** + * This is the amount of time in between sent keep alives. Must be greater than [KEEP_ALIVE_TIMEOUT] + */ private static final long KEEP_ALIVE_SEND_CADENCE = TimeUnit.SECONDS.toMillis(OkHttpWebSocketConnection.KEEPALIVE_FREQUENCY_SECONDS); - private static final long MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE = KEEP_ALIVE_SEND_CADENCE * 3; - private SignalWebSocket signalWebSocket; + /** + * This is the amount of time we will wait for a response to the keep alive before we consider the websockets dead. + * It is required that this value be less than [KEEP_ALIVE_SEND_CADENCE] + */ + private static final long KEEP_ALIVE_TIMEOUT = TimeUnit.SECONDS.toMillis(20); + + private final Executor executor = Executors.newSingleThreadExecutor(); private final SleepTimer sleepTimer; - - private volatile KeepAliveSender keepAliveSender; - - private final HealthState identified = new HealthState(); - private final HealthState unidentified = new HealthState(); + private SignalWebSocket webSocket = null; + private volatile KeepAliveSender keepAliveSender = null; + private boolean needsKeepAlive = false; + private long lastKeepAliveReceived = 0; public SignalWebSocketHealthMonitor(SleepTimer sleepTimer) { this.sleepTimer = sleepTimer; } - public void monitor(SignalWebSocket signalWebSocket) { - Preconditions.checkNotNull(signalWebSocket); - Preconditions.checkArgument(this.signalWebSocket == null, "monitor can only be called once"); + void monitor(SignalWebSocket webSocket) { + Preconditions.checkNotNull(webSocket); + Preconditions.checkArgument(this.webSocket == null, "monitor can only be called once"); - this.signalWebSocket = signalWebSocket; + executor.execute(() -> { - //noinspection ResultOfMethodCallIgnored - signalWebSocket.getWebSocketState() - .subscribeOn(Schedulers.computation()) - .observeOn(Schedulers.computation()) - .distinctUntilChanged() - .subscribe(s -> onStateChange(s, identified)); + this.webSocket = webSocket; - //noinspection ResultOfMethodCallIgnored - signalWebSocket.getUnidentifiedWebSocketState() - .subscribeOn(Schedulers.computation()) - .observeOn(Schedulers.computation()) - .distinctUntilChanged() - .subscribe(s -> onStateChange(s, unidentified)); + webSocket.getState() + .subscribeOn(Schedulers.computation()) + .observeOn(Schedulers.computation()) + .distinctUntilChanged() + .subscribe(this::onStateChanged); + + webSocket.addKeepAliveChangeListener(() -> { + executor.execute(this::updateKeepAliveSenderStatus); + return Unit.INSTANCE; + }); + }); } - private synchronized void onStateChange(WebSocketConnectionState connectionState, HealthState healthState) { - switch (connectionState) { - case CONNECTED -> logger.debug("WebSocket is now connected"); - case AUTHENTICATION_FAILED -> logger.debug("WebSocket authentication failed"); - case FAILED -> logger.debug("WebSocket connection failed"); - } + private void onStateChanged(WebSocketConnectionState connectionState) { + executor.execute(() -> { + needsKeepAlive = connectionState == WebSocketConnectionState.CONNECTED; - healthState.needsKeepAlive = connectionState == WebSocketConnectionState.CONNECTED; + updateKeepAliveSenderStatus(); + }); + } - if (keepAliveSender == null && isKeepAliveNecessary()) { + @Override + public void onKeepAliveResponse(long sentTimestamp, boolean isIdentifiedWebSocket) { + final var keepAliveTime = System.currentTimeMillis(); + executor.execute(() -> lastKeepAliveReceived = keepAliveTime); + } + + @Override + public void onMessageError(int status, boolean isIdentifiedWebSocket) { + } + + private void updateKeepAliveSenderStatus() { + if (keepAliveSender == null && sendKeepAlives()) { keepAliveSender = new KeepAliveSender(); keepAliveSender.start(); - } else if (keepAliveSender != null && !isKeepAliveNecessary()) { + } else if (keepAliveSender != null && !sendKeepAlives()) { keepAliveSender.shutdown(); keepAliveSender = null; } } - @Override - public void onKeepAliveResponse(long sentTimestamp, boolean isIdentifiedWebSocket) { - if (isIdentifiedWebSocket) { - identified.lastKeepAliveReceived = System.currentTimeMillis(); - } else { - unidentified.lastKeepAliveReceived = System.currentTimeMillis(); - } - } - - @Override - public void onMessageError(int status, boolean isIdentifiedWebSocket) { - if (status == 409) { - HealthState healthState = (isIdentifiedWebSocket ? identified : unidentified); - if (healthState.mismatchErrorTracker.addSample(System.currentTimeMillis())) { - logger.warn("Received too many mismatch device errors, forcing new websockets."); - signalWebSocket.forceNewWebSockets(); - signalWebSocket.connect(); - } - } - } - - private boolean isKeepAliveNecessary() { - return identified.needsKeepAlive || unidentified.needsKeepAlive; - } - - private static class HealthState { - - private final HttpErrorTracker mismatchErrorTracker = new HttpErrorTracker(5, TimeUnit.MINUTES.toMillis(1)); - - private volatile boolean needsKeepAlive; - private volatile long lastKeepAliveReceived; + private boolean sendKeepAlives() { + return needsKeepAlive && webSocket != null && webSocket.shouldSendKeepAlives(); } /** - * Sends periodic heartbeats/keep-alives over both WebSockets to prevent connection timeouts. If - * either WebSocket fails 3 times to get a return heartbeat both are forced to be recreated. + * Sends periodic heartbeats/keep-alives over the WebSocket to prevent connection timeouts. If + * the WebSocket fails to get a return heartbeat after [KEEP_ALIVE_TIMEOUT] seconds, it is forced to be recreated. */ - private class KeepAliveSender extends Thread { + private final class KeepAliveSender extends Thread { private volatile boolean shouldKeepRunning = true; + @Override public void run() { - identified.lastKeepAliveReceived = System.currentTimeMillis(); - unidentified.lastKeepAliveReceived = System.currentTimeMillis(); + logger.debug("[KeepAliveSender({})] started", this.threadId()); + lastKeepAliveReceived = System.currentTimeMillis(); - while (shouldKeepRunning && isKeepAliveNecessary()) { + var keepAliveSendTime = System.currentTimeMillis(); + while (shouldKeepRunning && sendKeepAlives()) { try { - sleepTimer.sleep(KEEP_ALIVE_SEND_CADENCE); + final var nextKeepAliveSendTime = keepAliveSendTime + KEEP_ALIVE_SEND_CADENCE; + sleepUntil(nextKeepAliveSendTime); - if (shouldKeepRunning && isKeepAliveNecessary()) { - long keepAliveRequiredSinceTime = System.currentTimeMillis() - - MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE; + if (shouldKeepRunning && sendKeepAlives()) { + keepAliveSendTime = System.currentTimeMillis(); + webSocket.sendKeepAlive(); + } - if (identified.lastKeepAliveReceived < keepAliveRequiredSinceTime - || unidentified.lastKeepAliveReceived < keepAliveRequiredSinceTime) { - logger.warn("Missed keep alives, identified last: " - + identified.lastKeepAliveReceived - + " unidentified last: " - + unidentified.lastKeepAliveReceived - + " needed by: " - + keepAliveRequiredSinceTime); - signalWebSocket.forceNewWebSockets(); - signalWebSocket.connect(); - } else { - signalWebSocket.sendKeepAlive(); + final var responseRequiredTime = keepAliveSendTime + KEEP_ALIVE_TIMEOUT; + sleepUntil(responseRequiredTime); + + if (shouldKeepRunning && sendKeepAlives()) { + if (lastKeepAliveReceived < keepAliveSendTime) { + logger.debug("Missed keep alive, last: {} needed by: {}", + lastKeepAliveReceived, + responseRequiredTime); + webSocket.forceNewWebSocket(); } } } catch (Throwable e) { - logger.warn("Error occurred in KeepAliveSender, ignoring ...", e); + logger.warn("Keep alive sender failed", e); + } + } + logger.debug("[KeepAliveSender({})] ended", threadId()); + } + + void sleepUntil(long timeMillis) { + while (System.currentTimeMillis() < timeMillis) { + final var waitTime = timeMillis - System.currentTimeMillis(); + if (waitTime > 0) { + try { + sleepTimer.sleep(waitTime); + } catch (InterruptedException e) { + logger.warn("WebSocket health monitor interrupted", e); + } } } } - public void shutdown() { + void shutdown() { shouldKeepRunning = false; } } - - private static final class HttpErrorTracker { - - private final long[] timestamps; - private final long errorTimeRange; - - public HttpErrorTracker(int samples, long errorTimeRange) { - this.timestamps = new long[samples]; - this.errorTimeRange = errorTimeRange; - } - - public synchronized boolean addSample(long now) { - long errorsMustBeAfter = now - errorTimeRange; - int count = 1; - int minIndex = 0; - - for (int i = 0; i < timestamps.length; i++) { - if (timestamps[i] < errorsMustBeAfter) { - timestamps[i] = 0; - } else if (timestamps[i] != 0) { - count++; - } - - if (timestamps[i] < timestamps[minIndex]) { - minIndex = i; - } - } - - timestamps[minIndex] = now; - - if (count >= timestamps.length) { - Arrays.fill(timestamps, 0); - return true; - } - return false; - } - } } + diff --git a/lib/src/main/java/org/asamk/signal/manager/jobs/SyncStorageJob.java b/lib/src/main/java/org/asamk/signal/manager/jobs/SyncStorageJob.java index 85cfbe2c..d695c7b1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/jobs/SyncStorageJob.java +++ b/lib/src/main/java/org/asamk/signal/manager/jobs/SyncStorageJob.java @@ -8,13 +8,27 @@ import java.io.IOException; public class SyncStorageJob implements Job { + private final boolean forcePush; + private static final Logger logger = LoggerFactory.getLogger(SyncStorageJob.class); + public SyncStorageJob() { + this.forcePush = false; + } + + public SyncStorageJob(final boolean forcePush) { + this.forcePush = forcePush; + } + @Override public void run(Context context) { logger.trace("Running storage sync job"); try { - context.getStorageHelper().syncDataWithStorage(); + if (forcePush) { + context.getStorageHelper().forcePushToStorage(); + } else { + context.getStorageHelper().syncDataWithStorage(); + } } catch (IOException e) { logger.warn("Failed to sync storage data", e); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/AccountDatabase.java b/lib/src/main/java/org/asamk/signal/manager/storage/AccountDatabase.java index 63ba5a37..9e1c6303 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/AccountDatabase.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/AccountDatabase.java @@ -611,7 +611,8 @@ public class AccountDatabase extends Database { } private static void createUuidMappingTable( - final Connection connection, final Statement statement + final Connection connection, + final Statement statement ) throws SQLException { statement.executeUpdate(""" CREATE TABLE tmp_mapping_table ( diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/AttachmentStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/AttachmentStore.java index 5e734174..d25f1a72 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/AttachmentStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/AttachmentStore.java @@ -22,7 +22,8 @@ public class AttachmentStore { } public void storeAttachmentPreview( - final SignalServiceAttachmentPointer pointer, final AttachmentStorer storer + final SignalServiceAttachmentPointer pointer, + final AttachmentStorer storer ) throws IOException { storeAttachment(getAttachmentPreviewFile(pointer.getRemoteId(), pointer.getFileName(), @@ -30,7 +31,8 @@ public class AttachmentStore { } public void storeAttachment( - final SignalServiceAttachmentPointer pointer, final AttachmentStorer storer + final SignalServiceAttachmentPointer pointer, + final AttachmentStorer storer ) throws IOException { storeAttachment(getAttachmentFile(pointer), storer); } @@ -54,22 +56,24 @@ public class AttachmentStore { } private File getAttachmentPreviewFile( - SignalServiceAttachmentRemoteId attachmentId, Optional filename, Optional contentType + SignalServiceAttachmentRemoteId attachmentId, + Optional filename, + Optional contentType ) { final var extension = getAttachmentExtension(filename, contentType); return new File(attachmentsPath, attachmentId.toString() + extension + ".preview"); } private File getAttachmentFile( - SignalServiceAttachmentRemoteId attachmentId, Optional filename, Optional contentType + SignalServiceAttachmentRemoteId attachmentId, + Optional filename, + Optional contentType ) { final var extension = getAttachmentExtension(filename, contentType); return new File(attachmentsPath, attachmentId.toString() + extension); } - private static String getAttachmentExtension( - final Optional filename, final Optional contentType - ) { + private static String getAttachmentExtension(final Optional filename, final Optional contentType) { return filename.filter(f -> f.contains(".")) .map(f -> f.substring(f.lastIndexOf(".") + 1)) .or(() -> contentType.flatMap(MimeUtils::guessExtensionFromMimeType)) diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/Database.java b/lib/src/main/java/org/asamk/signal/manager/storage/Database.java index 71704698..1d84daef 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/Database.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/Database.java @@ -24,7 +24,8 @@ public abstract class Database implements AutoCloseable { } public static T initDatabase( - File databaseFile, Function newDatabase + File databaseFile, + Function newDatabase ) throws SQLException { HikariDataSource dataSource = null; @@ -94,10 +95,12 @@ public abstract class Database implements AutoCloseable { sqliteConfig.setTransactionMode(SQLiteConfig.TransactionMode.IMMEDIATE); HikariConfig config = new HikariConfig(); - config.setJdbcUrl("jdbc:sqlite:" + databaseFile); + config.setJdbcUrl("jdbc:sqlite:" + databaseFile + "?foreign_keys=ON&journal_mode=wal"); config.setDataSourceProperties(sqliteConfig.toProperties()); config.setMinimumIdle(1); - config.setConnectionInitSql("PRAGMA foreign_keys=ON"); + config.setConnectionTimeout(90_000); + config.setMaximumPoolSize(50); + config.setMaxLifetime(0); return new HikariDataSource(config); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 87cf3aa3..83040906 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -65,10 +65,12 @@ import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.AccountEntropyPool; import org.whispersystems.signalservice.api.SignalServiceAccountDataStore; import org.whispersystems.signalservice.api.SignalServiceDataStore; import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.account.PreKeyCollection; +import org.whispersystems.signalservice.api.backup.MediaRootBackupKey; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.push.ServiceId; @@ -114,7 +116,7 @@ public class SignalAccount implements Closeable { private static final Logger logger = LoggerFactory.getLogger(SignalAccount.class); private static final int MINIMUM_STORAGE_VERSION = 1; - private static final int CURRENT_STORAGE_VERSION = 9; + private static final int CURRENT_STORAGE_VERSION = 10; private final Object LOCK = new Object(); @@ -138,6 +140,8 @@ public class SignalAccount implements Closeable { private String registrationLockPin; private MasterKey pinMasterKey; private StorageKey storageKey; + private AccountEntropyPool accountEntropyPool; + private MediaRootBackupKey mediaRootBackupKey; private ProfileKey profileKey; private Settings settings; @@ -189,7 +193,10 @@ public class SignalAccount implements Closeable { } public static SignalAccount load( - File dataPath, String accountPath, boolean waitForLock, final Settings settings + File dataPath, + String accountPath, + boolean waitForLock, + final Settings settings ) throws IOException { logger.trace("Opening account file"); final var fileName = getFileName(dataPath, accountPath); @@ -285,7 +292,9 @@ public class SignalAccount implements Closeable { final IdentityKeyPair aciIdentity, final IdentityKeyPair pniIdentity, final ProfileKey profileKey, - final MasterKey masterKey + final MasterKey masterKey, + final AccountEntropyPool accountEntropyPool, + final MediaRootBackupKey mediaRootBackupKey ) { this.deviceId = 0; this.number = number; @@ -301,7 +310,14 @@ public class SignalAccount implements Closeable { this.registered = false; this.isMultiDevice = true; setLastReceiveTimestamp(0L); - this.pinMasterKey = masterKey; + if (accountEntropyPool != null) { + this.pinMasterKey = null; + this.accountEntropyPool = accountEntropyPool; + } else { + this.pinMasterKey = masterKey; + this.accountEntropyPool = null; + } + this.mediaRootBackupKey = mediaRootBackupKey; getKeyValueStore().storeEntry(storageManifestVersion, -1L); this.setStorageManifest(null); this.storageKey = null; @@ -316,7 +332,9 @@ public class SignalAccount implements Closeable { } public void finishLinking( - final int deviceId, final PreKeyCollection aciPreKeys, final PreKeyCollection pniPreKeys + final int deviceId, + final PreKeyCollection aciPreKeys, + final PreKeyCollection pniPreKeys ) { this.registered = true; this.deviceId = deviceId; @@ -334,6 +352,7 @@ public class SignalAccount implements Closeable { final PreKeyCollection pniPreKeys ) { this.pinMasterKey = masterKey; + this.accountEntropyPool = null; getKeyValueStore().storeEntry(storageManifestVersion, -1L); this.setStorageManifest(null); this.storageKey = null; @@ -375,7 +394,9 @@ public class SignalAccount implements Closeable { } private void mergeRecipients( - final Connection connection, RecipientId recipientId, RecipientId toBeMergedRecipientId + final Connection connection, + RecipientId recipientId, + RecipientId toBeMergedRecipientId ) throws SQLException { getMessageCache().mergeRecipients(recipientId, toBeMergedRecipientId); getGroupStore().mergeRecipients(connection, recipientId, toBeMergedRecipientId); @@ -438,9 +459,7 @@ public class SignalAccount implements Closeable { return f.exists() && !f.isDirectory() && f.length() > 0L; } - private void load( - File dataPath, String accountPath, final Settings settings - ) throws IOException { + private void load(File dataPath, String accountPath, final Settings settings) throws IOException { logger.trace("Loading account file {}", accountPath); this.dataPath = dataPath; this.accountPath = accountPath; @@ -494,6 +513,12 @@ public class SignalAccount implements Closeable { if (storage.storageKey != null) { storageKey = new StorageKey(base64.decode(storage.storageKey)); } + if (storage.accountEntropyPool != null) { + accountEntropyPool = new AccountEntropyPool(storage.accountEntropyPool); + } + if (storage.mediaRootBackupKey != null) { + mediaRootBackupKey = new MediaRootBackupKey(base64.decode(storage.mediaRootBackupKey)); + } if (storage.profileKey != null) { try { profileKey = new ProfileKey(base64.decode(storage.profileKey)); @@ -786,7 +811,8 @@ public class SignalAccount implements Closeable { } private void loadLegacyStores( - final JsonNode rootNode, final LegacyJsonSignalProtocolStore legacySignalProtocolStore + final JsonNode rootNode, + final LegacyJsonSignalProtocolStore legacySignalProtocolStore ) { var legacyRecipientStoreNode = rootNode.get("recipientStore"); if (legacyRecipientStoreNode != null) { @@ -801,6 +827,7 @@ public class SignalAccount implements Closeable { if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyPreKeyStore() != null) { logger.debug("Migrating legacy pre key store."); + aciAccountData.getPreKeyStore().removeAllPreKeys(); for (var entry : legacySignalProtocolStore.getLegacyPreKeyStore().getPreKeys().entrySet()) { try { aciAccountData.getPreKeyStore().storePreKey(entry.getKey(), new PreKeyRecord(entry.getValue())); @@ -812,6 +839,7 @@ public class SignalAccount implements Closeable { if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySignedPreKeyStore() != null) { logger.debug("Migrating legacy signed pre key store."); + aciAccountData.getSignedPreKeyStore().removeAllSignedPreKeys(); for (var entry : legacySignalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) { try { aciAccountData.getSignedPreKeyStore() @@ -975,6 +1003,8 @@ public class SignalAccount implements Closeable { registrationLockPin, pinMasterKey == null ? null : base64.encodeToString(pinMasterKey.serialize()), storageKey == null ? null : base64.encodeToString(storageKey.serialize()), + accountEntropyPool == null ? null : accountEntropyPool.getValue(), + mediaRootBackupKey == null ? null : base64.encodeToString(mediaRootBackupKey.getValue()), profileKey == null ? null : base64.encodeToString(profileKey.serialize()), usernameLink == null ? null : base64.encodeToString(usernameLink.getEntropy()), usernameLink == null ? null : usernameLink.getServerId().toString()); @@ -1436,6 +1466,10 @@ public class SignalAccount implements Closeable { return selfRecipientId; } + public Profile getSelfRecipientProfile() { + return recipientStore.getProfile(selfRecipientId); + } + public String getSessionId(final String forNumber) { final var keyValueStore = getKeyValueStore(); final var sessionNumber = keyValueStore.getEntry(verificationSessionNumber); @@ -1506,16 +1540,28 @@ public class SignalAccount implements Closeable { public MasterKey getPinBackedMasterKey() { if (registrationLockPin == null) { return null; + } else if (!isPrimaryDevice()) { + return getMasterKey(); } - return pinMasterKey; + return getOrCreatePinMasterKey(); } public MasterKey getOrCreatePinMasterKey() { - if (pinMasterKey == null) { - pinMasterKey = KeyUtils.createMasterKey(); - save(); + final var key = getMasterKey(); + if (key != null) { + return key; } - return pinMasterKey; + + return getOrCreateAccountEntropyPool().deriveMasterKey(); + } + + private MasterKey getMasterKey() { + if (pinMasterKey != null) { + return pinMasterKey; + } else if (accountEntropyPool != null) { + return accountEntropyPool.deriveMasterKey(); + } + return null; } public void setMasterKey(MasterKey masterKey) { @@ -1523,14 +1569,19 @@ public class SignalAccount implements Closeable { return; } this.pinMasterKey = masterKey; + if (masterKey != null) { + this.storageKey = null; + } save(); } public StorageKey getOrCreateStorageKey() { - if (pinMasterKey != null) { - return pinMasterKey.deriveStorageServiceKey(); - } else if (storageKey != null) { + if (storageKey != null) { return storageKey; + } else if (pinMasterKey != null) { + return pinMasterKey.deriveStorageServiceKey(); + } else if (accountEntropyPool != null) { + return accountEntropyPool.deriveMasterKey().deriveStorageServiceKey(); } else if (!isPrimaryDevice() || !isMultiDevice()) { // Only upload storage, if a pin master key already exists or linked devices exist return null; @@ -1547,6 +1598,40 @@ public class SignalAccount implements Closeable { save(); } + public AccountEntropyPool getOrCreateAccountEntropyPool() { + if (accountEntropyPool == null) { + accountEntropyPool = AccountEntropyPool.Companion.generate(); + save(); + } + return accountEntropyPool; + } + + public void setAccountEntropyPool(final AccountEntropyPool accountEntropyPool) { + this.accountEntropyPool = accountEntropyPool; + if (accountEntropyPool != null) { + this.storageKey = null; + this.pinMasterKey = null; + } + save(); + } + + public boolean needsStorageKeyMigration() { + return isPrimaryDevice() && (storageKey != null || pinMasterKey != null); + } + + public MediaRootBackupKey getOrCreateMediaRootBackupKey() { + if (mediaRootBackupKey == null) { + mediaRootBackupKey = KeyUtils.createMediaRootBackupKey(); + save(); + } + return mediaRootBackupKey; + } + + public void setMediaRootBackupKey(final MediaRootBackupKey mediaRootBackupKey) { + this.mediaRootBackupKey = mediaRootBackupKey; + save(); + } + public String getRecoveryPassword() { final var masterKey = getPinBackedMasterKey(); if (masterKey == null) { @@ -1569,7 +1654,7 @@ public class SignalAccount implements Closeable { return Optional.empty(); } try (var inputStream = new FileInputStream(storageManifestFile)) { - return Optional.of(SignalStorageManifest.deserialize(inputStream.readAllBytes())); + return Optional.of(SignalStorageManifest.Companion.deserialize(inputStream.readAllBytes())); } catch (IOException e) { logger.warn("Failed to read local storage manifest.", e); return Optional.empty(); @@ -1876,6 +1961,8 @@ public class SignalAccount implements Closeable { String registrationLockPin, String pinMasterKey, String storageKey, + String accountEntropyPool, + String mediaRootBackupKey, String profileKey, String usernameLinkEntropy, String usernameLinkServerId diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/UnknownStorageIdStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/UnknownStorageIdStore.java index 75389751..5ea5eff0 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/UnknownStorageIdStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/UnknownStorageIdStore.java @@ -41,9 +41,7 @@ public class UnknownStorageIdStore { } } - public List getUnknownStorageIds( - Connection connection, Collection types - ) throws SQLException { + public List getUnknownStorageIds(Connection connection, Collection types) throws SQLException { final var typesCommaSeparated = types.stream().map(String::valueOf).collect(Collectors.joining(",")); final var sql = ( """ diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/Utils.java b/lib/src/main/java/org/asamk/signal/manager/storage/Utils.java index fa2e8916..e4858f21 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/Utils.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/Utils.java @@ -72,7 +72,8 @@ public class Utils { } public static T executeQuerySingleRow( - PreparedStatement statement, ResultSetMapper mapper + PreparedStatement statement, + ResultSetMapper mapper ) throws SQLException { final var resultSet = statement.executeQuery(); if (!resultSet.next()) { @@ -82,7 +83,8 @@ public class Utils { } public static Optional executeQueryForOptional( - PreparedStatement statement, ResultSetMapper mapper + PreparedStatement statement, + ResultSetMapper mapper ) throws SQLException { final var resultSet = statement.executeQuery(); if (!resultSet.next()) { @@ -92,7 +94,8 @@ public class Utils { } public static Stream executeQueryForStream( - PreparedStatement statement, ResultSetMapper mapper + PreparedStatement statement, + ResultSetMapper mapper ) throws SQLException { final var resultSet = statement.executeQuery(); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/accounts/AccountsStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/accounts/AccountsStore.java index 02379941..7be4a401 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/accounts/AccountsStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/accounts/AccountsStore.java @@ -1,6 +1,7 @@ package org.asamk.signal.manager.storage.accounts; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.i18n.phonenumbers.PhoneNumberUtil; import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.ServiceEnvironment; @@ -10,7 +11,6 @@ import org.asamk.signal.manager.util.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.push.ServiceId.ACI; -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -41,7 +41,9 @@ public class AccountsStore { private final AccountLoader accountLoader; public AccountsStore( - final File dataPath, final ServiceEnvironment serviceEnvironment, final AccountLoader accountLoader + final File dataPath, + final ServiceEnvironment serviceEnvironment, + final AccountLoader accountLoader ) throws IOException { this.dataPath = dataPath; this.serviceEnvironment = getServiceEnvironmentString(serviceEnvironment); @@ -179,7 +181,7 @@ public class AccountsStore { return Arrays.stream(files) .filter(File::isFile) .map(File::getName) - .filter(file -> PhoneNumberFormatter.isValidNumber(file, null)) + .filter(file -> PhoneNumberUtil.getInstance().isPossibleNumber(file, null)) .collect(Collectors.toSet()); } @@ -202,7 +204,9 @@ public class AccountsStore { } private AccountsStorage upgradeAccountsFile( - final FileChannel fileChannel, final AccountsStorage storage, final int accountsVersion + final FileChannel fileChannel, + final AccountsStorage storage, + final int accountsVersion ) { try { List newAccounts = storage.accounts(); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/configuration/ConfigurationStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/configuration/ConfigurationStore.java index 558d4f86..88f05725 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/configuration/ConfigurationStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/configuration/ConfigurationStore.java @@ -36,6 +36,10 @@ public class ConfigurationStore { return keyValueStore.getEntry(readReceipts); } + public Boolean getReadReceipts(final Connection connection) throws SQLException { + return keyValueStore.getEntry(connection, readReceipts); + } + public void setReadReceipts(final boolean value) { if (keyValueStore.storeEntry(readReceipts, value)) { recipientStore.rotateSelfStorageId(); @@ -52,6 +56,10 @@ public class ConfigurationStore { return keyValueStore.getEntry(unidentifiedDeliveryIndicators); } + public Boolean getUnidentifiedDeliveryIndicators(final Connection connection) throws SQLException { + return keyValueStore.getEntry(connection, unidentifiedDeliveryIndicators); + } + public void setUnidentifiedDeliveryIndicators(final boolean value) { if (keyValueStore.storeEntry(unidentifiedDeliveryIndicators, value)) { recipientStore.rotateSelfStorageId(); @@ -59,7 +67,8 @@ public class ConfigurationStore { } public void setUnidentifiedDeliveryIndicators( - final Connection connection, final boolean value + final Connection connection, + final boolean value ) throws SQLException { if (keyValueStore.storeEntry(connection, unidentifiedDeliveryIndicators, value)) { recipientStore.rotateSelfStorageId(connection); @@ -70,6 +79,10 @@ public class ConfigurationStore { return keyValueStore.getEntry(typingIndicators); } + public Boolean getTypingIndicators(final Connection connection) throws SQLException { + return keyValueStore.getEntry(connection, typingIndicators); + } + public void setTypingIndicators(final boolean value) { if (keyValueStore.storeEntry(typingIndicators, value)) { recipientStore.rotateSelfStorageId(); @@ -86,6 +99,10 @@ public class ConfigurationStore { return keyValueStore.getEntry(linkPreviews); } + public Boolean getLinkPreviews(final Connection connection) throws SQLException { + return keyValueStore.getEntry(connection, linkPreviews); + } + public void setLinkPreviews(final boolean value) { if (keyValueStore.storeEntry(linkPreviews, value)) { recipientStore.rotateSelfStorageId(); @@ -102,6 +119,10 @@ public class ConfigurationStore { return keyValueStore.getEntry(phoneNumberUnlisted); } + public Boolean getPhoneNumberUnlisted(final Connection connection) throws SQLException { + return keyValueStore.getEntry(connection, phoneNumberUnlisted); + } + public void setPhoneNumberUnlisted(final boolean value) { if (keyValueStore.storeEntry(phoneNumberUnlisted, value)) { recipientStore.rotateSelfStorageId(); @@ -118,6 +139,10 @@ public class ConfigurationStore { return keyValueStore.getEntry(phoneNumberSharingMode); } + public PhoneNumberSharingMode getPhoneNumberSharingMode(final Connection connection) throws SQLException { + return keyValueStore.getEntry(connection, phoneNumberSharingMode); + } + public void setPhoneNumberSharingMode(final PhoneNumberSharingMode value) { if (keyValueStore.storeEntry(phoneNumberSharingMode, value)) { recipientStore.rotateSelfStorageId(); @@ -125,7 +150,8 @@ public class ConfigurationStore { } public void setPhoneNumberSharingMode( - final Connection connection, final PhoneNumberSharingMode value + final Connection connection, + final PhoneNumberSharingMode value ) throws SQLException { if (keyValueStore.storeEntry(connection, phoneNumberSharingMode, value)) { recipientStore.rotateSelfStorageId(connection); @@ -136,6 +162,10 @@ public class ConfigurationStore { return keyValueStore.getEntry(usernameLinkColor); } + public String getUsernameLinkColor(final Connection connection) throws SQLException { + return keyValueStore.getEntry(connection, usernameLinkColor); + } + public void setUsernameLinkColor(final String color) { if (keyValueStore.storeEntry(usernameLinkColor, color)) { recipientStore.rotateSelfStorageId(); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java index 89306ecd..f15b94a8 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java @@ -31,7 +31,9 @@ public final class GroupInfoV2 extends GroupInfo { private final RecipientResolver recipientResolver; public GroupInfoV2( - final GroupIdV2 groupId, final GroupMasterKey masterKey, final RecipientResolver recipientResolver + final GroupIdV2 groupId, + final GroupMasterKey masterKey, + final RecipientResolver recipientResolver ) { this.groupId = groupId; this.masterKey = masterKey; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java index c4c075bf..26c4c08d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java @@ -121,7 +121,10 @@ public class GroupStore { } public void storeStorageRecord( - final Connection connection, final GroupId groupId, final StorageId storageId, final byte[] storageRecord + final Connection connection, + final GroupId groupId, + final StorageId storageId, + final byte[] storageRecord ) throws SQLException { final var groupTable = groupId instanceof GroupIdV1 ? TABLE_GROUP_V1 : TABLE_GROUP_V2; final var deleteSql = ( @@ -250,7 +253,8 @@ public class GroupStore { } public GroupInfoV2 getGroupOrPartialMigrate( - Connection connection, final GroupMasterKey groupMasterKey + Connection connection, + final GroupMasterKey groupMasterKey ) throws SQLException { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); final var groupId = GroupUtils.getGroupIdV2(groupSecretParams); @@ -258,9 +262,7 @@ public class GroupStore { return getGroupOrPartialMigrate(connection, groupMasterKey, groupId); } - public GroupInfoV2 getGroupOrPartialMigrate( - final GroupMasterKey groupMasterKey, final GroupIdV2 groupId - ) { + public GroupInfoV2 getGroupOrPartialMigrate(final GroupMasterKey groupMasterKey, final GroupIdV2 groupId) { try (final var connection = database.getConnection()) { return getGroupOrPartialMigrate(connection, groupMasterKey, groupId); } catch (SQLException e) { @@ -269,7 +271,9 @@ public class GroupStore { } private GroupInfoV2 getGroupOrPartialMigrate( - Connection connection, final GroupMasterKey groupMasterKey, final GroupIdV2 groupId + Connection connection, + final GroupMasterKey groupMasterKey, + final GroupIdV2 groupId ) throws SQLException { switch (getGroup(connection, (GroupId) groupId)) { case GroupInfoV1 groupInfoV1 -> { @@ -325,7 +329,9 @@ public class GroupStore { } public void mergeRecipients( - final Connection connection, final RecipientId recipientId, final RecipientId toBeMergedRecipientId + final Connection connection, + final RecipientId recipientId, + final RecipientId toBeMergedRecipientId ) throws SQLException { final var sql = ( """ @@ -360,7 +366,9 @@ public class GroupStore { } public void updateStorageIds( - Connection connection, Map storageIdV1Map, Map storageIdV2Map + Connection connection, + Map storageIdV1Map, + Map storageIdV2Map ) throws SQLException { final var sql = ( """ @@ -385,9 +393,7 @@ public class GroupStore { } } - public void updateStorageId( - Connection connection, GroupId groupId, StorageId storageId - ) throws SQLException { + public void updateStorageId(Connection connection, GroupId groupId, StorageId storageId) throws SQLException { final var sqlV1 = ( """ UPDATE %s @@ -460,7 +466,9 @@ public class GroupStore { } private void insertOrReplaceGroup( - final Connection connection, Long internalId, final GroupInfo group + final Connection connection, + Long internalId, + final GroupInfo group ) throws SQLException { if (group instanceof GroupInfoV1 groupV1) { if (internalId != null) { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/LegacyGroupStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/LegacyGroupStore.java index 1d8c8799..383912dd 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/LegacyGroupStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/LegacyGroupStore.java @@ -151,7 +151,8 @@ public class LegacyGroupStore { @Override public List deserialize( - JsonParser jsonParser, DeserializationContext deserializationContext + JsonParser jsonParser, + DeserializationContext deserializationContext ) throws IOException { var addresses = new ArrayList(); JsonNode node = jsonParser.getCodec().readTree(jsonParser); @@ -184,7 +185,8 @@ public class LegacyGroupStore { @Override public List deserialize( - JsonParser jsonParser, DeserializationContext deserializationContext + JsonParser jsonParser, + DeserializationContext deserializationContext ) throws IOException { var groups = new ArrayList<>(); JsonNode node = jsonParser.getCodec().readTree(jsonParser); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityInfo.java index d00d455c..0b9b8c17 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityInfo.java @@ -11,9 +11,7 @@ public class IdentityInfo { private final TrustLevel trustLevel; private final long addedTimestamp; - IdentityInfo( - final String address, IdentityKey identityKey, TrustLevel trustLevel, long addedTimestamp - ) { + IdentityInfo(final String address, IdentityKey identityKey, TrustLevel trustLevel, long addedTimestamp) { this.address = address; this.identityKey = identityKey; this.trustLevel = trustLevel; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java index a4b355ea..a2d03a21 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java @@ -8,6 +8,7 @@ import org.asamk.signal.manager.storage.recipients.RecipientStore; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.state.IdentityKeyStore.Direction; +import org.signal.libsignal.protocol.state.IdentityKeyStore.IdentityChange; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.push.ServiceId; @@ -49,7 +50,9 @@ public class IdentityKeyStore { } public IdentityKeyStore( - final Database database, final TrustNewIdentity trustNewIdentity, RecipientStore recipientStore + final Database database, + final TrustNewIdentity trustNewIdentity, + RecipientStore recipientStore ) { this.database = database; this.trustNewIdentity = trustNewIdentity; @@ -60,19 +63,21 @@ public class IdentityKeyStore { return identityChanges; } - public boolean saveIdentity(final ServiceId serviceId, final IdentityKey identityKey) { + public IdentityChange saveIdentity(final ServiceId serviceId, final IdentityKey identityKey) { return saveIdentity(serviceId.toString(), identityKey); } - public boolean saveIdentity( - final Connection connection, final ServiceId serviceId, final IdentityKey identityKey + public IdentityChange saveIdentity( + final Connection connection, + final ServiceId serviceId, + final IdentityKey identityKey ) throws SQLException { return saveIdentity(connection, serviceId.toString(), identityKey); } - boolean saveIdentity(final String address, final IdentityKey identityKey) { + IdentityChange saveIdentity(final String address, final IdentityKey identityKey) { if (isRetryingDecryption) { - return false; + return IdentityChange.NEW_OR_UNCHANGED; } try (final var connection = database.getConnection()) { return saveIdentity(connection, address, identityKey); @@ -81,18 +86,24 @@ public class IdentityKeyStore { } } - private boolean saveIdentity( - final Connection connection, final String address, final IdentityKey identityKey + private IdentityChange saveIdentity( + final Connection connection, + final String address, + final IdentityKey identityKey ) throws SQLException { final var identityInfo = loadIdentity(connection, address); - if (identityInfo != null && identityInfo.getIdentityKey().equals(identityKey)) { + if (identityInfo == null) { + saveNewIdentity(connection, address, identityKey, true); + return IdentityChange.NEW_OR_UNCHANGED; + } + if (identityInfo.getIdentityKey().equals(identityKey)) { // Identity already exists, not updating the trust level logger.trace("Not storing new identity for recipient {}, identity already stored", address); - return false; + return IdentityChange.NEW_OR_UNCHANGED; } - saveNewIdentity(connection, address, identityKey, identityInfo == null); - return true; + saveNewIdentity(connection, address, identityKey, false); + return IdentityChange.REPLACED_EXISTING; } public void setRetryingDecryption(final boolean retryingDecryption) { @@ -230,9 +241,7 @@ public class IdentityKeyStore { logger.debug("Complete identities migration took {}ms", (System.nanoTime() - start) / 1000000); } - private IdentityInfo loadIdentity( - final Connection connection, final String address - ) throws SQLException { + private IdentityInfo loadIdentity(final Connection connection, final String address) throws SQLException { final var sql = ( """ SELECT i.address, i.identity_key, i.added_timestamp, i.trust_level diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/identities/LegacyIdentityKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/identities/LegacyIdentityKeyStore.java index a7f6a4aa..fc6e93e1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/identities/LegacyIdentityKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/identities/LegacyIdentityKeyStore.java @@ -41,7 +41,9 @@ public class LegacyIdentityKeyStore { static final Pattern identityFileNamePattern = Pattern.compile("(\\d+)"); private static List getIdentities( - final File identitiesPath, final RecipientResolver resolver, final RecipientAddressResolver addressResolver + final File identitiesPath, + final RecipientResolver resolver, + final RecipientAddressResolver addressResolver ) { final var files = identitiesPath.listFiles(); if (files == null) { @@ -66,7 +68,9 @@ public class LegacyIdentityKeyStore { } private static IdentityInfo loadIdentityLocked( - final RecipientId recipientId, RecipientAddressResolver addressResolver, final File identitiesPath + final RecipientId recipientId, + RecipientAddressResolver addressResolver, + final File identitiesPath ) { final var file = getIdentityFile(recipientId, identitiesPath); if (!file.exists()) { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/identities/SignalIdentityKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/identities/SignalIdentityKeyStore.java index 5a1a676e..1ac7c9a6 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/identities/SignalIdentityKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/identities/SignalIdentityKeyStore.java @@ -33,7 +33,7 @@ public class SignalIdentityKeyStore implements org.signal.libsignal.protocol.sta } @Override - public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { + public IdentityChange saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { return identityKeyStore.saveIdentity(address.getName(), identityKey); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/keyValue/KeyValueStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/keyValue/KeyValueStore.java index c51f32f1..2f59c9d2 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/keyValue/KeyValueStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/keyValue/KeyValueStore.java @@ -10,6 +10,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; +import java.util.HashMap; import java.util.Objects; public class KeyValueStore { @@ -18,6 +19,7 @@ public class KeyValueStore { private static final Logger logger = LoggerFactory.getLogger(KeyValueStore.class); private final Database database; + private final HashMap, Object> cache = new HashMap<>(); public static void createSql(Connection connection) throws SQLException { // When modifying the CREATE statement here, also add a migration in AccountDatabase.java @@ -36,11 +38,18 @@ public class KeyValueStore { this.database = database; } + @SuppressWarnings("unchecked") public T getEntry(KeyValueEntry key) { + synchronized (cache) { + if (cache.containsKey(key)) { + logger.trace("Got entry for key {} from cache", key.key()); + return (T) cache.get(key); + } + } try (final var connection = database.getConnection()) { return getEntry(connection, key); } catch (SQLException e) { - throw new RuntimeException("Failed read from pre_key store", e); + throw new RuntimeException("Failed read from key_value store", e); } } @@ -52,7 +61,7 @@ public class KeyValueStore { } } - private T getEntry(final Connection connection, final KeyValueEntry key) throws SQLException { + public T getEntry(final Connection connection, final KeyValueEntry key) throws SQLException { final var sql = ( """ SELECT key, value @@ -63,20 +72,28 @@ public class KeyValueStore { try (final var statement = connection.prepareStatement(sql)) { statement.setString(1, key.key()); - final var result = Utils.executeQueryForOptional(statement, - resultSet -> readValueFromResultSet(key, resultSet)).orElse(null); + var result = Utils.executeQueryForOptional(statement, resultSet -> readValueFromResultSet(key, resultSet)) + .orElse(null); if (result == null) { - return key.defaultValue(); + logger.trace("Got entry for key {} from default value", key.key()); + result = key.defaultValue(); + } else { + logger.trace("Got entry for key {} from db", key.key()); + } + synchronized (cache) { + cache.put(key, result); } return result; } } public boolean storeEntry( - final Connection connection, final KeyValueEntry key, final T value + final Connection connection, + final KeyValueEntry key, + final T value ) throws SQLException { - final var entry = getEntry(key); + final var entry = getEntry(connection, key); if (Objects.equals(entry, value)) { return false; } @@ -93,12 +110,16 @@ public class KeyValueStore { setParameterValue(statement, 2, key.clazz(), value); statement.executeUpdate(); } + synchronized (cache) { + cache.put(key, value); + } return true; } @SuppressWarnings("unchecked") private static T readValueFromResultSet( - final KeyValueEntry key, final ResultSet resultSet + final KeyValueEntry key, + final ResultSet resultSet ) throws SQLException { Object value; final var clazz = key.clazz(); @@ -134,7 +155,10 @@ public class KeyValueStore { } private static void setParameterValue( - final PreparedStatement statement, final int parameterIndex, final Class clazz, final T value + final PreparedStatement statement, + final int parameterIndex, + final Class clazz, + final T value ) throws SQLException { if (clazz == int.class || clazz == Integer.class) { if (value == null) { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java b/lib/src/main/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java index 51882a44..e4fd8c01 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java @@ -10,6 +10,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.Arrays; import java.util.Collections; import java.util.Objects; @@ -75,7 +76,7 @@ public class MessageCache { return cachedMessage; } logger.debug("Moving cached message {} to {}", cachedMessage.getFile().toPath(), cacheFile.toPath()); - Files.move(cachedMessage.getFile().toPath(), cacheFile.toPath()); + Files.move(cachedMessage.getFile().toPath(), cacheFile.toPath(), StandardCopyOption.REPLACE_EXISTING); return new CachedMessage(cacheFile); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/prekeys/PreKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/prekeys/PreKeyStore.java index d2d55710..8e8bd2ab 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/prekeys/PreKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/prekeys/PreKeyStore.java @@ -4,8 +4,9 @@ import org.asamk.signal.manager.storage.Database; import org.asamk.signal.manager.storage.Utils; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyIdException; -import org.signal.libsignal.protocol.ecc.Curve; import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.signal.libsignal.protocol.ecc.ECPrivateKey; +import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.state.PreKeyRecord; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -176,8 +177,8 @@ public class PreKeyStore implements SignalServicePreKeyStore { private PreKeyRecord getPreKeyRecordFromResultSet(ResultSet resultSet) throws SQLException { try { final var keyId = resultSet.getInt("key_id"); - final var publicKey = Curve.decodePoint(resultSet.getBytes("public_key"), 0); - final var privateKey = Curve.decodePrivatePoint(resultSet.getBytes("private_key")); + final var publicKey = new ECPublicKey(resultSet.getBytes("public_key")); + final var privateKey = new ECPrivateKey(resultSet.getBytes("private_key")); return new PreKeyRecord(keyId, new ECKeyPair(publicKey, privateKey)); } catch (InvalidKeyException e) { return null; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/prekeys/SignedPreKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/prekeys/SignedPreKeyStore.java index d62e939d..4644c3ba 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/prekeys/SignedPreKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/prekeys/SignedPreKeyStore.java @@ -4,8 +4,9 @@ import org.asamk.signal.manager.storage.Database; import org.asamk.signal.manager.storage.Utils; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyIdException; -import org.signal.libsignal.protocol.ecc.Curve; import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.signal.libsignal.protocol.ecc.ECPrivateKey; +import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.state.SignedPreKeyRecord; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -238,8 +239,8 @@ public class SignedPreKeyStore implements org.signal.libsignal.protocol.state.Si private SignedPreKeyRecord getSignedPreKeyRecordFromResultSet(ResultSet resultSet) throws SQLException { try { final var keyId = resultSet.getInt("key_id"); - final var publicKey = Curve.decodePoint(resultSet.getBytes("public_key"), 0); - final var privateKey = Curve.decodePrivatePoint(resultSet.getBytes("private_key")); + final var publicKey = new ECPublicKey(resultSet.getBytes("public_key")); + final var privateKey = new ECPrivateKey(resultSet.getBytes("private_key")); final var signature = resultSet.getBytes("signature"); final var timestamp = resultSet.getLong("timestamp"); return new SignedPreKeyRecord(keyId, timestamp, new ECKeyPair(publicKey, privateKey), signature); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java index abb66470..39d6b7fb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java @@ -34,7 +34,8 @@ public class LegacyProfileStore { @Override public List deserialize( - JsonParser jsonParser, DeserializationContext deserializationContext + JsonParser jsonParser, + DeserializationContext deserializationContext ) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java index 5d3a95a8..ec91a628 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java @@ -18,6 +18,7 @@ public interface ProfileStore { void storeProfileKey(RecipientId recipientId, ProfileKey profileKey); void storeExpiringProfileKeyCredential( - RecipientId recipientId, ExpiringProfileKeyCredential expiringProfileKeyCredential + RecipientId recipientId, + ExpiringProfileKeyCredential expiringProfileKeyCredential ); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java index 0d12a303..632efc7e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java @@ -32,7 +32,9 @@ public class LegacyJsonIdentityKeyStore { private final int localRegistrationId; private LegacyJsonIdentityKeyStore( - final List identities, IdentityKeyPair identityKeyPair, int localRegistrationId + final List identities, + IdentityKeyPair identityKeyPair, + int localRegistrationId ) { this.identities = identities; this.identityKeyPair = identityKeyPair; @@ -77,7 +79,8 @@ public class LegacyJsonIdentityKeyStore { @Override public LegacyJsonIdentityKeyStore deserialize( - JsonParser jsonParser, DeserializationContext deserializationContext + JsonParser jsonParser, + DeserializationContext deserializationContext ) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonPreKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonPreKeyStore.java index 7412f95a..feb27e97 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonPreKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonPreKeyStore.java @@ -26,7 +26,8 @@ public class LegacyJsonPreKeyStore { @Override public LegacyJsonPreKeyStore deserialize( - JsonParser jsonParser, DeserializationContext deserializationContext + JsonParser jsonParser, + DeserializationContext deserializationContext ) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java index ee675d78..b971078c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java @@ -31,7 +31,8 @@ public class LegacyJsonSessionStore { @Override public LegacyJsonSessionStore deserialize( - JsonParser jsonParser, DeserializationContext deserializationContext + JsonParser jsonParser, + DeserializationContext deserializationContext ) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSignedPreKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSignedPreKeyStore.java index cdcba916..ed1b5d95 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSignedPreKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSignedPreKeyStore.java @@ -26,7 +26,8 @@ public class LegacyJsonSignedPreKeyStore { @Override public LegacyJsonSignedPreKeyStore deserialize( - JsonParser jsonParser, DeserializationContext deserializationContext + JsonParser jsonParser, + DeserializationContext deserializationContext ) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java index f334a2be..e1cf9a88 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java @@ -65,7 +65,7 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore { } @Override - public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { + public IdentityChange saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { return identityKeyStore.saveIdentity(address, identityKey); } @@ -172,7 +172,9 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore { @Override public void storeSenderKey( - final SignalProtocolAddress sender, final UUID distributionId, final SenderKeyRecord record + final SignalProtocolAddress sender, + final UUID distributionId, + final SenderKeyRecord record ) { senderKeyStore.storeSenderKey(sender, distributionId, record); } @@ -189,7 +191,8 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore { @Override public void markSenderKeySharedWith( - final DistributionId distributionId, final Collection addresses + final DistributionId distributionId, + final Collection addresses ) { senderKeyStore.markSenderKeySharedWith(distributionId, addresses); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/CdsiStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/CdsiStore.java index 1170e651..4b68adbe 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/CdsiStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/CdsiStore.java @@ -98,9 +98,7 @@ public class CdsiStore { } } - private static void removeNumbers( - final Connection connection, final Set numbers - ) throws SQLException { + private static void removeNumbers(final Connection connection, final Set numbers) throws SQLException { final var sql = ( """ DELETE FROM %s @@ -116,7 +114,9 @@ public class CdsiStore { } private static void addNumbers( - final Connection connection, final Set numbers, final long lastSeen + final Connection connection, + final Set numbers, + final long lastSeen ) throws SQLException { final var sql = ( """ @@ -135,7 +135,9 @@ public class CdsiStore { } private static void updateLastSeen( - final Connection connection, final Set numbers, final long lastSeen + final Connection connection, + final Set numbers, + final long lastSeen ) throws SQLException { final var sql = ( """ diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/InvalidAddress.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/InvalidAddress.java new file mode 100644 index 00000000..991bc166 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/InvalidAddress.java @@ -0,0 +1,8 @@ +package org.asamk.signal.manager.storage.recipients; + +public class InvalidAddress extends AssertionError { + + InvalidAddress(String message) { + super(message); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore.java index fb1e188b..907aafe4 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore.java @@ -27,7 +27,8 @@ public class LegacyRecipientStore { @Override public List deserialize( - JsonParser jsonParser, DeserializationContext deserializationContext + JsonParser jsonParser, + DeserializationContext deserializationContext ) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/MergeRecipientHelper.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/MergeRecipientHelper.java index edc1121e..1454c8a7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/MergeRecipientHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/MergeRecipientHelper.java @@ -15,7 +15,8 @@ public class MergeRecipientHelper { private static final Logger logger = LoggerFactory.getLogger(MergeRecipientHelper.class); static Pair> resolveRecipientTrustedLocked( - Store store, RecipientAddress address + Store store, + RecipientAddress address ) throws SQLException { // address has at least one of serviceId/pni and optionally number/username @@ -38,7 +39,7 @@ public class MergeRecipientHelper { ) ) || recipient.address().aci().equals(address.aci())) { logger.debug("Got existing recipient {}, updating with high trust address", recipient.id()); - store.updateRecipientAddress(recipient.id(), recipient.address().withIdentifiersFrom(address)); + store.updateRecipientAddress(recipient.id(), address.withOtherIdentifiersFrom(recipient.address())); return new Pair<>(recipient.id(), List.of()); } @@ -82,24 +83,25 @@ public class MergeRecipientHelper { recipientsToBeStripped.add(recipient); } - logger.debug("Got separate recipients for high trust identifiers {}, need to merge ({}) and strip ({})", + logger.debug("Got separate recipients for high trust identifiers {}, need to merge ({}, {}) and strip ({})", address, - recipientsToBeMerged.stream().map(r -> r.id().toString()).collect(Collectors.joining(", ")), - recipientsToBeStripped.stream().map(r -> r.id().toString()).collect(Collectors.joining(", "))); + resultingRecipient.map(RecipientWithAddress::address), + recipientsToBeMerged.stream().map(r -> r.address().toString()).collect(Collectors.joining(", ")), + recipientsToBeStripped.stream().map(r -> r.address().toString()).collect(Collectors.joining(", "))); RecipientAddress finalAddress = resultingRecipient.map(RecipientWithAddress::address).orElse(null); for (final var recipient : recipientsToBeMerged) { if (finalAddress == null) { finalAddress = recipient.address(); } else { - finalAddress = finalAddress.withIdentifiersFrom(recipient.address()); + finalAddress = finalAddress.withOtherIdentifiersFrom(recipient.address()); } store.removeRecipientAddress(recipient.id()); } if (finalAddress == null) { finalAddress = address; } else { - finalAddress = finalAddress.withIdentifiersFrom(address); + finalAddress = address.withOtherIdentifiersFrom(finalAddress); } for (final var recipient : recipientsToBeStripped) { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java index 56adb0a6..25dc3a3f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java @@ -27,7 +27,7 @@ public record RecipientAddress( pni = Optional.empty(); } if (aci.isEmpty() && pni.isEmpty() && number.isEmpty() && username.isEmpty()) { - throw new AssertionError("Must have either a ServiceId, username or E164 number!"); + throw new InvalidAddress("Must have either a ServiceId, username or E164 number!"); } } @@ -69,8 +69,8 @@ public record RecipientAddress( } public RecipientAddress(org.asamk.signal.manager.api.RecipientAddress address) { - this(address.aci().map(ACI::parseOrNull), - address.pni().map(PNI::parseOrNull), + this(address.aci().map(ACI::parseOrThrow), + address.pni().map(PNI::parseOrThrow), address.number(), address.username()); } @@ -79,11 +79,11 @@ public record RecipientAddress( this(Optional.of(serviceId), Optional.empty()); } - public RecipientAddress withIdentifiersFrom(RecipientAddress address) { - return new RecipientAddress(address.aci.or(this::aci), - address.pni.or(this::pni), - address.number.or(this::number), - address.username.or(this::username)); + public RecipientAddress withOtherIdentifiersFrom(RecipientAddress address) { + return new RecipientAddress(this.aci.or(address::aci), + this.pni.or(address::pni), + this.number.or(address::number), + this.username.or(address::username)); } public RecipientAddress removeIdentifiersFrom(RecipientAddress address) { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index 2aad97d6..442cbf4e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -208,7 +208,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } public RecipientId resolveRecipientByNumber( - final String number, Supplier serviceIdSupplier + final String number, + Supplier serviceIdSupplier ) throws UnregisteredRecipientException { final Optional byNumber; try (final var connection = database.getConnection()) { @@ -238,7 +239,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } public RecipientId resolveRecipientByUsername( - final String username, Supplier aciSupplier + final String username, + Supplier aciSupplier ) throws UnregisteredRecipientException { final Optional byUsername; try (final var connection = database.getConnection()) { @@ -301,7 +303,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re @Override public RecipientId resolveRecipientTrusted( - final Optional aci, final Optional pni, final Optional number + final Optional aci, + final Optional pni, + final Optional number ) { return resolveRecipientTrusted(new RecipientAddress(aci, pni, number, Optional.empty())); } @@ -335,7 +339,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re """ SELECT r._id, r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp FROM %s r - WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE + WHERE (r.number IS NOT NULL OR r.pni IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE """ ).formatted(TABLE_RECIPIENT, SQL_IS_CONTACT); try (final var connection = database.getConnection()) { @@ -388,11 +392,23 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re try (final var statement = connection.prepareStatement(sql)) { statement.setBytes(1, storageId.getRaw()); return Utils.executeQuerySingleRow(statement, this::getRecipientFromResultSet); + } catch (InvalidAddress e) { + try (final var statement = connection.prepareStatement(""" + UPDATE %s SET aci=NULL, pni=NULL, username=NULL, number=NULL, storage_id=NULL WHERE storage_id = ? + """.formatted(TABLE_RECIPIENT))) { + statement.setBytes(1, storageId.getRaw()); + statement.executeUpdate(); + } + connection.commit(); + throw e; } } public List getRecipients( - boolean onlyContacts, Optional blocked, Set recipientIds, Optional name + boolean onlyContacts, + Optional blocked, + Set recipientIds, + Optional name ) { final var sqlWhere = new ArrayList(); if (onlyContacts) { @@ -419,7 +435,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re r.discoverable, r.storage_record FROM %s r - WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s + WHERE (r.number IS NOT NULL OR r.pni IS NOT NULL OR r.aci IS NOT NULL) AND %s """ ).formatted(TABLE_RECIPIENT, sqlWhere.isEmpty() ? "TRUE" : String.join(" AND ", sqlWhere)); final var selfAddress = selfAddressProvider.getSelfAddress(); @@ -505,7 +521,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re """ SELECT r._id FROM %s r - WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) + WHERE (r.aci IS NOT NULL OR r.pni IS NOT NULL) """ ).formatted(TABLE_RECIPIENT); try (final var statement = connection.prepareStatement(sql)) { @@ -518,7 +534,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re """ SELECT r._id FROM %s r - WHERE r.storage_id IS NULL AND r.unregistered_timestamp IS NULL + WHERE r.storage_id IS NULL AND r.unregistered_timestamp IS NULL AND (r.aci IS NOT NULL OR r.pni IS NOT NULL) """ ).formatted(TABLE_RECIPIENT); final var updateSql = ( @@ -614,14 +630,17 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } public void storeProfileKey( - Connection connection, RecipientId recipientId, final ProfileKey profileKey + Connection connection, + RecipientId recipientId, + final ProfileKey profileKey ) throws SQLException { storeProfileKey(connection, recipientId, profileKey, true); } @Override public void storeExpiringProfileKeyCredential( - RecipientId recipientId, final ExpiringProfileKeyCredential profileKeyCredential + RecipientId recipientId, + final ExpiringProfileKeyCredential profileKeyCredential ) { try (final var connection = database.getConnection()) { storeExpiringProfileKeyCredential(connection, recipientId, profileKeyCredential); @@ -661,7 +680,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } public void updateStorageId( - Connection connection, RecipientId recipientId, StorageId storageId + Connection connection, + RecipientId recipientId, + StorageId storageId ) throws SQLException { final var sql = ( """ @@ -813,7 +834,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } public void storeContact( - final Connection connection, final RecipientId recipientId, final Contact contact + final Connection connection, + final RecipientId recipientId, + final Contact contact ) throws SQLException { final var sql = ( """ @@ -852,7 +875,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } public int removeStorageIdsFromLocalOnlyUnregisteredRecipients( - final Connection connection, final List storageIds + final Connection connection, + final List storageIds ) throws SQLException { final var sql = ( """ @@ -910,7 +934,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } public void markUndiscoverablePossiblyUnregistered(final Set numbers) { - logger.debug("Marking {} numbers as unregistered", numbers.size()); + logger.debug("Marking {} numbers as undiscoverable", numbers.size()); try (final var connection = database.getConnection()) { connection.setAutoCommit(false); for (final var number : numbers) { @@ -965,11 +989,17 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } private void markUnregisteredAndSplitIfNecessary( - final Connection connection, final RecipientId recipientId + final Connection connection, + final RecipientId recipientId ) throws SQLException { markUnregistered(connection, recipientId); final var address = resolveRecipientAddress(connection, recipientId); - if (address.aci().isPresent() && address.pni().isPresent()) { + final var needSplit = address.aci().isPresent() && address.pni().isPresent(); + logger.trace("Marking unregistered recipient {} as unregistered (and split={}): {}", + recipientId, + needSplit, + address); + if (needSplit) { final var numberAddress = new RecipientAddress(address.pni().get(), address.number().orElse(null)); updateRecipientAddress(connection, recipientId, address.removeIdentifiersFrom(numberAddress)); addNewRecipient(connection, numberAddress); @@ -977,7 +1007,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } private void markDiscoverable( - final Connection connection, final RecipientId recipientId, final boolean discoverable + final Connection connection, + final RecipientId recipientId, + final boolean discoverable ) throws SQLException { final var sql = ( """ @@ -993,9 +1025,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } } - private void markRegistered( - final Connection connection, final RecipientId recipientId - ) throws SQLException { + private void markRegistered(final Connection connection, final RecipientId recipientId) throws SQLException { final var sql = ( """ UPDATE %s @@ -1009,9 +1039,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } } - private void markUnregistered( - final Connection connection, final RecipientId recipientId - ) throws SQLException { + private void markUnregistered(final Connection connection, final RecipientId recipientId) throws SQLException { final var sql = ( """ UPDATE %s @@ -1046,7 +1074,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } public void storeProfile( - final Connection connection, final RecipientId recipientId, final Profile profile + final Connection connection, + final RecipientId recipientId, + final Profile profile ) throws SQLException { final var sql = ( """ @@ -1079,7 +1109,10 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } private void storeProfileKey( - Connection connection, RecipientId recipientId, final ProfileKey profileKey, boolean resetProfile + Connection connection, + RecipientId recipientId, + final ProfileKey profileKey, + boolean resetProfile ) throws SQLException { if (profileKey != null) { final var recipientProfileKey = getProfileKey(connection, recipientId); @@ -1111,7 +1144,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } private RecipientAddress resolveRecipientAddress( - final Connection connection, final RecipientId recipientId + final Connection connection, + final RecipientId recipientId ) throws SQLException { final var sql = ( """ @@ -1150,7 +1184,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } private Pair> resolveRecipientTrustedLocked( - final Connection connection, final RecipientAddress address, final boolean isSelf + final Connection connection, + final RecipientAddress address, + final boolean isSelf ) throws SQLException { if (address.hasSingleIdentifier() || ( !isSelf && selfAddressProvider.getSelfAddress().matches(address) @@ -1168,7 +1204,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } private void mergeRecipients( - final Connection connection, final RecipientId recipientId, final List toBeMergedRecipientIds + final Connection connection, + final RecipientId recipientId, + final List toBeMergedRecipientIds ) throws SQLException { for (final var toBeMergedRecipientId : toBeMergedRecipientIds) { recipientMergeHandler.mergeRecipients(connection, recipientId, toBeMergedRecipientId); @@ -1177,9 +1215,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } } - private RecipientId resolveRecipientLocked( - Connection connection, RecipientAddress address - ) throws SQLException { + private RecipientId resolveRecipientLocked(Connection connection, RecipientAddress address) throws SQLException { final var byAci = address.aci().isEmpty() ? Optional.empty() : findByServiceId(connection, address.aci().get()); @@ -1236,7 +1272,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } private RecipientId addNewRecipient( - final Connection connection, final RecipientAddress address + final Connection connection, + final RecipientAddress address ) throws SQLException { final var sql = ( """ @@ -1277,7 +1314,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } private void updateRecipientAddress( - Connection connection, RecipientId recipientId, final RecipientAddress address + Connection connection, + RecipientId recipientId, + final RecipientAddress address ) throws SQLException { recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId)); final var sql = ( @@ -1312,7 +1351,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } private void mergeRecipientsLocked( - Connection connection, RecipientId recipientId, RecipientId toBeMergedRecipientId + Connection connection, + RecipientId recipientId, + RecipientId toBeMergedRecipientId ) throws SQLException { final var contact = getContact(connection, recipientId); if (contact == null) { @@ -1343,7 +1384,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } private Optional findByNumber( - final Connection connection, final String number + final Connection connection, + final String number ) throws SQLException { final var sql = """ SELECT r._id, r.number, r.aci, r.pni, r.username @@ -1358,7 +1400,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } private Optional findByUsername( - final Connection connection, final String username + final Connection connection, + final String username ) throws SQLException { final var sql = """ SELECT r._id, r.number, r.aci, r.pni, r.username @@ -1373,7 +1416,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } private Optional findByServiceId( - final Connection connection, final ServiceId serviceId + final Connection connection, + final ServiceId serviceId ) throws SQLException { var recipientWithAddress = Optional.ofNullable(recipientAddressCache.get(serviceId)); if (recipientWithAddress.isPresent()) { @@ -1394,7 +1438,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } private Set findAllByAddress( - final Connection connection, final RecipientAddress address + final Connection connection, + final RecipientAddress address ) throws SQLException { final var sql = """ SELECT r._id, r.number, r.aci, r.pni, r.username @@ -1447,7 +1492,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } private ExpiringProfileKeyCredential getExpiringProfileKeyCredential( - final Connection connection, final RecipientId recipientId + final Connection connection, + final RecipientId recipientId ) throws SQLException { final var sql = ( """ @@ -1593,7 +1639,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re public interface RecipientMergeHandler { void mergeRecipients( - final Connection connection, RecipientId recipientId, RecipientId toBeMergedRecipientId + final Connection connection, + RecipientId recipientId, + RecipientId toBeMergedRecipientId ) throws SQLException; } @@ -1617,7 +1665,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re @Override public void updateRecipientAddress( - final RecipientId recipientId, final RecipientAddress address + final RecipientId recipientId, + final RecipientAddress address ) throws SQLException { RecipientStore.this.updateRecipientAddress(connection, recipientId, address); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientTrustedResolver.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientTrustedResolver.java index 598ef791..06d29060 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientTrustedResolver.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientTrustedResolver.java @@ -44,7 +44,9 @@ public interface RecipientTrustedResolver { @Override public RecipientId resolveRecipientTrusted( - final Optional aci, final Optional pni, final Optional number + final Optional aci, + final Optional pni, + final Optional number ) { return recipientTrustedResolverSupplier.get().resolveRecipientTrusted(aci, pni, number); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/sendLog/MessageSendLogStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/sendLog/MessageSendLogStore.java index 910da3cf..20bfdb6a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/sendLog/MessageSendLogStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/sendLog/MessageSendLogStore.java @@ -83,7 +83,10 @@ public class MessageSendLogStore implements AutoCloseable { } public List findMessages( - final ServiceId serviceId, final int deviceId, final long timestamp, final boolean isSenderKey + final ServiceId serviceId, + final int deviceId, + final long timestamp, + final boolean isSenderKey ) { final var sql = """ SELECT group_id, content, content_hint, urgent @@ -111,7 +114,10 @@ public class MessageSendLogStore implements AutoCloseable { } public long insertIfPossible( - long sentTimestamp, SendMessageResult sendMessageResult, ContentHint contentHint, boolean urgent + long sentTimestamp, + SendMessageResult sendMessageResult, + ContentHint contentHint, + boolean urgent ) { if (sendLogDisabled) { return -1; @@ -129,7 +135,10 @@ public class MessageSendLogStore implements AutoCloseable { } public long insertIfPossible( - long sentTimestamp, List sendMessageResults, ContentHint contentHint, boolean urgent + long sentTimestamp, + List sendMessageResults, + ContentHint contentHint, + boolean urgent ) { if (sendLogDisabled) { return -1; @@ -164,7 +173,8 @@ public class MessageSendLogStore implements AutoCloseable { } public void addRecipientToExistingEntryIfPossible( - final long contentId, final List sendMessageResults + final long contentId, + final List sendMessageResults ) { if (sendLogDisabled) { return; @@ -322,7 +332,8 @@ public class MessageSendLogStore implements AutoCloseable { } private void insertRecipientsForExistingContent( - final long contentId, final List recipientDevices + final long contentId, + final List recipientDevices ) { try (final var connection = database.getConnection()) { connection.setAutoCommit(false); @@ -334,7 +345,9 @@ public class MessageSendLogStore implements AutoCloseable { } private void insertRecipientsForExistingContent( - final long contentId, final List recipientDevices, final Connection connection + final long contentId, + final List recipientDevices, + final Connection connection ) throws SQLException { final var sql = """ INSERT INTO %s (address, device_id, content_id) diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java index 39e1df97..dd48fcdc 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java @@ -59,7 +59,9 @@ public class SenderKeyRecordStore implements SenderKeyStore { @Override public void storeSenderKey( - final SignalProtocolAddress address, final UUID distributionId, final SenderKeyRecord record + final SignalProtocolAddress address, + final UUID distributionId, + final SenderKeyRecord record ) { final var key = getKey(address, distributionId); @@ -165,7 +167,9 @@ public class SenderKeyRecordStore implements SenderKeyStore { } private void storeSenderKey( - final Connection connection, final Key key, final SenderKeyRecord senderKeyRecord + final Connection connection, + final Key key, + final SenderKeyRecord senderKeyRecord ) throws SQLException { final var sqlUpdate = """ UPDATE %s diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java index 284ba08f..175524f5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java @@ -65,7 +65,8 @@ public class SenderKeySharedStore { } public void markSenderKeySharedWith( - final DistributionId distributionId, final Collection addresses + final DistributionId distributionId, + final Collection addresses ) { final var newEntries = addresses.stream() .map(a -> new SenderKeySharedEntry(a.getName(), a.getDeviceId())) @@ -138,9 +139,7 @@ public class SenderKeySharedStore { } } - public void deleteSharedWith( - final ServiceId serviceId, final int deviceId, final DistributionId distributionId - ) { + public void deleteSharedWith(final ServiceId serviceId, final int deviceId, final DistributionId distributionId) { try (final var connection = database.getConnection()) { final var sql = ( """ @@ -192,7 +191,9 @@ public class SenderKeySharedStore { } private void markSenderKeysSharedWith( - final Connection connection, final DistributionId distributionId, final Set newEntries + final Connection connection, + final DistributionId distributionId, + final Set newEntries ) throws SQLException { final var sql = ( """ diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java index d60249b6..5ae2790e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java @@ -25,7 +25,9 @@ public class SenderKeyStore implements SignalServiceSenderKeyStore { @Override public void storeSenderKey( - final SignalProtocolAddress sender, final UUID distributionId, final SenderKeyRecord record + final SignalProtocolAddress sender, + final UUID distributionId, + final SenderKeyRecord record ) { senderKeyRecordStore.storeSenderKey(sender, distributionId, record); } @@ -42,7 +44,8 @@ public class SenderKeyStore implements SignalServiceSenderKeyStore { @Override public void markSenderKeySharedWith( - final DistributionId distributionId, final Collection addresses + final DistributionId distributionId, + final Collection addresses ) { senderKeySharedStore.markSenderKeySharedWith(distributionId, addresses); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java index 067dc5d3..86fe875e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java @@ -342,7 +342,9 @@ public class SessionStore implements SignalServiceSessionStore { } private void storeSession( - final Connection connection, final Key key, final SessionRecord session + final Connection connection, + final Key key, + final SessionRecord session ) throws SQLException { synchronized (cachedSessions) { cachedSessions.put(key, session); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/threads/LegacyJsonThreadStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/threads/LegacyJsonThreadStore.java index 428e9e95..efe65c64 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/threads/LegacyJsonThreadStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/threads/LegacyJsonThreadStore.java @@ -35,7 +35,9 @@ public class LegacyJsonThreadStore { @Override public void serialize( - final Map value, final JsonGenerator jgen, final SerializerProvider provider + final Map value, + final JsonGenerator jgen, + final SerializerProvider provider ) throws IOException { jgen.writeObject(value.values()); } @@ -45,7 +47,8 @@ public class LegacyJsonThreadStore { @Override public Map deserialize( - JsonParser jsonParser, DeserializationContext deserializationContext + JsonParser jsonParser, + DeserializationContext deserializationContext ) throws IOException { var threads = new HashMap(); JsonNode node = jsonParser.getCodec().readTree(jsonParser); diff --git a/lib/src/main/java/org/asamk/signal/manager/syncStorage/AccountRecordProcessor.java b/lib/src/main/java/org/asamk/signal/manager/syncStorage/AccountRecordProcessor.java index 31e7a90b..30816060 100644 --- a/lib/src/main/java/org/asamk/signal/manager/syncStorage/AccountRecordProcessor.java +++ b/lib/src/main/java/org/asamk/signal/manager/syncStorage/AccountRecordProcessor.java @@ -2,7 +2,6 @@ package org.asamk.signal.manager.syncStorage; import org.asamk.signal.manager.api.Profile; import org.asamk.signal.manager.internal.JobExecutor; -import org.asamk.signal.manager.jobs.CheckWhoAmIJob; import org.asamk.signal.manager.jobs.DownloadProfileAvatarJob; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.util.KeyUtils; @@ -11,9 +10,11 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.push.UsernameLinkComponents; +import org.whispersystems.signalservice.api.storage.IAPSubscriptionId; import org.whispersystems.signalservice.api.storage.SignalAccountRecord; -import org.whispersystems.signalservice.api.util.OptionalUtil; +import org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.storage.protos.AccountRecord; import org.whispersystems.signalservice.internal.storage.protos.OptionalBool; import java.sql.Connection; @@ -21,6 +22,13 @@ import java.sql.SQLException; import java.util.Arrays; import java.util.Optional; +import okio.ByteString; + +import static org.asamk.signal.manager.util.Utils.firstNonEmpty; +import static org.whispersystems.signalservice.api.storage.AccountRecordExtensionsKt.safeSetBackupsSubscriber; +import static org.whispersystems.signalservice.api.storage.AccountRecordExtensionsKt.safeSetPayments; +import static org.whispersystems.signalservice.api.storage.AccountRecordExtensionsKt.safeSetSubscriber; + /** * Processes {@link SignalAccountRecord}s. */ @@ -33,7 +41,9 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor 0 + ? remote.payments + : local.payments; + + final ByteString donationSubscriberId; + final String donationSubscriberCurrencyCode; + + if (remote.subscriberId.size() > 0) { + donationSubscriberId = remote.subscriberId; + donationSubscriberCurrencyCode = remote.subscriberCurrencyCode; + } else { + donationSubscriberId = local.subscriberId; + donationSubscriberCurrencyCode = local.subscriberCurrencyCode; + } + + final ByteString backupsSubscriberId; + final IAPSubscriptionId backupsPurchaseToken; + + final var remoteBackupSubscriberData = remote.backupSubscriberData; + if (remoteBackupSubscriberData != null && remoteBackupSubscriberData.subscriberId.size() > 0) { + backupsSubscriberId = remoteBackupSubscriberData.subscriberId; + backupsPurchaseToken = IAPSubscriptionId.Companion.from(remoteBackupSubscriberData); + } else { + backupsSubscriberId = local.backupSubscriberData != null + ? local.backupSubscriberData.subscriberId + : ByteString.EMPTY; + backupsPurchaseToken = IAPSubscriptionId.Companion.from(local.backupSubscriberData); + } + + final var mergedBuilder = remote.newBuilder() + .givenName(givenName) + .familyName(familyName) + .avatarUrlPath(firstNonEmpty(remote.avatarUrlPath, local.avatarUrlPath)) + .profileKey(firstNonEmpty(remote.profileKey, local.profileKey)) + .noteToSelfArchived(remote.noteToSelfArchived) + .noteToSelfMarkedUnread(remote.noteToSelfMarkedUnread) + .readReceipts(remote.readReceipts) + .typingIndicators(remote.typingIndicators) + .sealedSenderIndicators(remote.sealedSenderIndicators) + .linkPreviews(remote.linkPreviews) + .unlistedPhoneNumber(remote.unlistedPhoneNumber) + .phoneNumberSharingMode(remote.phoneNumberSharingMode) + .pinnedConversations(remote.pinnedConversations) + .preferContactAvatars(remote.preferContactAvatars) + .universalExpireTimer(remote.universalExpireTimer) + .preferredReactionEmoji(firstNonEmpty(remote.preferredReactionEmoji, local.preferredReactionEmoji)) + .subscriberId(firstNonEmpty(remote.subscriberId, local.subscriberId)) + .subscriberCurrencyCode(firstNonEmpty(remote.subscriberCurrencyCode, local.subscriberCurrencyCode)) + .displayBadgesOnProfile(remote.displayBadgesOnProfile) + .subscriptionManuallyCancelled(remote.subscriptionManuallyCancelled) + .keepMutedChatsArchived(remote.keepMutedChatsArchived) + .hasSetMyStoriesPrivacy(remote.hasSetMyStoriesPrivacy) + .hasViewedOnboardingStory(remote.hasViewedOnboardingStory || local.hasViewedOnboardingStory) + .storiesDisabled(remote.storiesDisabled) + .hasSeenGroupStoryEducationSheet(remote.hasSeenGroupStoryEducationSheet + || local.hasSeenGroupStoryEducationSheet) + .hasCompletedUsernameOnboarding(remote.hasCompletedUsernameOnboarding + || local.hasCompletedUsernameOnboarding) + .storyViewReceiptsEnabled(remote.storyViewReceiptsEnabled == OptionalBool.UNSET + ? local.storyViewReceiptsEnabled + : remote.storyViewReceiptsEnabled) + .username(remote.username) + .usernameLink(remote.usernameLink) + .avatarColor(remote.avatarColor); + safeSetPayments(mergedBuilder, + payments != null && payments.enabled, + payments == null ? null : payments.entropy.toByteArray()); + safeSetSubscriber(mergedBuilder, donationSubscriberId, donationSubscriberCurrencyCode); + safeSetBackupsSubscriber(mergedBuilder, backupsSubscriberId, backupsPurchaseToken); - final var mergedBuilder = new SignalAccountRecord.Builder(remote.getId().getRaw(), unknownFields).setGivenName( - givenName) - .setFamilyName(familyName) - .setAvatarUrlPath(avatarUrlPath) - .setProfileKey(profileKey) - .setNoteToSelfArchived(noteToSelfArchived) - .setNoteToSelfForcedUnread(noteToSelfForcedUnread) - .setReadReceiptsEnabled(readReceipts) - .setTypingIndicatorsEnabled(typingIndicators) - .setSealedSenderIndicatorsEnabled(sealedSenderIndicators) - .setLinkPreviewsEnabled(linkPreviews) - .setUnlistedPhoneNumber(unlisted) - .setPhoneNumberSharingMode(phoneNumberSharingMode) - .setPinnedConversations(pinnedConversations) - .setPreferContactAvatars(preferContactAvatars) - .setPayments(payments.isEnabled(), payments.getEntropy().orElse(null)) - .setUniversalExpireTimer(universalExpireTimer) - .setDefaultReactions(defaultReactions) - .setSubscriber(subscriber) - .setDisplayBadgesOnProfile(displayBadgesOnProfile) - .setSubscriptionManuallyCancelled(subscriptionManuallyCancelled) - .setKeepMutedChatsArchived(keepMutedChatsArchived) - .setHasSetMyStoriesPrivacy(hasSetMyStoriesPrivacy) - .setHasViewedOnboardingStory(hasViewedOnboardingStory) - .setStoriesDisabled(storiesDisabled) - .setHasSeenGroupStoryEducationSheet(hasSeenGroupStoryEducation) - .setHasCompletedUsernameOnboarding(hasSeenUsernameOnboarding) - .setStoryViewReceiptsState(storyViewReceiptsState) - .setUsername(username) - .setUsernameLink(usernameLink) - .setE164(e164); final var merged = mergedBuilder.build(); final var matchesRemote = doProtosMatch(merged, remote); if (matchesRemote) { - return remote; + return remoteRecord; } final var matchesLocal = doProtosMatch(merged, local); if (matchesLocal) { - return local; + return localRecord; } - return mergedBuilder.setId(KeyUtils.createRawStorageId()).build(); + return new SignalAccountRecord(StorageId.forAccount(KeyUtils.createRawStorageId()), mergedBuilder.build()); } @Override @@ -162,56 +176,51 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor update) throws SQLException { final var accountRecord = update.newRecord(); + final var accountProto = accountRecord.getProto(); - if (!accountRecord.getE164().equals(account.getNumber())) { - jobExecutor.enqueueJob(new CheckWhoAmIJob()); - } - - account.getConfigurationStore().setReadReceipts(connection, accountRecord.isReadReceiptsEnabled()); - account.getConfigurationStore().setTypingIndicators(connection, accountRecord.isTypingIndicatorsEnabled()); + account.getConfigurationStore().setReadReceipts(connection, accountProto.readReceipts); + account.getConfigurationStore().setTypingIndicators(connection, accountProto.typingIndicators); account.getConfigurationStore() - .setUnidentifiedDeliveryIndicators(connection, accountRecord.isSealedSenderIndicatorsEnabled()); - account.getConfigurationStore().setLinkPreviews(connection, accountRecord.isLinkPreviewsEnabled()); + .setUnidentifiedDeliveryIndicators(connection, accountProto.sealedSenderIndicators); + account.getConfigurationStore().setLinkPreviews(connection, accountProto.linkPreviews); account.getConfigurationStore() .setPhoneNumberSharingMode(connection, - StorageSyncModels.remoteToLocal(accountRecord.getPhoneNumberSharingMode())); - account.getConfigurationStore().setPhoneNumberUnlisted(connection, accountRecord.isPhoneNumberUnlisted()); + StorageSyncModels.remoteToLocal(accountProto.phoneNumberSharingMode)); + account.getConfigurationStore().setPhoneNumberUnlisted(connection, accountProto.unlistedPhoneNumber); - account.setUsername(accountRecord.getUsername() != null && !accountRecord.getUsername().isEmpty() - ? accountRecord.getUsername() - : null); - if (accountRecord.getUsernameLink() != null) { - final var usernameLink = accountRecord.getUsernameLink(); + account.setUsername(!accountProto.username.isEmpty() ? accountProto.username : null); + if (accountProto.usernameLink != null) { + final var usernameLink = accountProto.usernameLink; account.setUsernameLink(new UsernameLinkComponents(usernameLink.entropy.toByteArray(), UuidUtil.parseOrThrow(usernameLink.serverId.toByteArray()))); account.getConfigurationStore().setUsernameLinkColor(connection, usernameLink.color.name()); } - if (accountRecord.getProfileKey().isPresent()) { + if (accountProto.profileKey.size() > 0) { ProfileKey profileKey; try { - profileKey = new ProfileKey(accountRecord.getProfileKey().get()); + profileKey = new ProfileKey(accountProto.profileKey.toByteArray()); } catch (InvalidInputException e) { logger.debug("Received invalid profile key from storage"); profileKey = null; } if (profileKey != null) { account.setProfileKey(profileKey); - final var avatarPath = accountRecord.getAvatarUrlPath().orElse(null); + final var avatarPath = accountProto.avatarUrlPath.isEmpty() ? null : accountProto.avatarUrlPath; jobExecutor.enqueueJob(new DownloadProfileAvatarJob(avatarPath)); } } final var profile = account.getRecipientStore().getProfile(connection, account.getSelfRecipientId()); final var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile); - builder.withGivenName(accountRecord.getGivenName().orElse(null)); - builder.withFamilyName(accountRecord.getFamilyName().orElse(null)); + builder.withGivenName(accountProto.givenName); + builder.withFamilyName(accountProto.familyName); account.getRecipientStore().storeProfile(connection, account.getSelfRecipientId(), builder.build()); account.getRecipientStore() .storeStorageRecord(connection, account.getSelfRecipientId(), accountRecord.getId(), - accountRecord.toProto().encode()); + accountProto.encode()); } @Override @@ -219,7 +228,7 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor { private static final Logger logger = LoggerFactory.getLogger(ContactRecordProcessor.class); - private static final Pattern E164_PATTERN = Pattern.compile("^\\+[1-9]\\d{0,18}$"); + private static final Pattern E164_PATTERN = Pattern.compile("^\\+[1-9]\\d{6,18}$"); private final ACI selfAci; private final PNI selfPni; @@ -55,20 +61,24 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor getMatching(SignalContactRecord remote) throws SQLException { - final var address = getRecipientAddress(remote); + final var address = getRecipientAddress(remote.getProto()); final var recipientId = account.getRecipientStore().resolveRecipient(connection, address); final var recipient = account.getRecipientStore().getRecipient(connection, recipientId); @@ -85,143 +95,121 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor 0 && ( + !account.isPrimaryDevice() + || remote.identityState != local.identityState + || local.identityKey.size() == 0 )) { - identityState = remote.getIdentityState(); - identityKey = remote.getIdentityKey().get(); + identityState = remote.identityState; + identityKey = remote.identityKey; } else { - identityState = local.getIdentityState(); - identityKey = local.getIdentityKey().orElse(null); + identityState = local.identityState; + identityKey = local.identityKey.size() > 0 ? local.identityKey : ByteString.EMPTY; } - if (local.getAci().isPresent() - && local.getIdentityKey().isPresent() - && remote.getIdentityKey().isPresent() - && !Arrays.equals(local.getIdentityKey().get(), remote.getIdentityKey().get())) { + if (!local.aci.isEmpty() + && local.identityKey.size() > 0 + && remote.identityKey.size() > 0 + && !local.identityKey.equals(remote.identityKey)) { logger.debug("The local and remote identity keys do not match for {}. Enqueueing a profile fetch.", - local.getAci().orElse(null)); + local.aci); final var address = getRecipientAddress(local); jobExecutor.enqueueJob(new DownloadProfileJob(address)); } - final var e164sMatchButPnisDont = local.getNumber().isPresent() - && local.getNumber() - .get() - .equals(remote.getNumber().orElse(null)) - && local.getPni().isPresent() - && remote.getPni().isPresent() - && !local.getPni().get().equals(remote.getPni().get()); - - final var pnisMatchButE164sDont = local.getPni().isPresent() - && local.getPni() - .get() - .equals(remote.getPni().orElse(null)) - && local.getNumber().isPresent() - && remote.getNumber().isPresent() - && !local.getNumber().get().equals(remote.getNumber().get()); - - PNI pni; + String pni; String e164; - if (!account.isPrimaryDevice() && (e164sMatchButPnisDont || pnisMatchButE164sDont)) { - if (e164sMatchButPnisDont) { - logger.debug("Matching E164s, but the PNIs differ! Trusting our local pair."); - } else if (pnisMatchButE164sDont) { - logger.debug("Matching PNIs, but the E164s differ! Trusting our local pair."); + if (account.isPrimaryDevice()) { + final var e164sMatchButPnisDont = !local.e164.isEmpty() + && local.e164.equals(remote.e164) + && !local.pni.isEmpty() + && !remote.pni.isEmpty() + && !local.pni.equals(remote.pni); + + final var pnisMatchButE164sDont = !local.pni.isEmpty() + && local.pni.equals(remote.pni) + && !local.e164.isEmpty() + && !remote.e164.isEmpty() + && !local.e164.equals(remote.e164); + + if (e164sMatchButPnisDont || pnisMatchButE164sDont) { + if (e164sMatchButPnisDont) { + logger.debug("Matching E164s, but the PNIs differ! Trusting our local pair."); + } else if (pnisMatchButE164sDont) { + logger.debug("Matching PNIs, but the E164s differ! Trusting our local pair."); + } + jobExecutor.enqueueJob(new RefreshRecipientsJob()); + pni = local.pni; + e164 = local.e164; + } else { + pni = firstNonEmpty(remote.pni, local.pni); + e164 = firstNonEmpty(remote.e164, local.e164); } - jobExecutor.enqueueJob(new RefreshRecipientsJob()); - pni = local.getPni().get(); - e164 = local.getNumber().get(); } else { - pni = OptionalUtil.or(remote.getPni(), local.getPni()).orElse(null); - e164 = OptionalUtil.or(remote.getNumber(), local.getNumber()).orElse(null); + pni = firstNonEmpty(remote.pni, local.pni); + e164 = firstNonEmpty(remote.e164, local.e164); } - final var unknownFields = remote.serializeUnknownFields(); - final var aci = local.getAci().isEmpty() ? remote.getAci().orElse(null) : local.getAci().get(); - final var profileKey = OptionalUtil.or(remote.getProfileKey(), local.getProfileKey()).orElse(null); - final var username = OptionalUtil.or(remote.getUsername(), local.getUsername()).orElse(""); - final var blocked = remote.isBlocked(); - final var profileSharing = remote.isProfileSharingEnabled(); - final var archived = remote.isArchived(); - final var forcedUnread = remote.isForcedUnread(); - final var muteUntil = remote.getMuteUntil(); - final var hideStory = remote.shouldHideStory(); - final var unregisteredTimestamp = remote.getUnregisteredTimestamp(); - final var hidden = remote.isHidden(); - final var systemGivenName = account.isPrimaryDevice() - ? local.getSystemGivenName().orElse("") - : remote.getSystemGivenName().orElse(""); - final var systemFamilyName = account.isPrimaryDevice() - ? local.getSystemFamilyName().orElse("") - : remote.getSystemFamilyName().orElse(""); - final var systemNickname = remote.getSystemNickname().orElse(""); - final var nicknameGivenName = remote.getNicknameGivenName().orElse(""); - final var nicknameFamilyName = remote.getNicknameFamilyName().orElse(""); - final var pniSignatureVerified = remote.isPniSignatureVerified() || local.isPniSignatureVerified(); - final var note = remote.getNote().or(local::getNote).orElse(""); - - final var mergedBuilder = new SignalContactRecord.Builder(remote.getId().getRaw(), aci, unknownFields).setE164( - e164) - .setPni(pni) - .setProfileGivenName(profileGivenName) - .setProfileFamilyName(profileFamilyName) - .setSystemGivenName(systemGivenName) - .setSystemFamilyName(systemFamilyName) - .setSystemNickname(systemNickname) - .setProfileKey(profileKey) - .setUsername(username) - .setIdentityState(identityState) - .setIdentityKey(identityKey) - .setBlocked(blocked) - .setProfileSharingEnabled(profileSharing) - .setArchived(archived) - .setForcedUnread(forcedUnread) - .setMuteUntil(muteUntil) - .setHideStory(hideStory) - .setUnregisteredTimestamp(unregisteredTimestamp) - .setHidden(hidden) - .setPniSignatureVerified(pniSignatureVerified) - .setNicknameGivenName(nicknameGivenName) - .setNicknameFamilyName(nicknameFamilyName) - .setNote(note); + final var mergedBuilder = remote.newBuilder() + .aci(local.aci.isEmpty() ? remote.aci : local.aci) + .e164(e164) + .pni(pni) + .givenName(profileGivenName) + .familyName(profileFamilyName) + .systemGivenName(account.isPrimaryDevice() ? local.systemGivenName : remote.systemGivenName) + .systemFamilyName(account.isPrimaryDevice() ? local.systemFamilyName : remote.systemFamilyName) + .systemNickname(remote.systemNickname) + .profileKey(firstNonEmpty(remote.profileKey, local.profileKey)) + .username(firstNonEmpty(remote.username, local.username)) + .identityState(identityState) + .identityKey(identityKey) + .blocked(remote.blocked) + .whitelisted(remote.whitelisted) + .archived(remote.archived) + .markedUnread(remote.markedUnread) + .mutedUntilTimestamp(remote.mutedUntilTimestamp) + .hideStory(remote.hideStory) + .unregisteredAtTimestamp(remote.unregisteredAtTimestamp) + .hidden(remote.hidden) + .pniSignatureVerified(remote.pniSignatureVerified || local.pniSignatureVerified) + .nickname(remote.nickname) + .note(remote.note) + .avatarColor(remote.avatarColor); final var merged = mergedBuilder.build(); final var matchesRemote = doProtosMatch(merged, remote); if (matchesRemote) { - return remote; + return remoteRecord; } final var matchesLocal = doProtosMatch(merged, local); if (matchesLocal) { - return local; + return localRecord; } - return mergedBuilder.setId(KeyUtils.createRawStorageId()).build(); + return new SignalContactRecord(StorageId.forContact(KeyUtils.createRawStorageId()), mergedBuilder.build()); } @Override @@ -233,7 +221,8 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor update) throws SQLException { final var contactRecord = update.newRecord(); - final var address = getRecipientAddress(contactRecord); + final var contactProto = contactRecord.getProto(); + final var address = getRecipientAddress(contactProto); final var recipientId = account.getRecipientStore().resolveRecipientTrusted(connection, address); final var recipient = account.getRecipientStore().getRecipient(connection, recipientId); @@ -253,95 +242,98 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor