Merge remote-tracking branch 'AsamK/master' into master

This commit is contained in:
user-invalid 2020-09-20 17:32:45 +02:00
commit 7aa31842dd
91 changed files with 3917 additions and 1594 deletions

View file

@ -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

60
.github/workflows/codeql-analysis.yml vendored Normal file
View file

@ -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

View file

@ -4,6 +4,28 @@
<JavaCodeStyleSettings>
<option name="GENERATE_FINAL_LOCALS" value="true" />
<option name="GENERATE_FINAL_PARAMETERS" value="true" />
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="50" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="50" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
</value>
</option>
<option name="JD_P_AT_EMPTY_LINES" value="false" />
</JavaCodeStyleSettings>
<XML>

View file

@ -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

View file

@ -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"

Binary file not shown.

View file

@ -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

2
gradlew vendored
View file

@ -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

25
gradlew.bat vendored
View file

@ -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

View file

@ -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*::
Dont download attachments of received messages.
Dont 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": "<STICKER_PACK_TITLE>",
"author": "<STICKER_PACK_AUTHOR>",
"cover": { // Optional cover, by default the first sticker is used as cover
"file": "<name of webp file, mandatory>",
"emoji": "<optional>"
},
"stickers": [
{
"file": "<name of webp file, mandatory>",
"emoji": "<optional>"
}
...
]
}
----
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*::
Dont download attachments of received messages.
Dont 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 <asamk@gmx.de>, who is assisted by other open
source contributors. For more information about signal-cli development, see
Maintained by AsamK <asamk@gmx.de>, who is assisted by other open source contributors.
For more information about signal-cli development, see
<https://github.com/AsamK/signal-cli>.

View file

@ -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<String> attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException;
long sendMessage(String message, List<String> attachments, String recipient) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber;
void sendMessage(String message, List<String> attachments, List<String> recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException;
long sendMessage(String message, List<String> attachments, List<String> recipients) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UnregisteredUser, Error.UntrustedIdentity;
void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions;
void sendEndSessionMessage(List<String> recipients) throws Error.Failure, Error.InvalidNumber, Error.UnregisteredUser, Error.UntrustedIdentity;
void sendGroupMessage(String message, List<String> attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException;
long sendGroupMessage(String message, List<String> 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<byte[]> getGroupIds();
@ -35,17 +35,17 @@ public interface Signal extends DBusInterface {
List<String> getGroupMembers(byte[] groupId);
byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException;
byte[] updateGroup(byte[] groupId, String name, List<String> 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<String> attachments;
private final long timestamp;
private final String sender;
private final byte[] groupId;
private final String message;
private final List<String> attachments;
public MessageReceived(String objectpath, long timestamp, String sender, byte[] groupId, String message, List<String> 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<String> attachments;
public SyncMessageReceived(String objectpath, long timestamp, String source, String destination, byte[] groupId, String message, List<String> 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<String> 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);
}
}
}
}

View file

@ -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() {
}
}

View file

@ -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;

View file

@ -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<JsonAttachment> 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<>();
}
}
}

View file

@ -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<String> 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<String> getAttachments(SignalServiceDataMessage message, Manager m) {
List<String> 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);

View file

@ -1,10 +0,0 @@
package org.asamk.signal;
class JsonError {
String message;
JsonError(Throwable exception) {
this.message = exception.getMessage();
}
}

View file

@ -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);
}

View file

@ -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<String> blockedNumbers;
List<ReadMessage> 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;
}
}
}

View file

@ -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<String, Command> 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<String, Command> 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<String, Command> 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<String, Command> 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

View file

@ -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" : "<unavailable>") + (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);
}

View file

@ -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;

View file

@ -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<String, Command> getCommands() {

View file

@ -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);

View file

@ -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 {

View file

@ -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;

View file

@ -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) {
}

View file

@ -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;
}

View file

@ -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<String, List<JsonIdentityKeyStore.Identity>> 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<String, List<JsonIdentityKeyStore.Identity>> key = m.getIdentities(number);
for (JsonIdentityKeyStore.Identity id : key.second()) {
printIdentityFingerprint(m, key.first(), id);
List<JsonIdentityKeyStore.Identity> 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;

View file

@ -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);
}

View file

@ -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;

View file

