diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c1f00b0..1924d640 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ '1.8', '12.0.2' ] + java: [ '11', '14' ] steps: - uses: actions/checkout@v1 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..edf8a5a2 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,60 @@ +name: "CodeQL" + +on: + push: + branches: [master, ] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + schedule: + - cron: '0 7 * * 4' + +jobs: + analyse: + name: Analyse + runs-on: ubuntu-latest + + steps: + + - name: Setup Java JDK + uses: actions/setup-java@v1.3.0 + with: + java-version: 11 + + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + # Override language selection by uncommenting this and choosing your languages + # with: + # languages: go, javascript, csharp, python, cpp, java + + # 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@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 9f37145b..4953eaca 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -4,6 +4,28 @@ diff --git a/README.md b/README.md index b4bed3b8..fb0d9d88 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ signal-cli is primarily intended to be used on servers to notify admins of impor ## Installation -You can [build signal-cli](#building) yourself, or use the [provided binary files](https://github.com/AsamK/signal-cli/releases/latest), which should work on Linux, macOS and Windows. For Arch Linux there is also a [package in AUR](https://aur.archlinux.org/packages/signal-cli/). You need to have at least JRE 7 installed, to run signal-cli. +You can [build signal-cli](#building) yourself, or use the [provided binary files](https://github.com/AsamK/signal-cli/releases/latest), which should work on Linux, macOS and Windows. For Arch Linux there is also a [package in AUR](https://aur.archlinux.org/packages/signal-cli/) and there is a [FreeBSD port](https://www.freshports.org/net-im/signal-cli) available as well. You need to have at least JRE 8 installed, to run signal-cli. ### Install system-wide on Linux See [latest version](https://github.com/AsamK/signal-cli/releases). @@ -18,12 +18,12 @@ sudo tar xf signal-cli-"${VERSION}".tar.gz -C /opt sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/ ``` You can find further instructions on the Wiki: -- [Install on Ubuntu](https://github.com/AsamK/signal-cli/wiki/HowToUbuntu) +- [Quickstart](https://github.com/AsamK/signal-cli/wiki/Quickstart) - [DBus Service](https://github.com/AsamK/signal-cli/wiki/DBus-service) ## Usage -Important: The USERNAME (your phone number) must include the country calling code, i.e. the number must start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list of all country codes. +Important: The USERNAME (your phone number) must include the country calling code, i.e. the number must start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list of all country codes.) * Register a number (with SMS verification) @@ -31,7 +31,7 @@ Important: The USERNAME (your phone number) must include the country calling cod You can register Signal using a land line 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. -* Verify the number using the code received via SMS or voice +* Verify the number using the code received via SMS or voice, optionally add `--pin PIN_CODE` if you've added a pin code to your account signal-cli -u USERNAME verify CODE diff --git a/build.gradle b/build.gradle index a7312bb4..0607c12b 100644 --- a/build.gradle +++ b/build.gradle @@ -2,28 +2,26 @@ apply plugin: 'java' apply plugin: 'application' apply plugin: 'eclipse' -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 +sourceCompatibility = JavaVersion.VERSION_11 +targetCompatibility = JavaVersion.VERSION_11 mainClassName = 'org.asamk.signal.Main' -version = '0.6.5' +version = '0.6.10' compileJava.options.encoding = 'UTF-8' repositories { mavenLocal() - maven { - url "https://raw.github.com/AsamK/maven/master/releases/" - } mavenCentral() } dependencies { - compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_1' - compile 'org.bouncycastle:bcprov-jdk15on:1.64' - compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' - compile 'org.freedesktop.dbus:dbus-java:2.7.0' + implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_14' + implementation 'org.bouncycastle:bcprov-jdk15on:1.66' + implementation 'net.sourceforge.argparse4j:argparse4j:0.8.1' + implementation 'com.github.hypfvieh:dbus-java:3.2.3' + implementation 'org.slf4j:slf4j-nop:1.7.30' } jar { @@ -46,7 +44,6 @@ run { // Find any 3rd party libraries which have released new versions // to the central Maven repo since we last upgraded. -// http://daniel.gredler.net/2011/08/08/gradle-keeping-libraries-up-to-date/ task checkLibVersions { doLast { def checked = [:] @@ -54,20 +51,16 @@ task checkLibVersions { configurations.each { configuration -> configuration.allDependencies.each { dependency -> def version = dependency.version - if (!version.contains('SNAPSHOT') && !checked[dependency]) { + if (!checked[dependency]) { def group = dependency.group def path = group.replace('.', '/') def name = dependency.name def url = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml" try { def metadata = new XmlSlurper().parseText(url.toURL().text) - def versions = metadata.versioning.versions.version.collect { it.text() } - versions.removeAll { it.toLowerCase().contains('alpha') } - versions.removeAll { it.toLowerCase().contains('beta') } - versions.removeAll { it.toLowerCase().contains('rc') } - def newest = versions.max() - if (version != newest) { - println "$group:$name $version -> $newest" + def newest = metadata.versioning.latest; + if ("$version" != "$newest") { + println "UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}" } } catch (FileNotFoundException e) { logger.debug "Unable to download $url: $e.message" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f3d88b1c..e708b1c0 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 1b16c34a..12d38de6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 2fe81a7d..4f906e0c 100755 --- a/gradlew +++ b/gradlew @@ -82,6 +82,7 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -129,6 +130,7 @@ fi if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath diff --git a/gradlew.bat b/gradlew.bat index 24467a14..ac1b06f9 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -51,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -61,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 35a578f4..98a5da2a 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -5,254 +5,272 @@ vim:set ts=4 sw=4 tw=82 noet: = signal-cli (1) -Name ----- +== Name + signal-cli - A commandline and dbus interface for the Signal messenger -Synopsis --------- +== Synopsis + *signal-cli* [--config CONFIG] [-h | -v | -u USERNAME | --dbus | --dbus-system] command [command-options] -Description ------------ +== Description -signal-cli is a commandline interface for libsignal-service-java. It supports -registering, verifying, sending and receiving messages. For registering you need a -phone number where you can receive SMS or incoming calls. -signal-cli was primarily developed to be used on servers to notify admins of -important events. For this use-case, it has a dbus interface, that can be used to -send messages from any programming language that has dbus bindings. +signal-cli is a commandline interface for libsignal-service-java. +It supports registering, verifying, sending and receiving messages. +For registering you need a phone number where you can receive SMS or incoming calls. +signal-cli was primarily developed to be used on servers to notify admins of important events. +For this use-case, it has a dbus interface, that can be used to send messages from any programming language that has dbus bindings. -Options -------- +== Options *-h*, *--help*:: - Show help message and quit. +Show help message and quit. *-v*, *--version*:: - Print the version and quit. +Print the version and quit. *--config* CONFIG:: - Set the path, where to store the config. - Make sure you have full read/write access to the given directory. - (Default: `$XDG_DATA_HOME/signal-cli` (`$HOME/.local/share/signal-cli`)) +Set the path, where to store the config. +Make sure you have full read/write access to the given directory. +(Default: `$XDG_DATA_HOME/signal-cli` (`$HOME/.local/share/signal-cli`)) *-u* USERNAME, *--username* USERNAME:: - Specify your phone number, that will be your identifier. - The phone number must include the country calling code, i.e. the number must - start with a "+" sign. +Specify your phone number, that will be your identifier. +The phone number must include the country calling code, i.e. the number must start with a "+" sign. *--dbus*:: - Make request via user dbus. +Make request via user dbus. *--dbus-system*:: - Make request via system dbus. +Make request via system dbus. -Commands --------- +== Commands -register -~~~~~~~~ -Register a phone number with SMS or voice verification. Use the verify command to -complete the verification. +=== register + +Register a phone number with SMS or voice verification. +Use the verify command to complete the verification. *-v*, *--voice*:: - The verification should be done over voice, not SMS. +The verification should be done over voice, not SMS. + +=== verify -verify -~~~~~~ Verify the number using the code received via SMS or voice. VERIFICATIONCODE:: - The verification code. +The verification code. *-p* PIN, *--pin* PIN:: - The registration lock PIN, that was set by the user. Only required if a PIN was set. +The registration lock PIN, that was set by the user. +Only required if a PIN was set. + +=== unregister -unregister -~~~~~~~~~~ Disable push support for this device, i.e. this device won't receive any more messages. If this is the master device, other users can't send messages to this number anymore. Use "updateAccount" to undo this. To remove a linked device, use "removeDevice" from the master device. -updateAccount -~~~~~~~~~~~~~ +=== updateAccount + Update the account attributes on the signal server. Can fix problems with receiving messages. -setPin -~~~~~~ +=== setPin + Set a registration lock pin, to prevent others from registering this number. REGISTRATION_LOCK_PIN:: - The registration lock PIN, that will be required for new registrations (resets after 7 days of inactivity) +The registration lock PIN, that will be required for new registrations (resets after 7 days of inactivity) + +=== removePin -removePin -~~~~~~~~~ Remove the registration lock pin. -link -~~~~ -Link to an existing device, instead of registering a new number. This shows a -"tsdevice:/…" URI. If you want to connect to another signal-cli instance, you can -just use this URI. If you want to link to an Android/iOS device, create a QR code -with the URI (e.g. with qrencode) and scan that in the Signal app. +=== link + +Link to an existing device, instead of registering a new number. +This shows a "tsdevice:/…" URI. If you want to connect to another signal-cli instance, you can just use this URI. If you want to link to an Android/iOS device, create a QR code with the URI (e.g. with qrencode) and scan that in the Signal app. *-n* NAME, *--name* NAME:: - Optionally specify a name to describe this new device. By default "cli" will - be used. +Optionally specify a name to describe this new device. +By default "cli" will be used. -addDevice -~~~~~~~~~ -Link another device to this device. Only works, if this is the master device. +=== addDevice + +Link another device to this device. +Only works, if this is the master device. *--uri* URI:: - Specify the uri contained in the QR code shown by the new device. +Specify the uri contained in the QR code shown by the new device. You will need the full uri enclosed in quotation marks, such as "tsdevice:/?uuid=....." + +=== listDevices -listDevices -~~~~~~~~~~~ Show a list of connected devices. -removeDevice -~~~~~~~~~~~~ -Remove a connected device. Only works, if this is the master device. +=== removeDevice + +Remove a connected device. +Only works, if this is the master device. *-d* DEVICEID, *--deviceId* DEVICEID:: - Specify the device you want to remove. Use listDevices to see the deviceIds. +Specify the device you want to remove. +Use listDevices to see the deviceIds. + +=== send -send -~~~~ Send a message to another user or group. RECIPIENT:: - Specify the recipients’ phone number. +Specify the recipients’ phone number. *-g* GROUP, *--group* GROUP:: - Specify the recipient group ID in base64 encoding. +Specify the recipient group ID in base64 encoding. *-m* MESSAGE, *--message* MESSAGE:: - Specify the message, if missing, standard input is used. +Specify the message, if missing, standard input is used. *-a* [ATTACHMENT [ATTACHMENT ...]], *--attachment* [ATTACHMENT [ATTACHMENT ...]]:: - Add one or more files as attachment. +Add one or more files as attachment. *-e*, *--endsession*:: - Clear session state and send end session message. +Clear session state and send end session message. -receive -~~~~~~~ -Query the server for new messages. New messages are printed on standardoutput and -attachments are downloaded to the config directory. +=== sendReaction + +Send reaction to a previously received or sent message. + +RECIPIENT:: +Specify the recipients’ phone number. + +*-g* GROUP, *--group* GROUP:: +Specify the recipient group ID in base64 encoding. + +*-e* EMOJI, *--emoji* EMOJI:: +Specify the emoji, should be a single unicode grapheme cluster. + +*-a* NUMBER, *--target-author* NUMBER:: +Specify the number of the author of the message to which to react. + +*-t* TIMESTAMP, *--target-timestamp* TIMESTAMP:: +Specify the timestamp of the message to which to react. + +*-r*, *--remove*:: +Remove a reaction. + +=== receive + +Query the server for new messages. +New messages are printed on standardoutput and attachments are downloaded to the config directory. *-t* TIMEOUT, *--timeout* TIMEOUT:: - Number of seconds to wait for new messages (negative values disable timeout). - Default is 5 seconds. +Number of seconds to wait for new messages (negative values disable timeout). +Default is 5 seconds. *--ignore-attachments*:: - Don’t download attachments of received messages. +Don’t download attachments of received messages. *--json*:: - Output received messages in json format, one object per line. +Output received messages in json format, one object per line. + +=== updateGroup -updateGroup -~~~~~~~~~~~ Create or update a group. *-g* GROUP, *--group* GROUP:: - Specify the recipient group ID in base64 encoding. If not specified, a new - group with a new random ID is generated. +Specify the recipient group ID in base64 encoding. +If not specified, a new group with a new random ID is generated. *-n* NAME, *--name* NAME:: - Specify the new group name. +Specify the new group name. *-a* AVATAR, *--avatar* AVATAR:: - Specify a new group avatar image file. +Specify a new group avatar image file. *-m* [MEMBER [MEMBER ...]], *--member* [MEMBER [MEMBER ...]]:: - Specify one or more members to add to the group. +Specify one or more members to add to the group. + +=== quitGroup -quitGroup -~~~~~~~~~ Send a quit group message to all group members and remove self from member list. *-g* GROUP, *--group* GROUP:: - Specify the recipient group ID in base64 encoding. +Specify the recipient group ID in base64 encoding. + +=== listGroups -listGroups -~~~~~~~~~~~ Show a list of known groups. *-d*, *--detailed*:: - Include the list of members of each group. +Include the list of members of each group. -listIdentities -~~~~~~~~~~~~~~ -List all known identity keys and their trust status, fingerprint and safety -number. +=== listIdentities + +List all known identity keys and their trust status, fingerprint and safety number. *-n* NUMBER, *--number* NUMBER:: - Only show identity keys for the given phone number. +Only show identity keys for the given phone number. -trust -~~~~~ -Set the trust level of a given number. The first time a key for a number is seen, -it is trusted by default (TOFU). If the key changes, the new key must be trusted -manually. +=== trust + +Set the trust level of a given number. +The first time a key for a number is seen, it is trusted by default (TOFU). +If the key changes, the new key must be trusted manually. number:: - Specify the phone number, for which to set the trust. +Specify the phone number, for which to set the trust. *-a*, *--trust-all-known-keys*:: - Trust all known keys of this user, only use this for testing. +Trust all known keys of this user, only use this for testing. -*-v* VERIFIED_FINGERPRINT, *--verified-fingerprint* VERIFIED_FINGERPRINT:: - Specify the safety number or fingerprint of the key, only use this option if you have verified - the fingerprint. +*-v* VERIFIED_SAFETY_NUMBER, *--verified-safety-number* VERIFIED_SAFETY_NUMBER:: +Specify the safety number of the key, only use this option if you have verified the safety number. -updateProfile -~~~~~~~~~~~~~ -Update the name and/or avatar image visible by message recipients for the current users. -The profile is stored encrypted on the Signal servers. The decryption key is sent -with every outgoing messages (excluding group messages). +=== updateProfile + +Update the name and avatar image visible by message recipients for the current users. +The profile is stored encrypted on the Signal servers. +The decryption key is sent with every outgoing messages (excluding group messages). *--name*:: - New name visible by message recipients. +New name visible by message recipients. *--avatar*:: - Path to the new avatar visible by message recipients. +Path to the new avatar visible by message recipients. *--remove-avatar*:: - Remove the avatar visible by message recipients. +Remove the avatar visible by message recipients. -updateContact -~~~~~~~~~~~~~ -Update the info associated to a number on our contact list. This change is only -local but can be synchronized to other devices by using `sendContacts` (see -below). +=== updateContact + +Update the info associated to a number on our contact list. +This change is only local but can be synchronized to other devices by using `sendContacts` (see below). If the contact doesn't exist yet, it will be added. NUMBER:: - Specify the contact phone number. +Specify the contact phone number. *-n*, *--name*:: - Specify the new name for this contact. +Specify the new name for this contact. -block -~~~~~ -Block the given contacts or groups (no messages will be received). This change is only -local but can be synchronized to other devices by using `sendContacts` (see -below). +*-e*, *--expiration*:: +Set expiration time of messages (seconds). +To disable expiration set expiration time to 0. + +=== block + +Block the given contacts or groups (no messages will be received). +This change is only local but can be synchronized to other devices by using `sendContacts` (see below). [CONTACT [CONTACT ...]]:: - Specify the phone numbers of contacts that should be blocked. +Specify the phone numbers of contacts that should be blocked. *-g* [GROUP [GROUP ...]], *--group* [GROUP [GROUP ...]]:: - Specify the group IDs that should be blocked in base64 encoding. +Specify the group IDs that should be blocked in base64 encoding. -unblock -~~~~~~~ -Unblock the given contacts or groups (messages will be received again). This change is only -local but can be synchronized to other devices by using `sendContacts` (see -below). +=== unblock + +Unblock the given contacts or groups (messages will be received again). +This change is only local but can be synchronized to other devices by using `sendContacts` (see below). [CONTACT [CONTACT ...]]:: Specify the phone numbers of contacts that should be unblocked. @@ -260,60 +278,82 @@ Specify the phone numbers of contacts that should be unblocked. *-g* [GROUP [GROUP ...]], *--group* [GROUP [GROUP ...]]:: Specify the group IDs that should be unblocked in base64 encoding. -sendContacts -~~~~~~~~~~~~ +=== sendContacts + Send a synchronization message with the local contacts list to all linked devices. This command should only be used if this is the master device. -daemon -~~~~~~ -signal-cli can run in daemon mode and provides an experimental dbus interface. For -dbus support you need jni/unix-java.so installed on your system (Debian: -libunixsocket-java ArchLinux: libmatthew-unix-java (AUR)). +=== uploadStickerPack + +Upload a new sticker pack, consisting of a manifest file and the stickers in WebP format (maximum size for a sticker file is 100KiB). +The required manifest.json has the following format: + +[source,json] +---- +{ + "title": "", + "author": "", + "cover": { // Optional cover, by default the first sticker is used as cover + "file": "", + "emoji": "" + }, + "stickers": [ + { + "file": "", + "emoji": "" + } + ... + ] +} +---- + +PATH:: +The path of the manifest.json or a zip file containing the sticker pack you wish to upload. + +=== daemon + +signal-cli can run in daemon mode and provides an experimental dbus interface. *--system*:: - Use DBus system bus instead of user bus. +Use DBus system bus instead of user bus. *--ignore-attachments*:: - Don’t download attachments of received messages. +Don’t download attachments of received messages. - -Examples --------- +== Examples Register a number (with SMS verification):: - signal-cli -u USERNAME register +signal-cli -u USERNAME register Verify the number using the code received via SMS or voice:: - signal-cli -u USERNAME verify CODE +signal-cli -u USERNAME verify CODE Send a message to one or more recipients:: - signal-cli -u USERNAME send -m "This is a message" [RECIPIENT [RECIPIENT ...]] [-a [ATTACHMENT [ATTACHMENT ...]]] +signal-cli -u USERNAME send -m "This is a message" [RECIPIENT [RECIPIENT ...]] [-a [ATTACHMENT [ATTACHMENT ...]]] Pipe the message content from another process:: - uname -a | signal-cli -u USERNAME send [RECIPIENT [RECIPIENT ...]] +uname -a | signal-cli -u USERNAME send [RECIPIENT [RECIPIENT ...]] Create a group:: - signal-cli -u USERNAME updateGroup -n "Group name" -m [MEMBER [MEMBER ...]] +signal-cli -u USERNAME updateGroup -n "Group name" -m [MEMBER [MEMBER ...]] Add member to a group:: - signal-cli -u USERNAME updateGroup -g GROUP_ID -m "NEW_MEMBER" +signal-cli -u USERNAME updateGroup -g GROUP_ID -m "NEW_MEMBER" Leave a group:: - signal-cli -u USERNAME quitGroup -g GROUP_ID +signal-cli -u USERNAME quitGroup -g GROUP_ID Send a message to a group:: - signal-cli -u USERNAME send -m "This is a message" -g GROUP_ID +signal-cli -u USERNAME send -m "This is a message" -g GROUP_ID Trust new key, after having verified it:: - signal-cli -u USERNAME trust -v FINGER_PRINT NUMBER +signal-cli -u USERNAME trust -v SAFETY_NUMBER NUMBER Trust new key, without having verified it. Only use this if you don't care about security:: - signal-cli -u USERNAME trust -a NUMBER +signal-cli -u USERNAME trust -a NUMBER -Files ------ -The password and cryptographic keys are created when registering and stored in the -current users home directory, the directory can be changed with *--config*: +== Files + +The password and cryptographic keys are created when registering and stored in the current users home directory, the directory can be changed with *--config*: `$XDG_DATA_HOME/signal-cli/` (`$HOME/.local/share/signal-cli/`) @@ -323,10 +363,8 @@ For legacy users, the old config directories are used as a fallback: $HOME/.config/textsecure/ +== Authors -Authors -------- - -Maintained by AsamK , who is assisted by other open -source contributors. For more information about signal-cli development, see +Maintained by AsamK , who is assisted by other open source contributors. +For more information about signal-cli development, see . diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 8c9c525f..a93d6d86 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -1,33 +1,33 @@ package org.asamk; -import org.asamk.signal.AttachmentInvalidException; -import org.asamk.signal.GroupNotFoundException; -import org.freedesktop.dbus.DBusInterface; -import org.freedesktop.dbus.DBusSignal; import org.freedesktop.dbus.exceptions.DBusException; -import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; -import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.freedesktop.dbus.interfaces.DBusInterface; +import org.freedesktop.dbus.messages.DBusSignal; -import java.io.IOException; import java.util.List; +/** + * DBus interface for the org.asamk.Signal service. + * Including emitted Signals and returned Errors. + */ public interface Signal extends DBusInterface { - void sendMessage(String message, List attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException; + long sendMessage(String message, List attachments, String recipient) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber; - void sendMessage(String message, List attachments, List recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException; + long sendMessage(String message, List attachments, List recipients) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UnregisteredUser, Error.UntrustedIdentity; - void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions; + void sendEndSessionMessage(List recipients) throws Error.Failure, Error.InvalidNumber, Error.UnregisteredUser, Error.UntrustedIdentity; - void sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException; + long sendGroupMessage(String message, List attachments, byte[] groupId) throws Error.GroupNotFound, Error.Failure, Error.AttachmentInvalid, Error.UnregisteredUser, Error.UntrustedIdentity; - String getContactName(String number) throws InvalidNumberException; + String getContactName(String number) throws Error.InvalidNumber; - void setContactName(String number, String name) throws InvalidNumberException; + void setContactName(String number, String name) throws Error.InvalidNumber; - void setContactBlocked(String number, boolean blocked) throws InvalidNumberException; + void setContactBlocked(String number, boolean blocked) throws Error.InvalidNumber; - void setGroupBlocked(byte[] groupId, boolean blocked) throws GroupNotFoundException; + void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound; List getGroupIds(); @@ -35,17 +35,17 @@ public interface Signal extends DBusInterface { List getGroupMembers(byte[] groupId); - byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException; + byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.UnregisteredUser, Error.UntrustedIdentity; boolean isRegistered(); class MessageReceived extends DBusSignal { - private long timestamp; - private String sender; - private byte[] groupId; - private String message; - private List attachments; + private final long timestamp; + private final String sender; + private final byte[] groupId; + private final String message; + private final List attachments; public MessageReceived(String objectpath, long timestamp, String sender, byte[] groupId, String message, List attachments) throws DBusException { super(objectpath, timestamp, sender, groupId, message, attachments); @@ -79,8 +79,8 @@ public interface Signal extends DBusInterface { class ReceiptReceived extends DBusSignal { - private long timestamp; - private String sender; + private final long timestamp; + private final String sender; public ReceiptReceived(String objectpath, long timestamp, String sender) throws DBusException { super(objectpath, timestamp, sender); @@ -96,4 +96,93 @@ public interface Signal extends DBusInterface { return sender; } } + + class SyncMessageReceived extends DBusSignal { + + private final long timestamp; + private final String source; + private final String destination; + private final byte[] groupId; + private final String message; + private final List attachments; + + public SyncMessageReceived(String objectpath, long timestamp, String source, String destination, byte[] groupId, String message, List attachments) throws DBusException { + super(objectpath, timestamp, source, destination, groupId, message, attachments); + this.timestamp = timestamp; + this.source = source; + this.destination = destination; + this.groupId = groupId; + this.message = message; + this.attachments = attachments; + } + + public long getTimestamp() { + return timestamp; + } + + public String getSource() { + return source; + } + + public String getDestination() { + return destination; + } + + public byte[] getGroupId() { + return groupId; + } + + public String getMessage() { + return message; + } + + public List getAttachments() { + return attachments; + } + } + + interface Error { + + class AttachmentInvalid extends DBusExecutionException { + + public AttachmentInvalid(final String message) { + super(message); + } + } + + class Failure extends DBusExecutionException { + + public Failure(final String message) { + super(message); + } + } + + class GroupNotFound extends DBusExecutionException { + + public GroupNotFound(final String message) { + super(message); + } + } + + class InvalidNumber extends DBusExecutionException { + + public InvalidNumber(final String message) { + super(message); + } + } + + class UnregisteredUser extends DBusExecutionException { + + public UnregisteredUser(final String message) { + super(message); + } + } + + class UntrustedIdentity extends DBusExecutionException { + + public UntrustedIdentity(final String message) { + super(message); + } + } + } } diff --git a/src/main/java/org/asamk/signal/BaseConfig.java b/src/main/java/org/asamk/signal/BaseConfig.java new file mode 100644 index 00000000..afafc7d7 --- /dev/null +++ b/src/main/java/org/asamk/signal/BaseConfig.java @@ -0,0 +1,12 @@ +package org.asamk.signal; + +public class BaseConfig { + + public final static String PROJECT_NAME = BaseConfig.class.getPackage().getImplementationTitle(); + public final static String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion(); + + final static String USER_AGENT = PROJECT_NAME == null ? "signal-cli" : PROJECT_NAME + " " + PROJECT_VERSION; + + private BaseConfig() { + } +} diff --git a/src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java index cebabc18..8fb11a59 100644 --- a/src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java @@ -1,7 +1,7 @@ package org.asamk.signal; import org.asamk.signal.manager.Manager; -import org.freedesktop.dbus.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnection; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; diff --git a/src/main/java/org/asamk/signal/JsonDataMessage.java b/src/main/java/org/asamk/signal/JsonDataMessage.java deleted file mode 100644 index 34f6249e..00000000 --- a/src/main/java/org/asamk/signal/JsonDataMessage.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.asamk.signal; - -import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; -import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; - -import java.util.ArrayList; -import java.util.List; - -class JsonDataMessage { - - long timestamp; - String message; - int expiresInSeconds; - List attachments; - JsonGroupInfo groupInfo; - - JsonDataMessage(SignalServiceDataMessage dataMessage) { - this.timestamp = dataMessage.getTimestamp(); - if (dataMessage.getGroupInfo().isPresent()) { - this.groupInfo = new JsonGroupInfo(dataMessage.getGroupInfo().get()); - } - if (dataMessage.getBody().isPresent()) { - this.message = dataMessage.getBody().get(); - } - this.expiresInSeconds = dataMessage.getExpiresInSeconds(); - if (dataMessage.getAttachments().isPresent()) { - this.attachments = new ArrayList<>(dataMessage.getAttachments().get().size()); - for (SignalServiceAttachment attachment : dataMessage.getAttachments().get()) { - this.attachments.add(new JsonAttachment(attachment)); - } - } else { - this.attachments = new ArrayList<>(); - } - } -} diff --git a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java index 6b26ea0e..0728b871 100644 --- a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java @@ -2,13 +2,16 @@ package org.asamk.signal; import org.asamk.Signal; import org.asamk.signal.manager.Manager; -import org.freedesktop.dbus.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import java.util.ArrayList; import java.util.List; @@ -28,7 +31,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { static void sendReceivedMessageToDbus(SignalServiceEnvelope envelope, SignalServiceContent content, DBusConnection conn, final String objectPath, Manager m) { if (envelope.isReceipt()) { try { - conn.sendSignal(new Signal.ReceiptReceived( + conn.sendMessage(new Signal.ReceiptReceived( objectPath, envelope.getTimestamp(), !envelope.isUnidentifiedSender() && envelope.hasSource() ? envelope.getSourceE164().get() : content.getSender().getNumber().get() @@ -36,36 +39,81 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { } catch (DBusException e) { e.printStackTrace(); } - } else if (content != null && content.getDataMessage().isPresent()) { - SignalServiceDataMessage message = content.getDataMessage().get(); - - if (!message.isEndSession() && - !(message.getGroupInfo().isPresent() && - message.getGroupInfo().get().getType() != SignalServiceGroup.Type.DELIVER)) { - List attachments = new ArrayList<>(); - if (message.getAttachments().isPresent()) { - for (SignalServiceAttachment attachment : message.getAttachments().get()) { - if (attachment.isPointer()) { - attachments.add(m.getAttachmentFile(attachment.asPointer().getId()).getAbsolutePath()); + } else if (content != null) { + if (content.getReceiptMessage().isPresent()) { + final SignalServiceReceiptMessage receiptMessage = content.getReceiptMessage().get(); + if (receiptMessage.isDeliveryReceipt()) { + final String sender = !envelope.isUnidentifiedSender() && envelope.hasSource() ? envelope.getSourceE164().get() : content.getSender().getNumber().get(); + for (long timestamp : receiptMessage.getTimestamps()) { + try { + conn.sendMessage(new Signal.ReceiptReceived( + objectPath, + timestamp, + sender + )); + } catch (DBusException e) { + e.printStackTrace(); } } } + } else if (content.getDataMessage().isPresent()) { + SignalServiceDataMessage message = content.getDataMessage().get(); - try { - conn.sendSignal(new Signal.MessageReceived( - objectPath, - message.getTimestamp(), - envelope.isUnidentifiedSender() || !envelope.hasSource() ? content.getSender().getNumber().get() : envelope.getSourceE164().get(), - message.getGroupInfo().isPresent() ? message.getGroupInfo().get().getGroupId() : new byte[0], - message.getBody().isPresent() ? message.getBody().get() : "", - attachments)); - } catch (DBusException e) { - e.printStackTrace(); + if (!message.isEndSession() && + !(message.getGroupContext().isPresent() && + message.getGroupContext().get().getGroupV1Type() != SignalServiceGroup.Type.DELIVER)) { + try { + conn.sendMessage(new Signal.MessageReceived( + objectPath, + message.getTimestamp(), + envelope.isUnidentifiedSender() || !envelope.hasSource() ? content.getSender().getNumber().get() : envelope.getSourceE164().get(), + message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent() + ? message.getGroupContext().get().getGroupV1().get().getGroupId() : new byte[0], + message.getBody().isPresent() ? message.getBody().get() : "", + JsonDbusReceiveMessageHandler.getAttachments(message, m))); + } catch (DBusException e) { + e.printStackTrace(); + } + } + } else if (content.getSyncMessage().isPresent()) { + SignalServiceSyncMessage sync_message = content.getSyncMessage().get(); + if (sync_message.getSent().isPresent()) { + SentTranscriptMessage transcript = sync_message.getSent().get(); + + if (!envelope.isUnidentifiedSender() && envelope.hasSource() && (transcript.getDestination().isPresent() || transcript.getMessage().getGroupContext().isPresent())) { + SignalServiceDataMessage message = transcript.getMessage(); + + try { + conn.sendMessage(new Signal.SyncMessageReceived( + objectPath, + transcript.getTimestamp(), + envelope.getSourceAddress().getNumber().get(), + transcript.getDestination().isPresent() ? transcript.getDestination().get().getNumber().get() : "", + message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent() + ? message.getGroupContext().get().getGroupV1().get().getGroupId() : new byte[0], + message.getBody().isPresent() ? message.getBody().get() : "", + JsonDbusReceiveMessageHandler.getAttachments(message, m))); + } catch (DBusException e) { + e.printStackTrace(); + } + } } } } } + static private List getAttachments(SignalServiceDataMessage message, Manager m) { + List attachments = new ArrayList<>(); + if (message.getAttachments().isPresent()) { + for (SignalServiceAttachment attachment : message.getAttachments().get()) { + if (attachment.isPointer()) { + attachments.add(m.getAttachmentFile(attachment.asPointer().getRemoteId()).getAbsolutePath()); + } + } + } + return attachments; + } + @Override public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { super.handleMessage(envelope, content, exception); diff --git a/src/main/java/org/asamk/signal/JsonError.java b/src/main/java/org/asamk/signal/JsonError.java deleted file mode 100644 index 5ef2cd7c..00000000 --- a/src/main/java/org/asamk/signal/JsonError.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.asamk.signal; - -class JsonError { - - String message; - - JsonError(Throwable exception) { - this.message = exception.getMessage(); - } -} diff --git a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java index 1aea2327..dfe51fe7 100644 --- a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java @@ -5,9 +5,10 @@ import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.asamk.signal.json.JsonError; +import org.asamk.signal.json.JsonMessageEnvelope; import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; @@ -23,7 +24,6 @@ public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler this.m = m; this.jsonProcessor = new ObjectMapper(); jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect - jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES); jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); } diff --git a/src/main/java/org/asamk/signal/JsonSyncMessage.java b/src/main/java/org/asamk/signal/JsonSyncMessage.java deleted file mode 100644 index a6ecb459..00000000 --- a/src/main/java/org/asamk/signal/JsonSyncMessage.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.asamk.signal; - -import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; -import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; - - -import java.util.ArrayList; -import java.util.List; - -enum JsonSyncMessageType { - CONTACTS_SYNC, - GROUPS_SYNC, - REQUEST_SYNC -} - -class JsonSyncMessage { - - JsonSyncDataMessage sentMessage; - List blockedNumbers; - List readMessages; - JsonSyncMessageType type; - - JsonSyncMessage(SignalServiceSyncMessage syncMessage) { - if (syncMessage.getSent().isPresent()) { - this.sentMessage = new JsonSyncDataMessage(syncMessage.getSent().get()); - } - if (syncMessage.getBlockedList().isPresent()) { - this.blockedNumbers = new ArrayList<>(syncMessage.getBlockedList().get().getAddresses().size()); - for (SignalServiceAddress address : syncMessage.getBlockedList().get().getAddresses()) { - this.blockedNumbers.add(address.getNumber().get()); - } - } - if (syncMessage.getRead().isPresent()) { - this.readMessages = syncMessage.getRead().get(); - } - - if (syncMessage.getContacts().isPresent()) { - this.type = JsonSyncMessageType.CONTACTS_SYNC; - } else if (syncMessage.getGroups().isPresent()) { - this.type = JsonSyncMessageType.GROUPS_SYNC; - } else if (syncMessage.getRequest().isPresent()) { - this.type = JsonSyncMessageType.REQUEST_SYNC; - } - } -} diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 6ad3aee2..0e42e20d 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -31,16 +31,22 @@ import org.asamk.signal.commands.Commands; import org.asamk.signal.commands.DbusCommand; import org.asamk.signal.commands.ExtendedDbusCommand; import org.asamk.signal.commands.LocalCommand; -import org.asamk.signal.manager.BaseConfig; +import org.asamk.signal.commands.ProvisioningCommand; +import org.asamk.signal.dbus.DbusSignalImpl; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.ProvisioningManager; +import org.asamk.signal.manager.ServiceConfig; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.SecurityProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.freedesktop.dbus.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; +import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import java.io.File; +import java.io.IOException; import java.security.Security; import java.util.Map; @@ -68,82 +74,124 @@ public class Main { private static int handleCommands(Namespace ns) { final String username = ns.getString("username"); - Manager m; - Signal ts; - DBusConnection dBusConn = null; - try { - if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) { - try { - m = null; - int busType; - if (ns.getBoolean("dbus_system")) { - busType = DBusConnection.SYSTEM; - } else { - busType = DBusConnection.SESSION; - } - dBusConn = DBusConnection.getConnection(busType); - ts = dBusConn.getRemoteObject( + + if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) { + try { + DBusConnection.DBusBusType busType; + if (ns.getBoolean("dbus_system")) { + busType = DBusConnection.DBusBusType.SYSTEM; + } else { + busType = DBusConnection.DBusBusType.SESSION; + } + try (DBusConnection dBusConn = DBusConnection.getConnection(busType)) { + Signal ts = dBusConn.getRemoteObject( DbusConfig.SIGNAL_BUSNAME, DbusConfig.SIGNAL_OBJECTPATH, Signal.class); - } catch (UnsatisfiedLinkError e) { - System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); - return 1; - } catch (DBusException e) { - e.printStackTrace(); - if (dBusConn != null) { - dBusConn.disconnect(); - } - return 3; - } - } else { - String dataPath = ns.getString("config"); - if (isEmpty(dataPath)) { - dataPath = getDefaultDataPath(); - } - m = new Manager(username, dataPath); - ts = m; + return handleCommands(ns, ts, dBusConn); + } + } catch (UnsatisfiedLinkError e) { + System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); + return 1; + } catch (DBusException | IOException e) { + e.printStackTrace(); + return 3; + } + } else { + String dataPath = ns.getString("config"); + if (isEmpty(dataPath)) { + dataPath = getDefaultDataPath(); + } + + final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration(BaseConfig.USER_AGENT); + + if (username == null) { + ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT); + return handleCommands(ns, pm); + } + + Manager manager; + try { + manager = Manager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT); + } catch (Throwable e) { + System.err.println("Error loading state file: " + e.getMessage()); + return 2; + } + + try (Manager m = manager) { try { - m.init(); - } catch (Exception e) { - System.err.println("Error loading state file: " + e.getMessage()); + m.checkAccountState(); + } catch (AuthorizationFailedException e) { + if (!"register".equals(ns.getString("command"))) { + // Register command should still be possible, if current authorization fails + System.err.println("Authorization failed, was the number registered elsewhere?"); + return 2; + } + } catch (IOException e) { + System.err.println("Error while checking account: " + e.getMessage()); return 2; } - } - String commandKey = ns.getString("command"); - final Map commands = Commands.getCommands(); - if (commands.containsKey(commandKey)) { - Command command = commands.get(commandKey); - - if (dBusConn != null) { - if (command instanceof ExtendedDbusCommand) { - return ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn); - } else if (command instanceof DbusCommand) { - return ((DbusCommand) command).handleCommand(ns, ts); - } else { - System.err.println(commandKey + " is not yet implemented via dbus"); - return 1; - } - } else { - if (command instanceof LocalCommand) { - return ((LocalCommand) command).handleCommand(ns, m); - } else if (command instanceof DbusCommand) { - return ((DbusCommand) command).handleCommand(ns, ts); - } else { - System.err.println(commandKey + " is only works via dbus"); - return 1; - } - } - } - return 0; - } finally { - if (dBusConn != null) { - dBusConn.disconnect(); + return handleCommands(ns, m); + } catch (IOException e) { + e.printStackTrace(); + return 3; } } } + private static int handleCommands(Namespace ns, Signal ts, DBusConnection dBusConn) { + String commandKey = ns.getString("command"); + final Map commands = Commands.getCommands(); + if (commands.containsKey(commandKey)) { + Command command = commands.get(commandKey); + + if (command instanceof ExtendedDbusCommand) { + return ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn); + } else if (command instanceof DbusCommand) { + return ((DbusCommand) command).handleCommand(ns, ts); + } else { + System.err.println(commandKey + " is not yet implemented via dbus"); + return 1; + } + } + return 0; + } + + private static int handleCommands(Namespace ns, ProvisioningManager pm) { + String commandKey = ns.getString("command"); + final Map commands = Commands.getCommands(); + if (commands.containsKey(commandKey)) { + Command command = commands.get(commandKey); + + if (command instanceof ProvisioningCommand) { + return ((ProvisioningCommand) command).handleCommand(ns, pm); + } else { + System.err.println(commandKey + " only works with a username"); + return 1; + } + } + return 0; + } + + private static int handleCommands(Namespace ns, Manager m) { + String commandKey = ns.getString("command"); + final Map commands = Commands.getCommands(); + if (commands.containsKey(commandKey)) { + Command command = commands.get(commandKey); + + if (command instanceof LocalCommand) { + return ((LocalCommand) command).handleCommand(ns, m); + } else if (command instanceof DbusCommand) { + return ((DbusCommand) command).handleCommand(ns, new DbusSignalImpl(m)); + } else if (command instanceof ExtendedDbusCommand) { + System.err.println(commandKey + " only works via dbus"); + } + return 1; + } + return 0; + } + /** * Uses $XDG_DATA_HOME/signal-cli if it exists, or if none of the legacy directories exist: * - $HOME/.config/signal diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 4cb09440..9a75aa1c 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -76,7 +76,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { if (content == null) { System.out.println("Failed to decrypt message."); } else { - System.out.println(String.format("Sender: %s (device: %d)", content.getSender().getNumber().get(), content.getSenderDevice())); + ContactInfo sourceContact = m.getContact(content.getSender().getNumber().get()); + System.out.println(String.format("Sender: %s (device: %d)", (sourceContact == null ? "" : "“" + sourceContact.name + "” ") + content.getSender().getNumber().get(), content.getSenderDevice())); if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); handleSignalServiceDataMessage(message); @@ -102,7 +103,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { System.out.println("Received sync read messages list"); for (ReadMessage rm : syncMessage.getRead().get()) { ContactInfo fromContact = m.getContact(rm.getSender().getNumber().get()); - System.out.println("From: " + (fromContact == null ? "" : "“" + fromContact.name + "” ") + rm.getSender().getNumber() + " Message timestamp: " + DateUtils.formatTimestamp(rm.getTimestamp())); + System.out.println("From: " + (fromContact == null ? "" : "“" + fromContact.name + "” ") + rm.getSender().getNumber().get() + " Message timestamp: " + DateUtils.formatTimestamp(rm.getTimestamp())); } } if (syncMessage.getRequest().isPresent()) { @@ -113,6 +114,15 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { if (syncMessage.getRequest().get().isGroupsRequest()) { System.out.println(" - groups request"); } + if (syncMessage.getRequest().get().isBlockedListRequest()) { + System.out.println(" - blocked list request"); + } + if (syncMessage.getRequest().get().isConfigurationRequest()) { + System.out.println(" - configuration request"); + } + if (syncMessage.getRequest().get().isKeysRequest()) { + System.out.println(" - keys request"); + } } if (syncMessage.getSent().isPresent()) { System.out.println("Received sync sent message"); @@ -122,6 +132,13 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { String dest = sentTranscriptMessage.getDestination().get().getNumber().get(); ContactInfo destContact = m.getContact(dest); to = (destContact == null ? "" : "“" + destContact.name + "” ") + dest; + } else if (sentTranscriptMessage.getRecipients().size() > 0) { + StringBuilder toBuilder = new StringBuilder(); + for (SignalServiceAddress dest : sentTranscriptMessage.getRecipients()) { + ContactInfo destContact = m.getContact(dest.getNumber().get()); + toBuilder.append(destContact == null ? "" : "“" + destContact.name + "” ").append(dest.getNumber().get()).append(" "); + } + to = toBuilder.toString(); } else { to = "Unknown"; } @@ -137,14 +154,14 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { System.out.println("Blocked numbers:"); final BlockedListMessage blockedList = syncMessage.getBlockedList().get(); for (SignalServiceAddress address : blockedList.getAddresses()) { - System.out.println(" - " + address.getNumber()); + System.out.println(" - " + address.getNumber().get()); } } if (syncMessage.getVerified().isPresent()) { System.out.println("Received sync message with verified identities:"); final VerifiedMessage verifiedMessage = syncMessage.getVerified().get(); System.out.println(" - " + verifiedMessage.getDestination() + ": " + verifiedMessage.getVerified()); - String safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination().getNumber().get(), verifiedMessage.getIdentityKey())); + String safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey())); System.out.println(" " + safetyNumber); } if (syncMessage.getConfiguration().isPresent()) { @@ -161,7 +178,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { if (syncMessage.getViewOnceOpen().isPresent()) { final ViewOnceOpenMessage viewOnceOpenMessage = syncMessage.getViewOnceOpen().get(); System.out.println("Received sync message with view once open message:"); - System.out.println(" - Sender:" + viewOnceOpenMessage.getSender().getNumber()); + System.out.println(" - Sender:" + viewOnceOpenMessage.getSender().getNumber().get()); System.out.println(" - Timestamp:" + viewOnceOpenMessage.getTimestamp()); } if (syncMessage.getStickerPackOperations().isPresent()) { @@ -183,7 +200,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { SignalServiceCallMessage callMessage = content.getCallMessage().get(); if (callMessage.getAnswerMessage().isPresent()) { AnswerMessage answerMessage = callMessage.getAnswerMessage().get(); - System.out.println("Answer message: " + answerMessage.getId() + ": " + answerMessage.getDescription()); + System.out.println("Answer message: " + answerMessage.getId() + ": " + answerMessage.getSdp()); } if (callMessage.getBusyMessage().isPresent()) { BusyMessage busyMessage = callMessage.getBusyMessage().get(); @@ -201,7 +218,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { } if (callMessage.getOfferMessage().isPresent()) { OfferMessage offerMessage = callMessage.getOfferMessage().get(); - System.out.println("Offer message: " + offerMessage.getId() + ": " + offerMessage.getDescription()); + System.out.println("Offer message: " + offerMessage.getId() + ": " + offerMessage.getSdp()); } } if (content.getReceiptMessage().isPresent()) { @@ -246,8 +263,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { if (message.getBody().isPresent()) { System.out.println("Body: " + message.getBody().get()); } - if (message.getGroupInfo().isPresent()) { - SignalServiceGroup groupInfo = message.getGroupInfo().get(); + if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) { + SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); System.out.println("Group info:"); System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId())); if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) { @@ -311,10 +328,19 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { System.out.println("Profile key update, key length:" + message.getProfileKey().get().length); } + if (message.getReaction().isPresent()) { + final SignalServiceDataMessage.Reaction reaction = message.getReaction().get(); + System.out.println("Reaction:"); + System.out.println(" - Emoji: " + reaction.getEmoji()); + System.out.println(" - Target author: " + reaction.getTargetAuthor().getNumber().get()); + System.out.println(" - Target timestamp: " + reaction.getTargetSentTimestamp()); + System.out.println(" - Is remove: " + reaction.isRemove()); + } + if (message.getQuote().isPresent()) { SignalServiceDataMessage.Quote quote = message.getQuote().get(); System.out.println("Quote: (" + quote.getId() + ")"); - System.out.println(" Author: " + quote.getAuthor().getNumber()); + System.out.println(" Author: " + quote.getAuthor().getNumber().get()); System.out.println(" Text: " + quote.getText()); if (quote.getAttachments().size() > 0) { System.out.println(" Attachments: "); @@ -341,12 +367,12 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")"); if (attachment.isPointer()) { final SignalServiceAttachmentPointer pointer = attachment.asPointer(); - System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length); + System.out.println(" Id: " + pointer.getRemoteId() + " Key length: " + pointer.getKey().length); System.out.println(" Filename: " + (pointer.getFileName().isPresent() ? pointer.getFileName().get() : "-")); System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : "")); System.out.println(" Voice note: " + (pointer.getVoiceNote() ? "yes" : "no")); System.out.println(" Dimensions: " + pointer.getWidth() + "x" + pointer.getHeight()); - File file = m.getAttachmentFile(pointer.getId()); + File file = m.getAttachmentFile(pointer.getRemoteId()); if (file.exists()) { System.out.println(" Stored plaintext in: " + file); } diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index a49fc798..05f5c9ce 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -2,9 +2,10 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.GroupIdFormatException; -import org.asamk.signal.GroupNotFoundException; + +import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.util.InvalidNumberException; diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index 1ad0987a..183b40a0 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -22,6 +22,7 @@ public class Commands { addCommand("removeDevice", new RemoveDeviceCommand()); addCommand("removePin", new RemovePinCommand()); addCommand("send", new SendCommand()); + addCommand("sendReaction", new SendReactionCommand()); addCommand("sendContacts", new SendContactsCommand()); addCommand("updateContact", new UpdateContactCommand()); addCommand("setPin", new SetPinCommand()); @@ -32,6 +33,7 @@ public class Commands { addCommand("updateGroup", new UpdateGroupCommand()); addCommand("updateProfile", new UpdateProfileCommand()); addCommand("verify", new VerifyCommand()); + addCommand("uploadStickerPack", new UploadStickerPackCommand()); } public static Map getCommands() { diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 6a3247d5..5cdb8ba1 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -6,8 +6,9 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.DbusReceiveMessageHandler; import org.asamk.signal.JsonDbusReceiveMessageHandler; +import org.asamk.signal.dbus.DbusSignalImpl; import org.asamk.signal.manager.Manager; -import org.freedesktop.dbus.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import java.io.IOException; @@ -41,15 +42,15 @@ public class DaemonCommand implements LocalCommand { DBusConnection conn = null; try { try { - int busType; + DBusConnection.DBusBusType busType; String busName; if (ns.getBoolean("system")) { - busType = DBusConnection.SYSTEM; + busType = DBusConnection.DBusBusType.SYSTEM; } else { - busType = DBusConnection.SESSION; + busType = DBusConnection.DBusBusType.SESSION; } conn = DBusConnection.getConnection(busType); - conn.exportObject(SIGNAL_OBJECTPATH, m); + conn.exportObject(SIGNAL_OBJECTPATH, new DbusSignalImpl(m)); busName = ns.getString("busname"); if (busName == null) { conn.requestBusName(SIGNAL_BUSNAME); diff --git a/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java b/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java index b7f70dee..f9cd9de8 100644 --- a/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java +++ b/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java @@ -3,7 +3,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import org.asamk.Signal; -import org.freedesktop.dbus.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnection; public interface ExtendedDbusCommand extends Command { diff --git a/src/main/java/org/asamk/signal/commands/LinkCommand.java b/src/main/java/org/asamk/signal/commands/LinkCommand.java index 2a2d4c4b..45f59082 100644 --- a/src/main/java/org/asamk/signal/commands/LinkCommand.java +++ b/src/main/java/org/asamk/signal/commands/LinkCommand.java @@ -3,8 +3,8 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.UserAlreadyExists; -import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.ProvisioningManager; +import org.asamk.signal.manager.UserAlreadyExists; import org.whispersystems.libsignal.InvalidKeyException; import java.io.IOException; @@ -12,7 +12,7 @@ import java.util.concurrent.TimeoutException; import static org.asamk.signal.util.ErrorUtils.handleAssertionError; -public class LinkCommand implements LocalCommand { +public class LinkCommand implements ProvisioningCommand { @Override public void attachToSubparser(final Subparser subparser) { @@ -21,15 +21,15 @@ public class LinkCommand implements LocalCommand { } @Override - public int handleCommand(final Namespace ns, final Manager m) { + public int handleCommand(final Namespace ns, final ProvisioningManager m) { String deviceName = ns.getString("name"); if (deviceName == null) { deviceName = "cli"; } try { System.out.println(m.getDeviceLinkUri()); - m.finishDeviceLink(deviceName); - System.out.println("Associated with: " + m.getUsername()); + String username = m.finishDeviceLink(deviceName); + System.out.println("Associated with: " + username); } catch (TimeoutException e) { System.err.println("Link request timed out, please try again."); return 3; diff --git a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java index 1d2b7b31..24d6898c 100644 --- a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java @@ -5,9 +5,11 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; import org.asamk.signal.storage.contacts.ContactInfo; + import java.util.List; public class ListContactsCommand implements LocalCommand { + @Override public void attachToSubparser(final Subparser subparser) { } diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index 9758b0e3..0baa8744 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -6,19 +6,20 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; import org.asamk.signal.storage.groups.GroupInfo; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.util.Base64; import java.util.List; public class ListGroupsCommand implements LocalCommand { - private static void printGroup(GroupInfo group, boolean detailed) { + private static void printGroup(GroupInfo group, boolean detailed, SignalServiceAddress address) { if (detailed) { System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b Members: %s", - Base64.encodeBytes(group.groupId), group.name, group.active, group.blocked, group.members)); + Base64.encodeBytes(group.groupId), group.name, group.isMember(address), group.blocked, group.getMembersE164())); } else { System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b", - Base64.encodeBytes(group.groupId), group.name, group.active, group.blocked)); + Base64.encodeBytes(group.groupId), group.name, group.isMember(address), group.blocked)); } } @@ -40,7 +41,7 @@ public class ListGroupsCommand implements LocalCommand { boolean detailed = ns.getBoolean("detailed"); for (GroupInfo group : groups) { - printGroup(group, detailed); + printGroup(group, detailed, m.getSelfAddress()); } return 0; } diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index dd3e5e46..edb67c76 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -7,18 +7,16 @@ import org.asamk.signal.manager.Manager; import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; import org.asamk.signal.util.Hex; import org.asamk.signal.util.Util; -import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.util.List; -import java.util.Map; public class ListIdentitiesCommand implements LocalCommand { - private static void printIdentityFingerprint(Manager m, String theirUsername, JsonIdentityKeyStore.Identity theirId) { - String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirUsername, theirId.getIdentityKey())); - System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirUsername, - theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toStringCondensed(theirId.getFingerprint()), digits)); + private static void printIdentityFingerprint(Manager m, JsonIdentityKeyStore.Identity theirId) { + String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirId.getAddress(), theirId.getIdentityKey())); + System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirId.getAddress().getNumber().orNull(), + theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toString(theirId.getFingerprint()), digits)); } @Override @@ -34,20 +32,18 @@ public class ListIdentitiesCommand implements LocalCommand { return 1; } if (ns.get("number") == null) { - for (Map.Entry> keys : m.getIdentities().entrySet()) { - for (JsonIdentityKeyStore.Identity id : keys.getValue()) { - printIdentityFingerprint(m, keys.getKey(), id); - } + for (JsonIdentityKeyStore.Identity identity : m.getIdentities()) { + printIdentityFingerprint(m, identity); } } else { String number = ns.getString("number"); try { - Pair> key = m.getIdentities(number); - for (JsonIdentityKeyStore.Identity id : key.second()) { - printIdentityFingerprint(m, key.first(), id); + List identities = m.getIdentities(number); + for (JsonIdentityKeyStore.Identity id : identities) { + printIdentityFingerprint(m, id); } } catch (InvalidNumberException e) { - System.out.println("Invalid number: " + e.getMessage()); + System.err.println("Invalid number: " + e.getMessage()); } } return 0; diff --git a/src/main/java/org/asamk/signal/commands/ProvisioningCommand.java b/src/main/java/org/asamk/signal/commands/ProvisioningCommand.java new file mode 100644 index 00000000..12a612ff --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/ProvisioningCommand.java @@ -0,0 +1,10 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; + +import org.asamk.signal.manager.ProvisioningManager; + +public interface ProvisioningCommand extends Command { + + int handleCommand(Namespace ns, ProvisioningManager m); +} diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index 6e53cb2a..6db230f5 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -3,10 +3,10 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.GroupIdFormatException; -import org.asamk.signal.GroupNotFoundException; -import org.asamk.signal.NotAGroupMemberException; +import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.NotAGroupMemberException; +import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index 876b6832..bc3acbde 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -1,5 +1,11 @@ package org.asamk.signal.commands; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; @@ -7,10 +13,10 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; import org.asamk.signal.JsonReceiveMessageHandler; import org.asamk.signal.ReceiveMessageHandler; +import org.asamk.signal.json.JsonMessageEnvelope; import org.asamk.signal.manager.Manager; import org.asamk.signal.util.DateUtils; -import org.freedesktop.dbus.DBusConnection; -import org.freedesktop.dbus.DBusSigHandler; +import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.whispersystems.util.Base64; @@ -35,49 +41,102 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { } public int handleCommand(final Namespace ns, final Signal signal, DBusConnection dbusconnection) { - if (dbusconnection != null) { - try { - dbusconnection.addSigHandler(Signal.MessageReceived.class, new DBusSigHandler() { - @Override - public void handle(Signal.MessageReceived s) { - System.out.print(String.format("Envelope from: %s\nTimestamp: %s\nBody: %s\n", - s.getSender(), DateUtils.formatTimestamp(s.getTimestamp()), s.getMessage())); - if (s.getGroupId().length > 0) { - System.out.println("Group info:"); - System.out.println(" Id: " + Base64.encodeBytes(s.getGroupId())); - } - if (s.getAttachments().size() > 0) { - System.out.println("Attachments: "); - for (String attachment : s.getAttachments()) { - System.out.println("- Stored plaintext in: " + attachment); - } - } + final ObjectMapper jsonProcessor; + if (ns.getBoolean("json")) { + jsonProcessor = new ObjectMapper(); + jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect + jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); + } else { + jsonProcessor = null; + } + try { + dbusconnection.addSigHandler(Signal.MessageReceived.class, messageReceived -> { + if (jsonProcessor != null) { + JsonMessageEnvelope envelope = new JsonMessageEnvelope(messageReceived); + ObjectNode result = jsonProcessor.createObjectNode(); + result.putPOJO("envelope", envelope); + try { + jsonProcessor.writeValue(System.out, result); System.out.println(); + } catch (IOException e) { + e.printStackTrace(); } - }); - dbusconnection.addSigHandler(Signal.ReceiptReceived.class, new DBusSigHandler() { - @Override - public void handle(Signal.ReceiptReceived s) { - System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n", - s.getSender(), DateUtils.formatTimestamp(s.getTimestamp()))); + } else { + System.out.print(String.format("Envelope from: %s\nTimestamp: %s\nBody: %s\n", + messageReceived.getSender(), DateUtils.formatTimestamp(messageReceived.getTimestamp()), messageReceived.getMessage())); + if (messageReceived.getGroupId().length > 0) { + System.out.println("Group info:"); + System.out.println(" Id: " + Base64.encodeBytes(messageReceived.getGroupId())); } - }); - } catch (UnsatisfiedLinkError e) { - System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); - return 1; - } catch (DBusException e) { - e.printStackTrace(); - return 1; - } - while (true) { - try { - Thread.sleep(10000); - } catch (InterruptedException e) { - return 0; + if (messageReceived.getAttachments().size() > 0) { + System.out.println("Attachments: "); + for (String attachment : messageReceived.getAttachments()) { + System.out.println("- Stored plaintext in: " + attachment); + } + } + System.out.println(); } + }); + + dbusconnection.addSigHandler(Signal.ReceiptReceived.class, + receiptReceived -> { + if (jsonProcessor != null) { + JsonMessageEnvelope envelope = new JsonMessageEnvelope(receiptReceived); + ObjectNode result = jsonProcessor.createObjectNode(); + result.putPOJO("envelope", envelope); + try { + jsonProcessor.writeValue(System.out, result); + System.out.println(); + } catch (IOException e) { + e.printStackTrace(); + } + } else { + System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n", + receiptReceived.getSender(), DateUtils.formatTimestamp(receiptReceived.getTimestamp()))); + } + }); + + dbusconnection.addSigHandler(Signal.SyncMessageReceived.class, syncReceived -> { + if (jsonProcessor != null) { + JsonMessageEnvelope envelope = new JsonMessageEnvelope(syncReceived); + ObjectNode result = jsonProcessor.createObjectNode(); + result.putPOJO("envelope", envelope); + try { + jsonProcessor.writeValue(System.out, result); + System.out.println(); + } catch (IOException e) { + e.printStackTrace(); + } + } else { + System.out.print(String.format("Sync Envelope from: %s to: %s\nTimestamp: %s\nBody: %s\n", + syncReceived.getSource(), syncReceived.getDestination(), DateUtils.formatTimestamp(syncReceived.getTimestamp()), syncReceived.getMessage())); + if (syncReceived.getGroupId().length > 0) { + System.out.println("Group info:"); + System.out.println(" Id: " + Base64.encodeBytes(syncReceived.getGroupId())); + } + if (syncReceived.getAttachments().size() > 0) { + System.out.println("Attachments: "); + for (String attachment : syncReceived.getAttachments()) { + System.out.println("- Stored plaintext in: " + attachment); + } + } + System.out.println(); + } + }); + } catch (UnsatisfiedLinkError e) { + System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); + return 1; + } catch (DBusException e) { + e.printStackTrace(); + return 1; + } + while (true) { + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + return 0; } } - return 0; } @Override diff --git a/src/main/java/org/asamk/signal/commands/RegisterCommand.java b/src/main/java/org/asamk/signal/commands/RegisterCommand.java index 2e2b7c4f..e95487bf 100644 --- a/src/main/java/org/asamk/signal/commands/RegisterCommand.java +++ b/src/main/java/org/asamk/signal/commands/RegisterCommand.java @@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; +import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException; import java.io.IOException; @@ -22,6 +23,9 @@ public class RegisterCommand implements LocalCommand { try { m.register(ns.getBoolean("voice")); return 0; + } catch (CaptchaRequiredException e) { + System.err.println("Captcha required for verification (" + e.getMessage() + ")"); + return 1; } catch (IOException e) { System.err.println("Request verify error: " + e.getMessage()); return 3; diff --git a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java index 39eb1f1b..b7de5402 100644 --- a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java @@ -21,7 +21,7 @@ public class RemovePinCommand implements LocalCommand { return 1; } try { - m.setRegistrationLockPin(Optional.absent()); + m.setRegistrationLockPin(Optional.absent()); return 0; } catch (IOException e) { System.err.println("Remove pin error: " + e.getMessage()); diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 7320d76d..43166b5b 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -5,14 +5,10 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; -import org.asamk.signal.AttachmentInvalidException; -import org.asamk.signal.GroupIdFormatException; -import org.asamk.signal.GroupNotFoundException; -import org.asamk.signal.NotAGroupMemberException; +import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; -import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import java.io.IOException; import java.nio.charset.Charset; @@ -20,12 +16,7 @@ import java.util.ArrayList; import java.util.List; import static org.asamk.signal.util.ErrorUtils.handleAssertionError; -import static org.asamk.signal.util.ErrorUtils.handleDBusExecutionException; -import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions; import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException; -import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException; -import static org.asamk.signal.util.ErrorUtils.handleIOException; -import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException; public class SendCommand implements DbusCommand { @@ -61,19 +52,13 @@ public class SendCommand implements DbusCommand { if (ns.getBoolean("endsession")) { try { - signal.sendEndSessionMessage(ns.getList("recipient")); + signal.sendEndSessionMessage(ns.getList("recipient")); return 0; - } catch (IOException e) { - handleIOException(e); - return 3; - } catch (EncapsulatedExceptions e) { - handleEncapsulatedExceptions(e); - return 3; } catch (AssertionError e) { handleAssertionError(e); return 1; } catch (DBusExecutionException e) { - handleDBusExecutionException(e); + System.err.println("Failed to send message: " + e.getMessage()); return 1; } } @@ -89,42 +74,42 @@ public class SendCommand implements DbusCommand { } } + List attachments = ns.getList("attachment"); + if (attachments == null) { + attachments = new ArrayList<>(); + } + try { - List attachments = ns.getList("attachment"); - if (attachments == null) { - attachments = new ArrayList<>(); - } if (ns.getString("group") != null) { - byte[] groupId = Util.decodeGroupId(ns.getString("group")); - signal.sendGroupMessage(messageText, attachments, groupId); - } else { - signal.sendMessage(messageText, attachments, ns.getList("recipient")); + byte[] groupId; + try { + groupId = Util.decodeGroupId(ns.getString("group")); + } catch (GroupIdFormatException e) { + handleGroupIdFormatException(e); + return 1; + } + + long timestamp = signal.sendGroupMessage(messageText, attachments, groupId); + System.out.println(timestamp); + return 0; } - return 0; - } catch (IOException e) { - handleIOException(e); - return 3; - } catch (EncapsulatedExceptions e) { - handleEncapsulatedExceptions(e); - return 3; } catch (AssertionError e) { handleAssertionError(e); return 1; - } catch (GroupNotFoundException e) { - handleGroupNotFoundException(e); + } catch (DBusExecutionException e) { + System.err.println("Failed to send message: " + e.getMessage()); return 1; - } catch (NotAGroupMemberException e) { - handleNotAGroupMemberException(e); - return 1; - } catch (AttachmentInvalidException e) { - System.err.println("Failed to add attachment: " + e.getMessage()); - System.err.println("Aborting sending."); + } + + try { + long timestamp = signal.sendMessage(messageText, attachments, ns.getList("recipient")); + System.out.println(timestamp); + return 0; + } catch (AssertionError e) { + handleAssertionError(e); return 1; } catch (DBusExecutionException e) { - handleDBusExecutionException(e); - return 1; - } catch (GroupIdFormatException e) { - handleGroupIdFormatException(e); + System.err.println("Failed to send message: " + e.getMessage()); return 1; } } diff --git a/src/main/java/org/asamk/signal/commands/SendContactsCommand.java b/src/main/java/org/asamk/signal/commands/SendContactsCommand.java index 523292ab..20e81a60 100644 --- a/src/main/java/org/asamk/signal/commands/SendContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendContactsCommand.java @@ -2,6 +2,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java new file mode 100644 index 00000000..7e748866 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -0,0 +1,99 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.impl.Arguments; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; + +import org.asamk.signal.manager.GroupNotFoundException; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.NotAGroupMemberException; +import org.asamk.signal.util.GroupIdFormatException; +import org.asamk.signal.util.Util; +import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; +import org.whispersystems.signalservice.api.util.InvalidNumberException; + +import java.io.IOException; + +import static org.asamk.signal.util.ErrorUtils.handleAssertionError; +import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions; +import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException; +import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException; +import static org.asamk.signal.util.ErrorUtils.handleIOException; +import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException; +import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException; + +public class SendReactionCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.help("Send reaction to a previously received or sent message."); + subparser.addArgument("-g", "--group") + .help("Specify the recipient group ID."); + subparser.addArgument("recipient") + .help("Specify the recipients' phone number.") + .nargs("*"); + subparser.addArgument("-e", "--emoji") + .required(true) + .help("Specify the emoji, should be a single unicode grapheme cluster."); + subparser.addArgument("-a", "--target-author") + .required(true) + .help("Specify the number of the author of the message to which to react."); + subparser.addArgument("-t", "--target-timestamp") + .required(true) + .type(long.class) + .help("Specify the timestamp of the message to which to react."); + subparser.addArgument("-r", "--remove") + .help("Remove a reaction.") + .action(Arguments.storeTrue()); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + + if ((ns.getList("recipient") == null || ns.getList("recipient").size() == 0) && ns.getString("group") == null) { + System.err.println("No recipients given"); + System.err.println("Aborting sending."); + return 1; + } + + String emoji = ns.getString("emoji"); + boolean isRemove = ns.getBoolean("remove"); + String targetAuthor = ns.getString("target_author"); + long targetTimestamp = ns.getLong("target_timestamp"); + + try { + if (ns.getString("group") != null) { + byte[] groupId = Util.decodeGroupId(ns.getString("group")); + m.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId); + } else { + m.sendMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, ns.getList("recipient")); + } + return 0; + } catch (IOException e) { + handleIOException(e); + return 3; + } catch (EncapsulatedExceptions e) { + handleEncapsulatedExceptions(e); + return 3; + } catch (AssertionError e) { + handleAssertionError(e); + return 1; + } catch (GroupNotFoundException e) { + handleGroupNotFoundException(e); + return 1; + } catch (NotAGroupMemberException e) { + handleNotAGroupMemberException(e); + return 1; + } catch (GroupIdFormatException e) { + handleGroupIdFormatException(e); + return 1; + } catch (InvalidNumberException e) { + handleInvalidNumberException(e); + return 1; + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java index f2744545..2780dc46 100644 --- a/src/main/java/org/asamk/signal/commands/TrustCommand.java +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -6,7 +6,9 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; +import org.asamk.signal.util.ErrorUtils; import org.asamk.signal.util.Hex; +import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.util.Locale; @@ -21,8 +23,8 @@ public class TrustCommand implements LocalCommand { mutTrust.addArgument("-a", "--trust-all-known-keys") .help("Trust all known keys of this user, only use this for testing.") .action(Arguments.storeTrue()); - mutTrust.addArgument("-v", "--verified-fingerprint") - .help("Specify the fingerprint of the key, only use this option if you have verified the fingerprint."); + mutTrust.addArgument("-v", "--verified-safety-number", "--verified-fingerprint") + .help("Specify the safety number of the key, only use this option if you have verified the safety number."); } @Override @@ -39,34 +41,46 @@ public class TrustCommand implements LocalCommand { return 1; } } else { - String fingerprint = ns.getString("verified_fingerprint"); - if (fingerprint != null) { - fingerprint = fingerprint.replaceAll(" ", ""); - if (fingerprint.length() == 66) { + String safetyNumber = ns.getString("verified_safety_number"); + if (safetyNumber != null) { + safetyNumber = safetyNumber.replaceAll(" ", ""); + if (safetyNumber.length() == 66) { byte[] fingerprintBytes; try { - fingerprintBytes = Hex.toByteArray(fingerprint.toLowerCase(Locale.ROOT)); + fingerprintBytes = Hex.toByteArray(safetyNumber.toLowerCase(Locale.ROOT)); } catch (Exception e) { System.err.println("Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters."); return 1; } - boolean res = m.trustIdentityVerified(number, fingerprintBytes); + boolean res; + try { + res = m.trustIdentityVerified(number, fingerprintBytes); + } catch (InvalidNumberException e) { + ErrorUtils.handleInvalidNumberException(e); + return 1; + } if (!res) { System.err.println("Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct."); return 1; } - } else if (fingerprint.length() == 60) { - boolean res = m.trustIdentityVerifiedSafetyNumber(number, fingerprint); + } else if (safetyNumber.length() == 60) { + boolean res; + try { + res = m.trustIdentityVerifiedSafetyNumber(number, safetyNumber); + } catch (InvalidNumberException e) { + ErrorUtils.handleInvalidNumberException(e); + return 1; + } if (!res) { System.err.println("Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct."); return 1; } } else { - System.err.println("Fingerprint has invalid format, either specify the old hex fingerprint or the new safety number"); + System.err.println("Safety number has invalid format, either specify the old hex fingerprint or the new safety number"); return 1; } } else { - System.err.println("You need to specify the fingerprint you have verified with -v FINGERPRINT"); + System.err.println("You need to specify the fingerprint/safety number you have verified with -v SAFETY_NUMBER"); return 1; } } diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index be745cb0..a95aa328 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -2,9 +2,10 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.GroupIdFormatException; -import org.asamk.signal.GroupNotFoundException; + +import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.util.InvalidNumberException; diff --git a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java index 77f38e5e..d7fa3893 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java @@ -6,6 +6,8 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.util.InvalidNumberException; +import java.io.IOException; + public class UpdateContactCommand implements LocalCommand { @Override @@ -15,6 +17,10 @@ public class UpdateContactCommand implements LocalCommand { subparser.addArgument("-n", "--name") .required(true) .help("New contact name"); + subparser.addArgument("-e", "--expiration") + .required(false) + .type(int.class) + .help("Set expiration time of messages (seconds)"); subparser.help("Update the details of a given contact"); } @@ -30,8 +36,17 @@ public class UpdateContactCommand implements LocalCommand { try { m.setContactName(number, name); + + Integer expiration = ns.getInt("expiration"); + if (expiration != null) { + m.setExpirationTimer(number, expiration); + } } catch (InvalidNumberException e) { - System.out.println("Invalid contact number: " + e.getMessage()); + System.err.println("Invalid contact number: " + e.getMessage()); + return 1; + } catch (IOException e) { + System.err.println("Update contact error: " + e.getMessage()); + return 3; } return 0; diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 66071cb0..925b8c90 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -4,23 +4,16 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; -import org.asamk.signal.AttachmentInvalidException; -import org.asamk.signal.GroupIdFormatException; -import org.asamk.signal.GroupNotFoundException; -import org.asamk.signal.NotAGroupMemberException; +import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.Util; -import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; +import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.util.Base64; -import java.io.IOException; import java.util.ArrayList; import java.util.List; -import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions; +import static org.asamk.signal.util.ErrorUtils.handleAssertionError; import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException; -import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException; -import static org.asamk.signal.util.ErrorUtils.handleIOException; -import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException; public class UpdateGroupCommand implements DbusCommand { @@ -44,49 +37,48 @@ public class UpdateGroupCommand implements DbusCommand { return 1; } - try { - byte[] groupId = null; - if (ns.getString("group") != null) { + byte[] groupId = null; + if (ns.getString("group") != null) { + try { groupId = Util.decodeGroupId(ns.getString("group")); + } catch (GroupIdFormatException e) { + handleGroupIdFormatException(e); + return 1; } - if (groupId == null) { - groupId = new byte[0]; - } - String groupName = ns.getString("name"); - if (groupName == null) { - groupName = ""; - } - List groupMembers = ns.getList("member"); - if (groupMembers == null) { - groupMembers = new ArrayList<>(); - } - String groupAvatar = ns.getString("avatar"); - if (groupAvatar == null) { - groupAvatar = ""; - } + } + if (groupId == null) { + groupId = new byte[0]; + } + + String groupName = ns.getString("name"); + if (groupName == null) { + groupName = ""; + } + + List groupMembers = ns.getList("member"); + if (groupMembers == null) { + groupMembers = new ArrayList<>(); + } + + String groupAvatar = ns.getString("avatar"); + if (groupAvatar == null) { + groupAvatar = ""; + } + + try { byte[] newGroupId = signal.updateGroup(groupId, groupName, groupMembers, groupAvatar); if (groupId.length != newGroupId.length) { System.out.println("Creating new group \"" + Base64.encodeBytes(newGroupId) + "\" …"); } return 0; - } catch (IOException e) { - handleIOException(e); - return 3; - } catch (AttachmentInvalidException e) { + } catch (AssertionError e) { + handleAssertionError(e); + return 1; + } catch (Signal.Error.AttachmentInvalid e) { System.err.println("Failed to add avatar attachment for group\": " + e.getMessage()); - System.err.println("Aborting sending."); return 1; - } catch (GroupNotFoundException e) { - handleGroupNotFoundException(e); - return 1; - } catch (NotAGroupMemberException e) { - handleNotAGroupMemberException(e); - return 1; - } catch (EncapsulatedExceptions e) { - handleEncapsulatedExceptions(e); - return 3; - } catch (GroupIdFormatException e) { - handleGroupIdFormatException(e); + } catch (DBusExecutionException e) { + System.err.println("Failed to send message: " + e.getMessage()); return 1; } } diff --git a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java index a7b02937..218c8b77 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java @@ -14,16 +14,18 @@ public class UpdateProfileCommand implements LocalCommand { @Override public void attachToSubparser(final Subparser subparser) { - final MutuallyExclusiveGroup avatarOptions = subparser.addMutuallyExclusiveGroup(); + final MutuallyExclusiveGroup avatarOptions = subparser.addMutuallyExclusiveGroup() + .required(true); avatarOptions.addArgument("--avatar") .help("Path to new profile avatar"); avatarOptions.addArgument("--remove-avatar") .action(Arguments.storeTrue()); subparser.addArgument("--name") + .required(true) .help("New profile name"); - subparser.help("Set a name and/or avatar image for the user profile"); + subparser.help("Set a name and avatar image for the user profile"); } @Override @@ -34,38 +36,15 @@ public class UpdateProfileCommand implements LocalCommand { } String name = ns.getString("name"); - - if (name != null) { - try { - m.setProfileName(name); - } catch (IOException e) { - System.err.println("UpdateAccount error: " + e.getMessage()); - return 3; - } - } - String avatarPath = ns.getString("avatar"); - - if (avatarPath != null) { - File avatarFile = new File(avatarPath); - - try { - m.setProfileAvatar(avatarFile); - } catch (IOException e) { - System.err.println("UpdateAccount error: " + e.getMessage()); - return 3; - } - } - boolean removeAvatar = ns.getBoolean("remove_avatar"); - if (removeAvatar) { - try { - m.removeProfileAvatar(); - } catch (IOException e) { - System.err.println("UpdateAccount error: " + e.getMessage()); - return 3; - } + try { + File avatarFile = removeAvatar ? null : new File(avatarPath); + m.setProfile(name, avatarFile); + } catch (IOException e) { + System.err.println("UpdateAccount error: " + e.getMessage()); + return 3; } return 0; diff --git a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java new file mode 100644 index 00000000..77df2b22 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java @@ -0,0 +1,34 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; + +import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.StickerPackInvalidException; + +import java.io.IOException; + +public class UploadStickerPackCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("path") + .help("The path of the manifest.json or a zip file containing the sticker pack you wish to upload."); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + try { + String path = ns.getString("path"); + String url = m.uploadStickerPack(path); + System.out.println(url); + return 0; + } catch (IOException e) { + System.err.println("Upload error: " + e.getMessage()); + return 3; + } catch (StickerPackInvalidException e) { + System.err.println("Invalid sticker pack: " + e.getMessage()); + return 3; + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/VerifyCommand.java b/src/main/java/org/asamk/signal/commands/VerifyCommand.java index 9d99c0d2..0f336325 100644 --- a/src/main/java/org/asamk/signal/commands/VerifyCommand.java +++ b/src/main/java/org/asamk/signal/commands/VerifyCommand.java @@ -20,10 +20,6 @@ public class VerifyCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.userHasKeys()) { - System.err.println("User has no keys, first call register."); - return 1; - } if (m.isRegistered()) { System.err.println("User registration is already verified"); return 1; diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java new file mode 100644 index 00000000..17cc2caa --- /dev/null +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -0,0 +1,205 @@ +package org.asamk.signal.dbus; + +import org.asamk.Signal; +import org.asamk.signal.manager.AttachmentInvalidException; +import org.asamk.signal.manager.GroupNotFoundException; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.NotAGroupMemberException; +import org.asamk.signal.storage.groups.GroupInfo; +import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; +import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; +import org.whispersystems.signalservice.api.util.InvalidNumberException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class DbusSignalImpl implements Signal { + + private final Manager m; + + public DbusSignalImpl(final Manager m) { + this.m = m; + } + + @Override + public boolean isRemote() { + return false; + } + + @Override + public String getObjectPath() { + return null; + } + + @Override + public long sendMessage(final String message, final List attachments, final String recipient) { + List recipients = new ArrayList<>(1); + recipients.add(recipient); + return sendMessage(message, attachments, recipients); + } + + private static DBusExecutionException convertEncapsulatedExceptions(EncapsulatedExceptions e) { + if (e.getNetworkExceptions().size() + e.getUnregisteredUserExceptions().size() + e.getUntrustedIdentityExceptions().size() == 1) { + if (e.getNetworkExceptions().size() == 1) { + NetworkFailureException n = e.getNetworkExceptions().get(0); + return new Error.Failure("Network failure for \"" + n.getE164number() + "\": " + n.getMessage()); + } else if (e.getUnregisteredUserExceptions().size() == 1) { + UnregisteredUserException n = e.getUnregisteredUserExceptions().get(0); + return new Error.UnregisteredUser("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage()); + } else if (e.getUntrustedIdentityExceptions().size() == 1) { + UntrustedIdentityException n = e.getUntrustedIdentityExceptions().get(0); + return new Error.UntrustedIdentity("Untrusted Identity for \"" + n.getIdentifier() + "\": " + n.getMessage()); + } + } + + StringBuilder message = new StringBuilder(); + message.append("Failed to send (some) messages:").append('\n'); + for (NetworkFailureException n : e.getNetworkExceptions()) { + message.append("Network failure for \"").append(n.getE164number()).append("\": ").append(n.getMessage()).append('\n'); + } + for (UnregisteredUserException n : e.getUnregisteredUserExceptions()) { + message.append("Unregistered user \"").append(n.getE164Number()).append("\": ").append(n.getMessage()).append('\n'); + } + for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) { + message.append("Untrusted Identity for \"").append(n.getIdentifier()).append("\": ").append(n.getMessage()).append('\n'); + } + + return new Error.Failure(message.toString()); + } + + @Override + public long sendMessage(final String message, final List attachments, final List recipients) { + try { + return m.sendMessage(message, attachments, recipients); + } catch (EncapsulatedExceptions e) { + throw convertEncapsulatedExceptions(e); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); + } catch (AttachmentInvalidException e) { + throw new Error.AttachmentInvalid(e.getMessage()); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + + @Override + public void sendEndSessionMessage(final List recipients) { + try { + m.sendEndSessionMessage(recipients); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (EncapsulatedExceptions e) { + throw convertEncapsulatedExceptions(e); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); + } + } + + @Override + public long sendGroupMessage(final String message, final List attachments, final byte[] groupId) { + try { + return m.sendGroupMessage(message, attachments, groupId); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (EncapsulatedExceptions e) { + throw convertEncapsulatedExceptions(e); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (AttachmentInvalidException e) { + throw new Error.AttachmentInvalid(e.getMessage()); + } + } + + @Override + public String getContactName(final String number) { + try { + return m.getContactName(number); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); + } + } + + @Override + public void setContactName(final String number, final String name) { + try { + m.setContactName(number, name); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); + } + } + + @Override + public void setContactBlocked(final String number, final boolean blocked) { + try { + m.setContactBlocked(number, blocked); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); + } + } + + @Override + public void setGroupBlocked(final byte[] groupId, final boolean blocked) { + try { + m.setGroupBlocked(groupId, blocked); + } catch (GroupNotFoundException e) { + throw new Error.GroupNotFound(e.getMessage()); + } + } + + @Override + public List getGroupIds() { + List groups = m.getGroups(); + List ids = new ArrayList<>(groups.size()); + for (GroupInfo group : groups) { + ids.add(group.groupId); + } + return ids; + } + + @Override + public String getGroupName(final byte[] groupId) { + GroupInfo group = m.getGroup(groupId); + if (group == null) { + return ""; + } else { + return group.name; + } + } + + @Override + public List getGroupMembers(final byte[] groupId) { + GroupInfo group = m.getGroup(groupId); + if (group == null) { + return Collections.emptyList(); + } else { + return new ArrayList<>(group.getMembersE164()); + } + } + + @Override + public byte[] updateGroup(final byte[] groupId, final String name, final List members, final String avatar) { + try { + return m.updateGroup(groupId, name, members, avatar); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (EncapsulatedExceptions e) { + throw convertEncapsulatedExceptions(e); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); + } catch (AttachmentInvalidException e) { + throw new Error.AttachmentInvalid(e.getMessage()); + } + } + + @Override + public boolean isRegistered() { + return true; + } +} diff --git a/src/main/java/org/asamk/signal/JsonAttachment.java b/src/main/java/org/asamk/signal/json/JsonAttachment.java similarity index 81% rename from src/main/java/org/asamk/signal/JsonAttachment.java rename to src/main/java/org/asamk/signal/json/JsonAttachment.java index 58165639..1949171a 100644 --- a/src/main/java/org/asamk/signal/JsonAttachment.java +++ b/src/main/java/org/asamk/signal/json/JsonAttachment.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.json; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; @@ -15,7 +15,7 @@ class JsonAttachment { final SignalServiceAttachmentPointer pointer = attachment.asPointer(); if (attachment.isPointer()) { - this.id = String.valueOf(pointer.getId()); + this.id = String.valueOf(pointer.getRemoteId()); if (pointer.getFileName().isPresent()) { this.filename = pointer.getFileName().get(); } @@ -24,4 +24,8 @@ class JsonAttachment { } } } + + JsonAttachment(String filename) { + this.filename = filename; + } } diff --git a/src/main/java/org/asamk/signal/JsonCallMessage.java b/src/main/java/org/asamk/signal/json/JsonCallMessage.java similarity index 97% rename from src/main/java/org/asamk/signal/JsonCallMessage.java rename to src/main/java/org/asamk/signal/json/JsonCallMessage.java index 2c8518f9..c1b1d443 100644 --- a/src/main/java/org/asamk/signal/JsonCallMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonCallMessage.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.json; import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; import org.whispersystems.signalservice.api.messages.calls.BusyMessage; diff --git a/src/main/java/org/asamk/signal/json/JsonDataMessage.java b/src/main/java/org/asamk/signal/json/JsonDataMessage.java new file mode 100644 index 00000000..fc8538aa --- /dev/null +++ b/src/main/java/org/asamk/signal/json/JsonDataMessage.java @@ -0,0 +1,59 @@ +package org.asamk.signal.json; + +import org.asamk.Signal; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceGroup; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +class JsonDataMessage { + + long timestamp; + String message; + int expiresInSeconds; + List attachments; + JsonGroupInfo groupInfo; + + JsonDataMessage(SignalServiceDataMessage dataMessage) { + this.timestamp = dataMessage.getTimestamp(); + if (dataMessage.getGroupContext().isPresent() && dataMessage.getGroupContext().get().getGroupV1().isPresent()) { + SignalServiceGroup groupInfo = dataMessage.getGroupContext().get().getGroupV1().get(); + this.groupInfo = new JsonGroupInfo(groupInfo); + } + if (dataMessage.getBody().isPresent()) { + this.message = dataMessage.getBody().get(); + } + this.expiresInSeconds = dataMessage.getExpiresInSeconds(); + if (dataMessage.getAttachments().isPresent()) { + this.attachments = new ArrayList<>(dataMessage.getAttachments().get().size()); + for (SignalServiceAttachment attachment : dataMessage.getAttachments().get()) { + this.attachments.add(new JsonAttachment(attachment)); + } + } else { + this.attachments = new ArrayList<>(); + } + } + + public JsonDataMessage(Signal.MessageReceived messageReceived) { + timestamp = messageReceived.getTimestamp(); + message = messageReceived.getMessage(); + groupInfo = new JsonGroupInfo(messageReceived.getGroupId()); + attachments = messageReceived.getAttachments() + .stream() + .map(JsonAttachment::new) + .collect(Collectors.toList()); + } + + public JsonDataMessage(Signal.SyncMessageReceived messageReceived) { + timestamp = messageReceived.getTimestamp(); + message = messageReceived.getMessage(); + groupInfo = new JsonGroupInfo(messageReceived.getGroupId()); + attachments = messageReceived.getAttachments() + .stream() + .map(JsonAttachment::new) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/org/asamk/signal/json/JsonError.java b/src/main/java/org/asamk/signal/json/JsonError.java new file mode 100644 index 00000000..29d85c8b --- /dev/null +++ b/src/main/java/org/asamk/signal/json/JsonError.java @@ -0,0 +1,10 @@ +package org.asamk.signal.json; + +public class JsonError { + + String message; + + public JsonError(Throwable exception) { + this.message = exception.getMessage(); + } +} diff --git a/src/main/java/org/asamk/signal/JsonGroupInfo.java b/src/main/java/org/asamk/signal/json/JsonGroupInfo.java similarity index 87% rename from src/main/java/org/asamk/signal/JsonGroupInfo.java rename to src/main/java/org/asamk/signal/json/JsonGroupInfo.java index 5678b896..572623e4 100644 --- a/src/main/java/org/asamk/signal/JsonGroupInfo.java +++ b/src/main/java/org/asamk/signal/json/JsonGroupInfo.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.json; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -27,4 +27,8 @@ class JsonGroupInfo { } this.type = groupInfo.getType().toString(); } + + JsonGroupInfo(byte[] groupId) { + this.groupId = Base64.encodeBytes(groupId); + } } diff --git a/src/main/java/org/asamk/signal/JsonMessageEnvelope.java b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java similarity index 71% rename from src/main/java/org/asamk/signal/JsonMessageEnvelope.java rename to src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java index 9a275970..3279d941 100644 --- a/src/main/java/org/asamk/signal/JsonMessageEnvelope.java +++ b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java @@ -1,10 +1,11 @@ -package org.asamk.signal; +package org.asamk.signal.json; +import org.asamk.Signal; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -class JsonMessageEnvelope { +public class JsonMessageEnvelope { String source; int sourceDevice; @@ -44,4 +45,22 @@ class JsonMessageEnvelope { } } } + + public JsonMessageEnvelope(Signal.MessageReceived messageReceived) { + source = messageReceived.getSender(); + timestamp = messageReceived.getTimestamp(); + dataMessage = new JsonDataMessage(messageReceived); + } + + public JsonMessageEnvelope(Signal.ReceiptReceived receiptReceived) { + source = receiptReceived.getSender(); + timestamp = receiptReceived.getTimestamp(); + isReceipt = true; + } + + public JsonMessageEnvelope(Signal.SyncMessageReceived messageReceived) { + source = messageReceived.getSource(); + timestamp = messageReceived.getTimestamp(); + syncMessage = new JsonSyncMessage(messageReceived); + } } diff --git a/src/main/java/org/asamk/signal/JsonReceiptMessage.java b/src/main/java/org/asamk/signal/json/JsonReceiptMessage.java similarity index 95% rename from src/main/java/org/asamk/signal/JsonReceiptMessage.java rename to src/main/java/org/asamk/signal/json/JsonReceiptMessage.java index fd875af5..1b896053 100644 --- a/src/main/java/org/asamk/signal/JsonReceiptMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonReceiptMessage.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.json; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; diff --git a/src/main/java/org/asamk/signal/JsonSyncDataMessage.java b/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java similarity index 67% rename from src/main/java/org/asamk/signal/JsonSyncDataMessage.java rename to src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java index b72fb26d..d253b197 100644 --- a/src/main/java/org/asamk/signal/JsonSyncDataMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java @@ -1,5 +1,6 @@ -package org.asamk.signal; +package org.asamk.signal.json; +import org.asamk.Signal; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; class JsonSyncDataMessage extends JsonDataMessage { @@ -12,4 +13,9 @@ class JsonSyncDataMessage extends JsonDataMessage { this.destination = transcriptMessage.getDestination().get().getNumber().get(); } } + + JsonSyncDataMessage(Signal.SyncMessageReceived messageReceived) { + super(messageReceived); + destination = messageReceived.getDestination(); + } } diff --git a/src/main/java/org/asamk/signal/json/JsonSyncMessage.java b/src/main/java/org/asamk/signal/json/JsonSyncMessage.java new file mode 100644 index 00000000..27766bda --- /dev/null +++ b/src/main/java/org/asamk/signal/json/JsonSyncMessage.java @@ -0,0 +1,50 @@ +package org.asamk.signal.json; + +import org.asamk.Signal; +import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +import java.util.ArrayList; +import java.util.List; + +enum JsonSyncMessageType { + CONTACTS_SYNC, + GROUPS_SYNC, + REQUEST_SYNC +} + +class JsonSyncMessage { + + JsonSyncDataMessage sentMessage; + List blockedNumbers; + List readMessages; + JsonSyncMessageType type; + + JsonSyncMessage(SignalServiceSyncMessage syncMessage) { + if (syncMessage.getSent().isPresent()) { + this.sentMessage = new JsonSyncDataMessage(syncMessage.getSent().get()); + } + if (syncMessage.getBlockedList().isPresent()) { + this.blockedNumbers = new ArrayList<>(syncMessage.getBlockedList().get().getAddresses().size()); + for (SignalServiceAddress address : syncMessage.getBlockedList().get().getAddresses()) { + this.blockedNumbers.add(address.getNumber().get()); + } + } + if (syncMessage.getRead().isPresent()) { + this.readMessages = syncMessage.getRead().get(); + } + + if (syncMessage.getContacts().isPresent()) { + this.type = JsonSyncMessageType.CONTACTS_SYNC; + } else if (syncMessage.getGroups().isPresent()) { + this.type = JsonSyncMessageType.GROUPS_SYNC; + } else if (syncMessage.getRequest().isPresent()) { + this.type = JsonSyncMessageType.REQUEST_SYNC; + } + } + + JsonSyncMessage(Signal.SyncMessageReceived messageReceived) { + sentMessage = new JsonSyncDataMessage(messageReceived); + } +} diff --git a/src/main/java/org/asamk/signal/AttachmentInvalidException.java b/src/main/java/org/asamk/signal/manager/AttachmentInvalidException.java similarity index 57% rename from src/main/java/org/asamk/signal/AttachmentInvalidException.java rename to src/main/java/org/asamk/signal/manager/AttachmentInvalidException.java index 839c7940..78fba6e0 100644 --- a/src/main/java/org/asamk/signal/AttachmentInvalidException.java +++ b/src/main/java/org/asamk/signal/manager/AttachmentInvalidException.java @@ -1,8 +1,6 @@ -package org.asamk.signal; +package org.asamk.signal.manager; -import org.freedesktop.dbus.exceptions.DBusExecutionException; - -public class AttachmentInvalidException extends DBusExecutionException { +public class AttachmentInvalidException extends Exception { public AttachmentInvalidException(String message) { super(message); diff --git a/src/main/java/org/asamk/signal/manager/BaseConfig.java b/src/main/java/org/asamk/signal/manager/BaseConfig.java deleted file mode 100644 index 0f503352..00000000 --- a/src/main/java/org/asamk/signal/manager/BaseConfig.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.asamk.signal.manager; - -import org.whispersystems.signalservice.api.push.TrustStore; -import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; -import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; -import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; -import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; - -public class BaseConfig { - - public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle(); - public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion(); - - final static String USER_AGENT = PROJECT_NAME == null ? null : PROJECT_NAME + " " + PROJECT_VERSION; - final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"; - final static int PREKEY_MINIMUM_COUNT = 20; - final static int PREKEY_BATCH_SIZE = 100; - final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; - - private final static String URL = "https://textsecure-service.whispersystems.org"; - private final static String CDN_URL = "https://cdn.signal.org"; - private final static TrustStore TRUST_STORE = new WhisperTrustStore(); - - final static SignalServiceConfiguration serviceConfiguration = new SignalServiceConfiguration( - new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)}, - new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, - new SignalContactDiscoveryUrl[0] - ); - - private BaseConfig() { - } -} diff --git a/src/main/java/org/asamk/signal/GroupNotFoundException.java b/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java similarity index 51% rename from src/main/java/org/asamk/signal/GroupNotFoundException.java rename to src/main/java/org/asamk/signal/manager/GroupNotFoundException.java index f09014a0..0c0d6d2d 100644 --- a/src/main/java/org/asamk/signal/GroupNotFoundException.java +++ b/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java @@ -1,9 +1,8 @@ -package org.asamk.signal; +package org.asamk.signal.manager; -import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.util.Base64; -public class GroupNotFoundException extends DBusExecutionException { +public class GroupNotFoundException extends Exception { public GroupNotFoundException(byte[] groupId) { super("Group not found: " + Base64.encodeBytes(groupId)); diff --git a/src/main/java/org/asamk/signal/manager/HandleAction.java b/src/main/java/org/asamk/signal/manager/HandleAction.java new file mode 100644 index 00000000..2ef99062 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/HandleAction.java @@ -0,0 +1,156 @@ +package org.asamk.signal.manager; + +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +import java.util.Arrays; +import java.util.Objects; + +interface HandleAction { + + void execute(Manager m) throws Throwable; +} + +class SendReceiptAction implements HandleAction { + + private final SignalServiceAddress address; + private final long timestamp; + + public SendReceiptAction(final SignalServiceAddress address, final long timestamp) { + this.address = address; + this.timestamp = timestamp; + } + + @Override + public void execute(Manager m) throws Throwable { + m.sendReceipt(address, timestamp); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final SendReceiptAction that = (SendReceiptAction) o; + return timestamp == that.timestamp && + address.equals(that.address); + } + + @Override + public int hashCode() { + return Objects.hash(address, timestamp); + } +} + +class SendSyncContactsAction implements HandleAction { + + private static final SendSyncContactsAction INSTANCE = new SendSyncContactsAction(); + + private SendSyncContactsAction() { + } + + public static SendSyncContactsAction create() { + return INSTANCE; + } + + @Override + public void execute(Manager m) throws Throwable { + m.sendContacts(); + } +} + +class SendSyncGroupsAction implements HandleAction { + + private static final SendSyncGroupsAction INSTANCE = new SendSyncGroupsAction(); + + private SendSyncGroupsAction() { + } + + public static SendSyncGroupsAction create() { + return INSTANCE; + } + + @Override + public void execute(Manager m) throws Throwable { + m.sendGroups(); + } +} + +class SendSyncBlockedListAction implements HandleAction { + + private static final SendSyncBlockedListAction INSTANCE = new SendSyncBlockedListAction(); + + private SendSyncBlockedListAction() { + } + + public static SendSyncBlockedListAction create() { + return INSTANCE; + } + + @Override + public void execute(Manager m) throws Throwable { + m.sendBlockedList(); + } +} + +class SendGroupInfoRequestAction implements HandleAction { + + private final SignalServiceAddress address; + private final byte[] groupId; + + public SendGroupInfoRequestAction(final SignalServiceAddress address, final byte[] groupId) { + this.address = address; + this.groupId = groupId; + } + + @Override + public void execute(Manager m) throws Throwable { + m.sendGroupInfoRequest(groupId, address); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final SendGroupInfoRequestAction that = (SendGroupInfoRequestAction) o; + return address.equals(that.address) && + Arrays.equals(groupId, that.groupId); + } + + @Override + public int hashCode() { + int result = Objects.hash(address); + result = 31 * result + Arrays.hashCode(groupId); + return result; + } +} + +class SendGroupUpdateAction implements HandleAction { + + private final SignalServiceAddress address; + private final byte[] groupId; + + public SendGroupUpdateAction(final SignalServiceAddress address, final byte[] groupId) { + this.address = address; + this.groupId = groupId; + } + + @Override + public void execute(Manager m) throws Throwable { + m.sendUpdateGroupMessage(groupId, address); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final SendGroupUpdateAction that = (SendGroupUpdateAction) o; + return address.equals(that.address) && + Arrays.equals(groupId, that.groupId); + } + + @Override + public int hashCode() { + int result = Objects.hash(address); + result = 31 * result + Arrays.hashCode(groupId); + return result; + } +} diff --git a/src/main/java/org/asamk/signal/manager/JsonStickerPack.java b/src/main/java/org/asamk/signal/manager/JsonStickerPack.java new file mode 100644 index 00000000..a7e5eb7f --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/JsonStickerPack.java @@ -0,0 +1,29 @@ +package org.asamk.signal.manager; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +class JsonStickerPack { + + @JsonProperty + public String title; + + @JsonProperty + public String author; + + @JsonProperty + public JsonSticker cover; + + @JsonProperty + public List stickers; + + public static class JsonSticker { + + @JsonProperty + public String emoji; + + @JsonProperty + public String file; + } +} diff --git a/src/main/java/org/asamk/signal/manager/KeyUtils.java b/src/main/java/org/asamk/signal/manager/KeyUtils.java index 6ffc3f36..fff8179c 100644 --- a/src/main/java/org/asamk/signal/manager/KeyUtils.java +++ b/src/main/java/org/asamk/signal/manager/KeyUtils.java @@ -1,6 +1,8 @@ package org.asamk.signal.manager; import org.asamk.signal.util.RandomUtils; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.profiles.ProfileKey; import org.whispersystems.util.Base64; class KeyUtils { @@ -12,8 +14,12 @@ class KeyUtils { return getSecret(52); } - static byte[] createProfileKey() { - return getSecretBytes(32); + static ProfileKey createProfileKey() { + try { + return new ProfileKey(getSecretBytes(32)); + } catch (InvalidInputException e) { + throw new AssertionError("Profile key is guaranteed to be 32 bytes here"); + } } static String createPassword() { @@ -24,6 +30,14 @@ class KeyUtils { return getSecretBytes(16); } + static byte[] createUnrestrictedUnidentifiedAccess() { + return getSecretBytes(16); + } + + static byte[] createStickerUploadKey() { + return getSecretBytes(32); + } + private static String getSecret(int size) { byte[] secret = getSecretBytes(size); return Base64.encodeBytes(secret); diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 59061068..2ce59cdc 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -16,18 +16,15 @@ */ package org.asamk.signal.manager; -import org.asamk.Signal; -import org.asamk.signal.AttachmentInvalidException; -import org.asamk.signal.GroupNotFoundException; -import org.asamk.signal.NotAGroupMemberException; -import org.asamk.signal.TrustLevel; -import org.asamk.signal.UserAlreadyExists; +import com.fasterxml.jackson.databind.ObjectMapper; + import org.asamk.signal.storage.SignalAccount; import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.groups.JsonGroupStore; +import org.asamk.signal.storage.profiles.SignalProfile; +import org.asamk.signal.storage.profiles.SignalProfileEntry; import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; -import org.asamk.signal.storage.threads.ThreadInfo; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; import org.signal.libsignal.metadata.InvalidMetadataMessageException; @@ -41,6 +38,10 @@ import org.signal.libsignal.metadata.ProtocolLegacyMessageException; import org.signal.libsignal.metadata.ProtocolNoSessionException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.signal.libsignal.metadata.SelfSendException; +import org.signal.libsignal.metadata.certificate.InvalidCertificateException; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.profiles.ClientZkProfileOperations; +import org.signal.zkgroup.profiles.ProfileKey; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; @@ -59,18 +60,26 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessagePipe; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; +import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; +import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifestUpload; +import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifestUpload.StickerInfo; import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact; @@ -84,20 +93,27 @@ import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; +import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.ContactTokenDetails; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; +import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; +import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; +import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; +import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; +import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.util.Base64; +import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -106,6 +122,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; @@ -118,49 +136,66 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; -public class Manager implements Signal { +import static org.asamk.signal.manager.ServiceConfig.capabilities; + +public class Manager implements Closeable { - private final String settingsPath; - private final String dataPath; - private final String attachmentsPath; - private final String avatarsPath; private final SleepTimer timer = new UptimeSleepTimer(); + private final SignalServiceConfiguration serviceConfiguration; + private final String userAgent; - private SignalAccount account; - private String username; + private final SignalAccount account; + private final PathConfig pathConfig; private SignalServiceAccountManager accountManager; private SignalServiceMessagePipe messagePipe = null; private SignalServiceMessagePipe unidentifiedMessagePipe = null; + private boolean discoverableByPhoneNumber = true; - public Manager(String username, String settingsPath) { - this.username = username; - this.settingsPath = settingsPath; - this.dataPath = this.settingsPath + "/data"; - this.attachmentsPath = this.settingsPath + "/attachments"; - this.avatarsPath = this.settingsPath + "/avatars"; + public Manager(SignalAccount account, PathConfig pathConfig, SignalServiceConfiguration serviceConfiguration, String userAgent) { + this.account = account; + this.pathConfig = pathConfig; + this.serviceConfiguration = serviceConfiguration; + this.userAgent = userAgent; + this.accountManager = createSignalServiceAccountManager(); + this.account.setResolver(this::resolveSignalServiceAddress); } public String getUsername() { - return username; + return account.getUsername(); } - private SignalServiceAddress getSelfAddress() { - return new SignalServiceAddress(null, username); + public SignalServiceAddress getSelfAddress() { + return account.getSelfAddress(); } - private SignalServiceAccountManager getSignalServiceAccountManager() { - return new SignalServiceAccountManager(BaseConfig.serviceConfiguration, null, account.getUsername(), account.getPassword(), account.getDeviceId(), BaseConfig.USER_AGENT, timer); + private SignalServiceAccountManager createSignalServiceAccountManager() { + GroupsV2Operations groupsV2Operations; + try { + groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceConfiguration)); + } catch (Throwable ignored) { + groupsV2Operations = null; + } + return new SignalServiceAccountManager(serviceConfiguration, + new DynamicCredentialsProvider(account.getUuid(), account.getUsername(), account.getPassword(), null, account.getDeviceId()), + userAgent, + groupsV2Operations, + timer); } - private IdentityKey getIdentity() { - return account.getSignalProtocolStore().getIdentityKeyPair().getPublicKey(); + private IdentityKeyPair getIdentityKeyPair() { + return account.getSignalProtocolStore().getIdentityKeyPair(); } public int getDeviceId() { @@ -168,10 +203,14 @@ public class Manager implements Signal { } private String getMessageCachePath() { - return this.dataPath + "/" + username + ".d/msg-cache"; + return pathConfig.getDataPath() + "/" + account.getUsername() + ".d/msg-cache"; } private String getMessageCachePath(String sender) { + if (sender == null || sender.isEmpty()) { + return getMessageCachePath(); + } + return getMessageCachePath() + "/" + sender.replace("/", "_"); } @@ -181,28 +220,27 @@ public class Manager implements Signal { return new File(cachePath + "/" + now + "_" + timestamp); } - public boolean userHasKeys() { - return account != null && account.getSignalProtocolStore() != null; - } + public static Manager init(String username, String settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) throws IOException { + PathConfig pathConfig = PathConfig.createDefault(settingsPath); - public void init() throws IOException { - if (!SignalAccount.userExists(dataPath, username)) { - return; + if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) { + IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair(); + int registrationId = KeyHelper.generateRegistrationId(false); + + ProfileKey profileKey = KeyUtils.createProfileKey(); + SignalAccount account = SignalAccount.create(pathConfig.getDataPath(), username, identityKey, registrationId, profileKey); + account.save(); + + return new Manager(account, pathConfig, serviceConfiguration, userAgent); } - account = SignalAccount.load(dataPath, username); - migrateLegacyConfigs(); + SignalAccount account = SignalAccount.load(pathConfig.getDataPath(), username); - accountManager = getSignalServiceAccountManager(); - try { - if (account.isRegistered() && accountManager.getPreKeysCount() < BaseConfig.PREKEY_MINIMUM_COUNT) { - refreshPreKeys(); - account.save(); - } - } catch (AuthorizationFailedException e) { - System.err.println("Authorization failed, was the number registered elsewhere?"); - throw e; - } + Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent); + + m.migrateLegacyConfigs(); + + return m; } private void migrateLegacyConfigs() { @@ -211,10 +249,10 @@ public class Manager implements Signal { if (JsonGroupStore.groupsWithLegacyAvatarId.size() > 0) { for (GroupInfo g : JsonGroupStore.groupsWithLegacyAvatarId) { File avatarFile = getGroupAvatarFile(g.groupId); - File attachmentFile = getAttachmentFile(g.getAvatarId()); + File attachmentFile = getAttachmentFile(new SignalServiceAttachmentRemoteId(g.getAvatarId())); if (!avatarFile.exists() && attachmentFile.exists()) { try { - IOUtils.createPrivateDirectories(avatarsPath); + IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); Files.copy(attachmentFile.toPath(), avatarFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } catch (Exception e) { // Ignore @@ -231,33 +269,34 @@ public class Manager implements Signal { } } - private void createNewIdentity() throws IOException { - IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair(); - int registrationId = KeyHelper.generateRegistrationId(false); - if (username == null) { - account = SignalAccount.createTemporaryAccount(identityKey, registrationId); - } else { - byte[] profileKey = KeyUtils.createProfileKey(); - account = SignalAccount.create(dataPath, username, identityKey, registrationId, profileKey); - account.save(); + public void checkAccountState() throws IOException { + if (account.isRegistered()) { + if (accountManager.getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) { + refreshPreKeys(); + account.save(); + } + if (account.getUuid() == null) { + account.setUuid(accountManager.getOwnUuid()); + account.save(); + } } } public boolean isRegistered() { - return account != null && account.isRegistered(); + return account.isRegistered(); } public void register(boolean voiceVerification) throws IOException { - if (account == null) { - createNewIdentity(); - } account.setPassword(KeyUtils.createPassword()); - accountManager = getSignalServiceAccountManager(); + + // Resetting UUID, because registering doesn't work otherwise + account.setUuid(null); + accountManager = createSignalServiceAccountManager(); if (voiceVerification) { - accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.absent(), Optional.absent()); + accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.absent(), Optional.absent()); } else { - accountManager.requestSmsVerificationCode(false, Optional.absent(), Optional.absent()); + accountManager.requestSmsVerificationCode(false, Optional.absent(), Optional.absent()); } account.setRegistered(false); @@ -265,71 +304,25 @@ public class Manager implements Signal { } public void updateAccountAttributes() throws IOException { - accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), getSelfUnidentifiedAccessKey(), false); + accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities, discoverableByPhoneNumber); } - public void setProfileName(String name) throws IOException { - accountManager.setProfileName(account.getProfileKey(), name); - } - - public void setProfileAvatar(File avatar) throws IOException { - final StreamDetails streamDetails = Utils.createStreamDetailsFromFile(avatar); - accountManager.setProfileAvatar(account.getProfileKey(), streamDetails); - streamDetails.getStream().close(); - } - - public void removeProfileAvatar() throws IOException { - accountManager.setProfileAvatar(account.getProfileKey(), null); + public void setProfile(String name, File avatar) throws IOException { + try (final StreamDetails streamDetails = avatar == null ? null : Utils.createStreamDetailsFromFile(avatar)) { + accountManager.setVersionedProfile(account.getUuid(), account.getProfileKey(), name, streamDetails); + } } public void unregister() throws IOException { // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. // If this is the master 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. - accountManager.setGcmId(Optional.absent()); + accountManager.setGcmId(Optional.absent()); account.setRegistered(false); account.save(); } - public String getDeviceLinkUri() throws TimeoutException, IOException { - if (account == null) { - createNewIdentity(); - } - account.setPassword(KeyUtils.createPassword()); - accountManager = getSignalServiceAccountManager(); - String uuid = accountManager.getNewDeviceUuid(); - - return Utils.createDeviceLinkUri(new Utils.DeviceLinkInfo(uuid, getIdentity().getPublicKey())); - } - - public void finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists { - account.setSignalingKey(KeyUtils.createSignalingKey()); - SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(account.getSignalProtocolStore().getIdentityKeyPair(), account.getSignalingKey(), false, true, account.getSignalProtocolStore().getLocalRegistrationId(), deviceName); - - username = ret.getNumber(); - // TODO do this check before actually registering - if (SignalAccount.userExists(dataPath, username)) { - throw new UserAlreadyExists(username, SignalAccount.getFileName(dataPath, username)); - } - - // Create new account with the synced identity - byte[] profileKey = ret.getProfileKey(); - if (profileKey == null) { - profileKey = KeyUtils.createProfileKey(); - } - account = SignalAccount.createLinkedAccount(dataPath, username, account.getPassword(), ret.getDeviceId(), ret.getIdentity(), account.getSignalProtocolStore().getLocalRegistrationId(), account.getSignalingKey(), profileKey); - - refreshPreKeys(); - - requestSyncGroups(); - requestSyncContacts(); - requestSyncBlocked(); - requestSyncConfiguration(); - - account.save(); - } - public List getLinkedDevices() throws IOException { List devices = accountManager.getDevices(); account.setMultiDevice(devices.size() > 1); @@ -351,19 +344,19 @@ public class Manager implements Signal { } private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { - IdentityKeyPair identityKeyPair = account.getSignalProtocolStore().getIdentityKeyPair(); + IdentityKeyPair identityKeyPair = getIdentityKeyPair(); String verificationCode = accountManager.getNewDeviceVerificationCode(); - accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.of(account.getProfileKey()), verificationCode); + accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.of(account.getProfileKey().serialize()), verificationCode); account.setMultiDevice(true); account.save(); } private List generatePreKeys() { - List records = new ArrayList<>(BaseConfig.PREKEY_BATCH_SIZE); + List records = new ArrayList<>(ServiceConfig.PREKEY_BATCH_SIZE); final int offset = account.getPreKeyIdOffset(); - for (int i = 0; i < BaseConfig.PREKEY_BATCH_SIZE; i++) { + for (int i = 0; i < ServiceConfig.PREKEY_BATCH_SIZE; i++) { int preKeyId = (offset + i) % Medium.MAX_VALUE; ECKeyPair keyPair = Curve.generateKeyPair(); PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair); @@ -396,41 +389,107 @@ public class Manager implements Signal { verificationCode = verificationCode.replace("-", ""); account.setSignalingKey(KeyUtils.createSignalingKey()); // TODO make unrestricted unidentified access configurable - accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, getSelfUnidentifiedAccessKey(), false); + VerifyAccountResponse response = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, capabilities, discoverableByPhoneNumber); + UUID uuid = UuidUtil.parseOrNull(response.getUuid()); + // TODO response.isStorageCapable() //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); account.setRegistered(true); + account.setUuid(uuid); account.setRegistrationLockPin(pin); + account.getSignalProtocolStore().saveIdentity(account.getSelfAddress(), getIdentityKeyPair().getPublicKey(), TrustLevel.TRUSTED_VERIFIED); refreshPreKeys(); account.save(); } public void setRegistrationLockPin(Optional pin) throws IOException { - accountManager.setPin(pin); if (pin.isPresent()) { account.setRegistrationLockPin(pin.get()); + throw new RuntimeException("Not implemented anymore, will be replaced with KBS"); } else { account.setRegistrationLockPin(null); + accountManager.removeRegistrationLockV1(); } account.save(); } - private void refreshPreKeys() throws IOException { + void refreshPreKeys() throws IOException { List oneTimePreKeys = generatePreKeys(); - final IdentityKeyPair identityKeyPair = account.getSignalProtocolStore().getIdentityKeyPair(); + final IdentityKeyPair identityKeyPair = getIdentityKeyPair(); SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(identityKeyPair); - accountManager.setPreKeys(getIdentity(), signedPreKeyRecord, oneTimePreKeys); + accountManager.setPreKeys(identityKeyPair.getPublicKey(), signedPreKeyRecord, oneTimePreKeys); } private SignalServiceMessageReceiver getMessageReceiver() { - return new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, null, username, account.getPassword(), account.getDeviceId(), account.getSignalingKey(), BaseConfig.USER_AGENT, null, timer); + // TODO implement ZkGroup support + final ClientZkProfileOperations clientZkProfileOperations = null; + return new SignalServiceMessageReceiver(serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), account.getSignalingKey(), userAgent, null, timer, clientZkProfileOperations); } private SignalServiceMessageSender getMessageSender() { - return new SignalServiceMessageSender(BaseConfig.serviceConfiguration, null, username, account.getPassword(), - account.getDeviceId(), account.getSignalProtocolStore(), BaseConfig.USER_AGENT, account.isMultiDevice(), Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent()); + // TODO implement ZkGroup support + final ClientZkProfileOperations clientZkProfileOperations = null; + final boolean attachmentsV3 = false; + final ExecutorService executor = null; + return new SignalServiceMessageSender(serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), + account.getDeviceId(), account.getSignalProtocolStore(), userAgent, account.isMultiDevice(), attachmentsV3, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent(), clientZkProfileOperations, executor); + } + + private SignalServiceProfile getEncryptedRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess) throws IOException { + SignalServiceMessagePipe pipe = unidentifiedMessagePipe != null && unidentifiedAccess.isPresent() ? unidentifiedMessagePipe + : messagePipe; + + if (pipe != null) { + try { + return pipe.getProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS).getProfile(); + } catch (IOException | InterruptedException | ExecutionException | TimeoutException ignored) { + } + } + + SignalServiceMessageReceiver receiver = getMessageReceiver(); + try { + return receiver.retrieveProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS).getProfile(); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new IOException("Failed to retrieve profile", e); + } + } + + private SignalProfile getRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess, ProfileKey profileKey) throws IOException { + SignalProfileEntry profileEntry = account.getProfileStore().getProfile(address); + long now = new Date().getTime(); + // Profiles are cache for 24h before retrieving them again + if (profileEntry == null || profileEntry.getProfile() == null || now - profileEntry.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000) { + SignalProfile profile = retrieveRecipientProfile(address, unidentifiedAccess, profileKey); + account.getProfileStore().updateProfile(address, profileKey, now, profile); + return profile; + } + return profileEntry.getProfile(); + } + + private SignalProfile retrieveRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess, ProfileKey profileKey) throws IOException { + final SignalServiceProfile encryptedProfile = getEncryptedRecipientProfile(address, unidentifiedAccess); + + File avatarFile = null; + try { + avatarFile = encryptedProfile.getAvatar() == null ? null : retrieveProfileAvatar(address, encryptedProfile.getAvatar(), profileKey); + } catch (Throwable e) { + System.err.println("Failed to retrieve profile avatar, ignoring: " + e.getMessage()); + } + + ProfileCipher profileCipher = new ProfileCipher(profileKey); + try { + return new SignalProfile( + encryptedProfile.getIdentityKey(), + encryptedProfile.getName() == null ? null : new String(profileCipher.decryptName(Base64.decode(encryptedProfile.getName()))), + avatarFile, + encryptedProfile.getUnidentifiedAccess() == null || !profileCipher.verifyUnidentifiedAccess(Base64.decode(encryptedProfile.getUnidentifiedAccess())) ? null : encryptedProfile.getUnidentifiedAccess(), + encryptedProfile.isUnrestrictedUnidentifiedAccess(), + encryptedProfile.getCapabilities()); + } catch (InvalidCiphertextException e) { + return null; + } } private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { @@ -456,22 +515,19 @@ public class Manager implements Signal { if (g == null) { throw new GroupNotFoundException(groupId); } - for (String member : g.members) { - if (member.equals(this.username)) { - return g; - } + if (!g.isMember(account.getSelfAddress())) { + throw new NotAGroupMemberException(groupId, g.name); } - throw new NotAGroupMemberException(groupId, g.name); + return g; } public List getGroups() { return account.getGroupStore().getGroups(); } - @Override - public void sendGroupMessage(String messageText, List attachments, + public long sendGroupMessage(String messageText, List attachments, byte[] groupId) - throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(Utils.getSignalServiceAttachments(attachments)); @@ -482,20 +538,31 @@ public class Manager implements Signal { .build(); messageBuilder.asGroupMessage(group); } - ThreadInfo thread = account.getThreadStore().getThread(Base64.encodeBytes(groupId)); - if (thread != null) { - messageBuilder.withExpiration(thread.messageExpirationTime); - } final GroupInfo g = getGroupForSending(groupId); - // Don't send group message to ourself - final List membersSend = new ArrayList<>(g.members); - membersSend.remove(this.username); - sendMessageLegacy(messageBuilder, membersSend); + messageBuilder.withExpiration(g.messageExpirationTime); + + return sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); } - public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions { + public void sendGroupMessageReaction(String emoji, boolean remove, String targetAuthor, + long targetSentTimestamp, byte[] groupId) + throws IOException, EncapsulatedExceptions, InvalidNumberException, NotAGroupMemberException, GroupNotFoundException { + SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, canonicalizeAndResolveSignalServiceAddress(targetAuthor), targetSentTimestamp); + final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() + .withReaction(reaction); + if (groupId != null) { + SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER) + .withId(groupId) + .build(); + messageBuilder.asGroupMessage(group); + } + final GroupInfo g = getGroupForSending(groupId); + sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); + } + + public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, NotAGroupMemberException { SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) .withId(groupId) .build(); @@ -504,18 +571,18 @@ public class Manager implements Signal { .asGroupMessage(group); final GroupInfo g = getGroupForSending(groupId); - g.members.remove(this.username); + g.removeMember(account.getSelfAddress()); account.getGroupStore().updateGroup(g); - sendMessageLegacy(messageBuilder, g.members); + sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); } - private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { GroupInfo g; if (groupId == null) { // Create new group g = new GroupInfo(KeyUtils.createGroupId()); - g.members.add(username); + g.addMembers(Collections.singleton(account.getSelfAddress())); } else { g = getGroupForSending(groupId); } @@ -525,35 +592,28 @@ public class Manager implements Signal { } if (members != null) { - Set newMembers = new HashSet<>(); - for (String member : members) { - try { - member = Utils.canonicalizeNumber(member, username); - } catch (InvalidNumberException e) { - System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage()); - System.err.println("Aborting…"); - System.exit(1); - } - if (g.members.contains(member)) { + final Set newE164Members = new HashSet<>(); + for (SignalServiceAddress member : members) { + if (g.isMember(member) || !member.getNumber().isPresent()) { continue; } - newMembers.add(member); - g.members.add(member); + newE164Members.add(member.getNumber().get()); } - final List contacts = accountManager.getContacts(newMembers); - if (contacts.size() != newMembers.size()) { + + final List contacts = accountManager.getContacts(newE164Members); + if (contacts.size() != newE164Members.size()) { // Some of the new members are not registered on Signal for (ContactTokenDetails contact : contacts) { - newMembers.remove(contact.getNumber()); + newE164Members.remove(contact.getNumber()); } - System.err.println("Failed to add members " + Util.join(", ", newMembers) + " to group: Not registered on Signal"); - System.err.println("Aborting…"); - System.exit(1); + throw new IOException("Failed to add members " + Util.join(", ", newE164Members) + " to group: Not registered on Signal"); } + + g.addMembers(members); } if (avatarFile != null) { - IOUtils.createPrivateDirectories(avatarsPath); + IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); File aFile = getGroupAvatarFile(g.groupId); Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } @@ -562,32 +622,27 @@ public class Manager implements Signal { SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g); - // Don't send group message to ourself - final List membersSend = new ArrayList<>(g.members); - membersSend.remove(this.username); - sendMessageLegacy(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); return g.groupId; } - private void sendUpdateGroupMessage(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions { + void sendUpdateGroupMessage(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { if (groupId == null) { return; } GroupInfo g = getGroupForSending(groupId); - if (!g.members.contains(recipient)) { + if (!g.isMember(recipient)) { return; } SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g); // Send group message only to the recipient who requested it - final List membersSend = new ArrayList<>(); - membersSend.add(recipient); - sendMessageLegacy(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, Collections.singleton(recipient)); } - private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) { + private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) throws AttachmentInvalidException { SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE) .withId(g.groupId) .withName(g.name) @@ -602,18 +657,12 @@ public class Manager implements Signal { } } - SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() - .asGroupMessage(group.build()); - - ThreadInfo thread = account.getThreadStore().getThread(Base64.encodeBytes(g.groupId)); - if (thread != null) { - messageBuilder.withExpiration(thread.messageExpirationTime); - } - - return messageBuilder; + return SignalServiceDataMessage.newBuilder() + .asGroupMessage(group.build()) + .withExpiration(g.messageExpirationTime); } - private void sendGroupInfoRequest(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions { + void sendGroupInfoRequest(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions { if (groupId == null) { return; } @@ -624,29 +673,21 @@ public class Manager implements Signal { SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .asGroupMessage(group.build()); - ThreadInfo thread = account.getThreadStore().getThread(Base64.encodeBytes(groupId)); - if (thread != null) { - messageBuilder.withExpiration(thread.messageExpirationTime); - } - // Send group info request message to the recipient who sent us a message with this groupId - final List membersSend = new ArrayList<>(); - membersSend.add(recipient); - sendMessageLegacy(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, Collections.singleton(recipient)); } - @Override - public void sendMessage(String message, List attachments, String recipient) - throws EncapsulatedExceptions, AttachmentInvalidException, IOException { - List recipients = new ArrayList<>(1); - recipients.add(recipient); - sendMessage(message, attachments, recipients); + void sendReceipt(SignalServiceAddress remoteAddress, long messageId) throws IOException, UntrustedIdentityException { + SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY, + Collections.singletonList(messageId), + System.currentTimeMillis()); + + getMessageSender().sendReceipt(remoteAddress, getAccessFor(remoteAddress), receiptMessage); } - @Override - public void sendMessage(String messageText, List attachments, + public long sendMessage(String messageText, List attachments, List recipients) - throws IOException, EncapsulatedExceptions, AttachmentInvalidException { + throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { List attachmentStreams = Utils.getSignalServiceAttachments(attachments); @@ -664,22 +705,36 @@ public class Manager implements Signal { messageBuilder.withAttachments(attachmentPointers); } - messageBuilder.withProfileKey(account.getProfileKey()); - sendMessageLegacy(messageBuilder, recipients); + return sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients)); } - @Override - public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions { + public void sendMessageReaction(String emoji, boolean remove, String targetAuthor, + long targetSentTimestamp, List recipients) + throws IOException, EncapsulatedExceptions, InvalidNumberException { + SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, canonicalizeAndResolveSignalServiceAddress(targetAuthor), targetSentTimestamp); + final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() + .withReaction(reaction); + sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients)); + } + + public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions, InvalidNumberException { SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .asEndSessionMessage(); - sendMessageLegacy(messageBuilder, recipients); + final Collection signalServiceAddresses = getSignalServiceAddresses(recipients); + try { + sendMessageLegacy(messageBuilder, signalServiceAddresses); + } catch (Exception e) { + for (SignalServiceAddress address : signalServiceAddresses) { + handleEndSession(address); + } + account.save(); + throw e; + } } - @Override public String getContactName(String number) throws InvalidNumberException { - String canonicalizedNumber = Utils.canonicalizeNumber(number, username); - ContactInfo contact = account.getContactStore().getContact(canonicalizedNumber); + ContactInfo contact = account.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number)); if (contact == null) { return ""; } else { @@ -687,111 +742,200 @@ public class Manager implements Signal { } } - @Override public void setContactName(String number, String name) throws InvalidNumberException { - String canonicalizedNumber = Utils.canonicalizeNumber(number, username); - ContactInfo contact = account.getContactStore().getContact(canonicalizedNumber); + final SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(number); + ContactInfo contact = account.getContactStore().getContact(address); if (contact == null) { - contact = new ContactInfo(); - contact.number = canonicalizedNumber; - System.err.println("Add contact " + canonicalizedNumber + " named " + name); - } else { - System.err.println("Updating contact " + canonicalizedNumber + " name " + contact.name + " -> " + name); + contact = new ContactInfo(address); } contact.name = name; account.getContactStore().updateContact(contact); account.save(); } - @Override public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException { - number = Utils.canonicalizeNumber(number, username); - ContactInfo contact = account.getContactStore().getContact(number); + setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number), blocked); + } + + private void setContactBlocked(SignalServiceAddress address, boolean blocked) { + ContactInfo contact = account.getContactStore().getContact(address); if (contact == null) { - contact = new ContactInfo(); - contact.number = number; - System.err.println("Adding and " + (blocked ? "blocking" : "unblocking") + " contact " + number); - } else { - System.err.println((blocked ? "Blocking" : "Unblocking") + " contact " + number); + contact = new ContactInfo(address); } contact.blocked = blocked; account.getContactStore().updateContact(contact); account.save(); } - @Override public void setGroupBlocked(final byte[] groupId, final boolean blocked) throws GroupNotFoundException { GroupInfo group = getGroup(groupId); if (group == null) { throw new GroupNotFoundException(groupId); - } else { - System.err.println((blocked ? "Blocking" : "Unblocking") + " group " + Base64.encodeBytes(groupId)); - group.blocked = blocked; - account.getGroupStore().updateGroup(group); - account.save(); } + + group.blocked = blocked; + account.getGroupStore().updateGroup(group); + account.save(); } - @Override - public List getGroupIds() { - List groups = getGroups(); - List ids = new ArrayList<>(groups.size()); - for (GroupInfo group : groups) { - ids.add(group.groupId); - } - return ids; - } - - @Override - public String getGroupName(byte[] groupId) { - GroupInfo group = getGroup(groupId); - if (group == null) { - return ""; - } else { - return group.name; - } - } - - @Override - public List getGroupMembers(byte[] groupId) { - GroupInfo group = getGroup(groupId); - if (group == null) { - return new ArrayList<>(); - } else { - return new ArrayList<>(group.members); - } - } - - @Override - public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { if (groupId.length == 0) { groupId = null; } if (name.isEmpty()) { name = null; } - if (members.size() == 0) { + if (members.isEmpty()) { members = null; } if (avatar.isEmpty()) { avatar = null; } - return sendUpdateGroupMessage(groupId, name, members, avatar); + return sendUpdateGroupMessage(groupId, name, members == null ? null : getSignalServiceAddresses(members), avatar); } /** - * Change the expiration timer for a thread (number of groupId) - * - * @param numberOrGroupId - * @param messageExpirationTimer + * Change the expiration timer for a contact */ - public void setExpirationTimer(String numberOrGroupId, int messageExpirationTimer) { - ThreadInfo thread = account.getThreadStore().getThread(numberOrGroupId); - thread.messageExpirationTime = messageExpirationTimer; - account.getThreadStore().updateThread(thread); + public void setExpirationTimer(SignalServiceAddress address, int messageExpirationTimer) throws IOException { + ContactInfo contact = account.getContactStore().getContact(address); + contact.messageExpirationTime = messageExpirationTimer; + account.getContactStore().updateContact(contact); + sendExpirationTimerUpdate(address); + account.save(); } - private void requestSyncGroups() throws IOException { + private void sendExpirationTimerUpdate(SignalServiceAddress address) throws IOException { + final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() + .asExpirationUpdate(); + sendMessage(messageBuilder, Collections.singleton(address)); + } + + /** + * Change the expiration timer for a contact + */ + public void setExpirationTimer(String number, int messageExpirationTimer) throws IOException, InvalidNumberException { + SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(number); + setExpirationTimer(address, messageExpirationTimer); + } + + /** + * Change the expiration timer for a group + */ + public void setExpirationTimer(byte[] groupId, int messageExpirationTimer) { + GroupInfo g = account.getGroupStore().getGroup(groupId); + g.messageExpirationTime = messageExpirationTimer; + account.getGroupStore().updateGroup(g); + } + + /** + * Upload the sticker pack from path. + * + * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file + * @return if successful, returns the URL to install the sticker pack in the signal app + */ + public String uploadStickerPack(String path) throws IOException, StickerPackInvalidException { + SignalServiceStickerManifestUpload manifest = getSignalServiceStickerManifestUpload(path); + + SignalServiceMessageSender messageSender = getMessageSender(); + + byte[] packKey = KeyUtils.createStickerUploadKey(); + String packId = messageSender.uploadStickerManifest(manifest, packKey); + + try { + return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder.encode(packId, "utf-8") + "&pack_key=" + URLEncoder.encode(Hex.toStringCondensed(packKey), "utf-8")) + .toString(); + } catch (URISyntaxException e) { + throw new AssertionError(e); + } + } + + private SignalServiceStickerManifestUpload getSignalServiceStickerManifestUpload(final String path) throws IOException, StickerPackInvalidException { + ZipFile zip = null; + String rootPath = null; + + final File file = new File(path); + if (file.getName().endsWith(".zip")) { + zip = new ZipFile(file); + } else if (file.getName().equals("manifest.json")) { + rootPath = file.getParent(); + } else { + throw new StickerPackInvalidException("Could not find manifest.json"); + } + + JsonStickerPack pack = parseStickerPack(rootPath, zip); + + if (pack.stickers == null) { + throw new StickerPackInvalidException("Must set a 'stickers' field."); + } + + if (pack.stickers.isEmpty()) { + throw new StickerPackInvalidException("Must include stickers."); + } + + List stickers = new ArrayList<>(pack.stickers.size()); + for (JsonStickerPack.JsonSticker sticker : pack.stickers) { + if (sticker.file == null) { + throw new StickerPackInvalidException("Must set a 'file' field on each sticker."); + } + + Pair data; + try { + data = getInputStreamAndLength(rootPath, zip, sticker.file); + } catch (IOException ignored) { + throw new StickerPackInvalidException("Could not find find " + sticker.file); + } + + String contentType = Utils.getFileMimeType(new File(sticker.file), null); + StickerInfo stickerInfo = new StickerInfo(data.first(), data.second(), Optional.fromNullable(sticker.emoji).or(""), contentType); + stickers.add(stickerInfo); + } + + StickerInfo cover = null; + if (pack.cover != null) { + if (pack.cover.file == null) { + throw new StickerPackInvalidException("Must set a 'file' field on the cover."); + } + + Pair data; + try { + data = getInputStreamAndLength(rootPath, zip, pack.cover.file); + } catch (IOException ignored) { + throw new StickerPackInvalidException("Could not find find " + pack.cover.file); + } + + String contentType = Utils.getFileMimeType(new File(pack.cover.file), null); + cover = new StickerInfo(data.first(), data.second(), Optional.fromNullable(pack.cover.emoji).or(""), contentType); + } + + return new SignalServiceStickerManifestUpload( + pack.title, + pack.author, + cover, + stickers); + } + + private static JsonStickerPack parseStickerPack(String rootPath, ZipFile zip) throws IOException { + InputStream inputStream; + if (zip != null) { + inputStream = zip.getInputStream(zip.getEntry("manifest.json")); + } else { + inputStream = new FileInputStream((new File(rootPath, "manifest.json"))); + } + return new ObjectMapper().readValue(inputStream, JsonStickerPack.class); + } + + private static Pair getInputStreamAndLength(final String rootPath, final ZipFile zip, final String subfile) throws IOException { + if (zip != null) { + final ZipEntry entry = zip.getEntry(subfile); + return new Pair<>(zip.getInputStream(entry), entry.getSize()); + } else { + final File file = new File(rootPath, subfile); + return new Pair<>(new FileInputStream(file), file.length()); + } + } + + void requestSyncGroups() throws IOException { SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build(); SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); try { @@ -801,7 +945,7 @@ public class Manager implements Signal { } } - private void requestSyncContacts() throws IOException { + void requestSyncContacts() throws IOException { SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build(); SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); try { @@ -811,7 +955,7 @@ public class Manager implements Signal { } } - private void requestSyncBlocked() throws IOException { + void requestSyncBlocked() throws IOException { SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED).build(); SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); try { @@ -821,7 +965,7 @@ public class Manager implements Signal { } } - private void requestSyncConfiguration() throws IOException { + void requestSyncConfiguration() throws IOException { SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION).build(); SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); try { @@ -831,30 +975,106 @@ public class Manager implements Signal { } } + private byte[] getSenderCertificate() { + // TODO support UUID capable sender certificates + // byte[] certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy(); + byte[] certificate; + try { + certificate = accountManager.getSenderCertificate(); + } catch (IOException e) { + System.err.println("Failed to get sender certificate: " + e); + return null; + } + // TODO cache for a day + return certificate; + } + private byte[] getSelfUnidentifiedAccessKey() { return UnidentifiedAccess.deriveAccessKeyFrom(account.getProfileKey()); } private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) { - // TODO implement - return null; + ContactInfo contact = account.getContactStore().getContact(recipient); + if (contact == null || contact.profileKey == null) { + return null; + } + ProfileKey theirProfileKey; + try { + theirProfileKey = new ProfileKey(Base64.decode(contact.profileKey)); + } catch (InvalidInputException | IOException e) { + throw new AssertionError(e); + } + SignalProfile targetProfile; + try { + targetProfile = getRecipientProfile(recipient, Optional.absent(), theirProfileKey); + } catch (IOException e) { + System.err.println("Failed to get recipient profile: " + e); + return null; + } + + if (targetProfile == null || targetProfile.getUnidentifiedAccess() == null) { + return null; + } + + if (targetProfile.isUnrestrictedUnidentifiedAccess()) { + return KeyUtils.createUnrestrictedUnidentifiedAccess(); + } + + return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey); } private Optional getAccessForSync() { - // TODO implement - return Optional.absent(); + byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(); + byte[] selfUnidentifiedAccessCertificate = getSenderCertificate(); + + if (selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) { + return Optional.absent(); + } + + try { + return Optional.of(new UnidentifiedAccessPair( + new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate), + new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate) + )); + } catch (InvalidCertificateException e) { + return Optional.absent(); + } } private List> getAccessFor(Collection recipients) { List> result = new ArrayList<>(recipients.size()); for (SignalServiceAddress recipient : recipients) { - result.add(Optional.absent()); + result.add(getAccessFor(recipient)); } return result; } private Optional getAccessFor(SignalServiceAddress recipient) { - // TODO implement + byte[] recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient); + byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(); + byte[] selfUnidentifiedAccessCertificate = getSenderCertificate(); + + if (recipientUnidentifiedAccessKey == null || selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) { + return Optional.absent(); + } + + try { + return Optional.of(new UnidentifiedAccessPair( + new UnidentifiedAccess(recipientUnidentifiedAccessKey, selfUnidentifiedAccessCertificate), + new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate) + )); + } catch (InvalidCertificateException e) { + return Optional.absent(); + } + } + + private Optional getUnidentifiedAccess(SignalServiceAddress recipient) { + Optional unidentifiedAccess = getAccessFor(recipient); + + if (unidentifiedAccess.isPresent()) { + return unidentifiedAccess.get().getTargetUnidentifiedAccess(); + } + return Optional.absent(); } @@ -864,7 +1084,7 @@ public class Manager implements Signal { try { messageSender.sendMessage(message, getAccessForSync()); } catch (UntrustedIdentityException e) { - account.getSignalProtocolStore().saveIdentity(e.getIdentifier(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED); throw e; } } @@ -872,8 +1092,10 @@ public class Manager implements Signal { /** * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult. */ - private void sendMessageLegacy(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) + private long sendMessageLegacy(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) throws EncapsulatedExceptions, IOException { + final long timestamp = System.currentTimeMillis(); + messageBuilder.withTimestamp(timestamp); List results = sendMessage(messageBuilder, recipients); List untrustedIdentities = new LinkedList<>(); @@ -882,115 +1104,141 @@ public class Manager implements Signal { for (SendMessageResult result : results) { if (result.isUnregisteredFailure()) { - unregisteredUsers.add(new UnregisteredUserException(result.getAddress().getNumber().get(), null)); + unregisteredUsers.add(new UnregisteredUserException(result.getAddress().getLegacyIdentifier(), null)); } else if (result.isNetworkFailure()) { - networkExceptions.add(new NetworkFailureException(result.getAddress().getNumber().get(), null)); + networkExceptions.add(new NetworkFailureException(result.getAddress().getLegacyIdentifier(), null)); } else if (result.getIdentityFailure() != null) { - untrustedIdentities.add(new UntrustedIdentityException("Untrusted", result.getAddress().getNumber().get(), result.getIdentityFailure().getIdentityKey())); + untrustedIdentities.add(new UntrustedIdentityException("Untrusted", result.getAddress().getLegacyIdentifier(), result.getIdentityFailure().getIdentityKey())); } } if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) { throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions); } + return timestamp; } - private List sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) - throws IOException { - Set recipientsTS = Utils.getSignalServiceAddresses(recipients, username); - if (recipientsTS == null) { - account.save(); - return Collections.emptyList(); - } + private Collection getSignalServiceAddresses(Collection numbers) throws InvalidNumberException { + final Set signalServiceAddresses = new HashSet<>(numbers.size()); + for (String number : numbers) { + signalServiceAddresses.add(canonicalizeAndResolveSignalServiceAddress(number)); + } + return signalServiceAddresses; + } + + private List sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) + throws IOException { + if (messagePipe == null) { + messagePipe = getMessageReceiver().createMessagePipe(); + } + if (unidentifiedMessagePipe == null) { + unidentifiedMessagePipe = getMessageReceiver().createUnidentifiedMessagePipe(); + } SignalServiceDataMessage message = null; try { - SignalServiceMessageSender messageSender = getMessageSender(); - message = messageBuilder.build(); - if (message.getGroupInfo().isPresent()) { + if (message.getGroupContext().isPresent()) { try { + SignalServiceMessageSender messageSender = getMessageSender(); final boolean isRecipientUpdate = false; - List result = messageSender.sendMessage(new ArrayList<>(recipientsTS), getAccessFor(recipientsTS), isRecipientUpdate, message); + List result = messageSender.sendMessage(new ArrayList<>(recipients), getAccessFor(recipients), isRecipientUpdate, message); for (SendMessageResult r : result) { if (r.getIdentityFailure() != null) { - account.getSignalProtocolStore().saveIdentity(r.getAddress().getNumber().get(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED); + account.getSignalProtocolStore().saveIdentity(r.getAddress(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED); } } return result; } catch (UntrustedIdentityException e) { - account.getSignalProtocolStore().saveIdentity(e.getIdentifier(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED); return Collections.emptyList(); } - } else if (recipientsTS.size() == 1 && recipientsTS.contains(getSelfAddress())) { - SignalServiceAddress recipient = getSelfAddress(); - final Optional unidentifiedAccess = getAccessFor(recipient); - SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(recipient), - message.getTimestamp(), - message, - message.getExpiresInSeconds(), - Collections.singletonMap(recipient, unidentifiedAccess.isPresent()), - false); - SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript); - - List results = new ArrayList<>(recipientsTS.size()); - try { - messageSender.sendMessage(syncMessage, unidentifiedAccess); - } catch (UntrustedIdentityException e) { - account.getSignalProtocolStore().saveIdentity(e.getIdentifier(), e.getIdentityKey(), TrustLevel.UNTRUSTED); - results.add(SendMessageResult.identityFailure(recipient, e.getIdentityKey())); - } - return results; } else { // Send to all individually, so sync messages are sent correctly - List results = new ArrayList<>(recipientsTS.size()); - for (SignalServiceAddress address : recipientsTS) { - ThreadInfo thread = account.getThreadStore().getThread(address.getNumber().get()); - if (thread != null) { - messageBuilder.withExpiration(thread.messageExpirationTime); + List results = new ArrayList<>(recipients.size()); + for (SignalServiceAddress address : recipients) { + ContactInfo contact = account.getContactStore().getContact(address); + if (contact != null) { + messageBuilder.withExpiration(contact.messageExpirationTime); + messageBuilder.withProfileKey(account.getProfileKey().serialize()); } else { messageBuilder.withExpiration(0); + messageBuilder.withProfileKey(null); } message = messageBuilder.build(); - try { - SendMessageResult result = messageSender.sendMessage(address, getAccessFor(address), message); - results.add(result); - } catch (UntrustedIdentityException e) { - account.getSignalProtocolStore().saveIdentity(e.getIdentifier(), e.getIdentityKey(), TrustLevel.UNTRUSTED); - results.add(SendMessageResult.identityFailure(address, e.getIdentityKey())); + if (address.matches(account.getSelfAddress())) { + results.add(sendSelfMessage(message)); + } else { + results.add(sendMessage(address, message)); } } return results; } } finally { if (message != null && message.isEndSession()) { - for (SignalServiceAddress recipient : recipientsTS) { - handleEndSession(recipient.getNumber().get()); + for (SignalServiceAddress recipient : recipients) { + handleEndSession(recipient); } } account.save(); } } - private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, ProtocolUntrustedIdentityException, SelfSendException, UnsupportedDataMessageException { - SignalServiceCipher cipher = new SignalServiceCipher(getSelfAddress(), account.getSignalProtocolStore(), Utils.getCertificateValidator()); + private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) throws IOException { + SignalServiceMessageSender messageSender = getMessageSender(); + + SignalServiceAddress recipient = account.getSelfAddress(); + + final Optional unidentifiedAccess = getAccessFor(recipient); + SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(recipient), + message.getTimestamp(), + message, + message.getExpiresInSeconds(), + Collections.singletonMap(recipient, unidentifiedAccess.isPresent()), + false); + SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript); + try { - return cipher.decrypt(envelope); - } catch (ProtocolUntrustedIdentityException e) { - // TODO We don't get the new untrusted identity from ProtocolUntrustedIdentityException anymore ... we need to get it from somewhere else -// account.getSignalProtocolStore().saveIdentity(e.getSender(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED); - throw e; + messageSender.sendMessage(syncMessage, unidentifiedAccess); + return SendMessageResult.success(recipient, unidentifiedAccess.isPresent(), false); + } catch (UntrustedIdentityException e) { + account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED); + return SendMessageResult.identityFailure(recipient, e.getIdentityKey()); } } - private void handleEndSession(String source) { + private SendMessageResult sendMessage(SignalServiceAddress address, SignalServiceDataMessage message) throws IOException { + SignalServiceMessageSender messageSender = getMessageSender(); + + try { + return messageSender.sendMessage(address, getAccessFor(address), message); + } catch (UntrustedIdentityException e) { + account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED); + return SendMessageResult.identityFailure(address, e.getIdentityKey()); + } + } + + private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, org.whispersystems.libsignal.UntrustedIdentityException { + SignalServiceCipher cipher = new SignalServiceCipher(account.getSelfAddress(), account.getSignalProtocolStore(), Utils.getCertificateValidator()); + try { + return cipher.decrypt(envelope); + } catch (ProtocolUntrustedIdentityException e) { + if (e.getCause() instanceof org.whispersystems.libsignal.UntrustedIdentityException) { + org.whispersystems.libsignal.UntrustedIdentityException identityException = (org.whispersystems.libsignal.UntrustedIdentityException) e.getCause(); + account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(identityException.getName()), identityException.getUntrustedIdentity(), TrustLevel.UNTRUSTED); + throw identityException; + } + throw new AssertionError(e); + } + } + + private void handleEndSession(SignalServiceAddress source) { account.getSignalProtocolStore().deleteAllSessions(source); } - private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, SignalServiceAddress destination, boolean ignoreAttachments) { - String threadId; - if (message.getGroupInfo().isPresent()) { - SignalServiceGroup groupInfo = message.getGroupInfo().get(); - threadId = Base64.encodeBytes(groupInfo.getGroupId()); + private List handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, SignalServiceAddress source, SignalServiceAddress destination, boolean ignoreAttachments) { + List actions = new ArrayList<>(); + if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) { + SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId()); switch (groupInfo.getType()) { case UPDATE: @@ -1003,8 +1251,8 @@ public class Manager implements Signal { if (avatar.isPointer()) { try { retrieveGroupAvatarAttachment(avatar.asPointer(), group.groupId); - } catch (IOException | InvalidMessageException e) { - System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getId() + "): " + e.getMessage()); + } catch (IOException | InvalidMessageException | MissingConfigurationException e) { + System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getRemoteId() + "): " + e.getMessage()); } } } @@ -1014,63 +1262,56 @@ public class Manager implements Signal { } if (groupInfo.getMembers().isPresent()) { - group.addMembers(groupInfo.getMembers().get()); + group.addMembers(groupInfo.getMembers().get() + .stream() + .map(this::resolveSignalServiceAddress) + .collect(Collectors.toSet())); } account.getGroupStore().updateGroup(group); break; case DELIVER: - if (group == null) { - try { - sendGroupInfoRequest(groupInfo.getGroupId(), source); - } catch (IOException | EncapsulatedExceptions e) { - e.printStackTrace(); - } + if (group == null && !isSync) { + actions.add(new SendGroupInfoRequestAction(source, groupInfo.getGroupId())); } break; case QUIT: - if (group == null) { - try { - sendGroupInfoRequest(groupInfo.getGroupId(), source); - } catch (IOException | EncapsulatedExceptions e) { - e.printStackTrace(); - } - } else { - group.members.remove(source); + if (group != null) { + group.removeMember(source); account.getGroupStore().updateGroup(group); } break; case REQUEST_INFO: - if (group != null) { - try { - sendUpdateGroupMessage(groupInfo.getGroupId(), source); - } catch (IOException | EncapsulatedExceptions e) { - e.printStackTrace(); - } catch (NotAGroupMemberException e) { - // We have left this group, so don't send a group update message - } + if (group != null && !isSync) { + actions.add(new SendGroupUpdateAction(source, group.groupId)); } break; } - } else { - if (isSync) { - threadId = destination.getNumber().get(); - } else { - threadId = source; - } } + final SignalServiceAddress conversationPartnerAddress = isSync ? destination : source; if (message.isEndSession()) { - handleEndSession(isSync ? destination.getNumber().get() : source); + handleEndSession(conversationPartnerAddress); } if (message.isExpirationUpdate() || message.getBody().isPresent()) { - ThreadInfo thread = account.getThreadStore().getThread(threadId); - if (thread == null) { - thread = new ThreadInfo(); - thread.id = threadId; - } - if (thread.messageExpirationTime != message.getExpiresInSeconds()) { - thread.messageExpirationTime = message.getExpiresInSeconds(); - account.getThreadStore().updateThread(thread); + if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) { + SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); + GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId()); + if (group == null) { + group = new GroupInfo(groupInfo.getGroupId()); + } + if (group.messageExpirationTime != message.getExpiresInSeconds()) { + group.messageExpirationTime = message.getExpiresInSeconds(); + account.getGroupStore().updateGroup(group); + } + } else { + ContactInfo contact = account.getContactStore().getContact(conversationPartnerAddress); + if (contact == null) { + contact = new ContactInfo(conversationPartnerAddress); + } + if (contact.messageExpirationTime != message.getExpiresInSeconds()) { + contact.messageExpirationTime = message.getExpiresInSeconds(); + account.getContactStore().updateContact(contact); + } } } if (message.getAttachments().isPresent() && !ignoreAttachments) { @@ -1078,22 +1319,31 @@ public class Manager implements Signal { if (attachment.isPointer()) { try { retrieveAttachment(attachment.asPointer()); - } catch (IOException | InvalidMessageException e) { - System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage()); + } catch (IOException | InvalidMessageException | MissingConfigurationException e) { + System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getRemoteId() + "): " + e.getMessage()); } } } } if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) { - if (source.equals(username)) { - this.account.setProfileKey(message.getProfileKey().get()); + if (source.matches(account.getSelfAddress())) { + try { + this.account.setProfileKey(new ProfileKey(message.getProfileKey().get())); + } catch (InvalidInputException ignored) { + } + ContactInfo contact = account.getContactStore().getContact(source); + if (contact != null) { + contact.profileKey = Base64.encodeBytes(message.getProfileKey().get()); + account.getContactStore().updateContact(contact); + } + } else { + ContactInfo contact = account.getContactStore().getContact(source); + if (contact == null) { + contact = new ContactInfo(source); + } + contact.profileKey = Base64.encodeBytes(message.getProfileKey().get()); + account.getContactStore().updateContact(contact); } - ContactInfo contact = account.getContactStore().getContact(source); - if (contact == null) { - contact = new ContactInfo(); - contact.number = source; - } - contact.profileKey = Base64.encodeBytes(message.getProfileKey().get()); } if (message.getPreviews().isPresent()) { final List previews = message.getPreviews().get(); @@ -1102,12 +1352,13 @@ public class Manager implements Signal { SignalServiceAttachmentPointer attachment = preview.getImage().get().asPointer(); try { retrieveAttachment(attachment); - } catch (IOException | InvalidMessageException e) { - System.err.println("Failed to retrieve attachment (" + attachment.getId() + "): " + e.getMessage()); + } catch (IOException | InvalidMessageException | MissingConfigurationException e) { + System.err.println("Failed to retrieve attachment (" + attachment.getRemoteId() + "): " + e.getMessage()); } } } } + return actions; } private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { @@ -1117,6 +1368,7 @@ public class Manager implements Signal { } for (final File dir : Objects.requireNonNull(cachePath.listFiles())) { if (!dir.isDirectory()) { + retryFailedReceivedMessage(handler, ignoreAttachments, dir); continue; } @@ -1124,101 +1376,147 @@ public class Manager implements Signal { if (!fileEntry.isFile()) { continue; } - SignalServiceEnvelope envelope; - try { - envelope = Utils.loadEnvelope(fileEntry); - if (envelope == null) { - continue; - } - } catch (IOException e) { - e.printStackTrace(); - continue; - } - SignalServiceContent content = null; - if (!envelope.isReceipt()) { - try { - content = decryptMessage(envelope); - } catch (Exception e) { - continue; - } - handleMessage(envelope, content, ignoreAttachments); - } - account.save(); - handler.handleMessage(envelope, content, null); - try { - Files.delete(fileEntry.toPath()); - } catch (IOException e) { - System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage()); - } + retryFailedReceivedMessage(handler, ignoreAttachments, fileEntry); } // Try to delete directory if empty dir.delete(); } } + private void retryFailedReceivedMessage(final ReceiveMessageHandler handler, final boolean ignoreAttachments, final File fileEntry) { + SignalServiceEnvelope envelope; + try { + envelope = Utils.loadEnvelope(fileEntry); + if (envelope == null) { + return; + } + } catch (IOException e) { + e.printStackTrace(); + return; + } + SignalServiceContent content = null; + if (!envelope.isReceipt()) { + try { + content = decryptMessage(envelope); + } catch (Exception e) { + return; + } + List actions = handleMessage(envelope, content, ignoreAttachments); + for (HandleAction action : actions) { + try { + action.execute(this); + } catch (Throwable e) { + e.printStackTrace(); + } + } + } + account.save(); + handler.handleMessage(envelope, content, null); + try { + Files.delete(fileEntry.toPath()); + } catch (IOException e) { + System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage()); + } + } + public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException { retryFailedReceivedMessages(handler, ignoreAttachments); final SignalServiceMessageReceiver messageReceiver = getMessageReceiver(); - try { - if (messagePipe == null) { - messagePipe = messageReceiver.createMessagePipe(); - } + Set queuedActions = null; - while (true) { - SignalServiceEnvelope envelope; - SignalServiceContent content = null; - Exception exception = null; - final long now = new Date().getTime(); - try { - envelope = messagePipe.read(timeout, unit, new SignalServiceMessagePipe.MessagePipeCallback() { - @Override - public void onMessage(SignalServiceEnvelope envelope) { - // store message on disk, before acknowledging receipt to the server + if (messagePipe == null) { + messagePipe = messageReceiver.createMessagePipe(); + } + + boolean hasCaughtUpWithOldMessages = false; + + while (true) { + SignalServiceEnvelope envelope; + SignalServiceContent content = null; + Exception exception = null; + final long now = new Date().getTime(); + try { + Optional result = messagePipe.readOrEmpty(timeout, unit, envelope1 -> { + // store message on disk, before acknowledging receipt to the server + try { + String source = envelope1.getSourceE164().isPresent() ? envelope1.getSourceE164().get() : ""; + File cacheFile = getMessageCacheFile(source, now, envelope1.getTimestamp()); + Utils.storeEnvelope(envelope1, cacheFile); + } catch (IOException e) { + System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage()); + } + }); + if (result.isPresent()) { + envelope = result.get(); + } else { + // Received indicator that server queue is empty + hasCaughtUpWithOldMessages = true; + + if (queuedActions != null) { + for (HandleAction action : queuedActions) { try { - File cacheFile = getMessageCacheFile(envelope.getSourceE164().get(), now, envelope.getTimestamp()); - Utils.storeEnvelope(envelope, cacheFile); - } catch (IOException e) { - System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage()); + action.execute(this); + } catch (Throwable e) { + e.printStackTrace(); } } - }); - } catch (TimeoutException e) { - if (returnOnTimeout) - return; - continue; - } catch (InvalidVersionException e) { - System.err.println("Ignoring error: " + e.getMessage()); + queuedActions.clear(); + queuedActions = null; + } + + // Continue to wait another timeout for new messages continue; } - if (!envelope.isReceipt()) { - try { - content = decryptMessage(envelope); - } catch (Exception e) { - exception = e; - } - handleMessage(envelope, content, ignoreAttachments); + } catch (TimeoutException e) { + if (returnOnTimeout) + return; + continue; + } catch (InvalidVersionException e) { + System.err.println("Ignoring error: " + e.getMessage()); + continue; + } + if (envelope.hasSource()) { + // Store uuid if we don't have it already + SignalServiceAddress source = envelope.getSourceAddress(); + resolveSignalServiceAddress(source); + } + if (!envelope.isReceipt()) { + try { + content = decryptMessage(envelope); + } catch (Exception e) { + exception = e; } - account.save(); - if (!isMessageBlocked(envelope, content)) { - handler.handleMessage(envelope, content, exception); - } - if (!(exception instanceof ProtocolUntrustedIdentityException)) { - File cacheFile = null; - try { - cacheFile = getMessageCacheFile(envelope.getSourceE164().get(), now, envelope.getTimestamp()); - Files.delete(cacheFile.toPath()); - // Try to delete directory if empty - new File(getMessageCachePath()).delete(); - } catch (IOException e) { - System.err.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage()); + List actions = handleMessage(envelope, content, ignoreAttachments); + if (hasCaughtUpWithOldMessages) { + for (HandleAction action : actions) { + try { + action.execute(this); + } catch (Throwable e) { + e.printStackTrace(); + } } + } else { + if (queuedActions == null) { + queuedActions = new HashSet<>(); + } + queuedActions.addAll(actions); } } - } finally { - if (messagePipe != null) { - messagePipe.shutdown(); - messagePipe = null; + account.save(); + if (!isMessageBlocked(envelope, content)) { + handler.handleMessage(envelope, content, exception); + } + if (!(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) { + File cacheFile = null; + try { + cacheFile = getMessageCacheFile(envelope.getSourceE164().get(), now, envelope.getTimestamp()); + Files.delete(cacheFile.toPath()); + // Try to delete directory if empty + new File(getMessageCachePath()).delete(); + } catch (IOException e) { + System.err.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage()); + } } } } @@ -1232,15 +1530,15 @@ public class Manager implements Signal { } else { return false; } - ContactInfo sourceContact = getContact(source.getNumber().get()); + ContactInfo sourceContact = account.getContactStore().getContact(source); if (sourceContact != null && sourceContact.blocked) { return true; } if (content != null && content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); - if (message.getGroupInfo().isPresent()) { - SignalServiceGroup groupInfo = message.getGroupInfo().get(); + if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) { + SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); GroupInfo group = getGroup(groupInfo.getGroupId()); if (groupInfo.getType() == SignalServiceGroup.Type.DELIVER && group != null && group.blocked) { return true; @@ -1250,7 +1548,8 @@ public class Manager implements Signal { return false; } - private void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments) { + private List handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments) { + List actions = new ArrayList<>(); if (content != null) { SignalServiceAddress sender; if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { @@ -1258,39 +1557,35 @@ public class Manager implements Signal { } else { sender = content.getSender(); } + // Store uuid if we don't have it already + resolveSignalServiceAddress(sender); + if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); - handleSignalServiceDataMessage(message, false, sender.getNumber().get(), getSelfAddress(), ignoreAttachments); + + if (content.isNeedsReceipt()) { + actions.add(new SendReceiptAction(sender, message.getTimestamp())); + } + + actions.addAll(handleSignalServiceDataMessage(message, false, sender, account.getSelfAddress(), ignoreAttachments)); } if (content.getSyncMessage().isPresent()) { account.setMultiDevice(true); SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); if (syncMessage.getSent().isPresent()) { SentTranscriptMessage message = syncMessage.getSent().get(); - handleSignalServiceDataMessage(message.getMessage(), true, sender.getNumber().get(), message.getDestination().orNull(), ignoreAttachments); + actions.addAll(handleSignalServiceDataMessage(message.getMessage(), true, sender, message.getDestination().orNull(), ignoreAttachments)); } if (syncMessage.getRequest().isPresent()) { RequestMessage rm = syncMessage.getRequest().get(); if (rm.isContactsRequest()) { - try { - sendContacts(); - } catch (UntrustedIdentityException | IOException e) { - e.printStackTrace(); - } + actions.add(SendSyncContactsAction.create()); } if (rm.isGroupsRequest()) { - try { - sendGroups(); - } catch (UntrustedIdentityException | IOException e) { - e.printStackTrace(); - } + actions.add(SendSyncGroupsAction.create()); } if (rm.isBlockedListRequest()) { - try { - sendBlockedList(); - } catch (UntrustedIdentityException | IOException e) { - e.printStackTrace(); - } + actions.add(SendSyncBlockedListAction.create()); } // TODO Handle rm.isConfigurationRequest(); } @@ -1309,8 +1604,16 @@ public class Manager implements Signal { if (g.getName().isPresent()) { syncGroup.name = g.getName().get(); } - syncGroup.addMembers(g.getMembers()); - syncGroup.active = g.isActive(); + syncGroup.addMembers(g.getMembers() + .stream() + .map(this::resolveSignalServiceAddress) + .collect(Collectors.toSet())); + if (!g.isActive()) { + syncGroup.removeMember(account.getSelfAddress()); + } else { + // Add ourself to the member set as it's marked as active + syncGroup.addMembers(Collections.singleton(account.getSelfAddress())); + } syncGroup.blocked = g.isBlocked(); if (g.getColor().isPresent()) { syncGroup.color = g.getColor().get(); @@ -1319,6 +1622,8 @@ public class Manager implements Signal { if (g.getAvatar().isPresent()) { retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId); } + syncGroup.inboxPosition = g.getInboxPosition().orNull(); + syncGroup.archived = g.isArchived(); account.getGroupStore().updateGroup(syncGroup); } } @@ -1337,13 +1642,7 @@ public class Manager implements Signal { if (syncMessage.getBlockedList().isPresent()) { final BlockedListMessage blockedListMessage = syncMessage.getBlockedList().get(); for (SignalServiceAddress address : blockedListMessage.getAddresses()) { - if (address.getNumber().isPresent()) { - try { - setContactBlocked(address.getNumber().get(), true); - } catch (InvalidNumberException e) { - e.printStackTrace(); - } - } + setContactBlocked(resolveSignalServiceAddress(address), true); } for (byte[] groupId : blockedListMessage.getGroupIds()) { try { @@ -1368,10 +1667,10 @@ public class Manager implements Signal { if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) { account.setProfileKey(c.getProfileKey().get()); } - ContactInfo contact = account.getContactStore().getContact(c.getAddress().getNumber().get()); + final SignalServiceAddress address = resolveSignalServiceAddress(c.getAddress()); + ContactInfo contact = account.getContactStore().getContact(address); if (contact == null) { - contact = new ContactInfo(); - contact.number = c.getAddress().getNumber().get(); + contact = new ContactInfo(address); } if (c.getName().isPresent()) { contact.name = c.getName().get(); @@ -1380,22 +1679,18 @@ public class Manager implements Signal { contact.color = c.getColor().get(); } if (c.getProfileKey().isPresent()) { - contact.profileKey = Base64.encodeBytes(c.getProfileKey().get()); + contact.profileKey = Base64.encodeBytes(c.getProfileKey().get().serialize()); } if (c.getVerified().isPresent()) { final VerifiedMessage verifiedMessage = c.getVerified().get(); - account.getSignalProtocolStore().saveIdentity(verifiedMessage.getDestination().getNumber().get(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); + account.getSignalProtocolStore().setIdentityTrustLevel(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); } if (c.getExpirationTimer().isPresent()) { - ThreadInfo thread = account.getThreadStore().getThread(c.getAddress().getNumber().get()); - if (thread == null) { - thread = new ThreadInfo(); - thread.id = c.getAddress().getNumber().get(); - } - thread.messageExpirationTime = c.getExpirationTimer().get(); - account.getThreadStore().updateThread(thread); + contact.messageExpirationTime = c.getExpirationTimer().get(); } contact.blocked = c.isBlocked(); + contact.inboxPosition = c.getInboxPosition().orNull(); + contact.archived = c.isArchived(); account.getContactStore().updateContact(contact); if (c.getAvatar().isPresent()) { @@ -1417,21 +1712,22 @@ public class Manager implements Signal { } if (syncMessage.getVerified().isPresent()) { final VerifiedMessage verifiedMessage = syncMessage.getVerified().get(); - account.getSignalProtocolStore().saveIdentity(verifiedMessage.getDestination().getNumber().get(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); + account.getSignalProtocolStore().setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage.getDestination()), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); } if (syncMessage.getConfiguration().isPresent()) { // TODO } } } + return actions; } private File getContactAvatarFile(String number) { - return new File(avatarsPath, "contact-" + number); + return new File(pathConfig.getAvatarsPath(), "contact-" + number); } - private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException { - IOUtils.createPrivateDirectories(avatarsPath); + private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException, MissingConfigurationException { + IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); if (attachment.isPointer()) { SignalServiceAttachmentPointer pointer = attachment.asPointer(); return retrieveAttachment(pointer, getContactAvatarFile(number), false); @@ -1442,11 +1738,11 @@ public class Manager implements Signal { } private File getGroupAvatarFile(byte[] groupId) { - return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_")); + return new File(pathConfig.getAvatarsPath(), "group-" + Base64.encodeBytes(groupId).replace("/", "_")); } - private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException { - IOUtils.createPrivateDirectories(avatarsPath); + private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException, MissingConfigurationException { + IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); if (attachment.isPointer()) { SignalServiceAttachmentPointer pointer = attachment.asPointer(); return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false); @@ -1456,16 +1752,39 @@ public class Manager implements Signal { } } - public File getAttachmentFile(long attachmentId) { - return new File(attachmentsPath, attachmentId + ""); + private File getProfileAvatarFile(SignalServiceAddress address) { + return new File(pathConfig.getAvatarsPath(), "profile-" + address.getLegacyIdentifier()); } - private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException { - IOUtils.createPrivateDirectories(attachmentsPath); - return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true); + private File retrieveProfileAvatar(SignalServiceAddress address, String avatarPath, ProfileKey profileKey) throws IOException { + IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); + SignalServiceMessageReceiver receiver = getMessageReceiver(); + File outputFile = getProfileAvatarFile(address); + + File tmpFile = IOUtils.createTempFile(); + try (InputStream input = receiver.retrieveProfileAvatar(avatarPath, tmpFile, profileKey, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { + // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ... + IOUtils.copyStreamToFile(input, outputFile, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE); + } finally { + try { + Files.delete(tmpFile.toPath()); + } catch (IOException e) { + System.err.println("Failed to delete received avatar temp file “" + tmpFile + "”: " + e.getMessage()); + } + } + return outputFile; } - private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException { + public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { + return new File(pathConfig.getAttachmentsPath(), attachmentId.toString()); + } + + private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException, MissingConfigurationException { + IOUtils.createPrivateDirectories(pathConfig.getAttachmentsPath()); + return retrieveAttachment(pointer, getAttachmentFile(pointer.getRemoteId()), true); + } + + private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException, MissingConfigurationException { if (storePreview && pointer.getPreview().isPresent()) { File previewFile = new File(outputFile + ".preview"); try (OutputStream output = new FileOutputStream(previewFile)) { @@ -1480,18 +1799,8 @@ public class Manager implements Signal { final SignalServiceMessageReceiver messageReceiver = getMessageReceiver(); File tmpFile = IOUtils.createTempFile(); - try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, BaseConfig.MAX_ATTACHMENT_SIZE)) { - try (OutputStream output = new FileOutputStream(outputFile)) { - byte[] buffer = new byte[4096]; - int read; - - while ((read = input.read(buffer)) != -1) { - output.write(buffer, 0, read); - } - } catch (FileNotFoundException e) { - e.printStackTrace(); - return null; - } + try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE)) { + IOUtils.copyStreamToFile(input, outputFile); } finally { try { Files.delete(tmpFile.toPath()); @@ -1502,28 +1811,22 @@ public class Manager implements Signal { return outputFile; } - private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException { + private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException, MissingConfigurationException { final SignalServiceMessageReceiver messageReceiver = getMessageReceiver(); - return messageReceiver.retrieveAttachment(pointer, tmpFile, BaseConfig.MAX_ATTACHMENT_SIZE); + return messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); } - @Override - public boolean isRemote() { - return false; - } - - private void sendGroups() throws IOException, UntrustedIdentityException { + void sendGroups() throws IOException, UntrustedIdentityException { File groupsFile = IOUtils.createTempFile(); try { try (OutputStream fos = new FileOutputStream(groupsFile)) { DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos); for (GroupInfo record : account.getGroupStore().getGroups()) { - ThreadInfo info = account.getThreadStore().getThread(Base64.encodeBytes(record.groupId)); out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), new ArrayList<>(record.getMembers()), createGroupAvatarAttachment(record.groupId), - record.active, Optional.fromNullable(info != null ? info.messageExpirationTime : null), - Optional.fromNullable(record.color), record.blocked)); + record.isMember(account.getSelfAddress()), Optional.of(record.messageExpirationTime), + Optional.fromNullable(record.color), record.blocked, Optional.fromNullable(record.inboxPosition), record.archived)); } } @@ -1555,33 +1858,30 @@ public class Manager implements Signal { DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos); for (ContactInfo record : account.getContactStore().getContacts()) { VerifiedMessage verifiedMessage = null; - ThreadInfo info = account.getThreadStore().getThread(record.number); - if (getIdentities().containsKey(record.number)) { - JsonIdentityKeyStore.Identity currentIdentity = null; - for (JsonIdentityKeyStore.Identity id : getIdentities().get(record.number)) { - if (currentIdentity == null || id.getDateAdded().after(currentIdentity.getDateAdded())) { - currentIdentity = id; - } - } - if (currentIdentity != null) { - verifiedMessage = new VerifiedMessage(record.getAddress(), currentIdentity.getIdentityKey(), currentIdentity.getTrustLevel().toVerifiedState(), currentIdentity.getDateAdded().getTime()); - } + JsonIdentityKeyStore.Identity currentIdentity = account.getSignalProtocolStore().getIdentity(record.getAddress()); + if (currentIdentity != null) { + verifiedMessage = new VerifiedMessage(record.getAddress(), currentIdentity.getIdentityKey(), currentIdentity.getTrustLevel().toVerifiedState(), currentIdentity.getDateAdded().getTime()); } - byte[] profileKey = record.profileKey == null ? null : Base64.decode(record.profileKey); + ProfileKey profileKey = null; + try { + profileKey = record.profileKey == null ? null : new ProfileKey(Base64.decode(record.profileKey)); + } catch (InvalidInputException ignored) { + } out.write(new DeviceContact(record.getAddress(), Optional.fromNullable(record.name), createContactAvatarAttachment(record.number), Optional.fromNullable(record.color), Optional.fromNullable(verifiedMessage), Optional.fromNullable(profileKey), record.blocked, - Optional.fromNullable(info != null ? info.messageExpirationTime : null))); + Optional.of(record.messageExpirationTime), + Optional.fromNullable(record.inboxPosition), record.archived)); } if (account.getProfileKey() != null) { // Send our own profile key as well out.write(new DeviceContact(account.getSelfAddress(), - Optional.absent(), Optional.absent(), - Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent(), Optional.of(account.getProfileKey()), - false, Optional.absent())); + false, Optional.absent(), Optional.absent(), false)); } } @@ -1605,7 +1905,7 @@ public class Manager implements Signal { } } - private void sendBlockedList() throws IOException, UntrustedIdentityException { + void sendBlockedList() throws IOException, UntrustedIdentityException { List addresses = new ArrayList<>(); for (ContactInfo record : account.getContactStore().getContacts()) { if (record.blocked) { @@ -1631,20 +1931,19 @@ public class Manager implements Signal { } public ContactInfo getContact(String number) { - return account.getContactStore().getContact(number); + return account.getContactStore().getContact(Util.getSignalServiceAddressFromIdentifier(number)); } public GroupInfo getGroup(byte[] groupId) { return account.getGroupStore().getGroup(groupId); } - public Map> getIdentities() { + public List getIdentities() { return account.getSignalProtocolStore().getIdentities(); } - public Pair> getIdentities(String number) throws InvalidNumberException { - String canonicalizedNumber = Utils.canonicalizeNumber(number, username); - return new Pair<>(canonicalizedNumber, account.getSignalProtocolStore().getIdentities(canonicalizedNumber)); + public List getIdentities(String number) throws InvalidNumberException { + return account.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number)); } /** @@ -1653,8 +1952,9 @@ public class Manager implements Signal { * @param name username of the identity * @param fingerprint Fingerprint */ - public boolean trustIdentityVerified(String name, byte[] fingerprint) { - List ids = account.getSignalProtocolStore().getIdentities(name); + public boolean trustIdentityVerified(String name, byte[] fingerprint) throws InvalidNumberException { + SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(name); + List ids = account.getSignalProtocolStore().getIdentities(address); if (ids == null) { return false; } @@ -1663,9 +1963,9 @@ public class Manager implements Signal { continue; } - account.getSignalProtocolStore().saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + account.getSignalProtocolStore().setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); try { - sendVerifiedMessage(new SignalServiceAddress(null, name), id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); } catch (IOException | UntrustedIdentityException e) { e.printStackTrace(); } @@ -1681,19 +1981,20 @@ public class Manager implements Signal { * @param name username of the identity * @param safetyNumber Safety number */ - public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) { - List ids = account.getSignalProtocolStore().getIdentities(name); + public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) throws InvalidNumberException { + SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(name); + List ids = account.getSignalProtocolStore().getIdentities(address); if (ids == null) { return false; } for (JsonIdentityKeyStore.Identity id : ids) { - if (!safetyNumber.equals(computeSafetyNumber(name, id.getIdentityKey()))) { + if (!safetyNumber.equals(computeSafetyNumber(address, id.getIdentityKey()))) { continue; } - account.getSignalProtocolStore().saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + account.getSignalProtocolStore().setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); try { - sendVerifiedMessage(new SignalServiceAddress(null, name), id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); } catch (IOException | UntrustedIdentityException e) { e.printStackTrace(); } @@ -1709,15 +2010,16 @@ public class Manager implements Signal { * @param name username of the identity */ public boolean trustIdentityAllKeys(String name) { - List ids = account.getSignalProtocolStore().getIdentities(name); + SignalServiceAddress address = resolveSignalServiceAddress(name); + List ids = account.getSignalProtocolStore().getIdentities(address); if (ids == null) { return false; } for (JsonIdentityKeyStore.Identity id : ids) { if (id.getTrustLevel() == TrustLevel.UNTRUSTED) { - account.getSignalProtocolStore().saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); + account.getSignalProtocolStore().setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); try { - sendVerifiedMessage(new SignalServiceAddress(null, name), id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); + sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); } catch (IOException | UntrustedIdentityException e) { e.printStackTrace(); } @@ -1727,8 +2029,46 @@ public class Manager implements Signal { return true; } - public String computeSafetyNumber(String theirUsername, IdentityKey theirIdentityKey) { - return Utils.computeSafetyNumber(username, getIdentity(), theirUsername, theirIdentityKey); + public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { + return Utils.computeSafetyNumber(account.getSelfAddress(), getIdentityKeyPair().getPublicKey(), theirAddress, theirIdentityKey); + } + + void saveAccount() { + account.save(); + } + + public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException { + String canonicalizedNumber = UuidUtil.isUuid(identifier) ? identifier : Util.canonicalizeNumber(identifier, account.getUsername()); + return resolveSignalServiceAddress(canonicalizedNumber); + } + + public SignalServiceAddress resolveSignalServiceAddress(String identifier) { + SignalServiceAddress address = Util.getSignalServiceAddressFromIdentifier(identifier); + + return resolveSignalServiceAddress(address); + } + + public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) { + if (address.matches(account.getSelfAddress())) { + return account.getSelfAddress(); + } + + return account.getRecipientStore().resolveServiceAddress(address); + } + + @Override + public void close() throws IOException { + if (messagePipe != null) { + messagePipe.shutdown(); + messagePipe = null; + } + + if (unidentifiedMessagePipe != null) { + unidentifiedMessagePipe.shutdown(); + unidentifiedMessagePipe = null; + } + + account.close(); } public interface ReceiveMessageHandler { diff --git a/src/main/java/org/asamk/signal/NotAGroupMemberException.java b/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java similarity index 58% rename from src/main/java/org/asamk/signal/NotAGroupMemberException.java rename to src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java index cfdc7855..8c0e9be0 100644 --- a/src/main/java/org/asamk/signal/NotAGroupMemberException.java +++ b/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java @@ -1,9 +1,8 @@ -package org.asamk.signal; +package org.asamk.signal.manager; -import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.util.Base64; -public class NotAGroupMemberException extends DBusExecutionException { +public class NotAGroupMemberException extends Exception { public NotAGroupMemberException(byte[] groupId, String groupName) { super("User is not a member in group: " + groupName + " (" + Base64.encodeBytes(groupId) + ")"); diff --git a/src/main/java/org/asamk/signal/manager/PathConfig.java b/src/main/java/org/asamk/signal/manager/PathConfig.java new file mode 100644 index 00000000..2c2d938a --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/PathConfig.java @@ -0,0 +1,34 @@ +package org.asamk.signal.manager; + +public class PathConfig { + + private final String dataPath; + private final String attachmentsPath; + private final String avatarsPath; + + public static PathConfig createDefault(final String settingsPath) { + return new PathConfig( + settingsPath + "/data", + settingsPath + "/attachments", + settingsPath + "/avatars" + ); + } + + private PathConfig(final String dataPath, final String attachmentsPath, final String avatarsPath) { + this.dataPath = dataPath; + this.attachmentsPath = attachmentsPath; + this.avatarsPath = avatarsPath; + } + + public String getDataPath() { + return dataPath; + } + + public String getAttachmentsPath() { + return attachmentsPath; + } + + public String getAvatarsPath() { + return avatarsPath; + } +} diff --git a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java new file mode 100644 index 00000000..4f1aca18 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -0,0 +1,117 @@ +/* + Copyright (C) 2015-2020 AsamK and contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +package org.asamk.signal.manager; + +import org.asamk.signal.storage.SignalAccount; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.profiles.ProfileKey; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.util.KeyHelper; +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.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.SleepTimer; +import org.whispersystems.signalservice.api.util.UptimeSleepTimer; +import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; +import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +public class ProvisioningManager { + + private final PathConfig pathConfig; + private final SignalServiceConfiguration serviceConfiguration; + private final String userAgent; + + private final SignalServiceAccountManager accountManager; + private final IdentityKeyPair identityKey; + private final int registrationId; + private final String password; + + public ProvisioningManager(String settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) { + this.pathConfig = PathConfig.createDefault(settingsPath); + this.serviceConfiguration = serviceConfiguration; + this.userAgent = userAgent; + + identityKey = KeyHelper.generateIdentityKeyPair(); + registrationId = KeyHelper.generateRegistrationId(false); + password = KeyUtils.createPassword(); + final SleepTimer timer = new UptimeSleepTimer(); + GroupsV2Operations groupsV2Operations; + try { + groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceConfiguration)); + } catch (Throwable ignored) { + groupsV2Operations = null; + } + accountManager = new SignalServiceAccountManager(serviceConfiguration, + new DynamicCredentialsProvider(null, null, password, null, SignalServiceAddress.DEFAULT_DEVICE_ID), + userAgent, + groupsV2Operations, + timer); + } + + public String getDeviceLinkUri() throws TimeoutException, IOException { + String deviceUuid = accountManager.getNewDeviceUuid(); + + return Utils.createDeviceLinkUri(new Utils.DeviceLinkInfo(deviceUuid, identityKey.getPublicKey().getPublicKey())); + } + + public String finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists { + String signalingKey = KeyUtils.createSignalingKey(); + SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(identityKey, signalingKey, false, true, registrationId, deviceName); + + String username = ret.getNumber(); + // TODO do this check before actually registering + if (SignalAccount.userExists(pathConfig.getDataPath(), username)) { + throw new UserAlreadyExists(username, SignalAccount.getFileName(pathConfig.getDataPath(), username)); + } + + // Create new account with the synced identity + byte[] profileKeyBytes = ret.getProfileKey(); + ProfileKey profileKey; + if (profileKeyBytes == null) { + profileKey = KeyUtils.createProfileKey(); + } else { + try { + profileKey = new ProfileKey(profileKeyBytes); + } catch (InvalidInputException e) { + throw new IOException("Received invalid profileKey", e); + } + } + + try (SignalAccount account = SignalAccount.createLinkedAccount(pathConfig.getDataPath(), username, ret.getUuid(), password, ret.getDeviceId(), ret.getIdentity(), registrationId, signalingKey, profileKey)) { + account.save(); + + try (Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent)) { + + m.refreshPreKeys(); + + m.requestSyncGroups(); + m.requestSyncContacts(); + m.requestSyncBlocked(); + m.requestSyncConfiguration(); + + m.saveAccount(); + } + } + + return username; + } +} diff --git a/src/main/java/org/asamk/signal/manager/ServiceConfig.java b/src/main/java/org/asamk/signal/manager/ServiceConfig.java new file mode 100644 index 00000000..a8b0c5b6 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/ServiceConfig.java @@ -0,0 +1,80 @@ +package org.asamk.signal.manager; + +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; +import org.whispersystems.signalservice.api.push.TrustStore; +import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; +import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; +import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl; +import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; +import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; +import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl; +import org.whispersystems.util.Base64; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import okhttp3.Dns; +import okhttp3.Interceptor; + +public class ServiceConfig { + + final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"; + final static int PREKEY_MINIMUM_COUNT = 20; + final static int PREKEY_BATCH_SIZE = 100; + final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; + final static long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024; + + private final static String URL = "https://textsecure-service.whispersystems.org"; + private final static String CDN_URL = "https://cdn.signal.org"; + private final static String CDN2_URL = "https://cdn2.signal.org"; + private final static String SIGNAL_KEY_BACKUP_URL = "https://api.backup.signal.org"; + private final static String STORAGE_URL = "https://storage.signal.org"; + private final static TrustStore TRUST_STORE = new WhisperTrustStore(); + + private final static Optional dns = Optional.absent(); + + private final static String zkGroupServerPublicParamsHex = "AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0="; + + static final SignalServiceProfile.Capabilities capabilities = new SignalServiceProfile.Capabilities(false, false, false); + + public static SignalServiceConfiguration createDefaultServiceConfiguration(String userAgent) { + final Interceptor userAgentInterceptor = chain -> + chain.proceed(chain.request().newBuilder() + .header("User-Agent", userAgent) + .build()); + + final List interceptors = Collections.singletonList(userAgentInterceptor); + + final byte[] zkGroupServerPublicParams; + try { + zkGroupServerPublicParams = Base64.decode(zkGroupServerPublicParamsHex); + } catch (IOException e) { + throw new AssertionError(e); + } + + return new SignalServiceConfiguration( + new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)}, + makeSignalCdnUrlMapFor(new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}), + new SignalContactDiscoveryUrl[0], + new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)}, + new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)}, + interceptors, + dns, + zkGroupServerPublicParams + ); + } + + private static Map makeSignalCdnUrlMapFor(SignalCdnUrl[] cdn0Urls, SignalCdnUrl[] cdn2Urls) { + Map result = new HashMap<>(); + result.put(0, cdn0Urls); + result.put(2, cdn2Urls); + return Collections.unmodifiableMap(result); + } + + private ServiceConfig() { + } +} diff --git a/src/main/java/org/asamk/signal/manager/StickerPackInvalidException.java b/src/main/java/org/asamk/signal/manager/StickerPackInvalidException.java new file mode 100644 index 00000000..52869acd --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/StickerPackInvalidException.java @@ -0,0 +1,8 @@ +package org.asamk.signal.manager; + +public class StickerPackInvalidException extends Exception { + + public StickerPackInvalidException(String message) { + super(message); + } +} diff --git a/src/main/java/org/asamk/signal/TrustLevel.java b/src/main/java/org/asamk/signal/manager/TrustLevel.java similarity index 97% rename from src/main/java/org/asamk/signal/TrustLevel.java rename to src/main/java/org/asamk/signal/manager/TrustLevel.java index 5eaf960a..c9fa7a5e 100644 --- a/src/main/java/org/asamk/signal/TrustLevel.java +++ b/src/main/java/org/asamk/signal/manager/TrustLevel.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.manager; import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; diff --git a/src/main/java/org/asamk/signal/UserAlreadyExists.java b/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java similarity index 92% rename from src/main/java/org/asamk/signal/UserAlreadyExists.java rename to src/main/java/org/asamk/signal/manager/UserAlreadyExists.java index 28836f28..a07c455b 100644 --- a/src/main/java/org/asamk/signal/UserAlreadyExists.java +++ b/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.manager; public class UserAlreadyExists extends Exception { diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java index b253a2ee..05fcfb5e 100644 --- a/src/main/java/org/asamk/signal/manager/Utils.java +++ b/src/main/java/org/asamk/signal/manager/Utils.java @@ -1,6 +1,5 @@ package org.asamk.signal.manager; -import org.asamk.signal.AttachmentInvalidException; import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; @@ -13,9 +12,9 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -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.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec; import org.whispersystems.util.Base64; import java.io.BufferedInputStream; @@ -35,12 +34,10 @@ import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.file.Files; import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.UUID; import static org.whispersystems.signalservice.internal.util.Util.isEmpty; @@ -61,7 +58,7 @@ class Utils { return signalServiceAttachments; } - private static String getFileMimeType(File file) throws IOException { + static String getFileMimeType(File file, String defaultMimeType) throws IOException { String mime = Files.probeContentType(file.toPath()); if (mime == null) { try (InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) { @@ -69,7 +66,7 @@ class Utils { } } if (mime == null) { - mime = "application/octet-stream"; + return defaultMimeType; } return mime; } @@ -77,12 +74,14 @@ class Utils { static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException { InputStream attachmentStream = new FileInputStream(attachmentFile); final long attachmentSize = attachmentFile.length(); - final String mime = getFileMimeType(attachmentFile); - // TODO mabybe add a parameter to set the voiceNote, preview, width, height and caption option + final String mime = getFileMimeType(attachmentFile, "application/octet-stream"); + // TODO mabybe add a parameter to set the voiceNote, borderless, preview, width, height and caption option + final long uploadTimestamp = System.currentTimeMillis(); Optional preview = Optional.absent(); Optional caption = Optional.absent(); Optional blurHash = Optional.absent(); - return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, caption, blurHash, null); + final Optional resumableUploadSpec = Optional.absent(); + return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, false, preview, 0, 0, uploadTimestamp, caption, blurHash, null, null, resumableUploadSpec); } static StreamDetails createStreamDetailsFromFile(File file) throws IOException { @@ -97,7 +96,7 @@ class Utils { static CertificateValidator getCertificateValidator() { try { - ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(BaseConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0); + ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(ServiceConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0); return new CertificateValidator(unidentifiedSenderTrustRoot); } catch (InvalidKeyException | IOException e) { throw new AssertionError(e); @@ -149,38 +148,19 @@ class Utils { return new DeviceLinkInfo(deviceIdentifier, deviceKey); } - static Set getSignalServiceAddresses(Collection recipients, String localNumber) { - Set recipientsTS = new HashSet<>(recipients.size()); - for (String recipient : recipients) { - try { - recipientsTS.add(getPushAddress(recipient, localNumber)); - } catch (InvalidNumberException e) { - System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); - System.err.println("Aborting sending."); - return null; - } - } - return recipientsTS; - } - - static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException { - return PhoneNumberFormatter.formatNumber(number, localNumber); - } - - private static SignalServiceAddress getPushAddress(String number, String localNumber) throws InvalidNumberException { - String e164number = canonicalizeNumber(number, localNumber); - return new SignalServiceAddress(null, e164number); - } - static SignalServiceEnvelope loadEnvelope(File file) throws IOException { try (FileInputStream f = new FileInputStream(file)) { DataInputStream in = new DataInputStream(f); int version = in.readInt(); - if (version > 2) { + if (version > 4) { return null; } int type = in.readInt(); String source = in.readUTF(); + UUID sourceUuid = null; + if (version >= 3) { + sourceUuid = UuidUtil.parseOrNull(in.readUTF()); + } int sourceDevice = in.readInt(); if (version == 1) { // read legacy relay field @@ -199,25 +179,33 @@ class Utils { legacyMessage = new byte[legacyMessageLen]; in.readFully(legacyMessage); } - long serverTimestamp = 0; + long serverReceivedTimestamp = 0; String uuid = null; - if (version == 2) { - serverTimestamp = in.readLong(); + if (version >= 2) { + serverReceivedTimestamp = in.readLong(); uuid = in.readUTF(); if ("".equals(uuid)) { uuid = null; } } - return new SignalServiceEnvelope(type, Optional.of(new SignalServiceAddress(null, source)), sourceDevice, timestamp, legacyMessage, content, serverTimestamp, uuid); + long serverDeliveredTimestamp = 0; + if (version >= 4) { + serverDeliveredTimestamp = in.readLong(); + } + Optional addressOptional = sourceUuid == null && source.isEmpty() + ? Optional.absent() + : Optional.of(new SignalServiceAddress(sourceUuid, source)); + return new SignalServiceEnvelope(type, addressOptional, sourceDevice, timestamp, legacyMessage, content, serverReceivedTimestamp, serverDeliveredTimestamp, uuid); } } static void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException { try (FileOutputStream f = new FileOutputStream(file)) { try (DataOutputStream out = new DataOutputStream(f)) { - out.writeInt(2); // version + out.writeInt(4); // version out.writeInt(envelope.getType()); - out.writeUTF(envelope.getSourceE164().get()); + out.writeUTF(envelope.getSourceE164().isPresent() ? envelope.getSourceE164().get() : ""); + out.writeUTF(envelope.getSourceUuid().isPresent() ? envelope.getSourceUuid().get() : ""); out.writeInt(envelope.getSourceDevice()); out.writeLong(envelope.getTimestamp()); if (envelope.hasContent()) { @@ -232,9 +220,10 @@ class Utils { } else { out.writeInt(0); } - out.writeLong(envelope.getServerTimestamp()); + out.writeLong(envelope.getServerReceivedTimestamp()); String uuid = envelope.getUuid(); out.writeUTF(uuid == null ? "" : uuid); + out.writeLong(envelope.getServerDeliveredTimestamp()); } } } @@ -256,10 +245,28 @@ class Utils { return outputFile; } - static String computeSafetyNumber(String ownUsername, IdentityKey ownIdentityKey, String theirUsername, IdentityKey theirIdentityKey) { - // Version 1: E164 user - // Version 2: UUID user - Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(1, ownUsername.getBytes(), ownIdentityKey, theirUsername.getBytes(), theirIdentityKey); + static String computeSafetyNumber(SignalServiceAddress ownAddress, IdentityKey ownIdentityKey, SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { + int version; + byte[] ownId; + byte[] theirId; + + if (ServiceConfig.capabilities.isUuid() + && ownAddress.getUuid().isPresent() && theirAddress.getUuid().isPresent()) { + // Version 2: UUID user + version = 2; + ownId = UuidUtil.toByteArray(ownAddress.getUuid().get()); + theirId = UuidUtil.toByteArray(theirAddress.getUuid().get()); + } else { + // Version 1: E164 user + version = 1; + if (!ownAddress.getNumber().isPresent() || !theirAddress.getNumber().isPresent()) { + return "INVALID ID"; + } + ownId = ownAddress.getNumber().get().getBytes(); + theirId = theirAddress.getNumber().get().getBytes(); + } + + Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(version, ownId, ownIdentityKey, theirId, theirIdentityKey); return fingerprint.getDisplayableFingerprint().getDisplayText(); } diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index 1ba78f6b..6043d803 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -10,39 +10,57 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.contacts.JsonContactsStore; +import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.groups.JsonGroupStore; +import org.asamk.signal.storage.profiles.ProfileStore; +import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; -import org.asamk.signal.storage.threads.JsonThreadStore; +import org.asamk.signal.storage.protocol.RecipientStore; +import org.asamk.signal.storage.protocol.SessionInfo; +import org.asamk.signal.storage.protocol.SignalServiceAddressResolver; +import org.asamk.signal.storage.threads.LegacyJsonThreadStore; +import org.asamk.signal.storage.threads.ThreadInfo; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.profiles.ProfileKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.Medium; +import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.util.Base64; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.channels.Channels; +import java.nio.channels.ClosedChannelException; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.util.Collection; +import java.util.UUID; +import java.util.stream.Collectors; -public class SignalAccount { +public class SignalAccount implements Closeable { private final ObjectMapper jsonProcessor = new ObjectMapper(); - private FileChannel fileChannel; - private FileLock lock; + private final FileChannel fileChannel; + private final FileLock lock; private String username; + private UUID uuid; private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; private boolean isMultiDevice = false; private String password; private String registrationLockPin; private String signalingKey; - private byte[] profileKey; + private ProfileKey profileKey; private int preKeyIdOffset; private int nextSignedPreKeyId; @@ -51,72 +69,82 @@ public class SignalAccount { private JsonSignalProtocolStore signalProtocolStore; private JsonGroupStore groupStore; private JsonContactsStore contactStore; - private JsonThreadStore threadStore; + private RecipientStore recipientStore; + private ProfileStore profileStore; - private SignalAccount() { + private SignalAccount(final FileChannel fileChannel, final FileLock lock) { + this.fileChannel = fileChannel; + this.lock = lock; jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it. - jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES); jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE); jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); } public static SignalAccount load(String dataPath, String username) throws IOException { - SignalAccount account = new SignalAccount(); - IOUtils.createPrivateDirectories(dataPath); - account.openFileChannel(getFileName(dataPath, username)); - account.load(); - return account; + final String fileName = getFileName(dataPath, username); + final Pair pair = openFileChannel(fileName); + try { + SignalAccount account = new SignalAccount(pair.first(), pair.second()); + account.load(); + return account; + } catch (Throwable e) { + pair.second().close(); + pair.first().close(); + throw e; + } } - public static SignalAccount create(String dataPath, String username, IdentityKeyPair identityKey, int registrationId, byte[] profileKey) throws IOException { + public static SignalAccount create(String dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey) throws IOException { IOUtils.createPrivateDirectories(dataPath); + String fileName = getFileName(dataPath, username); + if (!new File(fileName).exists()) { + IOUtils.createPrivateFile(fileName); + } - SignalAccount account = new SignalAccount(); - account.openFileChannel(getFileName(dataPath, username)); + final Pair pair = openFileChannel(fileName); + SignalAccount account = new SignalAccount(pair.first(), pair.second()); account.username = username; account.profileKey = profileKey; account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId); account.groupStore = new JsonGroupStore(); - account.threadStore = new JsonThreadStore(); account.contactStore = new JsonContactsStore(); + account.recipientStore = new RecipientStore(); + account.profileStore = new ProfileStore(); account.registered = false; return account; } - public static SignalAccount createLinkedAccount(String dataPath, String username, String password, int deviceId, IdentityKeyPair identityKey, int registrationId, String signalingKey, byte[] profileKey) throws IOException { + public static SignalAccount createLinkedAccount(String dataPath, String username, UUID uuid, String password, int deviceId, IdentityKeyPair identityKey, int registrationId, String signalingKey, ProfileKey profileKey) throws IOException { IOUtils.createPrivateDirectories(dataPath); + String fileName = getFileName(dataPath, username); + if (!new File(fileName).exists()) { + IOUtils.createPrivateFile(fileName); + } - SignalAccount account = new SignalAccount(); - account.openFileChannel(getFileName(dataPath, username)); + final Pair pair = openFileChannel(fileName); + SignalAccount account = new SignalAccount(pair.first(), pair.second()); account.username = username; + account.uuid = uuid; account.password = password; account.profileKey = profileKey; account.deviceId = deviceId; account.signalingKey = signalingKey; account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId); account.groupStore = new JsonGroupStore(); - account.threadStore = new JsonThreadStore(); account.contactStore = new JsonContactsStore(); + account.recipientStore = new RecipientStore(); + account.profileStore = new ProfileStore(); account.registered = true; account.isMultiDevice = true; return account; } - public static SignalAccount createTemporaryAccount(IdentityKeyPair identityKey, int registrationId) { - SignalAccount account = new SignalAccount(); - - account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId); - account.registered = false; - - return account; - } - public static String getFileName(String dataPath, String username) { return dataPath + "/" + username; } @@ -136,6 +164,14 @@ public class SignalAccount { rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel)); } + JsonNode uuidNode = rootNode.get("uuid"); + if (uuidNode != null && !uuidNode.isNull()) { + try { + uuid = UUID.fromString(uuidNode.asText()); + } catch (IllegalArgumentException e) { + throw new IOException("Config file contains an invalid uuid, needs to be a valid UUID", e); + } + } JsonNode node = rootNode.get("deviceId"); if (node != null) { deviceId = node.asInt(); @@ -161,7 +197,11 @@ public class SignalAccount { nextSignedPreKeyId = 0; } if (rootNode.has("profileKey")) { - profileKey = Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText()); + try { + profileKey = new ProfileKey(Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText())); + } catch (InvalidInputException e) { + throw new IOException("Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes", e); + } } signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class); @@ -181,12 +221,66 @@ public class SignalAccount { if (contactStore == null) { contactStore = new JsonContactsStore(); } + + JsonNode recipientStoreNode = rootNode.get("recipientStore"); + if (recipientStoreNode != null) { + recipientStore = jsonProcessor.convertValue(recipientStoreNode, RecipientStore.class); + } + if (recipientStore == null) { + recipientStore = new RecipientStore(); + + recipientStore.resolveServiceAddress(getSelfAddress()); + + for (ContactInfo contact : contactStore.getContacts()) { + recipientStore.resolveServiceAddress(contact.getAddress()); + } + + for (GroupInfo group : groupStore.getGroups()) { + group.members = group.members.stream() + .map(m -> recipientStore.resolveServiceAddress(m)) + .collect(Collectors.toSet()); + } + + for (SessionInfo session : signalProtocolStore.getSessions()) { + session.address = recipientStore.resolveServiceAddress(session.address); + } + + for (JsonIdentityKeyStore.Identity identity : signalProtocolStore.getIdentities()) { + identity.setAddress(recipientStore.resolveServiceAddress(identity.getAddress())); + } + } + + JsonNode profileStoreNode = rootNode.get("profileStore"); + if (profileStoreNode != null) { + profileStore = jsonProcessor.convertValue(profileStoreNode, ProfileStore.class); + } + if (profileStore == null) { + profileStore = new ProfileStore(); + } + JsonNode threadStoreNode = rootNode.get("threadStore"); if (threadStoreNode != null) { - threadStore = jsonProcessor.convertValue(threadStoreNode, JsonThreadStore.class); - } - if (threadStore == null) { - threadStore = new JsonThreadStore(); + LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class); + // Migrate thread info to group and contact store + for (ThreadInfo thread : threadStore.getThreads()) { + if (thread.id == null || thread.id.isEmpty()) { + continue; + } + try { + ContactInfo contactInfo = contactStore.getContact(new SignalServiceAddress(null, thread.id)); + if (contactInfo != null) { + contactInfo.messageExpirationTime = thread.messageExpirationTime; + contactStore.updateContact(contactInfo); + } else { + GroupInfo groupInfo = groupStore.getGroup(Base64.decode(thread.id)); + if (groupInfo != null) { + groupInfo.messageExpirationTime = thread.messageExpirationTime; + groupStore.updateGroup(groupInfo); + } + } + } catch (Exception ignored) { + } + } } } @@ -196,6 +290,7 @@ public class SignalAccount { } ObjectNode rootNode = jsonProcessor.createObjectNode(); rootNode.put("username", username) + .put("uuid", uuid == null ? null : uuid.toString()) .put("deviceId", deviceId) .put("isMultiDevice", isMultiDevice) .put("password", password) @@ -203,40 +298,44 @@ public class SignalAccount { .put("signalingKey", signalingKey) .put("preKeyIdOffset", preKeyIdOffset) .put("nextSignedPreKeyId", nextSignedPreKeyId) - .put("profileKey", Base64.encodeBytes(profileKey)) + .put("profileKey", Base64.encodeBytes(profileKey.serialize())) .put("registered", registered) .putPOJO("axolotlStore", signalProtocolStore) .putPOJO("groupStore", groupStore) .putPOJO("contactStore", contactStore) - .putPOJO("threadStore", threadStore) + .putPOJO("recipientStore", recipientStore) + .putPOJO("profileStore", profileStore) ; try { - synchronized (fileChannel) { - fileChannel.position(0); - jsonProcessor.writeValue(Channels.newOutputStream(fileChannel), rootNode); - fileChannel.truncate(fileChannel.position()); - fileChannel.force(false); + try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { + // Write to memory first to prevent corrupting the file in case of serialization errors + jsonProcessor.writeValue(output, rootNode); + ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray()); + synchronized (fileChannel) { + fileChannel.position(0); + input.transferTo(Channels.newOutputStream(fileChannel)); + fileChannel.truncate(fileChannel.position()); + fileChannel.force(false); + } } } catch (Exception e) { System.err.println(String.format("Error saving file: %s", e.getMessage())); } } - private void openFileChannel(String fileName) throws IOException { - if (fileChannel != null) { - return; - } - - if (!new File(fileName).exists()) { - IOUtils.createPrivateFile(fileName); - } - fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel(); - lock = fileChannel.tryLock(); + private static Pair openFileChannel(String fileName) throws IOException { + FileChannel fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel(); + FileLock lock = fileChannel.tryLock(); if (lock == null) { System.err.println("Config file is in use by another instance, waiting…"); lock = fileChannel.lock(); System.err.println("Config file lock acquired."); } + return new Pair<>(fileChannel, lock); + } + + public void setResolver(final SignalServiceAddressResolver resolver) { + signalProtocolStore.setResolver(resolver); } public void addPreKeys(Collection records) { @@ -263,16 +362,28 @@ public class SignalAccount { return contactStore; } - public JsonThreadStore getThreadStore() { - return threadStore; + public RecipientStore getRecipientStore() { + return recipientStore; + } + + public ProfileStore getProfileStore() { + return profileStore; } public String getUsername() { return username; } + public UUID getUuid() { + return uuid; + } + + public void setUuid(final UUID uuid) { + this.uuid = uuid; + } + public SignalServiceAddress getSelfAddress() { - return new SignalServiceAddress(null, username); + return new SignalServiceAddress(uuid, username); } public int getDeviceId() { @@ -291,6 +402,10 @@ public class SignalAccount { return registrationLockPin; } + public String getRegistrationLock() { + return null; // TODO implement KBS + } + public void setRegistrationLockPin(final String registrationLockPin) { this.registrationLockPin = registrationLockPin; } @@ -303,11 +418,11 @@ public class SignalAccount { this.signalingKey = signalingKey; } - public byte[] getProfileKey() { + public ProfileKey getProfileKey() { return profileKey; } - public void setProfileKey(final byte[] profileKey) { + public void setProfileKey(final ProfileKey profileKey) { this.profileKey = profileKey; } @@ -334,4 +449,15 @@ public class SignalAccount { public void setMultiDevice(final boolean multiDevice) { isMultiDevice = multiDevice; } + + @Override + public void close() throws IOException { + synchronized (fileChannel) { + try { + lock.close(); + } catch (ClosedChannelException ignored) { + } + fileChannel.close(); + } + } } diff --git a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java index be69b40c..4d3a5e95 100644 --- a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java +++ b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java @@ -5,6 +5,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import java.util.UUID; + public class ContactInfo { @JsonProperty @@ -13,17 +15,37 @@ public class ContactInfo { @JsonProperty public String number; + @JsonProperty + public UUID uuid; + @JsonProperty public String color; + @JsonProperty(defaultValue = "0") + public int messageExpirationTime; + @JsonProperty public String profileKey; @JsonProperty(defaultValue = "false") public boolean blocked; + @JsonProperty + public Integer inboxPosition; + + @JsonProperty(defaultValue = "false") + public boolean archived; + + public ContactInfo() { + } + + public ContactInfo(SignalServiceAddress address) { + this.number = address.getNumber().orNull(); + this.uuid = address.getUuid().orNull(); + } + @JsonIgnore public SignalServiceAddress getAddress() { - return new SignalServiceAddress(null, number); + return new SignalServiceAddress(uuid, number); } } diff --git a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java b/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java index c10dfbb7..bb81b0c9 100644 --- a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java +++ b/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java @@ -1,41 +1,46 @@ package org.asamk.signal.storage.contacts; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import java.io.IOException; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; public class JsonContactsStore { - private static final ObjectMapper jsonProcessor = new ObjectMapper(); @JsonProperty("contacts") - @JsonSerialize(using = JsonContactsStore.MapToListSerializer.class) - @JsonDeserialize(using = ContactsDeserializer.class) - private Map contacts = new HashMap<>(); + private List contacts = new ArrayList<>(); public void updateContact(ContactInfo contact) { - contacts.put(contact.number, contact); + final SignalServiceAddress contactAddress = contact.getAddress(); + for (int i = 0; i < contacts.size(); i++) { + if (contacts.get(i).getAddress().matches(contactAddress)) { + contacts.set(i, contact); + return; + } + } + + contacts.add(contact); } - public ContactInfo getContact(String number) { - return contacts.get(number); + public ContactInfo getContact(SignalServiceAddress address) { + for (ContactInfo contact : contacts) { + if (contact.getAddress().matches(address)) { + if (contact.uuid == null) { + contact.uuid = address.getUuid().orNull(); + } else if (contact.number == null) { + contact.number = address.getNumber().orNull(); + } + + return contact; + } + } + return null; } public List getContacts() { - return new ArrayList<>(contacts.values()); + return new ArrayList<>(contacts); } /** @@ -44,27 +49,4 @@ public class JsonContactsStore { public void clear() { contacts.clear(); } - - private static class MapToListSerializer extends JsonSerializer> { - - @Override - public void serialize(final Map value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException { - jgen.writeObject(value.values()); - } - } - - private static class ContactsDeserializer extends JsonDeserializer> { - - @Override - public Map deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { - Map contacts = new HashMap<>(); - JsonNode node = jsonParser.getCodec().readTree(jsonParser); - for (JsonNode n : node) { - ContactInfo c = jsonProcessor.treeToValue(n, ContactInfo.class); - contacts.put(c.number, c); - } - - return contacts; - } - } } diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java index 1a4e0ec2..4b0adcd0 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java @@ -2,15 +2,29 @@ package org.asamk.signal.storage.groups; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import java.io.IOException; import java.util.Collection; import java.util.HashSet; import java.util.Set; +import java.util.UUID; public class GroupInfo { + private static final ObjectMapper jsonProcessor = new ObjectMapper(); + @JsonProperty public final byte[] groupId; @@ -18,27 +32,40 @@ public class GroupInfo { public String name; @JsonProperty - public Set members = new HashSet<>(); - @JsonProperty - public boolean active; + @JsonDeserialize(using = MembersDeserializer.class) + @JsonSerialize(using = MembersSerializer.class) + public Set members = new HashSet<>(); @JsonProperty public String color; + @JsonProperty(defaultValue = "0") + public int messageExpirationTime; @JsonProperty(defaultValue = "false") public boolean blocked; + @JsonProperty + public Integer inboxPosition; + @JsonProperty(defaultValue = "false") + public boolean archived; private long avatarId; + @JsonProperty + @JsonIgnore + private boolean active; + public GroupInfo(byte[] groupId) { this.groupId = groupId; } - public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color, @JsonProperty("blocked") boolean blocked) { + public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color, @JsonProperty("blocked") boolean blocked, @JsonProperty("inboxPosition") Integer inboxPosition, @JsonProperty("archived") boolean archived, @JsonProperty("messageExpirationTime") int messageExpirationTime) { this.groupId = groupId; this.name = name; this.members.addAll(members); this.avatarId = avatarId; this.color = color; this.blocked = blocked; + this.inboxPosition = inboxPosition; + this.archived = archived; + this.messageExpirationTime = messageExpirationTime; } @JsonIgnore @@ -48,16 +75,111 @@ public class GroupInfo { @JsonIgnore public Set getMembers() { - Set addresses = new HashSet<>(members.size()); - for (String member : members) { - addresses.add(new SignalServiceAddress(null, member)); - } - return addresses; + return members; } - public void addMembers(Collection members) { + @JsonIgnore + public Set getMembersE164() { + Set membersE164 = new HashSet<>(); for (SignalServiceAddress member : members) { - this.members.add(member.getNumber().get()); + if (!member.getNumber().isPresent()) { + continue; + } + membersE164.add(member.getNumber().get()); + } + return membersE164; + } + + @JsonIgnore + public Set getMembersWithout(SignalServiceAddress address) { + Set members = new HashSet<>(this.members.size()); + for (SignalServiceAddress member : this.members) { + if (!member.matches(address)) { + members.add(member); + } + } + return members; + } + + public void addMembers(Collection addresses) { + for (SignalServiceAddress address : addresses) { + if (this.members.contains(address)) { + continue; + } + removeMember(address); + this.members.add(address); + } + } + + public void removeMember(SignalServiceAddress address) { + this.members.removeIf(member -> member.matches(address)); + } + + @JsonIgnore + public boolean isMember(SignalServiceAddress address) { + for (SignalServiceAddress member : this.members) { + if (member.matches(address)) { + return true; + } + } + return false; + } + + private static final class JsonSignalServiceAddress { + + @JsonProperty + private UUID uuid; + + @JsonProperty + private String number; + + JsonSignalServiceAddress(@JsonProperty("uuid") final UUID uuid, @JsonProperty("number") final String number) { + this.uuid = uuid; + this.number = number; + } + + JsonSignalServiceAddress(SignalServiceAddress address) { + this.uuid = address.getUuid().orNull(); + this.number = address.getNumber().orNull(); + } + + SignalServiceAddress toSignalServiceAddress() { + return new SignalServiceAddress(uuid, number); + } + } + + private static class MembersSerializer extends JsonSerializer> { + + @Override + public void serialize(final Set value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException { + jgen.writeStartArray(value.size()); + for (SignalServiceAddress address : value) { + if (address.getUuid().isPresent()) { + jgen.writeObject(new JsonSignalServiceAddress(address)); + } else { + jgen.writeString(address.getNumber().get()); + } + } + jgen.writeEndArray(); + } + } + + private static class MembersDeserializer extends JsonDeserializer> { + + @Override + public Set deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + Set addresses = new HashSet<>(); + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + for (JsonNode n : node) { + if (n.isTextual()) { + addresses.add(new SignalServiceAddress(null, n.textValue())); + } else { + JsonSignalServiceAddress address = jsonProcessor.treeToValue(n, JsonSignalServiceAddress.class); + addresses.add(address.toSignalServiceAddress()); + } + } + + return addresses; } } } diff --git a/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java b/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java new file mode 100644 index 00000000..24b08968 --- /dev/null +++ b/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java @@ -0,0 +1,111 @@ +package org.asamk.signal.storage.profiles; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.profiles.ProfileKey; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.util.Base64; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; + +public class ProfileStore { + + private static final ObjectMapper jsonProcessor = new ObjectMapper(); + + @JsonProperty("profiles") + @JsonDeserialize(using = ProfileStoreDeserializer.class) + @JsonSerialize(using = ProfileStoreSerializer.class) + private final List profiles = new ArrayList<>(); + + public SignalProfileEntry getProfile(SignalServiceAddress serviceAddress) { + for (SignalProfileEntry entry : profiles) { + if (entry.getServiceAddress().matches(serviceAddress)) { + return entry; + } + } + return null; + } + + public void updateProfile(SignalServiceAddress serviceAddress, ProfileKey profileKey, long now, SignalProfile profile) { + SignalProfileEntry newEntry = new SignalProfileEntry(serviceAddress, profileKey, now, profile); + for (int i = 0; i < profiles.size(); i++) { + if (profiles.get(i).getServiceAddress().matches(serviceAddress)) { + profiles.set(i, newEntry); + return; + } + } + + profiles.add(newEntry); + } + + public static class ProfileStoreDeserializer extends JsonDeserializer> { + + @Override + public List deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + + List addresses = new ArrayList<>(); + + if (node.isArray()) { + for (JsonNode entry : node) { + String name = entry.hasNonNull("name") + ? entry.get("name").asText() + : null; + UUID uuid = entry.hasNonNull("uuid") + ? UuidUtil.parseOrNull(entry.get("uuid").asText()) + : null; + final SignalServiceAddress serviceAddress = new SignalServiceAddress(uuid, name); + ProfileKey profileKey = null; + try { + profileKey = new ProfileKey(Base64.decode(entry.get("profileKey").asText())); + } catch (InvalidInputException ignored) { + } + long lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong(); + SignalProfile profile = jsonProcessor.treeToValue(entry.get("profile"), SignalProfile.class); + addresses.add(new SignalProfileEntry(serviceAddress, profileKey, lastUpdateTimestamp, profile)); + } + } + + return addresses; + } + } + + public static class ProfileStoreSerializer extends JsonSerializer> { + + @Override + public void serialize(List profiles, JsonGenerator json, SerializerProvider serializerProvider) throws IOException { + json.writeStartArray(); + for (SignalProfileEntry profileEntry : profiles) { + final SignalServiceAddress address = profileEntry.getServiceAddress(); + json.writeStartObject(); + if (address.getNumber().isPresent()) { + json.writeStringField("name", address.getNumber().get()); + } + if (address.getUuid().isPresent()) { + json.writeStringField("uuid", address.getUuid().get().toString()); + } + json.writeStringField("profileKey", Base64.encodeBytes(profileEntry.getProfileKey().serialize())); + json.writeNumberField("lastUpdateTimestamp", profileEntry.getLastUpdateTimestamp()); + json.writeObjectField("profile", profileEntry.getProfile()); + json.writeEndObject(); + } + json.writeEndArray(); + } + } +} diff --git a/src/main/java/org/asamk/signal/storage/profiles/SignalProfile.java b/src/main/java/org/asamk/signal/storage/profiles/SignalProfile.java new file mode 100644 index 00000000..71ab60e6 --- /dev/null +++ b/src/main/java/org/asamk/signal/storage/profiles/SignalProfile.java @@ -0,0 +1,81 @@ +package org.asamk.signal.storage.profiles; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; + +import java.io.File; + +public class SignalProfile { + + @JsonProperty + private final String identityKey; + + @JsonProperty + private final String name; + + private final File avatarFile; + + @JsonProperty + private final String unidentifiedAccess; + + @JsonProperty + private final boolean unrestrictedUnidentifiedAccess; + + @JsonProperty + private final SignalServiceProfile.Capabilities capabilities; + + public SignalProfile(final String identityKey, final String name, final File avatarFile, final String unidentifiedAccess, final boolean unrestrictedUnidentifiedAccess, final SignalServiceProfile.Capabilities capabilities) { + this.identityKey = identityKey; + this.name = name; + this.avatarFile = avatarFile; + this.unidentifiedAccess = unidentifiedAccess; + this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess; + this.capabilities = capabilities; + } + + public SignalProfile(@JsonProperty("identityKey") final String identityKey, @JsonProperty("name") final String name, @JsonProperty("unidentifiedAccess") final String unidentifiedAccess, @JsonProperty("unrestrictedUnidentifiedAccess") final boolean unrestrictedUnidentifiedAccess, @JsonProperty("capabilities") final SignalServiceProfile.Capabilities capabilities) { + this.identityKey = identityKey; + this.name = name; + this.avatarFile = null; + this.unidentifiedAccess = unidentifiedAccess; + this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess; + this.capabilities = capabilities; + } + + public String getIdentityKey() { + return identityKey; + } + + public String getName() { + return name; + } + + public File getAvatarFile() { + return avatarFile; + } + + public String getUnidentifiedAccess() { + return unidentifiedAccess; + } + + public boolean isUnrestrictedUnidentifiedAccess() { + return unrestrictedUnidentifiedAccess; + } + + public SignalServiceProfile.Capabilities getCapabilities() { + return capabilities; + } + + @Override + public String toString() { + return "SignalProfile{" + + "identityKey='" + identityKey + '\'' + + ", name='" + name + '\'' + + ", avatarFile=" + avatarFile + + ", unidentifiedAccess='" + unidentifiedAccess + '\'' + + ", unrestrictedUnidentifiedAccess=" + unrestrictedUnidentifiedAccess + + ", capabilities=" + capabilities + + '}'; + } +} diff --git a/src/main/java/org/asamk/signal/storage/profiles/SignalProfileEntry.java b/src/main/java/org/asamk/signal/storage/profiles/SignalProfileEntry.java new file mode 100644 index 00000000..ed1f7127 --- /dev/null +++ b/src/main/java/org/asamk/signal/storage/profiles/SignalProfileEntry.java @@ -0,0 +1,38 @@ +package org.asamk.signal.storage.profiles; + +import org.signal.zkgroup.profiles.ProfileKey; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +public class SignalProfileEntry { + + private final SignalServiceAddress serviceAddress; + + private final ProfileKey profileKey; + + private final long lastUpdateTimestamp; + + private final SignalProfile profile; + + public SignalProfileEntry(final SignalServiceAddress serviceAddress, final ProfileKey profileKey, final long lastUpdateTimestamp, final SignalProfile profile) { + this.serviceAddress = serviceAddress; + this.profileKey = profileKey; + this.lastUpdateTimestamp = lastUpdateTimestamp; + this.profile = profile; + } + + public SignalServiceAddress getServiceAddress() { + return serviceAddress; + } + + public ProfileKey getProfileKey() { + return profileKey; + } + + public long getLastUpdateTimestamp() { + return lastUpdateTimestamp; + } + + public SignalProfile getProfile() { + return profile; + } +} diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java index e6f0194d..45c4024c 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java @@ -8,33 +8,49 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; -import org.asamk.signal.TrustLevel; +import org.asamk.signal.manager.TrustLevel; +import org.asamk.signal.util.Util; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.state.IdentityKeyStore; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.util.Base64; import java.io.IOException; import java.util.ArrayList; import java.util.Date; -import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.UUID; public class JsonIdentityKeyStore implements IdentityKeyStore { - private final Map> trustedKeys = new HashMap<>(); + private final List identities = new ArrayList<>(); private final IdentityKeyPair identityKeyPair; private final int localRegistrationId; + private SignalServiceAddressResolver resolver; + public JsonIdentityKeyStore(IdentityKeyPair identityKeyPair, int localRegistrationId) { this.identityKeyPair = identityKeyPair; this.localRegistrationId = localRegistrationId; } + public void setResolver(final SignalServiceAddressResolver resolver) { + this.resolver = resolver; + } + + private SignalServiceAddress resolveSignalServiceAddress(String identifier) { + if (resolver != null) { + return resolver.resolveSignalServiceAddress(identifier); + } else { + return Util.getSignalServiceAddressFromIdentifier(identifier); + } + } + @Override public IdentityKeyPair getIdentityKeyPair() { return identityKeyPair; @@ -47,85 +63,116 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { @Override public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { - return saveIdentity(address.getName(), identityKey, TrustLevel.TRUSTED_UNVERIFIED, null); + return saveIdentity(resolveSignalServiceAddress(address.getName()), identityKey, TrustLevel.TRUSTED_UNVERIFIED, null); } /** - * Adds or updates the given identityKey for the user name and sets the trustLevel and added timestamp. + * Adds the given identityKey for the user name and sets the trustLevel and added timestamp. + * If the identityKey already exists, the trustLevel and added timestamp are NOT updated. * - * @param name User name, i.e. phone number - * @param identityKey The user's public key - * @param trustLevel - * @param added Added timestamp, if null and the key is newly added, the current time is used. + * @param serviceAddress User address, i.e. phone number and/or uuid + * @param identityKey The user's public key + * @param trustLevel Level of trust: untrusted, trusted, trusted and verified + * @param added Added timestamp, if null and the key is newly added, the current time is used. */ - public boolean saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel, Date added) { - List identities = trustedKeys.get(name); - if (identities == null) { - identities = new ArrayList<>(); - trustedKeys.put(name, identities); - } else { - for (Identity id : identities) { - if (!id.identityKey.equals(identityKey)) - continue; - - if (id.trustLevel.compareTo(trustLevel) < 0) { - id.trustLevel = trustLevel; - } - if (added != null) { - id.added = added; - } - return true; + public boolean saveIdentity(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel, Date added) { + for (Identity id : identities) { + if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) { + continue; } + + if (!id.address.getUuid().isPresent() || !id.address.getNumber().isPresent()) { + id.address = serviceAddress; + } + // Identity already exists, not updating the trust level + return true; } - identities.add(new Identity(identityKey, trustLevel, added != null ? added : new Date())); + + identities.add(new Identity(serviceAddress, identityKey, trustLevel, added != null ? added : new Date())); return false; } + /** + * Update trustLevel for the given identityKey for the user name. + * + * @param serviceAddress User address, i.e. phone number and/or uuid + * @param identityKey The user's public key + * @param trustLevel Level of trust: untrusted, trusted, trusted and verified + */ + public void setIdentityTrustLevel(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) { + for (Identity id : identities) { + if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) { + continue; + } + + if (!id.address.getUuid().isPresent() || !id.address.getNumber().isPresent()) { + id.address = serviceAddress; + } + id.trustLevel = trustLevel; + return; + } + + identities.add(new Identity(serviceAddress, identityKey, trustLevel, new Date())); + } + @Override public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) { // TODO implement possibility for different handling of incoming/outgoing trust decisions - List identities = trustedKeys.get(address.getName()); - if (identities == null) { - // Trust on first use - return true; - } + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); + boolean trustOnFirstUse = true; for (Identity id : identities) { + if (!id.address.matches(serviceAddress)) { + continue; + } + if (id.identityKey.equals(identityKey)) { return id.isTrusted(); + } else { + trustOnFirstUse = false; } } - return false; + return trustOnFirstUse; } @Override public IdentityKey getIdentity(SignalProtocolAddress address) { - List identities = trustedKeys.get(address.getName()); - if (identities == null || identities.size() == 0) { - return null; - } + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); + Identity identity = getIdentity(serviceAddress); + return identity == null ? null : identity.getIdentityKey(); + } + public Identity getIdentity(SignalServiceAddress serviceAddress) { long maxDate = 0; Identity maxIdentity = null; - for (Identity id : identities) { + for (Identity id : this.identities) { + if (!id.address.matches(serviceAddress)) { + continue; + } + final long time = id.getDateAdded().getTime(); if (maxIdentity == null || maxDate <= time) { maxDate = time; maxIdentity = id; } } - return maxIdentity.getIdentityKey(); + return maxIdentity; } - public Map> getIdentities() { + public List getIdentities() { // TODO deep copy - return trustedKeys; + return identities; } - public List getIdentities(String name) { - // TODO deep copy - return trustedKeys.get(name); + public List getIdentities(SignalServiceAddress serviceAddress) { + List identities = new ArrayList<>(); + for (Identity identity : this.identities) { + if (identity.address.matches(serviceAddress)) { + identities.add(identity); + } + } + return identities; } public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer { @@ -143,12 +190,26 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { JsonNode trustedKeysNode = node.get("trustedKeys"); if (trustedKeysNode.isArray()) { for (JsonNode trustedKey : trustedKeysNode) { - String trustedKeyName = trustedKey.get("name").asText(); + String trustedKeyName = trustedKey.hasNonNull("name") + ? trustedKey.get("name").asText() + : null; + + if (UuidUtil.isUuid(trustedKeyName)) { + // Ignore identities that were incorrectly created with UUIDs as name + continue; + } + + UUID uuid = trustedKey.hasNonNull("uuid") + ? UuidUtil.parseOrNull(trustedKey.get("uuid").asText()) + : null; + final SignalServiceAddress serviceAddress = uuid == null + ? Util.getSignalServiceAddressFromIdentifier(trustedKeyName) + : new SignalServiceAddress(uuid, trustedKeyName); try { IdentityKey id = new IdentityKey(Base64.decode(trustedKey.get("identityKey").asText()), 0); TrustLevel trustLevel = trustedKey.has("trustLevel") ? TrustLevel.fromInt(trustedKey.get("trustLevel").asInt()) : TrustLevel.TRUSTED_UNVERIFIED; Date added = trustedKey.has("addedTimestamp") ? new Date(trustedKey.get("addedTimestamp").asLong()) : new Date(); - keyStore.saveIdentity(trustedKeyName, id, trustLevel, added); + keyStore.saveIdentity(serviceAddress, id, trustLevel, added); } catch (InvalidKeyException | IOException e) { System.out.println(String.format("Error while decoding key for: %s", trustedKeyName)); } @@ -170,39 +231,53 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { json.writeNumberField("registrationId", jsonIdentityKeyStore.getLocalRegistrationId()); json.writeStringField("identityKey", Base64.encodeBytes(jsonIdentityKeyStore.getIdentityKeyPair().serialize())); json.writeArrayFieldStart("trustedKeys"); - for (Map.Entry> trustedKey : jsonIdentityKeyStore.trustedKeys.entrySet()) { - for (Identity id : trustedKey.getValue()) { - json.writeStartObject(); - json.writeStringField("name", trustedKey.getKey()); - json.writeStringField("identityKey", Base64.encodeBytes(id.identityKey.serialize())); - json.writeNumberField("trustLevel", id.trustLevel.ordinal()); - json.writeNumberField("addedTimestamp", id.added.getTime()); - json.writeEndObject(); + for (Identity trustedKey : jsonIdentityKeyStore.identities) { + json.writeStartObject(); + if (trustedKey.getAddress().getNumber().isPresent()) { + json.writeStringField("name", trustedKey.getAddress().getNumber().get()); } + if (trustedKey.getAddress().getUuid().isPresent()) { + json.writeStringField("uuid", trustedKey.getAddress().getUuid().get().toString()); + } + json.writeStringField("identityKey", Base64.encodeBytes(trustedKey.identityKey.serialize())); + json.writeNumberField("trustLevel", trustedKey.trustLevel.ordinal()); + json.writeNumberField("addedTimestamp", trustedKey.added.getTime()); + json.writeEndObject(); } json.writeEndArray(); json.writeEndObject(); } } - public class Identity { + public static class Identity { + SignalServiceAddress address; IdentityKey identityKey; TrustLevel trustLevel; Date added; - public Identity(IdentityKey identityKey, TrustLevel trustLevel) { + public Identity(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel) { + this.address = address; this.identityKey = identityKey; this.trustLevel = trustLevel; this.added = new Date(); } - Identity(IdentityKey identityKey, TrustLevel trustLevel, Date added) { + Identity(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel, Date added) { + this.address = address; this.identityKey = identityKey; this.trustLevel = trustLevel; this.added = added; } + public SignalServiceAddress getAddress() { + return address; + } + + public void setAddress(final SignalServiceAddress address) { + this.address = address; + } + boolean isTrusted() { return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java index 16248c02..d09b5d02 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java @@ -70,7 +70,7 @@ class JsonPreKeyStore implements PreKeyStore { try { preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText())); } catch (IOException e) { - System.out.println(String.format("Error while decoding prekey for: %s", preKeyId)); + System.err.println(String.format("Error while decoding prekey for: %s", preKeyId)); } } } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java index f7bbf204..b1ca622a 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java @@ -8,51 +8,72 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; +import org.asamk.signal.util.Util; import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SessionStore; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.util.Base64; import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; import java.util.LinkedList; import java.util.List; -import java.util.Map; +import java.util.UUID; class JsonSessionStore implements SessionStore { - private final Map sessions = new HashMap<>(); + private final List sessions = new ArrayList<>(); + + private SignalServiceAddressResolver resolver; public JsonSessionStore() { - } - private void addSessions(Map sessions) { - this.sessions.putAll(sessions); + public void setResolver(final SignalServiceAddressResolver resolver) { + this.resolver = resolver; } - @Override - public synchronized SessionRecord loadSession(SignalProtocolAddress remoteAddress) { - try { - if (containsSession(remoteAddress)) { - return new SessionRecord(sessions.get(remoteAddress)); - } else { - return new SessionRecord(); - } - } catch (IOException e) { - throw new AssertionError(e); + private SignalServiceAddress resolveSignalServiceAddress(String identifier) { + if (resolver != null) { + return resolver.resolveSignalServiceAddress(identifier); + } else { + return Util.getSignalServiceAddressFromIdentifier(identifier); } } @Override - public synchronized List getSubDeviceSessions(String name) { - List deviceIds = new LinkedList<>(); + public synchronized SessionRecord loadSession(SignalProtocolAddress address) { + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); + for (SessionInfo info : sessions) { + if (info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()) { + try { + return new SessionRecord(info.sessionRecord); + } catch (IOException e) { + System.err.println("Failed to load session, resetting session: " + e); + final SessionRecord sessionRecord = new SessionRecord(); + info.sessionRecord = sessionRecord.serialize(); + return sessionRecord; + } + } + } - for (SignalProtocolAddress key : sessions.keySet()) { - if (key.getName().equals(name) && - key.getDeviceId() != 1) { - deviceIds.add(key.getDeviceId()); + return new SessionRecord(); + } + + public synchronized List getSessions() { + return sessions; + } + + @Override + public synchronized List getSubDeviceSessions(String name) { + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(name); + + List deviceIds = new LinkedList<>(); + for (SessionInfo info : sessions) { + if (info.address.matches(serviceAddress) && info.deviceId != 1) { + deviceIds.add(info.deviceId); } } @@ -61,26 +82,45 @@ class JsonSessionStore implements SessionStore { @Override public synchronized void storeSession(SignalProtocolAddress address, SessionRecord record) { - sessions.put(address, record.serialize()); + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); + for (SessionInfo info : sessions) { + if (info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()) { + if (!info.address.getUuid().isPresent() || !info.address.getNumber().isPresent()) { + info.address = serviceAddress; + } + info.sessionRecord = record.serialize(); + return; + } + } + + sessions.add(new SessionInfo(serviceAddress, address.getDeviceId(), record.serialize())); } @Override public synchronized boolean containsSession(SignalProtocolAddress address) { - return sessions.containsKey(address); + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); + for (SessionInfo info : sessions) { + if (info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()) { + return true; + } + } + return false; } @Override public synchronized void deleteSession(SignalProtocolAddress address) { - sessions.remove(address); + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); + sessions.removeIf(info -> info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()); } @Override public synchronized void deleteAllSessions(String name) { - for (SignalProtocolAddress key : new ArrayList<>(sessions.keySet())) { - if (key.getName().equals(name)) { - sessions.remove(key); - } - } + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(name); + deleteAllSessions(serviceAddress); + } + + public synchronized void deleteAllSessions(SignalServiceAddress serviceAddress) { + sessions.removeIf(info -> info.address.matches(serviceAddress)); } public static class JsonSessionStoreDeserializer extends JsonDeserializer { @@ -89,39 +129,58 @@ class JsonSessionStore implements SessionStore { public JsonSessionStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); - Map sessionMap = new HashMap<>(); + JsonSessionStore sessionStore = new JsonSessionStore(); + if (node.isArray()) { for (JsonNode session : node) { - String sessionName = session.get("name").asText(); + String sessionName = session.hasNonNull("name") + ? session.get("name").asText() + : null; + if (UuidUtil.isUuid(sessionName)) { + // Ignore sessions that were incorrectly created with UUIDs as name + continue; + } + + UUID uuid = session.hasNonNull("uuid") + ? UuidUtil.parseOrNull(session.get("uuid").asText()) + : null; + final SignalServiceAddress serviceAddress = uuid == null + ? Util.getSignalServiceAddressFromIdentifier(sessionName) + : new SignalServiceAddress(uuid, sessionName); + final int deviceId = session.get("deviceId").asInt(); + final String record = session.get("record").asText(); try { - sessionMap.put(new SignalProtocolAddress(sessionName, session.get("deviceId").asInt()), Base64.decode(session.get("record").asText())); + SessionInfo sessionInfo = new SessionInfo(serviceAddress, deviceId, Base64.decode(record)); + sessionStore.sessions.add(sessionInfo); } catch (IOException e) { - System.out.println(String.format("Error while decoding session for: %s", sessionName)); + System.err.println(String.format("Error while decoding session for: %s", sessionName)); } } } - JsonSessionStore sessionStore = new JsonSessionStore(); - sessionStore.addSessions(sessionMap); - return sessionStore; - } } - public static class JsonPreKeyStoreSerializer extends JsonSerializer { + public static class JsonSessionStoreSerializer extends JsonSerializer { @Override public void serialize(JsonSessionStore jsonSessionStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException { json.writeStartArray(); - for (Map.Entry preKey : jsonSessionStore.sessions.entrySet()) { + for (SessionInfo sessionInfo : jsonSessionStore.sessions) { json.writeStartObject(); - json.writeStringField("name", preKey.getKey().getName()); - json.writeNumberField("deviceId", preKey.getKey().getDeviceId()); - json.writeStringField("record", Base64.encodeBytes(preKey.getValue())); + if (sessionInfo.address.getNumber().isPresent()) { + json.writeStringField("name", sessionInfo.address.getNumber().get()); + } + if (sessionInfo.address.getUuid().isPresent()) { + json.writeStringField("uuid", sessionInfo.address.getUuid().get().toString()); + } + json.writeNumberField("deviceId", sessionInfo.deviceId); + json.writeStringField("record", Base64.encodeBytes(sessionInfo.sessionRecord)); json.writeEndObject(); } json.writeEndArray(); } } + } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java index 65ee4a6e..9a8802b4 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.asamk.signal.TrustLevel; +import org.asamk.signal.manager.TrustLevel; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyIdException; @@ -13,9 +13,9 @@ import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.state.SignedPreKeyRecord; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.List; -import java.util.Map; public class JsonSignalProtocolStore implements SignalProtocolStore { @@ -26,7 +26,7 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { @JsonProperty("sessionStore") @JsonDeserialize(using = JsonSessionStore.JsonSessionStoreDeserializer.class) - @JsonSerialize(using = JsonSessionStore.JsonPreKeyStoreSerializer.class) + @JsonSerialize(using = JsonSessionStore.JsonSessionStoreSerializer.class) private JsonSessionStore sessionStore; @JsonProperty("signedPreKeyStore") @@ -56,6 +56,11 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { this.identityKeyStore = new JsonIdentityKeyStore(identityKeyPair, registrationId); } + public void setResolver(final SignalServiceAddressResolver resolver) { + sessionStore.setResolver(resolver); + identityKeyStore.setResolver(resolver); + } + @Override public IdentityKeyPair getIdentityKeyPair() { return identityKeyStore.getIdentityKeyPair(); @@ -71,16 +76,20 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { return identityKeyStore.saveIdentity(address, identityKey); } - public void saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel) { - identityKeyStore.saveIdentity(name, identityKey, trustLevel, null); + public void saveIdentity(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) { + identityKeyStore.saveIdentity(serviceAddress, identityKey, trustLevel, null); } - public Map> getIdentities() { + public void setIdentityTrustLevel(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) { + identityKeyStore.setIdentityTrustLevel(serviceAddress, identityKey, trustLevel); + } + + public List getIdentities() { return identityKeyStore.getIdentities(); } - public List getIdentities(String name) { - return identityKeyStore.getIdentities(name); + public List getIdentities(SignalServiceAddress serviceAddress) { + return identityKeyStore.getIdentities(serviceAddress); } @Override @@ -93,6 +102,10 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { return identityKeyStore.getIdentity(address); } + public JsonIdentityKeyStore.Identity getIdentity(SignalServiceAddress serviceAddress) { + return identityKeyStore.getIdentity(serviceAddress); + } + @Override public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { return preKeyStore.loadPreKey(preKeyId); @@ -118,6 +131,10 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { return sessionStore.loadSession(address); } + public List getSessions() { + return sessionStore.getSessions(); + } + @Override public List getSubDeviceSessions(String name) { return sessionStore.getSubDeviceSessions(name); @@ -143,6 +160,10 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { sessionStore.deleteAllSessions(name); } + public void deleteAllSessions(SignalServiceAddress serviceAddress) { + sessionStore.deleteAllSessions(serviceAddress); + } + @Override public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { return signedPreKeyStore.loadSignedPreKey(signedPreKeyId); diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java index edad8e7e..3927b98d 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java @@ -87,7 +87,7 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore { try { preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText())); } catch (IOException e) { - System.out.println(String.format("Error while decoding prekey for: %s", preKeyId)); + System.err.println(String.format("Error while decoding prekey for: %s", preKeyId)); } } } diff --git a/src/main/java/org/asamk/signal/storage/protocol/RecipientStore.java b/src/main/java/org/asamk/signal/storage/protocol/RecipientStore.java new file mode 100644 index 00000000..47f06c46 --- /dev/null +++ b/src/main/java/org/asamk/signal/storage/protocol/RecipientStore.java @@ -0,0 +1,84 @@ +package org.asamk.signal.storage.protocol; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class RecipientStore { + + @JsonProperty("recipientStore") + @JsonDeserialize(using = RecipientStoreDeserializer.class) + @JsonSerialize(using = RecipientStoreSerializer.class) + private final Set addresses = new HashSet<>(); + + public SignalServiceAddress resolveServiceAddress(SignalServiceAddress serviceAddress) { + if (addresses.contains(serviceAddress)) { + // If the Set already contains the exact address with UUID and Number, + // we can just return it here. + return serviceAddress; + } + + for (SignalServiceAddress address : addresses) { + if (address.matches(serviceAddress)) { + return address; + } + } + + if (serviceAddress.getNumber().isPresent() && serviceAddress.getUuid().isPresent()) { + addresses.add(serviceAddress); + } + + return serviceAddress; + } + + public static class RecipientStoreDeserializer extends JsonDeserializer> { + + @Override + public Set deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + + Set addresses = new HashSet<>(); + + if (node.isArray()) { + for (JsonNode recipient : node) { + String recipientName = recipient.get("name").asText(); + UUID uuid = UuidUtil.parseOrThrow(recipient.get("uuid").asText()); + final SignalServiceAddress serviceAddress = new SignalServiceAddress(uuid, recipientName); + addresses.add(serviceAddress); + } + } + + return addresses; + } + } + + public static class RecipientStoreSerializer extends JsonSerializer> { + + @Override + public void serialize(Set addresses, JsonGenerator json, SerializerProvider serializerProvider) throws IOException { + json.writeStartArray(); + for (SignalServiceAddress address : addresses) { + json.writeStartObject(); + json.writeStringField("name", address.getNumber().get()); + json.writeStringField("uuid", address.getUuid().get().toString()); + json.writeEndObject(); + } + json.writeEndArray(); + } + } +} diff --git a/src/main/java/org/asamk/signal/storage/protocol/SessionInfo.java b/src/main/java/org/asamk/signal/storage/protocol/SessionInfo.java new file mode 100644 index 00000000..00221233 --- /dev/null +++ b/src/main/java/org/asamk/signal/storage/protocol/SessionInfo.java @@ -0,0 +1,18 @@ +package org.asamk.signal.storage.protocol; + +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +public class SessionInfo { + + public SignalServiceAddress address; + + public int deviceId; + + public byte[] sessionRecord; + + public SessionInfo(final SignalServiceAddress address, final int deviceId, final byte[] sessionRecord) { + this.address = address; + this.deviceId = deviceId; + this.sessionRecord = sessionRecord; + } +} diff --git a/src/main/java/org/asamk/signal/storage/protocol/SignalServiceAddressResolver.java b/src/main/java/org/asamk/signal/storage/protocol/SignalServiceAddressResolver.java new file mode 100644 index 00000000..b1c5fb38 --- /dev/null +++ b/src/main/java/org/asamk/signal/storage/protocol/SignalServiceAddressResolver.java @@ -0,0 +1,13 @@ +package org.asamk.signal.storage.protocol; + +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +public interface SignalServiceAddressResolver { + + /** + * Get a SignalServiceAddress with number and/or uuid from an identifier name. + * + * @param identifier can be either a serialized uuid or a e164 phone number + */ + SignalServiceAddress resolveSignalServiceAddress(String identifier); +} diff --git a/src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java b/src/main/java/org/asamk/signal/storage/threads/LegacyJsonThreadStore.java similarity index 87% rename from src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java rename to src/main/java/org/asamk/signal/storage/threads/LegacyJsonThreadStore.java index a4a89ccd..6bea1bfe 100644 --- a/src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java +++ b/src/main/java/org/asamk/signal/storage/threads/LegacyJsonThreadStore.java @@ -18,23 +18,15 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -public class JsonThreadStore { +public class LegacyJsonThreadStore { private static final ObjectMapper jsonProcessor = new ObjectMapper(); @JsonProperty("threads") - @JsonSerialize(using = JsonThreadStore.MapToListSerializer.class) + @JsonSerialize(using = MapToListSerializer.class) @JsonDeserialize(using = ThreadsDeserializer.class) private Map threads = new HashMap<>(); - public void updateThread(ThreadInfo thread) { - threads.put(thread.id, thread); - } - - public ThreadInfo getThread(String id) { - return threads.get(id); - } - public List getThreads() { return new ArrayList<>(threads.values()); } diff --git a/src/main/java/org/asamk/signal/util/ErrorUtils.java b/src/main/java/org/asamk/signal/util/ErrorUtils.java index 38f1986e..a09be3d0 100644 --- a/src/main/java/org/asamk/signal/util/ErrorUtils.java +++ b/src/main/java/org/asamk/signal/util/ErrorUtils.java @@ -1,13 +1,12 @@ package org.asamk.signal.util; -import org.asamk.signal.GroupIdFormatException; -import org.asamk.signal.GroupNotFoundException; -import org.asamk.signal.NotAGroupMemberException; -import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.asamk.signal.manager.GroupNotFoundException; +import org.asamk.signal.manager.NotAGroupMemberException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; +import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; @@ -50,13 +49,13 @@ public class ErrorUtils { System.err.println("Aborting sending."); } - public static void handleDBusExecutionException(DBusExecutionException e) { - System.err.println("Cannot connect to dbus: " + e.getMessage()); - System.err.println("Aborting."); - } - public static void handleGroupIdFormatException(GroupIdFormatException e) { System.err.println(e.getMessage()); System.err.println("Aborting sending."); } + + public static void handleInvalidNumberException(InvalidNumberException e) { + System.err.println("Failed to parse recipient: " + e.getMessage()); + System.err.println("Aborting sending."); + } } diff --git a/src/main/java/org/asamk/signal/GroupIdFormatException.java b/src/main/java/org/asamk/signal/util/GroupIdFormatException.java similarity index 89% rename from src/main/java/org/asamk/signal/GroupIdFormatException.java rename to src/main/java/org/asamk/signal/util/GroupIdFormatException.java index 62add535..5a5c4570 100644 --- a/src/main/java/org/asamk/signal/GroupIdFormatException.java +++ b/src/main/java/org/asamk/signal/util/GroupIdFormatException.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.util; import java.io.IOException; diff --git a/src/main/java/org/asamk/signal/util/Hex.java b/src/main/java/org/asamk/signal/util/Hex.java index 95b2d26f..8c6c62a2 100644 --- a/src/main/java/org/asamk/signal/util/Hex.java +++ b/src/main/java/org/asamk/signal/util/Hex.java @@ -9,6 +9,15 @@ public class Hex { private Hex() { } + public static String toString(byte[] bytes) { + StringBuffer buf = new StringBuffer(); + for (final byte aByte : bytes) { + appendHexChar(buf, aByte); + buf.append(" "); + } + return buf.toString(); + } + public static String toStringCondensed(byte[] bytes) { StringBuffer buf = new StringBuffer(); for (final byte aByte : bytes) { @@ -20,7 +29,6 @@ public class Hex { private static void appendHexChar(StringBuffer buf, int b) { buf.append(HEX_DIGITS[(b >> 4) & 0xf]); buf.append(HEX_DIGITS[b & 0xf]); - buf.append(" "); } public static byte[] toByteArray(String s) { diff --git a/src/main/java/org/asamk/signal/util/IOUtils.java b/src/main/java/org/asamk/signal/util/IOUtils.java index 434669de..1163d079 100644 --- a/src/main/java/org/asamk/signal/util/IOUtils.java +++ b/src/main/java/org/asamk/signal/util/IOUtils.java @@ -1,8 +1,13 @@ package org.asamk.signal.util; +import org.whispersystems.signalservice.internal.util.Util; + +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.StringWriter; import java.nio.charset.Charset; import java.nio.file.Files; @@ -35,6 +40,12 @@ public class IOUtils { return output.toString(); } + public static byte[] readFully(InputStream in) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Util.copy(in, baos); + return baos.toByteArray(); + } + public static void createPrivateDirectories(String directoryPath) throws IOException { final File file = new File(directoryPath); if (file.exists()) { @@ -68,4 +79,19 @@ public class IOUtils { return System.getProperty("user.home") + "/.local/share"; } + + public static void copyStreamToFile(InputStream input, File outputFile) throws IOException { + copyStreamToFile(input, outputFile, 8192); + } + + public static void copyStreamToFile(InputStream input, File outputFile, int bufferSize) throws IOException { + try (OutputStream output = new FileOutputStream(outputFile)) { + byte[] buffer = new byte[bufferSize]; + int read; + + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + } + } } diff --git a/src/main/java/org/asamk/signal/util/RandomUtils.java b/src/main/java/org/asamk/signal/util/RandomUtils.java index d0463b47..19c3f18c 100644 --- a/src/main/java/org/asamk/signal/util/RandomUtils.java +++ b/src/main/java/org/asamk/signal/util/RandomUtils.java @@ -5,17 +5,14 @@ import java.security.SecureRandom; public class RandomUtils { - private static final ThreadLocal LOCAL_RANDOM = new ThreadLocal() { - @Override - protected SecureRandom initialValue() { - SecureRandom rand = getSecureRandomUnseeded(); + private static final ThreadLocal LOCAL_RANDOM = ThreadLocal.withInitial(() -> { + SecureRandom rand = getSecureRandomUnseeded(); - // Let the SecureRandom seed it self initially - rand.nextBoolean(); + // Let the SecureRandom seed it self initially + rand.nextBoolean(); - return rand; - } - }; + return rand; + }); private static SecureRandom getSecureRandomUnseeded() { try { diff --git a/src/main/java/org/asamk/signal/util/SecurityProvider.java b/src/main/java/org/asamk/signal/util/SecurityProvider.java index 9177a781..8a0e53f8 100644 --- a/src/main/java/org/asamk/signal/util/SecurityProvider.java +++ b/src/main/java/org/asamk/signal/util/SecurityProvider.java @@ -11,15 +11,15 @@ public class SecurityProvider extends Provider { private static final String info = "Security Provider v1.0"; public SecurityProvider() { - super(PROVIDER_NAME, 1.0, info); + super(PROVIDER_NAME, "1.0", info); put("SecureRandom.DEFAULT", DefaultRandom.class.getName()); // Workaround for BKS truststore - put("KeyStore.BKS", "org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi$Std"); - put("KeyStore.BKS-V1", "org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi$Version1"); - put("KeyStore.BouncyCastle", "org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi$BouncyCastleStore"); - put("KeyFactory.X.509", "org.bouncycastle.jcajce.provider.asymmetric.x509.KeyFactory"); - put("CertificateFactory.X.509", "org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory"); + put("KeyStore.BKS", org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi.Std.class.getCanonicalName()); + put("KeyStore.BKS-V1", org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi.Version1.class.getCanonicalName()); + put("KeyStore.BouncyCastle", org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi.BouncyCastleStore.class.getCanonicalName()); + put("KeyFactory.X.509", org.bouncycastle.jcajce.provider.asymmetric.x509.KeyFactory.class.getCanonicalName()); + put("CertificateFactory.X.509", org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory.class.getCanonicalName()); } public static class DefaultRandom extends SecureRandomSpi { diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index 01d8b2b1..d6b467b0 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -2,7 +2,10 @@ package org.asamk.signal.util; import com.fasterxml.jackson.databind.JsonNode; -import org.asamk.signal.GroupIdFormatException; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.util.Base64; import java.io.IOException; @@ -51,4 +54,16 @@ public class Util { throw new GroupIdFormatException(groupId, e); } } + + public static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException { + return PhoneNumberFormatter.formatNumber(number, localNumber); + } + + public static SignalServiceAddress getSignalServiceAddressFromIdentifier(final String identifier) { + if (UuidUtil.isUuid(identifier)) { + return new SignalServiceAddress(UuidUtil.parseOrNull(identifier), null); + } else { + return new SignalServiceAddress(null, identifier); + } + } }