@ -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<Signal.MessageReceived>() {
@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<Signal.ReceiptReceived>() {
@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

View file

@ -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;

View file

@ -21,7 +21,7 @@ public class RemovePinCommand implements LocalCommand {
return 1;
}
try {
m.setRegistrationLockPin(Optional.<String>absent());
m.setRegistrationLockPin(Optional.absent());
return 0;
} catch (IOException e) {
System.err.println("Remove pin error: " + e.getMessage());

View file

@ -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.<String>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<String> attachments = ns.getList("attachment");
if (attachments == null) {
attachments = new ArrayList<>();
}
try {
List<String> 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.<String>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;
}
}

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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<String> 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<String> 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;
}
}

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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;

View file

@ -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<String> attachments, final String recipient) {
List<String> 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<String> attachments, final List<String> 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<String> 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<String> 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<byte[]> getGroupIds() {
List<GroupInfo> groups = m.getGroups();
List<byte[]> 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<String> 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<String> 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;
}
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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<JsonAttachment> 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());
}
}

View file

@ -0,0 +1,10 @@
package org.asamk.signal.json;
public class JsonError {
String message;
public JsonError(Throwable exception) {
this.message = exception.getMessage();
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal;
package org.asamk.signal.json;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;

View file

@ -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();
}
}

View file

@ -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<String> blockedNumbers;
List<ReadMessage> 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);
}
}

View file

@ -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);

View file

@ -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() {
}
}

View file

@ -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));

View file

@ -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;
}
}

View file

@ -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<JsonSticker> stickers;
public static class JsonSticker {
@JsonProperty
public String emoji;
@JsonProperty
public String file;
}
}

View file

@ -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);

File diff suppressed because it is too large Load diff

View file

@ -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) + ")");

View file

@ -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;
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View file

@ -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> 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<Interceptor> 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<Integer, SignalCdnUrl[]> makeSignalCdnUrlMapFor(SignalCdnUrl[] cdn0Urls, SignalCdnUrl[] cdn2Urls) {
Map<Integer, SignalCdnUrl[]> result = new HashMap<>();
result.put(0, cdn0Urls);
result.put(2, cdn2Urls);
return Collections.unmodifiableMap(result);
}
private ServiceConfig() {
}
}

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager;
public class StickerPackInvalidException extends Exception {
public StickerPackInvalidException(String message) {
super(message);
}
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal;
package org.asamk.signal.manager;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;

View file

@ -1,4 +1,4 @@
package org.asamk.signal;
package org.asamk.signal.manager;
public class UserAlreadyExists extends Exception {

View file

@ -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<byte[]> preview = Optional.absent();
Optional<String> caption = Optional.absent();
Optional<String> blurHash = Optional.absent();
return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, caption, blurHash, null);
final Optional<ResumableUploadSpec> 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<SignalServiceAddress> getSignalServiceAddresses(Collection<String> recipients, String localNumber) {
Set<SignalServiceAddress> 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<SignalServiceAddress> 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();
}

View file

@ -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<FileChannel, FileLock> 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<FileChannel, FileLock> 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<FileChannel, FileLock> 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<FileChannel, FileLock> 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<PreKeyRecord> 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();
}
}
}

View file

@ -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);
}
}

View file

@ -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<String, ContactInfo> contacts = new HashMap<>();
private List<ContactInfo> 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<ContactInfo> 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<Map<?, ?>> {
@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<Map<String, ContactInfo>> {
@Override
public Map<String, ContactInfo> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
Map<String, ContactInfo> 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;
}
}
}

View file

@ -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<String> members = new HashSet<>();
@JsonProperty
public boolean active;
@JsonDeserialize(using = MembersDeserializer.class)
@JsonSerialize(using = MembersSerializer.class)
public Set<SignalServiceAddress> 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<String> 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<SignalServiceAddress> 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<SignalServiceAddress> getMembers() {
Set<SignalServiceAddress> addresses = new HashSet<>(members.size());
for (String member : members) {
addresses.add(new SignalServiceAddress(null, member));
}
return addresses;
return members;
}
public void addMembers(Collection<SignalServiceAddress> members) {
@JsonIgnore
public Set<String> getMembersE164() {
Set<String> 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<SignalServiceAddress> getMembersWithout(SignalServiceAddress address) {
Set<SignalServiceAddress> members = new HashSet<>(this.members.size());
for (SignalServiceAddress member : this.members) {
if (!member.matches(address)) {
members.add(member);
}
}
return members;
}
public void addMembers(Collection<SignalServiceAddress> 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<Set<SignalServiceAddress>> {
@Override
public void serialize(final Set<SignalServiceAddress> 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<Set<SignalServiceAddress>> {
@Override
public Set<SignalServiceAddress> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
Set<SignalServiceAddress> 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;
}
}
}

View file

@ -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<SignalProfileEntry> 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<List<SignalProfileEntry>> {
@Override
public List<SignalProfileEntry> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
List<SignalProfileEntry> 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<List<SignalProfileEntry>> {
@Override
public void serialize(List<SignalProfileEntry> 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();
}
}
}

View file

@ -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 +
'}';
}
}

View file

@ -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;
}
}

View file

@ -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<String, List<Identity>> trustedKeys = new HashMap<>();
private final List<Identity> 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<Identity> 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<Identity> 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<Identity> 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<String, List<Identity>> getIdentities() {
public List<Identity> getIdentities() {
// TODO deep copy
return trustedKeys;
return identities;
}
public List<Identity> getIdentities(String name) {
// TODO deep copy
return trustedKeys.get(name);
public List<Identity> getIdentities(SignalServiceAddress serviceAddress) {
List<Identity> identities = new ArrayList<>();
for (Identity identity : this.identities) {
if (identity.address.matches(serviceAddress)) {
identities.add(identity);
}
}
return identities;
}
public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer<JsonIdentityKeyStore> {
@ -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<String, List<Identity>> 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;

View file

@ -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));
}
}
}

View file

@ -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<SignalProtocolAddress, byte[]> sessions = new HashMap<>();
private final List<SessionInfo> sessions = new ArrayList<>();
private SignalServiceAddressResolver resolver;
public JsonSessionStore() {
}
private void addSessions(Map<SignalProtocolAddress, byte[]> 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<Integer> getSubDeviceSessions(String name) {
List<Integer> 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<SessionInfo> getSessions() {
return sessions;
}
@Override
public synchronized List<Integer> getSubDeviceSessions(String name) {
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(name);
List<Integer> 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<JsonSessionStore> {
@ -89,39 +129,58 @@ class JsonSessionStore implements SessionStore {
public JsonSessionStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
Map<SignalProtocolAddress, byte[]> 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<JsonSessionStore> {
public static class JsonSessionStoreSerializer extends JsonSerializer<JsonSessionStore> {
@Override
public void serialize(JsonSessionStore jsonSessionStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException {
json.writeStartArray();
for (Map.Entry<SignalProtocolAddress, byte[]> 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();
}
}
}

View file

@ -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<String, List<JsonIdentityKeyStore.Identity>> getIdentities() {
public void setIdentityTrustLevel(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) {
identityKeyStore.setIdentityTrustLevel(serviceAddress, identityKey, trustLevel);
}
public List<JsonIdentityKeyStore.Identity> getIdentities() {
return identityKeyStore.getIdentities();
}
public List<JsonIdentityKeyStore.Identity> getIdentities(String name) {
return identityKeyStore.getIdentities(name);
public List<JsonIdentityKeyStore.Identity> 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<SessionInfo> getSessions() {
return sessionStore.getSessions();
}
@Override
public List<Integer> 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);

View file

@ -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));
}
}
}

View file

@ -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<SignalServiceAddress> 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<Set<SignalServiceAddress>> {
@Override
public Set<SignalServiceAddress> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
Set<SignalServiceAddress> 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<Set<SignalServiceAddress>> {
@Override
public void serialize(Set<SignalServiceAddress> 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();
}
}
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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<String, ThreadInfo> threads = new HashMap<>();
public void updateThread(ThreadInfo thread) {
threads.put(thread.id, thread);
}
public ThreadInfo getThread(String id) {
return threads.get(id);
}
public List<ThreadInfo> getThreads() {
return new ArrayList<>(threads.values());
}

View file

@ -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.");
}
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal;
package org.asamk.signal.util;
import java.io.IOException;

View file

@ -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) {

View file

@ -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);
}
}
}
}

View file

@ -5,17 +5,14 @@ import java.security.SecureRandom;
public class RandomUtils {
private static final ThreadLocal<SecureRandom> LOCAL_RANDOM = new ThreadLocal<SecureRandom>() {
@Override
protected SecureRandom initialValue() {
SecureRandom rand = getSecureRandomUnseeded();
private static final ThreadLocal<SecureRandom> 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 {

View file

@ -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 {

View file

@ -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);
}
}
}