mirror of
https://github.com/oxen-io/session-android.git
synced 2025-04-30 14:30:46 +00:00
Move libtextsecure into independent repository.
This commit is contained in:
commit
30623fb200
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.gradle
|
||||||
|
.idea
|
||||||
|
*.iml
|
||||||
|
local.properties
|
||||||
|
build/
|
98
README.md
Normal file
98
README.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# libtextsecure-java
|
||||||
|
|
||||||
|
A Java library for communicating via TextSecure.
|
||||||
|
|
||||||
|
## Implementing the Axolotl interfaces
|
||||||
|
|
||||||
|
The axolotl encryption protocol is a stateful protocol, so libtextsecure users
|
||||||
|
need to implement the storage interface `AxolotlStore`, which handles load/store
|
||||||
|
of your key and session information to durable media.
|
||||||
|
|
||||||
|
## Creating keys
|
||||||
|
|
||||||
|
`````
|
||||||
|
IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair();
|
||||||
|
List<PreKeyRecord> oneTimePreKeys = KeyHelper.generatePreKeys(100);
|
||||||
|
PreKeyRecord lastResortKey = KeyHelper.generateLastResortKey();
|
||||||
|
SignedPreKeyRecord signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKey, signedPreKeyId);
|
||||||
|
`````
|
||||||
|
|
||||||
|
The above are then stored locally so that they're available for load via the `AxolotlStore`.
|
||||||
|
|
||||||
|
## Registering
|
||||||
|
|
||||||
|
At install time, clients need to register with the TextSecure server.
|
||||||
|
|
||||||
|
`````
|
||||||
|
private final String URL = "https://my.textsecure.server.com";
|
||||||
|
private final TrustStore TRUST_STORE = new MyTrustStoreImpl();
|
||||||
|
private final String USERNAME = "+14151231234";
|
||||||
|
private final String PASSWORD = generateRandomPassword();
|
||||||
|
|
||||||
|
TextSecureAccountManager accountManager = new TextSecureAccountManager(URL, TRUST_STORE,
|
||||||
|
USERNAME, PASSWORD);
|
||||||
|
|
||||||
|
accountManager.requestSmsVerificationCode();
|
||||||
|
accountManager.verifyAccount(receivedSmsVerificationCode, generateRandomSignalingKey(),
|
||||||
|
false, generateRandomInstallId());
|
||||||
|
accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
|
||||||
|
accountManager.setPreKeys(identityKey.getPublic(), lastResortKey, signedPreKey, oneTimePreKeys);
|
||||||
|
`````
|
||||||
|
|
||||||
|
## Sending text messages
|
||||||
|
|
||||||
|
`````
|
||||||
|
TextSecureMessageSender messageSender = new TextSecureMessageSender(URL, TRUST_STORE, USERNAME, PASSWORD,
|
||||||
|
localRecipientId, new MyAxolotlStore(),
|
||||||
|
Optional.absent());
|
||||||
|
|
||||||
|
long recipientId = getRecipientIdFor("+14159998888");
|
||||||
|
TextSecureAddress destination = new TextSecureAddress(recipientId, "+14159998888", null);
|
||||||
|
TextSecureMessage message = new TextSecureMessage(System.currentTimeMillis(), "Hello, world!");
|
||||||
|
|
||||||
|
messageSender.sendMessage(destination, message);
|
||||||
|
`````
|
||||||
|
|
||||||
|
## Sending media messages
|
||||||
|
|
||||||
|
`````
|
||||||
|
TextSecureMessageSender messageSender = new TextSecureMessageSender(URL, TRUST_STORE, USERNAME, PASSWORD,
|
||||||
|
localRecipientId, new MyAxolotlStore(),
|
||||||
|
Optional.absent());
|
||||||
|
|
||||||
|
long recipientId = getRecipientIdFor("+14159998888");
|
||||||
|
TextSecureAddress destination = new TextSecureAddress(recipientId, "+14159998888", null);
|
||||||
|
|
||||||
|
File myAttachment = new File("/path/to/my.attachment");
|
||||||
|
FileInputStream attachmentStream = new FileInputStream(myAttachment);
|
||||||
|
TextSecureAttachment attachment = new TextSecureAttachmentStream(attachmentStream, "image/png", myAttachment.size());
|
||||||
|
TextSecureMessage message = new TextSecureMessage(System.currentTimeMillis(), attachment, "Hello, world!");
|
||||||
|
|
||||||
|
messageSender.sendMessage(destination, message);
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
## Receiving messages
|
||||||
|
|
||||||
|
`````
|
||||||
|
TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, USERNAME, PASSWORD, mySignalingKey);
|
||||||
|
TextSecureMessagePipe messagePipe;
|
||||||
|
|
||||||
|
try {
|
||||||
|
messagePipe = messageReciever.createMessagePipe();
|
||||||
|
|
||||||
|
while (listeningForMessages) {
|
||||||
|
TextSecureEnvelope envelope = messagePipe.read(timeout, timeoutTimeUnit);
|
||||||
|
TextSecureCipher cipher = new TextSecureCipher(new MyAxolotlStore(),
|
||||||
|
getRecipientIdFor(envelope.getSource()),
|
||||||
|
envelope.getSourceDevice());
|
||||||
|
TextSecureMessage message = cipher.decrypt(envelope);
|
||||||
|
|
||||||
|
System.out.println("Received message: " + message.getBody().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
if (messagePipe != null)
|
||||||
|
messagePipe.close();
|
||||||
|
}
|
||||||
|
`````
|
53
build.gradle
Normal file
53
build.gradle
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:1.0.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'maven'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile 'com.google.protobuf:protobuf-java:2.5.0'
|
||||||
|
compile 'com.googlecode.libphonenumber:libphonenumber:6.1'
|
||||||
|
compile 'com.fasterxml.jackson.core:jackson-databind:2.5.0'
|
||||||
|
|
||||||
|
compile 'org.whispersystems:axolotl-android:1.0.0'
|
||||||
|
compile 'com.squareup.okhttp:okhttp:2.2.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 21
|
||||||
|
buildToolsVersion '21.1.2'
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_7
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.whenTaskAdded { task ->
|
||||||
|
if (task.name.equals("lint")) {
|
||||||
|
task.enabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
version '0.1'
|
||||||
|
group 'org.whispersystems.textsecure'
|
||||||
|
archivesBaseName = 'libtextsecure'
|
||||||
|
|
||||||
|
uploadArchives {
|
||||||
|
repositories {
|
||||||
|
mavenDeployer {
|
||||||
|
repository(url: mavenLocal().getUrl())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#Fri Feb 27 17:12:50 PST 2015
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-bin.zip
|
164
gradlew
vendored
Executable file
164
gradlew
vendored
Executable file
@ -0,0 +1,164 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS=""
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn ( ) {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die ( ) {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# For Cygwin, ensure paths are in UNIX format before anything is touched.
|
||||||
|
if $cygwin ; then
|
||||||
|
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >&-
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >&-
|
||||||
|
|
||||||
|
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
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin, switch paths to Windows format before running java
|
||||||
|
if $cygwin ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
(0) set -- ;;
|
||||||
|
(1) set -- "$args0" ;;
|
||||||
|
(2) set -- "$args0" "$args1" ;;
|
||||||
|
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||||
|
function splitJvmOpts() {
|
||||||
|
JVM_OPTS=("$@")
|
||||||
|
}
|
||||||
|
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||||
|
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
90
gradlew.bat
vendored
Normal file
90
gradlew.bat
vendored
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
@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=
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:init
|
||||||
|
@rem Get command-line arguments, handling Windowz variants
|
||||||
|
|
||||||
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
|
if "%@eval[2+2]" == "4" goto 4NT_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=%*
|
||||||
|
goto execute
|
||||||
|
|
||||||
|
:4NT_args
|
||||||
|
@rem Get arguments from the 4NT Shell from JP Software
|
||||||
|
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%
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
59
protobuf/IncomingPushMessageSignal.proto
Normal file
59
protobuf/IncomingPushMessageSignal.proto
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package textsecure;
|
||||||
|
|
||||||
|
option java_package = "org.whispersystems.textsecure.internal.push";
|
||||||
|
option java_outer_classname = "PushMessageProtos";
|
||||||
|
|
||||||
|
message IncomingPushMessageSignal {
|
||||||
|
enum Type {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
CIPHERTEXT = 1;
|
||||||
|
KEY_EXCHANGE = 2;
|
||||||
|
PREKEY_BUNDLE = 3;
|
||||||
|
PLAINTEXT = 4;
|
||||||
|
RECEIPT = 5;
|
||||||
|
}
|
||||||
|
optional Type type = 1;
|
||||||
|
optional string source = 2;
|
||||||
|
optional uint32 sourceDevice = 7;
|
||||||
|
optional string relay = 3;
|
||||||
|
optional uint64 timestamp = 5;
|
||||||
|
optional bytes message = 6; // Contains an encrypted PushMessageContent
|
||||||
|
// repeated string destinations = 4; // No longer supported
|
||||||
|
}
|
||||||
|
|
||||||
|
message PushMessageContent {
|
||||||
|
message AttachmentPointer {
|
||||||
|
optional fixed64 id = 1;
|
||||||
|
optional string contentType = 2;
|
||||||
|
optional bytes key = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupContext {
|
||||||
|
enum Type {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
UPDATE = 1;
|
||||||
|
DELIVER = 2;
|
||||||
|
QUIT = 3;
|
||||||
|
}
|
||||||
|
optional bytes id = 1;
|
||||||
|
optional Type type = 2;
|
||||||
|
optional string name = 3;
|
||||||
|
repeated string members = 4;
|
||||||
|
optional AttachmentPointer avatar = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SyncMessageContext {
|
||||||
|
optional string destination = 1;
|
||||||
|
optional uint64 timestamp = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Flags {
|
||||||
|
END_SESSION = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional string body = 1;
|
||||||
|
repeated AttachmentPointer attachments = 2;
|
||||||
|
optional GroupContext group = 3;
|
||||||
|
optional uint32 flags = 4;
|
||||||
|
optional SyncMessageContext sync = 5;
|
||||||
|
}
|
3
protobuf/Makefile
Normal file
3
protobuf/Makefile
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
all:
|
||||||
|
protoc --java_out=../src/main/java/ IncomingPushMessageSignal.proto Provisioning.proto WebSocketResources.proto
|
16
protobuf/Provisioning.proto
Normal file
16
protobuf/Provisioning.proto
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package textsecure;
|
||||||
|
|
||||||
|
option java_package = "org.whispersystems.textsecure.internal.push";
|
||||||
|
option java_outer_classname = "ProvisioningProtos";
|
||||||
|
|
||||||
|
message ProvisionEnvelope {
|
||||||
|
optional bytes publicKey = 1;
|
||||||
|
optional bytes body = 2; // Encrypted ProvisionMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
message ProvisionMessage {
|
||||||
|
optional bytes identityKeyPublic = 1;
|
||||||
|
optional bytes identityKeyPrivate = 2;
|
||||||
|
optional string number = 3;
|
||||||
|
optional string provisioningCode = 4;
|
||||||
|
}
|
46
protobuf/WebSocketResources.proto
Normal file
46
protobuf/WebSocketResources.proto
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014-2015 Open WhisperSystems
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package textsecure;
|
||||||
|
|
||||||
|
option java_package = "org.whispersystems.textsecure.internal.websocket";
|
||||||
|
option java_outer_classname = "WebSocketProtos";
|
||||||
|
|
||||||
|
message WebSocketRequestMessage {
|
||||||
|
optional string verb = 1;
|
||||||
|
optional string path = 2;
|
||||||
|
optional bytes body = 3;
|
||||||
|
optional uint64 id = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WebSocketResponseMessage {
|
||||||
|
optional uint64 id = 1;
|
||||||
|
optional uint32 status = 2;
|
||||||
|
optional string message = 3;
|
||||||
|
optional bytes body = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WebSocketMessage {
|
||||||
|
enum Type {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
REQUEST = 1;
|
||||||
|
RESPONSE = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Type type = 1;
|
||||||
|
optional WebSocketRequestMessage request = 2;
|
||||||
|
optional WebSocketResponseMessage response = 3;
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package org.whispersystems.textsecure.push;
|
||||||
|
|
||||||
|
import android.test.AndroidTestCase;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecure.internal.push.PushTransportDetails;
|
||||||
|
|
||||||
|
public class PushTransportDetailsTest extends AndroidTestCase {
|
||||||
|
|
||||||
|
private final PushTransportDetails transportV2 = new PushTransportDetails(2);
|
||||||
|
private final PushTransportDetails transportV3 = new PushTransportDetails(3);
|
||||||
|
|
||||||
|
public void testV3Padding() {
|
||||||
|
for (int i=0;i<159;i++) {
|
||||||
|
byte[] message = new byte[i];
|
||||||
|
assertEquals(transportV3.getPaddedMessageBody(message).length, 159);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i=159;i<319;i++) {
|
||||||
|
byte[] message = new byte[i];
|
||||||
|
assertEquals(transportV3.getPaddedMessageBody(message).length, 319);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i=319;i<479;i++) {
|
||||||
|
byte[] message = new byte[i];
|
||||||
|
assertEquals(transportV3.getPaddedMessageBody(message).length, 479);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testV2Padding() {
|
||||||
|
for (int i=0;i<480;i++) {
|
||||||
|
byte[] message = new byte[i];
|
||||||
|
assertTrue(transportV2.getPaddedMessageBody(message).length == message.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
src/main/AndroidManifest.xml
Normal file
8
src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="org.whispersystems.textsecure"
|
||||||
|
android:versionCode="1"
|
||||||
|
android:versionName="0.1">
|
||||||
|
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="16"/>
|
||||||
|
<application />
|
||||||
|
</manifest>
|
@ -0,0 +1,256 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
|
import org.whispersystems.libaxolotl.IdentityKey;
|
||||||
|
import org.whispersystems.libaxolotl.IdentityKeyPair;
|
||||||
|
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||||
|
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
||||||
|
import org.whispersystems.libaxolotl.state.PreKeyRecord;
|
||||||
|
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
|
||||||
|
import org.whispersystems.libaxolotl.util.guava.Optional;
|
||||||
|
import org.whispersystems.textsecure.api.push.ContactTokenDetails;
|
||||||
|
import org.whispersystems.textsecure.api.push.SignedPreKeyEntity;
|
||||||
|
import org.whispersystems.textsecure.api.push.TrustStore;
|
||||||
|
import org.whispersystems.textsecure.internal.crypto.ProvisioningCipher;
|
||||||
|
import org.whispersystems.textsecure.internal.push.PushServiceSocket;
|
||||||
|
import org.whispersystems.textsecure.internal.util.Base64;
|
||||||
|
import org.whispersystems.textsecure.internal.util.StaticCredentialsProvider;
|
||||||
|
import org.whispersystems.textsecure.internal.util.Util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.whispersystems.textsecure.internal.push.ProvisioningProtos.ProvisionMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main interface for creating, registering, and
|
||||||
|
* managing a TextSecure account.
|
||||||
|
*
|
||||||
|
* @author Moxie Marlinspike
|
||||||
|
*/
|
||||||
|
public class TextSecureAccountManager {
|
||||||
|
|
||||||
|
private final PushServiceSocket pushServiceSocket;
|
||||||
|
private final String user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a TextSecureAccountManager.
|
||||||
|
*
|
||||||
|
* @param url The URL for the TextSecure server.
|
||||||
|
* @param trustStore The {@link org.whispersystems.textsecure.api.push.TrustStore} for the TextSecure server's TLS certificate.
|
||||||
|
* @param user A TextSecure phone number.
|
||||||
|
* @param password A TextSecure password.
|
||||||
|
*/
|
||||||
|
public TextSecureAccountManager(String url, TrustStore trustStore,
|
||||||
|
String user, String password)
|
||||||
|
{
|
||||||
|
this.pushServiceSocket = new PushServiceSocket(url, trustStore, new StaticCredentialsProvider(user, password, null));
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register/Unregister a Google Cloud Messaging registration ID.
|
||||||
|
*
|
||||||
|
* @param gcmRegistrationId The GCM id to register. A call with an absent value will unregister.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void setGcmId(Optional<String> gcmRegistrationId) throws IOException {
|
||||||
|
if (gcmRegistrationId.isPresent()) {
|
||||||
|
this.pushServiceSocket.registerGcmId(gcmRegistrationId.get());
|
||||||
|
} else {
|
||||||
|
this.pushServiceSocket.unregisterGcmId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request an SMS verification code. On success, the server will send
|
||||||
|
* an SMS verification code to this TextSecure user.
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void requestSmsVerificationCode() throws IOException {
|
||||||
|
this.pushServiceSocket.createAccount(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request a Voice verification code. On success, the server will
|
||||||
|
* make a voice call to this TextSecure user.
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void requestVoiceVerificationCode() throws IOException {
|
||||||
|
this.pushServiceSocket.createAccount(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a TextSecure account.
|
||||||
|
*
|
||||||
|
* @param verificationCode The verification code received via SMS or Voice
|
||||||
|
* (see {@link #requestSmsVerificationCode} and
|
||||||
|
* {@link #requestVoiceVerificationCode}).
|
||||||
|
* @param signalingKey 52 random bytes. A 32 byte AES key and a 20 byte Hmac256 key,
|
||||||
|
* concatenated.
|
||||||
|
* @param supportsSms Indicate whether this client is capable of supporting encrypted SMS.
|
||||||
|
* @param axolotlRegistrationId A random 14-bit number that identifies this TextSecure install.
|
||||||
|
* This value should remain consistent across registrations for the
|
||||||
|
* same install, but probabilistically differ across registrations
|
||||||
|
* for separate installs.
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void verifyAccount(String verificationCode, String signalingKey,
|
||||||
|
boolean supportsSms, int axolotlRegistrationId)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
this.pushServiceSocket.verifyAccount(verificationCode, signalingKey,
|
||||||
|
supportsSms, axolotlRegistrationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an identity key, last resort key, signed prekey, and list of one time prekeys
|
||||||
|
* with the server.
|
||||||
|
*
|
||||||
|
* @param identityKey The client's long-term identity keypair.
|
||||||
|
* @param lastResortKey The client's "last resort" prekey.
|
||||||
|
* @param signedPreKey The client's signed prekey.
|
||||||
|
* @param oneTimePreKeys The client's list of one-time prekeys.
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void setPreKeys(IdentityKey identityKey, PreKeyRecord lastResortKey,
|
||||||
|
SignedPreKeyRecord signedPreKey, List<PreKeyRecord> oneTimePreKeys)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
this.pushServiceSocket.registerPreKeys(identityKey, lastResortKey, signedPreKey, oneTimePreKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The server's count of currently available (eg. unused) prekeys for this user.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public int getPreKeysCount() throws IOException {
|
||||||
|
return this.pushServiceSocket.getAvailablePreKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the client's signed prekey.
|
||||||
|
*
|
||||||
|
* @param signedPreKey The client's new signed prekey.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void setSignedPreKey(SignedPreKeyRecord signedPreKey) throws IOException {
|
||||||
|
this.pushServiceSocket.setCurrentSignedPreKey(signedPreKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The server's view of the client's current signed prekey.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public SignedPreKeyEntity getSignedPreKey() throws IOException {
|
||||||
|
return this.pushServiceSocket.getCurrentSignedPreKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a contact is currently registered with the server.
|
||||||
|
*
|
||||||
|
* @param e164number The contact to check.
|
||||||
|
* @return An optional ContactTokenDetails, present if registered, absent if not.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public Optional<ContactTokenDetails> getContact(String e164number) throws IOException {
|
||||||
|
String contactToken = createDirectoryServerToken(e164number);
|
||||||
|
ContactTokenDetails contactTokenDetails = this.pushServiceSocket.getContactTokenDetails(contactToken);
|
||||||
|
|
||||||
|
if (contactTokenDetails != null) {
|
||||||
|
contactTokenDetails.setNumber(e164number);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.fromNullable(contactTokenDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks which contacts in a set are registered with the server.
|
||||||
|
*
|
||||||
|
* @param e164numbers The contacts to check.
|
||||||
|
* @return A list of ContactTokenDetails for the registered users.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public List<ContactTokenDetails> getContacts(Set<String> e164numbers)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
Map<String, String> contactTokensMap = createDirectoryServerTokenMap(e164numbers);
|
||||||
|
List<ContactTokenDetails> activeTokens = this.pushServiceSocket.retrieveDirectory(contactTokensMap.keySet());
|
||||||
|
|
||||||
|
for (ContactTokenDetails activeToken : activeTokens) {
|
||||||
|
activeToken.setNumber(contactTokensMap.get(activeToken.getToken()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNewDeviceVerificationCode() throws IOException {
|
||||||
|
return this.pushServiceSocket.getNewDeviceVerificationCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addDevice(String deviceIdentifier,
|
||||||
|
ECPublicKey deviceKey,
|
||||||
|
IdentityKeyPair identityKeyPair,
|
||||||
|
String code)
|
||||||
|
throws InvalidKeyException, IOException
|
||||||
|
{
|
||||||
|
ProvisioningCipher cipher = new ProvisioningCipher(deviceKey);
|
||||||
|
ProvisionMessage message = ProvisionMessage.newBuilder()
|
||||||
|
.setIdentityKeyPublic(ByteString.copyFrom(identityKeyPair.getPublicKey().serialize()))
|
||||||
|
.setIdentityKeyPrivate(ByteString.copyFrom(identityKeyPair.getPrivateKey().serialize()))
|
||||||
|
.setNumber(user)
|
||||||
|
.setProvisioningCode(code)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
byte[] ciphertext = cipher.encrypt(message);
|
||||||
|
this.pushServiceSocket.sendProvisioningMessage(deviceIdentifier, ciphertext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createDirectoryServerToken(String e164number) {
|
||||||
|
try {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance("SHA1");
|
||||||
|
byte[] token = Util.trim(digest.digest(e164number.getBytes()), 10);
|
||||||
|
return Base64.encodeBytesWithoutPadding(token);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> createDirectoryServerTokenMap(Collection<String> e164numbers) {
|
||||||
|
Map<String,String> tokenMap = new HashMap<>(e164numbers.size());
|
||||||
|
|
||||||
|
for (String number : e164numbers) {
|
||||||
|
tokenMap.put(createDirectoryServerToken(number), number);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,129 @@
|
|||||||
|
package org.whispersystems.textsecure.api;
|
||||||
|
|
||||||
|
import org.whispersystems.libaxolotl.InvalidVersionException;
|
||||||
|
import org.whispersystems.textsecure.api.messages.TextSecureEnvelope;
|
||||||
|
import org.whispersystems.textsecure.api.util.CredentialsProvider;
|
||||||
|
import org.whispersystems.textsecure.internal.websocket.WebSocketConnection;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
import static org.whispersystems.textsecure.internal.websocket.WebSocketProtos.WebSocketRequestMessage;
|
||||||
|
import static org.whispersystems.textsecure.internal.websocket.WebSocketProtos.WebSocketResponseMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A TextSecureMessagePipe represents a dedicated connection
|
||||||
|
* to the TextSecure server, which the server can push messages
|
||||||
|
* down.
|
||||||
|
*/
|
||||||
|
public class TextSecureMessagePipe {
|
||||||
|
|
||||||
|
private final WebSocketConnection websocket;
|
||||||
|
private final CredentialsProvider credentialsProvider;
|
||||||
|
|
||||||
|
TextSecureMessagePipe(WebSocketConnection websocket, CredentialsProvider credentialsProvider) {
|
||||||
|
this.websocket = websocket;
|
||||||
|
this.credentialsProvider = credentialsProvider;
|
||||||
|
|
||||||
|
this.websocket.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A blocking call that reads a message off the pipe. When this
|
||||||
|
* call returns, the message has been acknowledged and will not
|
||||||
|
* be retransmitted.
|
||||||
|
*
|
||||||
|
* @param timeout The timeout to wait for.
|
||||||
|
* @param unit The timeout time unit.
|
||||||
|
* @return A new message.
|
||||||
|
*
|
||||||
|
* @throws InvalidVersionException
|
||||||
|
* @throws IOException
|
||||||
|
* @throws TimeoutException
|
||||||
|
*/
|
||||||
|
public TextSecureEnvelope read(long timeout, TimeUnit unit)
|
||||||
|
throws InvalidVersionException, IOException, TimeoutException
|
||||||
|
{
|
||||||
|
return read(timeout, unit, new NullMessagePipeCallback());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A blocking call that reads a message off the pipe (see {@link #read(long, java.util.concurrent.TimeUnit)}
|
||||||
|
*
|
||||||
|
* Unlike {@link #read(long, java.util.concurrent.TimeUnit)}, this method allows you
|
||||||
|
* to specify a callback that will be called before the received message is acknowledged.
|
||||||
|
* This allows you to write the received message to durable storage before acknowledging
|
||||||
|
* receipt of it to the server.
|
||||||
|
*
|
||||||
|
* @param timeout The timeout to wait for.
|
||||||
|
* @param unit The timeout time unit.
|
||||||
|
* @param callback A callback that will be called before the message receipt is
|
||||||
|
* acknowledged to the server.
|
||||||
|
* @return The message read (same as the message sent through the callback).
|
||||||
|
* @throws TimeoutException
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidVersionException
|
||||||
|
*/
|
||||||
|
public TextSecureEnvelope read(long timeout, TimeUnit unit, MessagePipeCallback callback)
|
||||||
|
throws TimeoutException, IOException, InvalidVersionException
|
||||||
|
{
|
||||||
|
while (true) {
|
||||||
|
WebSocketRequestMessage request = websocket.readRequest(unit.toMillis(timeout));
|
||||||
|
WebSocketResponseMessage response = createWebSocketResponse(request);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isTextSecureEnvelope(request)) {
|
||||||
|
TextSecureEnvelope envelope = new TextSecureEnvelope(request.getBody().toByteArray(),
|
||||||
|
credentialsProvider.getSignalingKey());
|
||||||
|
|
||||||
|
callback.onMessage(envelope);
|
||||||
|
return envelope;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
websocket.sendResponse(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close this connection to the server.
|
||||||
|
*/
|
||||||
|
public void shutdown() {
|
||||||
|
websocket.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTextSecureEnvelope(WebSocketRequestMessage message) {
|
||||||
|
return "PUT".equals(message.getVerb()) && "/api/v1/message".equals(message.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebSocketResponseMessage createWebSocketResponse(WebSocketRequestMessage request) {
|
||||||
|
if (isTextSecureEnvelope(request)) {
|
||||||
|
return WebSocketResponseMessage.newBuilder()
|
||||||
|
.setId(request.getId())
|
||||||
|
.setStatus(200)
|
||||||
|
.setMessage("OK")
|
||||||
|
.build();
|
||||||
|
} else {
|
||||||
|
return WebSocketResponseMessage.newBuilder()
|
||||||
|
.setId(request.getId())
|
||||||
|
.setStatus(400)
|
||||||
|
.setMessage("Unknown")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For receiving a callback when a new message has been
|
||||||
|
* received.
|
||||||
|
*/
|
||||||
|
public static interface MessagePipeCallback {
|
||||||
|
public void onMessage(TextSecureEnvelope envelope);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NullMessagePipeCallback implements MessagePipeCallback {
|
||||||
|
@Override
|
||||||
|
public void onMessage(TextSecureEnvelope envelope) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api;
|
||||||
|
|
||||||
|
import org.whispersystems.libaxolotl.InvalidMessageException;
|
||||||
|
import org.whispersystems.textsecure.api.crypto.AttachmentCipherInputStream;
|
||||||
|
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer;
|
||||||
|
import org.whispersystems.textsecure.api.push.TrustStore;
|
||||||
|
import org.whispersystems.textsecure.api.util.CredentialsProvider;
|
||||||
|
import org.whispersystems.textsecure.internal.push.PushServiceSocket;
|
||||||
|
import org.whispersystems.textsecure.internal.util.StaticCredentialsProvider;
|
||||||
|
import org.whispersystems.textsecure.internal.websocket.WebSocketConnection;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The primary interface for receiving TextSecure messages.
|
||||||
|
*
|
||||||
|
* @author Moxie Marlinspike
|
||||||
|
*/
|
||||||
|
public class TextSecureMessageReceiver {
|
||||||
|
|
||||||
|
private final PushServiceSocket socket;
|
||||||
|
private final TrustStore trustStore;
|
||||||
|
private final String url;
|
||||||
|
private final CredentialsProvider credentialsProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a TextSecureMessageReceiver.
|
||||||
|
*
|
||||||
|
* @param url The URL of the TextSecure server.
|
||||||
|
* @param trustStore The {@link org.whispersystems.textsecure.api.push.TrustStore} containing
|
||||||
|
* the server's TLS signing certificate.
|
||||||
|
* @param user The TextSecure user's username (eg. phone number).
|
||||||
|
* @param password The TextSecure user's password.
|
||||||
|
* @param signalingKey The 52 byte signaling key assigned to this user at registration.
|
||||||
|
*/
|
||||||
|
public TextSecureMessageReceiver(String url, TrustStore trustStore,
|
||||||
|
String user, String password, String signalingKey)
|
||||||
|
{
|
||||||
|
this(url, trustStore, new StaticCredentialsProvider(user, password, signalingKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a TextSecureMessageReceiver.
|
||||||
|
*
|
||||||
|
* @param url The URL of the TextSecure server.
|
||||||
|
* @param trustStore The {@link org.whispersystems.textsecure.api.push.TrustStore} containing
|
||||||
|
* the server's TLS signing certificate.
|
||||||
|
* @param credentials The TextSecure user's credentials.
|
||||||
|
*/
|
||||||
|
public TextSecureMessageReceiver(String url, TrustStore trustStore, CredentialsProvider credentials) {
|
||||||
|
this.url = url;
|
||||||
|
this.trustStore = trustStore;
|
||||||
|
this.credentialsProvider = credentials;
|
||||||
|
this.socket = new PushServiceSocket(url, trustStore, credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a TextSecure attachment.
|
||||||
|
*
|
||||||
|
* @param pointer The {@link org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer}
|
||||||
|
* received in a {@link org.whispersystems.textsecure.api.messages.TextSecureMessage}.
|
||||||
|
* @param destination The download destination for this attachment.
|
||||||
|
*
|
||||||
|
* @return An InputStream that streams the plaintext attachment contents.
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidMessageException
|
||||||
|
*/
|
||||||
|
public InputStream retrieveAttachment(TextSecureAttachmentPointer pointer, File destination)
|
||||||
|
throws IOException, InvalidMessageException
|
||||||
|
{
|
||||||
|
socket.retrieveAttachment(pointer.getRelay().orNull(), pointer.getId(), destination);
|
||||||
|
return new AttachmentCipherInputStream(destination, pointer.getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a pipe for receiving TextSecure messages.
|
||||||
|
*
|
||||||
|
* Callers must call {@link TextSecureMessagePipe#shutdown()} when finished with the pipe.
|
||||||
|
*
|
||||||
|
* @return A TextSecureMessagePipe for receiving TextSecure messages.
|
||||||
|
*/
|
||||||
|
public TextSecureMessagePipe createMessagePipe() {
|
||||||
|
WebSocketConnection webSocket = new WebSocketConnection(url, trustStore, credentialsProvider);
|
||||||
|
return new TextSecureMessagePipe(webSocket, credentialsProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,393 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
|
|
||||||
|
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||||
|
import org.whispersystems.libaxolotl.SessionBuilder;
|
||||||
|
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
|
||||||
|
import org.whispersystems.libaxolotl.state.AxolotlStore;
|
||||||
|
import org.whispersystems.libaxolotl.state.PreKeyBundle;
|
||||||
|
import org.whispersystems.libaxolotl.util.guava.Optional;
|
||||||
|
import org.whispersystems.textsecure.api.crypto.TextSecureCipher;
|
||||||
|
import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException;
|
||||||
|
import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
|
||||||
|
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentStream;
|
||||||
|
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
|
||||||
|
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
|
||||||
|
import org.whispersystems.textsecure.api.push.TextSecureAddress;
|
||||||
|
import org.whispersystems.textsecure.api.push.TrustStore;
|
||||||
|
import org.whispersystems.textsecure.api.push.exceptions.NetworkFailureException;
|
||||||
|
import org.whispersystems.textsecure.api.push.exceptions.PushNetworkException;
|
||||||
|
import org.whispersystems.textsecure.internal.push.MismatchedDevices;
|
||||||
|
import org.whispersystems.textsecure.internal.push.OutgoingPushMessage;
|
||||||
|
import org.whispersystems.textsecure.internal.push.OutgoingPushMessageList;
|
||||||
|
import org.whispersystems.textsecure.internal.push.PushAttachmentData;
|
||||||
|
import org.whispersystems.textsecure.internal.push.PushBody;
|
||||||
|
import org.whispersystems.textsecure.internal.push.PushServiceSocket;
|
||||||
|
import org.whispersystems.textsecure.internal.push.SendMessageResponse;
|
||||||
|
import org.whispersystems.textsecure.internal.push.StaleDevices;
|
||||||
|
import org.whispersystems.textsecure.api.push.exceptions.UnregisteredUserException;
|
||||||
|
import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions;
|
||||||
|
import org.whispersystems.textsecure.internal.push.exceptions.MismatchedDevicesException;
|
||||||
|
import org.whispersystems.textsecure.internal.push.exceptions.StaleDevicesException;
|
||||||
|
import org.whispersystems.textsecure.internal.util.StaticCredentialsProvider;
|
||||||
|
import org.whispersystems.textsecure.internal.util.Util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.IncomingPushMessageSignal.Type;
|
||||||
|
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent;
|
||||||
|
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.AttachmentPointer;
|
||||||
|
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.GroupContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main interface for sending TextSecure messages.
|
||||||
|
*
|
||||||
|
* @author Moxie Marlinspike
|
||||||
|
*/
|
||||||
|
public class TextSecureMessageSender {
|
||||||
|
|
||||||
|
private static final String TAG = TextSecureMessageSender.class.getSimpleName();
|
||||||
|
|
||||||
|
private final PushServiceSocket socket;
|
||||||
|
private final AxolotlStore store;
|
||||||
|
private final TextSecureAddress syncAddress;
|
||||||
|
private final Optional<EventListener> eventListener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a TextSecureMessageSender.
|
||||||
|
*
|
||||||
|
* @param url The URL of the TextSecure server.
|
||||||
|
* @param trustStore The trust store containing the TextSecure server's signing TLS certificate.
|
||||||
|
* @param user The TextSecure username (eg phone number).
|
||||||
|
* @param password The TextSecure user's password.
|
||||||
|
* @param userId The axolotl recipient id for the local TextSecure user.
|
||||||
|
* @param store The AxolotlStore.
|
||||||
|
* @param eventListener An optional event listener, which fires whenever sessions are
|
||||||
|
* setup or torn down for a recipient.
|
||||||
|
*/
|
||||||
|
public TextSecureMessageSender(String url, TrustStore trustStore,
|
||||||
|
String user, String password,
|
||||||
|
long userId, AxolotlStore store,
|
||||||
|
Optional<EventListener> eventListener)
|
||||||
|
{
|
||||||
|
this.socket = new PushServiceSocket(url, trustStore, new StaticCredentialsProvider(user, password, null));
|
||||||
|
this.store = store;
|
||||||
|
this.syncAddress = new TextSecureAddress(userId, user, null);
|
||||||
|
this.eventListener = eventListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a delivery receipt for a received message. It is not necessary to call this
|
||||||
|
* when receiving messages through {@link org.whispersystems.textsecure.api.TextSecureMessagePipe}.
|
||||||
|
* @param recipient The sender of the received message you're acknowledging.
|
||||||
|
* @param messageId The message id of the received message you're acknowledging.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void sendDeliveryReceipt(TextSecureAddress recipient, long messageId) throws IOException {
|
||||||
|
this.socket.sendReceipt(recipient.getNumber(), messageId, recipient.getRelay());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message to a single recipient.
|
||||||
|
*
|
||||||
|
* @param recipient The message's destination.
|
||||||
|
* @param message The message.
|
||||||
|
* @throws UntrustedIdentityException
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void sendMessage(TextSecureAddress recipient, TextSecureMessage message)
|
||||||
|
throws UntrustedIdentityException, IOException
|
||||||
|
{
|
||||||
|
byte[] content = createMessageContent(message);
|
||||||
|
long timestamp = message.getTimestamp();
|
||||||
|
SendMessageResponse response = sendMessage(recipient, timestamp, content);
|
||||||
|
|
||||||
|
if (response != null && response.getNeedsSync()) {
|
||||||
|
byte[] syncMessage = createSyncMessageContent(content, recipient, timestamp);
|
||||||
|
sendMessage(syncAddress, timestamp, syncMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.isEndSession()) {
|
||||||
|
store.deleteAllSessions(recipient.getRecipientId());
|
||||||
|
|
||||||
|
if (eventListener.isPresent()) {
|
||||||
|
eventListener.get().onSecurityEvent(recipient.getRecipientId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message to a group.
|
||||||
|
*
|
||||||
|
* @param recipients The group members.
|
||||||
|
* @param message The group message.
|
||||||
|
* @throws IOException
|
||||||
|
* @throws EncapsulatedExceptions
|
||||||
|
*/
|
||||||
|
public void sendMessage(List<TextSecureAddress> recipients, TextSecureMessage message)
|
||||||
|
throws IOException, EncapsulatedExceptions
|
||||||
|
{
|
||||||
|
byte[] content = createMessageContent(message);
|
||||||
|
sendMessage(recipients, message.getTimestamp(), content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] createMessageContent(TextSecureMessage message) throws IOException {
|
||||||
|
PushMessageContent.Builder builder = PushMessageContent.newBuilder();
|
||||||
|
List<AttachmentPointer> pointers = createAttachmentPointers(message.getAttachments());
|
||||||
|
|
||||||
|
if (!pointers.isEmpty()) {
|
||||||
|
builder.addAllAttachments(pointers);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.getBody().isPresent()) {
|
||||||
|
builder.setBody(message.getBody().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.getGroupInfo().isPresent()) {
|
||||||
|
builder.setGroup(createGroupContent(message.getGroupInfo().get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.isEndSession()) {
|
||||||
|
builder.setFlags(PushMessageContent.Flags.END_SESSION_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build().toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] createSyncMessageContent(byte[] content, TextSecureAddress recipient, long timestamp) {
|
||||||
|
try {
|
||||||
|
PushMessageContent.Builder builder = PushMessageContent.parseFrom(content).toBuilder();
|
||||||
|
builder.setSync(PushMessageContent.SyncMessageContext.newBuilder()
|
||||||
|
.setDestination(recipient.getNumber())
|
||||||
|
.setTimestamp(timestamp)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
return builder.build().toByteArray();
|
||||||
|
} catch (InvalidProtocolBufferException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroupContext createGroupContent(TextSecureGroup group) throws IOException {
|
||||||
|
GroupContext.Builder builder = GroupContext.newBuilder();
|
||||||
|
builder.setId(ByteString.copyFrom(group.getGroupId()));
|
||||||
|
|
||||||
|
if (group.getType() != TextSecureGroup.Type.DELIVER) {
|
||||||
|
if (group.getType() == TextSecureGroup.Type.UPDATE) builder.setType(GroupContext.Type.UPDATE);
|
||||||
|
else if (group.getType() == TextSecureGroup.Type.QUIT) builder.setType(GroupContext.Type.QUIT);
|
||||||
|
else throw new AssertionError("Unknown type: " + group.getType());
|
||||||
|
|
||||||
|
if (group.getName().isPresent()) builder.setName(group.getName().get());
|
||||||
|
if (group.getMembers().isPresent()) builder.addAllMembers(group.getMembers().get());
|
||||||
|
|
||||||
|
if (group.getAvatar().isPresent() && group.getAvatar().get().isStream()) {
|
||||||
|
AttachmentPointer pointer = createAttachmentPointer(group.getAvatar().get().asStream());
|
||||||
|
builder.setAvatar(pointer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder.setType(GroupContext.Type.DELIVER);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendMessage(List<TextSecureAddress> recipients, long timestamp, byte[] content)
|
||||||
|
throws IOException, EncapsulatedExceptions
|
||||||
|
{
|
||||||
|
List<UntrustedIdentityException> untrustedIdentities = new LinkedList<>();
|
||||||
|
List<UnregisteredUserException> unregisteredUsers = new LinkedList<>();
|
||||||
|
List<NetworkFailureException> networkExceptions = new LinkedList<>();
|
||||||
|
|
||||||
|
for (TextSecureAddress recipient : recipients) {
|
||||||
|
try {
|
||||||
|
sendMessage(recipient, timestamp, content);
|
||||||
|
} catch (UntrustedIdentityException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
untrustedIdentities.add(e);
|
||||||
|
} catch (UnregisteredUserException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
unregisteredUsers.add(e);
|
||||||
|
} catch (PushNetworkException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
networkExceptions.add(new NetworkFailureException(recipient.getNumber(), e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) {
|
||||||
|
throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SendMessageResponse sendMessage(TextSecureAddress recipient, long timestamp, byte[] content)
|
||||||
|
throws UntrustedIdentityException, IOException
|
||||||
|
{
|
||||||
|
for (int i=0;i<3;i++) {
|
||||||
|
try {
|
||||||
|
OutgoingPushMessageList messages = getEncryptedMessages(socket, recipient, timestamp, content);
|
||||||
|
return socket.sendMessage(messages);
|
||||||
|
} catch (MismatchedDevicesException mde) {
|
||||||
|
Log.w(TAG, mde);
|
||||||
|
handleMismatchedDevices(socket, recipient, mde.getMismatchedDevices());
|
||||||
|
} catch (StaleDevicesException ste) {
|
||||||
|
Log.w(TAG, ste);
|
||||||
|
handleStaleDevices(recipient, ste.getStaleDevices());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IOException("Failed to resolve conflicts after 3 attempts!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<AttachmentPointer> createAttachmentPointers(Optional<List<TextSecureAttachment>> attachments) throws IOException {
|
||||||
|
List<AttachmentPointer> pointers = new LinkedList<>();
|
||||||
|
|
||||||
|
if (!attachments.isPresent() || attachments.get().isEmpty()) {
|
||||||
|
Log.w(TAG, "No attachments present...");
|
||||||
|
return pointers;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (TextSecureAttachment attachment : attachments.get()) {
|
||||||
|
if (attachment.isStream()) {
|
||||||
|
Log.w(TAG, "Found attachment, creating pointer...");
|
||||||
|
pointers.add(createAttachmentPointer(attachment.asStream()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pointers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AttachmentPointer createAttachmentPointer(TextSecureAttachmentStream attachment)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
byte[] attachmentKey = Util.getSecretBytes(64);
|
||||||
|
PushAttachmentData attachmentData = new PushAttachmentData(attachment.getContentType(),
|
||||||
|
attachment.getInputStream(),
|
||||||
|
attachment.getLength(),
|
||||||
|
attachmentKey);
|
||||||
|
|
||||||
|
long attachmentId = socket.sendAttachment(attachmentData);
|
||||||
|
|
||||||
|
return AttachmentPointer.newBuilder()
|
||||||
|
.setContentType(attachment.getContentType())
|
||||||
|
.setId(attachmentId)
|
||||||
|
.setKey(ByteString.copyFrom(attachmentKey))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private OutgoingPushMessageList getEncryptedMessages(PushServiceSocket socket,
|
||||||
|
TextSecureAddress recipient,
|
||||||
|
long timestamp,
|
||||||
|
byte[] plaintext)
|
||||||
|
throws IOException, UntrustedIdentityException
|
||||||
|
{
|
||||||
|
List<OutgoingPushMessage> messages = new LinkedList<>();
|
||||||
|
|
||||||
|
if (!recipient.equals(syncAddress)) {
|
||||||
|
PushBody masterBody = getEncryptedMessage(socket, recipient, TextSecureAddress.DEFAULT_DEVICE_ID, plaintext);
|
||||||
|
messages.add(new OutgoingPushMessage(recipient, TextSecureAddress.DEFAULT_DEVICE_ID, masterBody));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int deviceId : store.getSubDeviceSessions(recipient.getRecipientId())) {
|
||||||
|
PushBody body = getEncryptedMessage(socket, recipient, deviceId, plaintext);
|
||||||
|
messages.add(new OutgoingPushMessage(recipient, deviceId, body));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OutgoingPushMessageList(recipient.getNumber(), timestamp, recipient.getRelay(), messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PushBody getEncryptedMessage(PushServiceSocket socket, TextSecureAddress recipient, int deviceId, byte[] plaintext)
|
||||||
|
throws IOException, UntrustedIdentityException
|
||||||
|
{
|
||||||
|
if (!store.containsSession(recipient.getRecipientId(), deviceId)) {
|
||||||
|
try {
|
||||||
|
List<PreKeyBundle> preKeys = socket.getPreKeys(recipient, deviceId);
|
||||||
|
|
||||||
|
for (PreKeyBundle preKey : preKeys) {
|
||||||
|
try {
|
||||||
|
SessionBuilder sessionBuilder = new SessionBuilder(store, recipient.getRecipientId(), deviceId);
|
||||||
|
sessionBuilder.process(preKey);
|
||||||
|
} catch (org.whispersystems.libaxolotl.UntrustedIdentityException e) {
|
||||||
|
throw new UntrustedIdentityException("Untrusted identity key!", recipient.getNumber(), preKey.getIdentityKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventListener.isPresent()) {
|
||||||
|
eventListener.get().onSecurityEvent(recipient.getRecipientId());
|
||||||
|
}
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextSecureCipher cipher = new TextSecureCipher(store, recipient.getRecipientId(), deviceId);
|
||||||
|
CiphertextMessage message = cipher.encrypt(plaintext);
|
||||||
|
int remoteRegistrationId = cipher.getRemoteRegistrationId();
|
||||||
|
|
||||||
|
if (message.getType() == CiphertextMessage.PREKEY_TYPE) {
|
||||||
|
return new PushBody(Type.PREKEY_BUNDLE_VALUE, remoteRegistrationId, message.serialize());
|
||||||
|
} else if (message.getType() == CiphertextMessage.WHISPER_TYPE) {
|
||||||
|
return new PushBody(Type.CIPHERTEXT_VALUE, remoteRegistrationId, message.serialize());
|
||||||
|
} else {
|
||||||
|
throw new AssertionError("Unknown ciphertext type: " + message.getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleMismatchedDevices(PushServiceSocket socket, TextSecureAddress recipient,
|
||||||
|
MismatchedDevices mismatchedDevices)
|
||||||
|
throws IOException, UntrustedIdentityException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
for (int extraDeviceId : mismatchedDevices.getExtraDevices()) {
|
||||||
|
store.deleteSession(recipient.getRecipientId(), extraDeviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int missingDeviceId : mismatchedDevices.getMissingDevices()) {
|
||||||
|
PreKeyBundle preKey = socket.getPreKey(recipient, missingDeviceId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
SessionBuilder sessionBuilder = new SessionBuilder(store, recipient.getRecipientId(), missingDeviceId);
|
||||||
|
sessionBuilder.process(preKey);
|
||||||
|
} catch (org.whispersystems.libaxolotl.UntrustedIdentityException e) {
|
||||||
|
throw new UntrustedIdentityException("Untrusted identity key!", recipient.getNumber(), preKey.getIdentityKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleStaleDevices(TextSecureAddress recipient, StaleDevices staleDevices) {
|
||||||
|
long recipientId = recipient.getRecipientId();
|
||||||
|
|
||||||
|
for (int staleDeviceId : staleDevices.getStaleDevices()) {
|
||||||
|
store.deleteSession(recipientId, staleDeviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static interface EventListener {
|
||||||
|
public void onSecurityEvent(long recipientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,220 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2013-2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.crypto;
|
||||||
|
|
||||||
|
import org.whispersystems.libaxolotl.InvalidMacException;
|
||||||
|
import org.whispersystems.libaxolotl.InvalidMessageException;
|
||||||
|
import org.whispersystems.textsecure.internal.util.Util;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import javax.crypto.ShortBufferException;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for streaming an encrypted push attachment off disk.
|
||||||
|
*
|
||||||
|
* @author Moxie Marlinspike
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class AttachmentCipherInputStream extends FileInputStream {
|
||||||
|
|
||||||
|
private static final int BLOCK_SIZE = 16;
|
||||||
|
private static final int CIPHER_KEY_SIZE = 32;
|
||||||
|
private static final int MAC_KEY_SIZE = 32;
|
||||||
|
|
||||||
|
private Cipher cipher;
|
||||||
|
private boolean done;
|
||||||
|
private long totalDataSize;
|
||||||
|
private long totalRead;
|
||||||
|
private byte[] overflowBuffer;
|
||||||
|
|
||||||
|
public AttachmentCipherInputStream(File file, byte[] combinedKeyMaterial)
|
||||||
|
throws IOException, InvalidMessageException
|
||||||
|
{
|
||||||
|
super(file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[][] parts = Util.split(combinedKeyMaterial, CIPHER_KEY_SIZE, MAC_KEY_SIZE);
|
||||||
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
|
|
||||||
|
mac.init(new SecretKeySpec(parts[1], "HmacSHA256"));
|
||||||
|
|
||||||
|
if (file.length() <= BLOCK_SIZE + mac.getMacLength()) {
|
||||||
|
throw new InvalidMessageException("Message shorter than crypto overhead!");
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyMac(file, mac);
|
||||||
|
|
||||||
|
byte[] iv = new byte[BLOCK_SIZE];
|
||||||
|
readFully(iv);
|
||||||
|
|
||||||
|
this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||||
|
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(parts[0], "AES"), new IvParameterSpec(iv));
|
||||||
|
|
||||||
|
this.done = false;
|
||||||
|
this.totalRead = 0;
|
||||||
|
this.totalDataSize = file.length() - cipher.getBlockSize() - mac.getMacLength();
|
||||||
|
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} catch (InvalidMacException e) {
|
||||||
|
throw new InvalidMessageException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buffer) throws IOException {
|
||||||
|
return read(buffer, 0, buffer.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buffer, int offset, int length) throws IOException {
|
||||||
|
if (totalRead != totalDataSize) return readIncremental(buffer, offset, length);
|
||||||
|
else if (!done) return readFinal(buffer, offset, length);
|
||||||
|
else return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean markSupported() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long skip(long byteCount) throws IOException {
|
||||||
|
long skipped = 0L;
|
||||||
|
while (skipped < byteCount) {
|
||||||
|
byte[] buf = new byte[Math.min(4096, (int)(byteCount-skipped))];
|
||||||
|
int read = read(buf);
|
||||||
|
|
||||||
|
skipped += read;
|
||||||
|
}
|
||||||
|
|
||||||
|
return skipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int readFinal(byte[] buffer, int offset, int length) throws IOException {
|
||||||
|
try {
|
||||||
|
int flourish = cipher.doFinal(buffer, offset);
|
||||||
|
|
||||||
|
done = true;
|
||||||
|
return flourish;
|
||||||
|
} catch (IllegalBlockSizeException | BadPaddingException | ShortBufferException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int readIncremental(byte[] buffer, int offset, int length) throws IOException {
|
||||||
|
int readLength = 0;
|
||||||
|
if (null != overflowBuffer) {
|
||||||
|
if (overflowBuffer.length > length) {
|
||||||
|
System.arraycopy(overflowBuffer, 0, buffer, offset, length);
|
||||||
|
overflowBuffer = Arrays.copyOfRange(overflowBuffer, length, overflowBuffer.length);
|
||||||
|
return length;
|
||||||
|
} else if (overflowBuffer.length == length) {
|
||||||
|
System.arraycopy(overflowBuffer, 0, buffer, offset, length);
|
||||||
|
overflowBuffer = null;
|
||||||
|
return length;
|
||||||
|
} else {
|
||||||
|
System.arraycopy(overflowBuffer, 0, buffer, offset, overflowBuffer.length);
|
||||||
|
readLength += overflowBuffer.length;
|
||||||
|
offset += readLength;
|
||||||
|
length -= readLength;
|
||||||
|
overflowBuffer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length + totalRead > totalDataSize)
|
||||||
|
length = (int)(totalDataSize - totalRead);
|
||||||
|
|
||||||
|
byte[] internalBuffer = new byte[length];
|
||||||
|
int read = super.read(internalBuffer, 0, internalBuffer.length <= cipher.getBlockSize() ? internalBuffer.length : internalBuffer.length - cipher.getBlockSize());
|
||||||
|
totalRead += read;
|
||||||
|
|
||||||
|
try {
|
||||||
|
int outputLen = cipher.getOutputSize(read);
|
||||||
|
|
||||||
|
if (outputLen <= length) {
|
||||||
|
readLength += cipher.update(internalBuffer, 0, read, buffer, offset);
|
||||||
|
return readLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] transientBuffer = new byte[outputLen];
|
||||||
|
outputLen = cipher.update(internalBuffer, 0, read, transientBuffer, 0);
|
||||||
|
if (outputLen <= length) {
|
||||||
|
System.arraycopy(transientBuffer, 0, buffer, offset, outputLen);
|
||||||
|
readLength += outputLen;
|
||||||
|
} else {
|
||||||
|
System.arraycopy(transientBuffer, 0, buffer, offset, length);
|
||||||
|
overflowBuffer = Arrays.copyOfRange(transientBuffer, length, outputLen);
|
||||||
|
readLength += length;
|
||||||
|
}
|
||||||
|
return readLength;
|
||||||
|
} catch (ShortBufferException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyMac(File file, Mac mac) throws FileNotFoundException, InvalidMacException {
|
||||||
|
try {
|
||||||
|
FileInputStream fin = new FileInputStream(file);
|
||||||
|
int remainingData = (int) file.length() - mac.getMacLength();
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
|
||||||
|
while (remainingData > 0) {
|
||||||
|
int read = fin.read(buffer, 0, Math.min(buffer.length, remainingData));
|
||||||
|
mac.update(buffer, 0, read);
|
||||||
|
remainingData -= read;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] ourMac = mac.doFinal();
|
||||||
|
byte[] theirMac = new byte[mac.getMacLength()];
|
||||||
|
Util.readFully(fin, theirMac);
|
||||||
|
|
||||||
|
if (!Arrays.equals(ourMac, theirMac)) {
|
||||||
|
throw new InvalidMacException("MAC doesn't match!");
|
||||||
|
}
|
||||||
|
} catch (IOException e1) {
|
||||||
|
throw new InvalidMacException(e1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readFully(byte[] buffer) throws IOException {
|
||||||
|
int offset = 0;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
int read = super.read(buffer, offset, buffer.length - offset);
|
||||||
|
|
||||||
|
if (read + offset < buffer.length) offset += read;
|
||||||
|
else return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.crypto;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecure.internal.util.Util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
public class AttachmentCipherOutputStream extends OutputStream {
|
||||||
|
|
||||||
|
private final Cipher cipher;
|
||||||
|
private final Mac mac;
|
||||||
|
private final OutputStream outputStream;
|
||||||
|
|
||||||
|
private long ciphertextLength = 0;
|
||||||
|
|
||||||
|
public AttachmentCipherOutputStream(byte[] combinedKeyMaterial,
|
||||||
|
OutputStream outputStream)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
this.outputStream = outputStream;
|
||||||
|
this.cipher = initializeCipher();
|
||||||
|
this.mac = initializeMac();
|
||||||
|
|
||||||
|
byte[][] keyParts = Util.split(combinedKeyMaterial, 32, 32);
|
||||||
|
|
||||||
|
this.cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(keyParts[0], "AES"));
|
||||||
|
this.mac.init(new SecretKeySpec(keyParts[1], "HmacSHA256"));
|
||||||
|
|
||||||
|
mac.update(cipher.getIV());
|
||||||
|
outputStream.write(cipher.getIV());
|
||||||
|
ciphertextLength += cipher.getIV().length;
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte[] buffer) throws IOException {
|
||||||
|
write(buffer, 0, buffer.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte[] buffer, int offset, int length) throws IOException {
|
||||||
|
byte[] ciphertext = cipher.update(buffer, offset, length);
|
||||||
|
|
||||||
|
if (ciphertext != null) {
|
||||||
|
mac.update(ciphertext);
|
||||||
|
outputStream.write(ciphertext);
|
||||||
|
ciphertextLength += ciphertext.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(int b) {
|
||||||
|
throw new AssertionError("NYI");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void flush() throws IOException {
|
||||||
|
try {
|
||||||
|
byte[] ciphertext = cipher.doFinal();
|
||||||
|
byte[] auth = mac.doFinal(ciphertext);
|
||||||
|
|
||||||
|
outputStream.write(ciphertext);
|
||||||
|
outputStream.write(auth);
|
||||||
|
|
||||||
|
ciphertextLength += ciphertext.length;
|
||||||
|
ciphertextLength += auth.length;
|
||||||
|
|
||||||
|
outputStream.flush();
|
||||||
|
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getCiphertextLength(long plaintextLength) {
|
||||||
|
return 16 + (((plaintextLength / 16) +1) * 16) + 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mac initializeMac() {
|
||||||
|
try {
|
||||||
|
return Mac.getInstance("HmacSHA256");
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cipher initializeCipher() {
|
||||||
|
try {
|
||||||
|
return Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||||
|
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,167 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.crypto;
|
||||||
|
|
||||||
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
|
|
||||||
|
import org.whispersystems.libaxolotl.DuplicateMessageException;
|
||||||
|
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||||
|
import org.whispersystems.libaxolotl.InvalidKeyIdException;
|
||||||
|
import org.whispersystems.libaxolotl.InvalidMessageException;
|
||||||
|
import org.whispersystems.libaxolotl.InvalidVersionException;
|
||||||
|
import org.whispersystems.libaxolotl.LegacyMessageException;
|
||||||
|
import org.whispersystems.libaxolotl.NoSessionException;
|
||||||
|
import org.whispersystems.libaxolotl.SessionCipher;
|
||||||
|
import org.whispersystems.libaxolotl.UntrustedIdentityException;
|
||||||
|
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
|
||||||
|
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
|
||||||
|
import org.whispersystems.libaxolotl.protocol.WhisperMessage;
|
||||||
|
import org.whispersystems.libaxolotl.state.AxolotlStore;
|
||||||
|
import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
|
||||||
|
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer;
|
||||||
|
import org.whispersystems.textsecure.api.messages.TextSecureEnvelope;
|
||||||
|
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
|
||||||
|
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
|
||||||
|
import org.whispersystems.textsecure.internal.push.PushTransportDetails;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent;
|
||||||
|
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.GroupContext.Type.DELIVER;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is used to decrypt received {@link org.whispersystems.textsecure.api.messages.TextSecureEnvelope}s.
|
||||||
|
*
|
||||||
|
* @author Moxie Marlinspike
|
||||||
|
*/
|
||||||
|
public class TextSecureCipher {
|
||||||
|
|
||||||
|
private final SessionCipher sessionCipher;
|
||||||
|
|
||||||
|
public TextSecureCipher(AxolotlStore axolotlStore, long recipientId, int deviceId) {
|
||||||
|
this.sessionCipher = new SessionCipher(axolotlStore, recipientId, deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CiphertextMessage encrypt(byte[] unpaddedMessage) {
|
||||||
|
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion());
|
||||||
|
return sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt a received {@link org.whispersystems.textsecure.api.messages.TextSecureEnvelope}
|
||||||
|
*
|
||||||
|
* @param envelope The received TextSecureEnvelope
|
||||||
|
* @return a decrypted TextSecureMessage
|
||||||
|
* @throws InvalidVersionException
|
||||||
|
* @throws InvalidMessageException
|
||||||
|
* @throws InvalidKeyException
|
||||||
|
* @throws DuplicateMessageException
|
||||||
|
* @throws InvalidKeyIdException
|
||||||
|
* @throws UntrustedIdentityException
|
||||||
|
* @throws LegacyMessageException
|
||||||
|
* @throws NoSessionException
|
||||||
|
*/
|
||||||
|
public TextSecureMessage decrypt(TextSecureEnvelope envelope)
|
||||||
|
throws InvalidVersionException, InvalidMessageException, InvalidKeyException,
|
||||||
|
DuplicateMessageException, InvalidKeyIdException, UntrustedIdentityException,
|
||||||
|
LegacyMessageException, NoSessionException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
byte[] paddedMessage;
|
||||||
|
|
||||||
|
if (envelope.isPreKeyWhisperMessage()) {
|
||||||
|
paddedMessage = sessionCipher.decrypt(new PreKeyWhisperMessage(envelope.getMessage()));
|
||||||
|
} else if (envelope.isWhisperMessage()) {
|
||||||
|
paddedMessage = sessionCipher.decrypt(new WhisperMessage(envelope.getMessage()));
|
||||||
|
} else if (envelope.isPlaintext()) {
|
||||||
|
paddedMessage = envelope.getMessage();
|
||||||
|
} else {
|
||||||
|
throw new InvalidMessageException("Unknown type: " + envelope.getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion());
|
||||||
|
PushMessageContent content = PushMessageContent.parseFrom(transportDetails.getStrippedPaddingMessageBody(paddedMessage));
|
||||||
|
|
||||||
|
return createTextSecureMessage(envelope, content);
|
||||||
|
} catch (InvalidProtocolBufferException e) {
|
||||||
|
throw new InvalidMessageException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRemoteRegistrationId() {
|
||||||
|
return sessionCipher.getRemoteRegistrationId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextSecureMessage createTextSecureMessage(TextSecureEnvelope envelope, PushMessageContent content) {
|
||||||
|
TextSecureGroup groupInfo = createGroupInfo(envelope, content);
|
||||||
|
List<TextSecureAttachment> attachments = new LinkedList<>();
|
||||||
|
boolean endSession = ((content.getFlags() & PushMessageContent.Flags.END_SESSION_VALUE) != 0);
|
||||||
|
boolean secure = envelope.isWhisperMessage() || envelope.isPreKeyWhisperMessage();
|
||||||
|
|
||||||
|
for (PushMessageContent.AttachmentPointer pointer : content.getAttachmentsList()) {
|
||||||
|
attachments.add(new TextSecureAttachmentPointer(pointer.getId(),
|
||||||
|
pointer.getContentType(),
|
||||||
|
pointer.getKey().toByteArray(),
|
||||||
|
envelope.getRelay()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TextSecureMessage(envelope.getTimestamp(), groupInfo, attachments,
|
||||||
|
content.getBody(), secure, endSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextSecureGroup createGroupInfo(TextSecureEnvelope envelope, PushMessageContent content) {
|
||||||
|
if (!content.hasGroup()) return null;
|
||||||
|
|
||||||
|
TextSecureGroup.Type type;
|
||||||
|
|
||||||
|
switch (content.getGroup().getType()) {
|
||||||
|
case DELIVER: type = TextSecureGroup.Type.DELIVER; break;
|
||||||
|
case UPDATE: type = TextSecureGroup.Type.UPDATE; break;
|
||||||
|
case QUIT: type = TextSecureGroup.Type.QUIT; break;
|
||||||
|
default: type = TextSecureGroup.Type.UNKNOWN; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.getGroup().getType() != DELIVER) {
|
||||||
|
String name = null;
|
||||||
|
List<String> members = null;
|
||||||
|
TextSecureAttachmentPointer avatar = null;
|
||||||
|
|
||||||
|
if (content.getGroup().hasName()) {
|
||||||
|
name = content.getGroup().getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.getGroup().getMembersCount() > 0) {
|
||||||
|
members = content.getGroup().getMembersList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.getGroup().hasAvatar()) {
|
||||||
|
avatar = new TextSecureAttachmentPointer(content.getGroup().getAvatar().getId(),
|
||||||
|
content.getGroup().getAvatar().getContentType(),
|
||||||
|
content.getGroup().getAvatar().getKey().toByteArray(),
|
||||||
|
envelope.getRelay());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TextSecureGroup(type, content.getGroup().getId().toByteArray(), name, members, avatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TextSecureGroup(content.getGroup().getId().toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.crypto;
|
||||||
|
|
||||||
|
import org.whispersystems.libaxolotl.IdentityKey;
|
||||||
|
|
||||||
|
public class UntrustedIdentityException extends Exception {
|
||||||
|
|
||||||
|
private final IdentityKey identityKey;
|
||||||
|
private final String e164number;
|
||||||
|
|
||||||
|
public UntrustedIdentityException(String s, String e164number, IdentityKey identityKey) {
|
||||||
|
super(s);
|
||||||
|
this.e164number = e164number;
|
||||||
|
this.identityKey = identityKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UntrustedIdentityException(UntrustedIdentityException e) {
|
||||||
|
this(e.getMessage(), e.getE164Number(), e.getIdentityKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
public IdentityKey getIdentityKey() {
|
||||||
|
return identityKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getE164Number() {
|
||||||
|
return e164number;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.messages;
|
||||||
|
|
||||||
|
public abstract class TextSecureAttachment {
|
||||||
|
|
||||||
|
private final String contentType;
|
||||||
|
|
||||||
|
protected TextSecureAttachment(String contentType) {
|
||||||
|
this.contentType = contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContentType() {
|
||||||
|
return contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract boolean isStream();
|
||||||
|
public abstract boolean isPointer();
|
||||||
|
|
||||||
|
public TextSecureAttachmentStream asStream() {
|
||||||
|
return (TextSecureAttachmentStream)this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextSecureAttachmentPointer asPointer() {
|
||||||
|
return (TextSecureAttachmentPointer)this;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.messages;
|
||||||
|
|
||||||
|
import org.whispersystems.libaxolotl.util.guava.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a received TextSecureMessage attachment "handle." This
|
||||||
|
* is a pointer to the actual attachment content, which needs to be
|
||||||
|
* retrieved using {@link org.whispersystems.textsecure.api.TextSecureMessageReceiver#retrieveAttachment(TextSecureAttachmentPointer, java.io.File)}
|
||||||
|
*
|
||||||
|
* @author Moxie Marlinspike
|
||||||
|
*/
|
||||||
|
public class TextSecureAttachmentPointer extends TextSecureAttachment {
|
||||||
|
|
||||||
|
private final long id;
|
||||||
|
private final byte[] key;
|
||||||
|
private final Optional<String> relay;
|
||||||
|
|
||||||
|
public TextSecureAttachmentPointer(long id, String contentType, byte[] key, String relay) {
|
||||||
|
super(contentType);
|
||||||
|
this.id = id;
|
||||||
|
this.key = key;
|
||||||
|
this.relay = Optional.fromNullable(relay);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isStream() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPointer() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getRelay() {
|
||||||
|
return relay;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.messages;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a local TextSecureAttachment to be sent.
|
||||||
|
*/
|
||||||
|
public class TextSecureAttachmentStream extends TextSecureAttachment {
|
||||||
|
|
||||||
|
private final InputStream inputStream;
|
||||||
|
private final long length;
|
||||||
|
|
||||||
|
public TextSecureAttachmentStream(InputStream inputStream, String contentType, long length) {
|
||||||
|
super(contentType);
|
||||||
|
this.inputStream = inputStream;
|
||||||
|
this.length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isStream() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPointer() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getInputStream() {
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLength() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,252 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.messages;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
|
import org.whispersystems.libaxolotl.InvalidVersionException;
|
||||||
|
import org.whispersystems.textsecure.internal.push.PushMessageProtos.IncomingPushMessageSignal;
|
||||||
|
import org.whispersystems.textsecure.internal.util.Base64;
|
||||||
|
import org.whispersystems.textsecure.internal.util.Hex;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents an encrypted TextSecure envelope.
|
||||||
|
*
|
||||||
|
* The envelope contains the wrapping information, such as the sender, the
|
||||||
|
* message timestamp, the encrypted message type, etc.
|
||||||
|
*
|
||||||
|
* @author Moxie Marlinspike
|
||||||
|
*/
|
||||||
|
public class TextSecureEnvelope {
|
||||||
|
|
||||||
|
private static final String TAG = TextSecureEnvelope.class.getSimpleName();
|
||||||
|
|
||||||
|
private static final int SUPPORTED_VERSION = 1;
|
||||||
|
private static final int CIPHER_KEY_SIZE = 32;
|
||||||
|
private static final int MAC_KEY_SIZE = 20;
|
||||||
|
private static final int MAC_SIZE = 10;
|
||||||
|
|
||||||
|
private static final int VERSION_OFFSET = 0;
|
||||||
|
private static final int VERSION_LENGTH = 1;
|
||||||
|
private static final int IV_OFFSET = VERSION_OFFSET + VERSION_LENGTH;
|
||||||
|
private static final int IV_LENGTH = 16;
|
||||||
|
private static final int CIPHERTEXT_OFFSET = IV_OFFSET + IV_LENGTH;
|
||||||
|
|
||||||
|
private final IncomingPushMessageSignal signal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an envelope from a serialized, Base64 encoded TextSecureEnvelope, encrypted
|
||||||
|
* with a signaling key.
|
||||||
|
*
|
||||||
|
* @param message The serialized TextSecureEnvelope, base64 encoded and encrypted.
|
||||||
|
* @param signalingKey The signaling key.
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidVersionException
|
||||||
|
*/
|
||||||
|
public TextSecureEnvelope(String message, String signalingKey)
|
||||||
|
throws IOException, InvalidVersionException
|
||||||
|
{
|
||||||
|
this(Base64.decode(message), signalingKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an envelope from a serialized TextSecureEnvelope, encrypted with a signaling key.
|
||||||
|
*
|
||||||
|
* @param ciphertext The serialized and encrypted TextSecureEnvelope.
|
||||||
|
* @param signalingKey The signaling key.
|
||||||
|
* @throws InvalidVersionException
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public TextSecureEnvelope(byte[] ciphertext, String signalingKey)
|
||||||
|
throws InvalidVersionException, IOException
|
||||||
|
{
|
||||||
|
if (ciphertext.length < VERSION_LENGTH || ciphertext[VERSION_OFFSET] != SUPPORTED_VERSION)
|
||||||
|
throw new InvalidVersionException("Unsupported version!");
|
||||||
|
|
||||||
|
SecretKeySpec cipherKey = getCipherKey(signalingKey);
|
||||||
|
SecretKeySpec macKey = getMacKey(signalingKey);
|
||||||
|
|
||||||
|
verifyMac(ciphertext, macKey);
|
||||||
|
|
||||||
|
this.signal = IncomingPushMessageSignal.parseFrom(getPlaintext(ciphertext, cipherKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextSecureEnvelope(int type, String source, int sourceDevice,
|
||||||
|
String relay, long timestamp, byte[] message)
|
||||||
|
{
|
||||||
|
this.signal = IncomingPushMessageSignal.newBuilder()
|
||||||
|
.setType(IncomingPushMessageSignal.Type.valueOf(type))
|
||||||
|
.setSource(source)
|
||||||
|
.setSourceDevice(sourceDevice)
|
||||||
|
.setRelay(relay)
|
||||||
|
.setTimestamp(timestamp)
|
||||||
|
.setMessage(ByteString.copyFrom(message))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The envelope's sender.
|
||||||
|
*/
|
||||||
|
public String getSource() {
|
||||||
|
return signal.getSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The envelope's sender device ID.
|
||||||
|
*/
|
||||||
|
public int getSourceDevice() {
|
||||||
|
return signal.getSourceDevice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The envelope content type.
|
||||||
|
*/
|
||||||
|
public int getType() {
|
||||||
|
return signal.getType().getNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The federated server this envelope came from.
|
||||||
|
*/
|
||||||
|
public String getRelay() {
|
||||||
|
return signal.getRelay();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The timestamp this envelope was sent.
|
||||||
|
*/
|
||||||
|
public long getTimestamp() {
|
||||||
|
return signal.getTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The envelope's containing message.
|
||||||
|
*/
|
||||||
|
public byte[] getMessage() {
|
||||||
|
return signal.getMessage().toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the containing message is a {@link org.whispersystems.libaxolotl.protocol.WhisperMessage}
|
||||||
|
*/
|
||||||
|
public boolean isWhisperMessage() {
|
||||||
|
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.CIPHERTEXT_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the containing message is a {@link org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage}
|
||||||
|
*/
|
||||||
|
public boolean isPreKeyWhisperMessage() {
|
||||||
|
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.PREKEY_BUNDLE_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the containing message is plaintext.
|
||||||
|
*/
|
||||||
|
public boolean isPlaintext() {
|
||||||
|
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.PLAINTEXT_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the containing message is a delivery receipt.
|
||||||
|
*/
|
||||||
|
public boolean isReceipt() {
|
||||||
|
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.RECEIPT_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getPlaintext(byte[] ciphertext, SecretKeySpec cipherKey) throws IOException {
|
||||||
|
try {
|
||||||
|
byte[] ivBytes = new byte[IV_LENGTH];
|
||||||
|
System.arraycopy(ciphertext, IV_OFFSET, ivBytes, 0, ivBytes.length);
|
||||||
|
IvParameterSpec iv = new IvParameterSpec(ivBytes);
|
||||||
|
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, cipherKey, iv);
|
||||||
|
|
||||||
|
return cipher.doFinal(ciphertext, CIPHERTEXT_OFFSET,
|
||||||
|
ciphertext.length - VERSION_LENGTH - IV_LENGTH - MAC_SIZE);
|
||||||
|
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} catch (BadPaddingException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
throw new IOException("Bad padding?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyMac(byte[] ciphertext, SecretKeySpec macKey) throws IOException {
|
||||||
|
try {
|
||||||
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
|
mac.init(macKey);
|
||||||
|
|
||||||
|
if (ciphertext.length < MAC_SIZE + 1)
|
||||||
|
throw new IOException("Invalid MAC!");
|
||||||
|
|
||||||
|
mac.update(ciphertext, 0, ciphertext.length - MAC_SIZE);
|
||||||
|
|
||||||
|
byte[] ourMacFull = mac.doFinal();
|
||||||
|
byte[] ourMacBytes = new byte[MAC_SIZE];
|
||||||
|
System.arraycopy(ourMacFull, 0, ourMacBytes, 0, ourMacBytes.length);
|
||||||
|
|
||||||
|
byte[] theirMacBytes = new byte[MAC_SIZE];
|
||||||
|
System.arraycopy(ciphertext, ciphertext.length-MAC_SIZE, theirMacBytes, 0, theirMacBytes.length);
|
||||||
|
|
||||||
|
Log.w(TAG, "Our MAC: " + Hex.toString(ourMacBytes));
|
||||||
|
Log.w(TAG, "Thr MAC: " + Hex.toString(theirMacBytes));
|
||||||
|
|
||||||
|
if (!Arrays.equals(ourMacBytes, theirMacBytes)) {
|
||||||
|
throw new IOException("Invalid MAC compare!");
|
||||||
|
}
|
||||||
|
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private SecretKeySpec getCipherKey(String signalingKey) throws IOException {
|
||||||
|
byte[] signalingKeyBytes = Base64.decode(signalingKey);
|
||||||
|
byte[] cipherKey = new byte[CIPHER_KEY_SIZE];
|
||||||
|
System.arraycopy(signalingKeyBytes, 0, cipherKey, 0, cipherKey.length);
|
||||||
|
|
||||||
|
return new SecretKeySpec(cipherKey, "AES");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private SecretKeySpec getMacKey(String signalingKey) throws IOException {
|
||||||
|
byte[] signalingKeyBytes = Base64.decode(signalingKey);
|
||||||
|
byte[] macKey = new byte[MAC_KEY_SIZE];
|
||||||
|
System.arraycopy(signalingKeyBytes, CIPHER_KEY_SIZE, macKey, 0, macKey.length);
|
||||||
|
|
||||||
|
return new SecretKeySpec(macKey, "HmacSHA256");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.messages;
|
||||||
|
|
||||||
|
import org.whispersystems.libaxolotl.util.guava.Optional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group information to include in TextSecureMessages destined to groups.
|
||||||
|
*
|
||||||
|
* This class represents a "context" that is included with textsecure messages
|
||||||
|
* to make them group messages. There are three types of context:
|
||||||
|
*
|
||||||
|
* 1) Update -- Sent when either creating a group, or updating the properties
|
||||||
|
* of a group (such as the avatar icon, membership list, or title).
|
||||||
|
* 2) Deliver -- Sent when a message is to be delivered to an existing group.
|
||||||
|
* 3) Quit -- Sent when the sender wishes to leave an existing group.
|
||||||
|
*
|
||||||
|
* @author Moxie Marlinspike
|
||||||
|
*/
|
||||||
|
public class TextSecureGroup {
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
UNKNOWN,
|
||||||
|
UPDATE,
|
||||||
|
DELIVER,
|
||||||
|
QUIT
|
||||||
|
}
|
||||||
|
|
||||||
|
private final byte[] groupId;
|
||||||
|
private final Type type;
|
||||||
|
private final Optional<String> name;
|
||||||
|
private final Optional<List<String>> members;
|
||||||
|
private final Optional<TextSecureAttachment> avatar;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a DELIVER group context.
|
||||||
|
* @param groupId
|
||||||
|
*/
|
||||||
|
public TextSecureGroup(byte[] groupId) {
|
||||||
|
this(Type.DELIVER, groupId, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a group context.
|
||||||
|
* @param type The group message type (update, deliver, quit).
|
||||||
|
* @param groupId The group ID.
|
||||||
|
* @param name The group title.
|
||||||
|
* @param members The group membership list.
|
||||||
|
* @param avatar The group avatar icon.
|
||||||
|
*/
|
||||||
|
public TextSecureGroup(Type type, byte[] groupId, String name,
|
||||||
|
List<String> members,
|
||||||
|
TextSecureAttachment avatar)
|
||||||
|
{
|
||||||
|
this.type = type;
|
||||||
|
this.groupId = groupId;
|
||||||
|
this.name = Optional.fromNullable(name);
|
||||||
|
this.members = Optional.fromNullable(members);
|
||||||
|
this.avatar = Optional.fromNullable(avatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getGroupId() {
|
||||||
|
return groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<List<String>> getMembers() {
|
||||||
|
return members;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<TextSecureAttachment> getAvatar() {
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.messages;
|
||||||
|
|
||||||
|
import org.whispersystems.libaxolotl.util.guava.Optional;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a decrypted text secure message.
|
||||||
|
*/
|
||||||
|
public class TextSecureMessage {
|
||||||
|
|
||||||
|
private final long timestamp;
|
||||||
|
private final Optional<List<TextSecureAttachment>> attachments;
|
||||||
|
private final Optional<String> body;
|
||||||
|
private final Optional<TextSecureGroup> group;
|
||||||
|
private final boolean secure;
|
||||||
|
private final boolean endSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a TextSecureMessage with a body and no attachments.
|
||||||
|
*
|
||||||
|
* @param timestamp The sent timestamp.
|
||||||
|
* @param body The message contents.
|
||||||
|
*/
|
||||||
|
public TextSecureMessage(long timestamp, String body) {
|
||||||
|
this(timestamp, (List<TextSecureAttachment>)null, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextSecureMessage(final long timestamp, final TextSecureAttachment attachment, final String body) {
|
||||||
|
this(timestamp, new LinkedList<TextSecureAttachment>() {{add(attachment);}}, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a TextSecureMessage with a body and list of attachments.
|
||||||
|
*
|
||||||
|
* @param timestamp The sent timestamp.
|
||||||
|
* @param attachments The attachments.
|
||||||
|
* @param body The message contents.
|
||||||
|
*/
|
||||||
|
public TextSecureMessage(long timestamp, List<TextSecureAttachment> attachments, String body) {
|
||||||
|
this(timestamp, null, attachments, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a TextSecure group message with attachments and body.
|
||||||
|
*
|
||||||
|
* @param timestamp The sent timestamp.
|
||||||
|
* @param group The group information.
|
||||||
|
* @param attachments The attachments.
|
||||||
|
* @param body The message contents.
|
||||||
|
*/
|
||||||
|
public TextSecureMessage(long timestamp, TextSecureGroup group, List<TextSecureAttachment> attachments, String body) {
|
||||||
|
this(timestamp, group, attachments, body, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a TextSecureMessage.
|
||||||
|
*
|
||||||
|
* @param timestamp The sent timestamp.
|
||||||
|
* @param group The group information (or null if none).
|
||||||
|
* @param attachments The attachments (or null if none).
|
||||||
|
* @param body The message contents.
|
||||||
|
* @param secure Flag indicating whether this message is to be encrypted.
|
||||||
|
* @param endSession Flag indicating whether this message should close a session.
|
||||||
|
*/
|
||||||
|
public TextSecureMessage(long timestamp, TextSecureGroup group, List<TextSecureAttachment> attachments, String body, boolean secure, boolean endSession) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.body = Optional.fromNullable(body);
|
||||||
|
this.group = Optional.fromNullable(group);
|
||||||
|
this.secure = secure;
|
||||||
|
this.endSession = endSession;
|
||||||
|
|
||||||
|
if (attachments != null && !attachments.isEmpty()) {
|
||||||
|
this.attachments = Optional.of(attachments);
|
||||||
|
} else {
|
||||||
|
this.attachments = Optional.absent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The message timestamp.
|
||||||
|
*/
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The message attachments (if any).
|
||||||
|
*/
|
||||||
|
public Optional<List<TextSecureAttachment>> getAttachments() {
|
||||||
|
return attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The message body (if any).
|
||||||
|
*/
|
||||||
|
public Optional<String> getBody() {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The message group info (if any).
|
||||||
|
*/
|
||||||
|
public Optional<TextSecureGroup> getGroupInfo() {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSecure() {
|
||||||
|
return secure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEndSession() {
|
||||||
|
return endSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGroupUpdate() {
|
||||||
|
return group.isPresent() && group.get().getType() != TextSecureGroup.Type.DELIVER;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.push;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that represents a contact's registration state.
|
||||||
|
*/
|
||||||
|
public class ContactTokenDetails {
|
||||||
|
|
||||||
|
private String token;
|
||||||
|
private String relay;
|
||||||
|
private String number;
|
||||||
|
private boolean supportsSms;
|
||||||
|
|
||||||
|
public ContactTokenDetails() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The "anonymized" token (truncated hash) that's transmitted to the server.
|
||||||
|
*/
|
||||||
|
public String getToken() {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The federated server this contact is registered with, or null if on your server.
|
||||||
|
*/
|
||||||
|
public String getRelay() {
|
||||||
|
return relay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Whether this contact supports receiving encrypted SMS.
|
||||||
|
*/
|
||||||
|
public boolean isSupportsSms() {
|
||||||
|
return supportsSms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNumber(String number) {
|
||||||
|
this.number = number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return This contact's username (e164 formatted number).
|
||||||
|
*/
|
||||||
|
public String getNumber() {
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.push;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
|
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||||
|
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.libaxolotl.ecc.ECPublicKey;
|
||||||
|
import org.whispersystems.textsecure.internal.push.PreKeyEntity;
|
||||||
|
import org.whispersystems.textsecure.internal.util.Base64;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class SignedPreKeyEntity extends PreKeyEntity {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@JsonSerialize(using = ByteArraySerializer.class)
|
||||||
|
@JsonDeserialize(using = ByteArrayDeserializer.class)
|
||||||
|
private byte[] signature;
|
||||||
|
|
||||||
|
public SignedPreKeyEntity() {}
|
||||||
|
|
||||||
|
public SignedPreKeyEntity(int keyId, ECPublicKey publicKey, byte[] signature) {
|
||||||
|
super(keyId, publicKey);
|
||||||
|
this.signature = signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getSignature() {
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ByteArraySerializer extends JsonSerializer<byte[]> {
|
||||||
|
@Override
|
||||||
|
public void serialize(byte[] value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||||
|
gen.writeString(Base64.encodeBytesWithoutPadding(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ByteArrayDeserializer extends JsonDeserializer<byte[]> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||||
|
return Base64.decodeWithoutPadding(p.getValueAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.push;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class representing a message destination or origin.
|
||||||
|
*/
|
||||||
|
public class TextSecureAddress {
|
||||||
|
|
||||||
|
public static final int DEFAULT_DEVICE_ID = 1;
|
||||||
|
|
||||||
|
private final long recipientId;
|
||||||
|
private final String e164number;
|
||||||
|
private final String relay;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a PushAddress.
|
||||||
|
*
|
||||||
|
* @param recipientId The axolotl recipient ID of this destination.
|
||||||
|
* @param e164number The TextSecure username of this destination (eg e164 representation of a phone number).
|
||||||
|
* @param relay The TextSecure federated server this user is registered with (if not your own server).
|
||||||
|
*/
|
||||||
|
public TextSecureAddress(long recipientId, String e164number, String relay) {
|
||||||
|
this.recipientId = recipientId;
|
||||||
|
this.e164number = e164number;
|
||||||
|
this.relay = relay;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNumber() {
|
||||||
|
return e164number;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRelay() {
|
||||||
|
return relay;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getRecipientId() {
|
||||||
|
return recipientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (other == null || !(other instanceof TextSecureAddress)) return false;
|
||||||
|
|
||||||
|
TextSecureAddress that = (TextSecureAddress)other;
|
||||||
|
|
||||||
|
return this.recipientId == that.recipientId &&
|
||||||
|
equals(this.e164number, that.e164number) &&
|
||||||
|
equals(this.relay, that.relay);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int hashCode = (int)this.recipientId;
|
||||||
|
|
||||||
|
if (this.e164number != null) hashCode ^= this.e164number.hashCode();
|
||||||
|
if (this.relay != null) hashCode ^= this.relay.hashCode();
|
||||||
|
|
||||||
|
return hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean equals(String one, String two) {
|
||||||
|
if (one == null) return two == null;
|
||||||
|
return one.equals(two);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.push;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that represents a Java {@link java.security.KeyStore} and
|
||||||
|
* its associated password.
|
||||||
|
*/
|
||||||
|
public interface TrustStore {
|
||||||
|
public InputStream getKeyStoreInputStream();
|
||||||
|
public String getKeyStorePassword();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.push.exceptions;
|
||||||
|
|
||||||
|
public class AuthorizationFailedException extends NonSuccessfulResponseCodeException {
|
||||||
|
public AuthorizationFailedException(String s) {
|
||||||
|
super(s);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.push.exceptions;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class EncapsulatedExceptions extends Throwable {
|
||||||
|
|
||||||
|
private final List<UntrustedIdentityException> untrustedIdentityExceptions;
|
||||||
|
private final List<UnregisteredUserException> unregisteredUserExceptions;
|
||||||
|
private final List<NetworkFailureException> networkExceptions;
|
||||||
|
|
||||||
|
public EncapsulatedExceptions(List<UntrustedIdentityException> untrustedIdentities,
|
||||||
|
List<UnregisteredUserException> unregisteredUsers,
|
||||||
|
List<NetworkFailureException> networkExceptions)
|
||||||
|
{
|
||||||
|
this.untrustedIdentityExceptions = untrustedIdentities;
|
||||||
|
this.unregisteredUserExceptions = unregisteredUsers;
|
||||||
|
this.networkExceptions = networkExceptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UntrustedIdentityException> getUntrustedIdentityExceptions() {
|
||||||
|
return untrustedIdentityExceptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UnregisteredUserException> getUnregisteredUserExceptions() {
|
||||||
|
return unregisteredUserExceptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<NetworkFailureException> getNetworkExceptions() {
|
||||||
|
return networkExceptions;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.push.exceptions;
|
||||||
|
|
||||||
|
public class ExpectationFailedException extends NonSuccessfulResponseCodeException {
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package org.whispersystems.textsecure.api.push.exceptions;
|
||||||
|
|
||||||
|
public class NetworkFailureException extends Exception {
|
||||||
|
|
||||||
|
private final String e164number;
|
||||||
|
|
||||||
|
public NetworkFailureException(String e164number, Exception nested) {
|
||||||
|
super(nested);
|
||||||
|
this.e164number = e164number;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getE164number() {
|
||||||
|
return e164number;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.push.exceptions;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class NonSuccessfulResponseCodeException extends IOException {
|
||||||
|
|
||||||
|
public NonSuccessfulResponseCodeException() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public NonSuccessfulResponseCodeException(String s) {
|
||||||
|
super(s);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.push.exceptions;
|
||||||
|
|
||||||
|
public class NotFoundException extends NonSuccessfulResponseCodeException {
|
||||||
|
public NotFoundException(String s) {
|
||||||
|
super(s);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.push.exceptions;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class PushNetworkException extends IOException {
|
||||||
|
|
||||||
|
public PushNetworkException(Exception exception) {
|
||||||
|
super(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PushNetworkException(String s) {
|
||||||
|
super(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.push.exceptions;
|
||||||
|
|
||||||
|
public class RateLimitException extends NonSuccessfulResponseCodeException {
|
||||||
|
public RateLimitException(String s) {
|
||||||
|
super(s);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.push.exceptions;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class UnregisteredUserException extends IOException {
|
||||||
|
|
||||||
|
private final String e164number;
|
||||||
|
|
||||||
|
public UnregisteredUserException(String e164number, Exception exception) {
|
||||||
|
super(exception);
|
||||||
|
this.e164number = e164number;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getE164Number() {
|
||||||
|
return e164number;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package org.whispersystems.textsecure.api.util;
|
||||||
|
|
||||||
|
public interface CredentialsProvider {
|
||||||
|
public String getUser();
|
||||||
|
public String getPassword();
|
||||||
|
public String getSignalingKey();
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.util;
|
||||||
|
|
||||||
|
public class InvalidNumberException extends Throwable {
|
||||||
|
public InvalidNumberException(String s) {
|
||||||
|
super(s);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,137 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.api.util;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.i18n.phonenumbers.NumberParseException;
|
||||||
|
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||||
|
import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat;
|
||||||
|
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phone number formats are a pain.
|
||||||
|
*
|
||||||
|
* @author Moxie Marlinspike
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class PhoneNumberFormatter {
|
||||||
|
|
||||||
|
private static final String TAG = PhoneNumberFormatter.class.getSimpleName();
|
||||||
|
|
||||||
|
public static boolean isValidNumber(String number) {
|
||||||
|
return number.matches("^\\+[0-9]{10,}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String impreciseFormatNumber(String number, String localNumber)
|
||||||
|
throws InvalidNumberException
|
||||||
|
{
|
||||||
|
number = number.replaceAll("[^0-9+]", "");
|
||||||
|
|
||||||
|
if (number.charAt(0) == '+')
|
||||||
|
return number;
|
||||||
|
|
||||||
|
if (localNumber.charAt(0) == '+')
|
||||||
|
localNumber = localNumber.substring(1);
|
||||||
|
|
||||||
|
if (localNumber.length() == number.length() || number.length() > localNumber.length())
|
||||||
|
return "+" + number;
|
||||||
|
|
||||||
|
int difference = localNumber.length() - number.length();
|
||||||
|
|
||||||
|
return "+" + localNumber.substring(0, difference) + number;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatNumberInternational(String number) {
|
||||||
|
try {
|
||||||
|
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
|
||||||
|
PhoneNumber parsedNumber = util.parse(number, null);
|
||||||
|
return util.format(parsedNumber, PhoneNumberFormat.INTERNATIONAL);
|
||||||
|
} catch (NumberParseException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatNumber(String number, String localNumber)
|
||||||
|
throws InvalidNumberException
|
||||||
|
{
|
||||||
|
if (number.contains("@")) {
|
||||||
|
throw new InvalidNumberException("Possible attempt to use email address.");
|
||||||
|
}
|
||||||
|
|
||||||
|
number = number.replaceAll("[^0-9+]", "");
|
||||||
|
|
||||||
|
if (number.length() == 0) {
|
||||||
|
throw new InvalidNumberException("No valid characters found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (number.charAt(0) == '+')
|
||||||
|
return number;
|
||||||
|
|
||||||
|
try {
|
||||||
|
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
|
||||||
|
PhoneNumber localNumberObject = util.parse(localNumber, null);
|
||||||
|
|
||||||
|
String localCountryCode = util.getRegionCodeForNumber(localNumberObject);
|
||||||
|
Log.w(TAG, "Got local CC: " + localCountryCode);
|
||||||
|
|
||||||
|
PhoneNumber numberObject = util.parse(number, localCountryCode);
|
||||||
|
return util.format(numberObject, PhoneNumberFormat.E164);
|
||||||
|
} catch (NumberParseException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
return impreciseFormatNumber(number, localNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getRegionDisplayName(String regionCode) {
|
||||||
|
return (regionCode == null || regionCode.equals("ZZ") || regionCode.equals(PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY))
|
||||||
|
? "Unknown country" : new Locale("", regionCode).getDisplayCountry(Locale.getDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatE164(String countryCode, String number) {
|
||||||
|
try {
|
||||||
|
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
|
||||||
|
int parsedCountryCode = Integer.parseInt(countryCode);
|
||||||
|
PhoneNumber parsedNumber = util.parse(number,
|
||||||
|
util.getRegionCodeForCountryCode(parsedCountryCode));
|
||||||
|
|
||||||
|
return util.format(parsedNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
|
||||||
|
} catch (NumberParseException | NumberFormatException npe) {
|
||||||
|
Log.w(TAG, npe);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "+" +
|
||||||
|
countryCode.replaceAll("[^0-9]", "").replaceAll("^0*", "") +
|
||||||
|
number.replaceAll("[^0-9]", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getInternationalFormatFromE164(String e164number) {
|
||||||
|
try {
|
||||||
|
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
|
||||||
|
PhoneNumber parsedNumber = util.parse(e164number, null);
|
||||||
|
return util.format(parsedNumber, PhoneNumberFormat.INTERNATIONAL);
|
||||||
|
} catch (NumberParseException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
return e164number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
package org.whispersystems.textsecure.internal.crypto;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
|
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||||
|
import org.whispersystems.libaxolotl.ecc.Curve;
|
||||||
|
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
|
||||||
|
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
||||||
|
import org.whispersystems.libaxolotl.kdf.HKDFv3;
|
||||||
|
import org.whispersystems.textsecure.internal.util.Util;
|
||||||
|
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
import static org.whispersystems.textsecure.internal.push.ProvisioningProtos.ProvisionEnvelope;
|
||||||
|
import static org.whispersystems.textsecure.internal.push.ProvisioningProtos.ProvisionMessage;
|
||||||
|
|
||||||
|
|
||||||
|
public class ProvisioningCipher {
|
||||||
|
|
||||||
|
private static final String TAG = ProvisioningCipher.class.getSimpleName();
|
||||||
|
|
||||||
|
private final ECPublicKey theirPublicKey;
|
||||||
|
|
||||||
|
public ProvisioningCipher(ECPublicKey theirPublicKey) {
|
||||||
|
this.theirPublicKey = theirPublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] encrypt(ProvisionMessage message) throws InvalidKeyException {
|
||||||
|
ECKeyPair ourKeyPair = Curve.generateKeyPair();
|
||||||
|
byte[] sharedSecret = Curve.calculateAgreement(theirPublicKey, ourKeyPair.getPrivateKey());
|
||||||
|
byte[] derivedSecret = new HKDFv3().deriveSecrets(sharedSecret, "TextSecure Provisioning Message".getBytes(), 64);
|
||||||
|
byte[][] parts = Util.split(derivedSecret, 32, 32);
|
||||||
|
|
||||||
|
byte[] version = {0x01};
|
||||||
|
byte[] ciphertext = getCiphertext(parts[0], message.toByteArray());
|
||||||
|
byte[] mac = getMac(parts[1], Util.join(version, ciphertext));
|
||||||
|
byte[] body = Util.join(version, ciphertext, mac);
|
||||||
|
|
||||||
|
return ProvisionEnvelope.newBuilder()
|
||||||
|
.setPublicKey(ByteString.copyFrom(ourKeyPair.getPublicKey().serialize()))
|
||||||
|
.setBody(ByteString.copyFrom(body))
|
||||||
|
.build()
|
||||||
|
.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getCiphertext(byte[] key, byte[] message) {
|
||||||
|
try {
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"));
|
||||||
|
|
||||||
|
return Util.join(cipher.getIV(), cipher.doFinal(message));
|
||||||
|
} catch (NoSuchAlgorithmException | NoSuchPaddingException | java.security.InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getMac(byte[] key, byte[] message) {
|
||||||
|
try {
|
||||||
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
|
mac.init(new SecretKeySpec(key, "HmacSHA256"));
|
||||||
|
|
||||||
|
return mac.doFinal(message);
|
||||||
|
} catch (NoSuchAlgorithmException | java.security.InvalidKeyException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.push;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class AccountAttributes {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String signalingKey;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private boolean supportsSms;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int registrationId;
|
||||||
|
|
||||||
|
public AccountAttributes(String signalingKey, boolean supportsSms, int registrationId) {
|
||||||
|
this.signalingKey = signalingKey;
|
||||||
|
this.supportsSms = supportsSms;
|
||||||
|
this.registrationId = registrationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountAttributes() {}
|
||||||
|
|
||||||
|
public String getSignalingKey() {
|
||||||
|
return signalingKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSupportsSms() {
|
||||||
|
return supportsSms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRegistrationId() {
|
||||||
|
return registrationId;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.push;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecure.api.push.ContactTokenDetails;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ContactTokenDetailsList {
|
||||||
|
|
||||||
|
private List<ContactTokenDetails> contacts;
|
||||||
|
|
||||||
|
public ContactTokenDetailsList() {}
|
||||||
|
|
||||||
|
public List<ContactTokenDetails> getContacts() {
|
||||||
|
return contacts;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.push;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ContactTokenList {
|
||||||
|
|
||||||
|
private List<String> contacts;
|
||||||
|
|
||||||
|
public ContactTokenList(List<String> contacts) {
|
||||||
|
this.contacts = contacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContactTokenList() {}
|
||||||
|
|
||||||
|
public List<String> getContacts() {
|
||||||
|
return contacts;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package org.whispersystems.textsecure.internal.push;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class DeviceCode {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String verificationCode;
|
||||||
|
|
||||||
|
public String getVerificationCode() {
|
||||||
|
return verificationCode;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.push;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MismatchedDevices {
|
||||||
|
@JsonProperty
|
||||||
|
private List<Integer> missingDevices;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<Integer> extraDevices;
|
||||||
|
|
||||||
|
public List<Integer> getMissingDevices() {
|
||||||
|
return missingDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Integer> getExtraDevices() {
|
||||||
|
return extraDevices;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2013 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.push;
|
||||||
|
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecure.api.push.TextSecureAddress;
|
||||||
|
import org.whispersystems.textsecure.internal.util.Base64;
|
||||||
|
|
||||||
|
public class OutgoingPushMessage {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int type;
|
||||||
|
@JsonProperty
|
||||||
|
private int destinationDeviceId;
|
||||||
|
@JsonProperty
|
||||||
|
private int destinationRegistrationId;
|
||||||
|
@JsonProperty
|
||||||
|
private String body;
|
||||||
|
|
||||||
|
public OutgoingPushMessage(TextSecureAddress address, int deviceId, PushBody body) {
|
||||||
|
this.type = body.getType();
|
||||||
|
this.destinationDeviceId = deviceId;
|
||||||
|
this.destinationRegistrationId = body.getRemoteRegistrationId();
|
||||||
|
this.body = Base64.encodeBytes(body.getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDestinationDeviceId() {
|
||||||
|
return destinationDeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBody() {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDestinationRegistrationId() {
|
||||||
|
return destinationRegistrationId;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.push;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class OutgoingPushMessageList {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String destination;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String relay;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long timestamp;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<OutgoingPushMessage> messages;
|
||||||
|
|
||||||
|
public OutgoingPushMessageList(String destination, long timestamp, String relay,
|
||||||
|
List<OutgoingPushMessage> messages)
|
||||||
|
{
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.destination = destination;
|
||||||
|
this.relay = relay;
|
||||||
|
this.messages = messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDestination() {
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<OutgoingPushMessage> getMessages() {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRelay() {
|
||||||
|
return relay;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.push;
|
||||||
|
|
||||||
|
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.JsonSerializer;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
|
||||||
|
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||||
|
import org.whispersystems.libaxolotl.ecc.Curve;
|
||||||
|
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
||||||
|
import org.whispersystems.textsecure.internal.util.Base64;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class PreKeyEntity {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int keyId;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@JsonSerialize(using = ECPublicKeySerializer.class)
|
||||||
|
@JsonDeserialize(using = ECPublicKeyDeserializer.class)
|
||||||
|
private ECPublicKey publicKey;
|
||||||
|
|
||||||
|
public PreKeyEntity() {}
|
||||||
|
|
||||||
|
public PreKeyEntity(int keyId, ECPublicKey publicKey) {
|
||||||
|
this.keyId = keyId;
|
||||||
|
this.publicKey = publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getKeyId() {
|
||||||
|
return keyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ECPublicKey getPublicKey() {
|
||||||
|
return publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ECPublicKeySerializer extends JsonSerializer<ECPublicKey> {
|
||||||
|
@Override
|
||||||
|
public void serialize(ECPublicKey value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||||
|
gen.writeString(Base64.encodeBytesWithoutPadding(value.serialize()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ECPublicKeyDeserializer extends JsonDeserializer<ECPublicKey> {
|
||||||
|
@Override
|
||||||
|
public ECPublicKey deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||||
|
try {
|
||||||
|
return Curve.decodePoint(Base64.decodeWithoutPadding(p.getValueAsString()), 0);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.push;
|
||||||
|
|
||||||
|
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.JsonSerializer;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
|
||||||
|
import org.whispersystems.libaxolotl.IdentityKey;
|
||||||
|
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||||
|
import org.whispersystems.textsecure.internal.util.Base64;
|
||||||
|
import org.whispersystems.textsecure.internal.util.JsonUtil;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PreKeyResponse {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@JsonSerialize(using = JsonUtil.IdentityKeySerializer.class)
|
||||||
|
@JsonDeserialize(using = JsonUtil.IdentityKeyDeserializer.class)
|
||||||
|
private IdentityKey identityKey;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<PreKeyResponseItem> devices;
|
||||||
|
|
||||||
|
public IdentityKey getIdentityKey() {
|
||||||
|
return identityKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PreKeyResponseItem> getDevices() {
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.push;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecure.api.push.SignedPreKeyEntity;
|
||||||
|
|
||||||
|
public class PreKeyResponseItem {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int deviceId;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int registrationId;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private SignedPreKeyEntity signedPreKey;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private PreKeyEntity preKey;
|
||||||
|
|
||||||
|
public int getDeviceId() {
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRegistrationId() {
|
||||||
|
return registrationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignedPreKeyEntity getSignedPreKey() {
|
||||||
|
return signedPreKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreKeyEntity getPreKey() {
|
||||||
|
return preKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package org.whispersystems.textsecure.internal.push;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
|
||||||
|
import org.whispersystems.libaxolotl.IdentityKey;
|
||||||
|
import org.whispersystems.textsecure.api.push.SignedPreKeyEntity;
|
||||||
|
import org.whispersystems.textsecure.internal.util.JsonUtil;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PreKeyState {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@JsonSerialize(using = JsonUtil.IdentityKeySerializer.class)
|
||||||
|
@JsonDeserialize(using = JsonUtil.IdentityKeyDeserializer.class)
|
||||||
|
private IdentityKey identityKey;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<PreKeyEntity> preKeys;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private PreKeyEntity lastResortKey;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private SignedPreKeyEntity signedPreKey;
|
||||||
|
|
||||||
|
|
||||||
|
public PreKeyState(List<PreKeyEntity> preKeys, PreKeyEntity lastResortKey,
|
||||||
|
SignedPreKeyEntity signedPreKey, IdentityKey identityKey)
|
||||||
|
{
|
||||||
|
this.preKeys = preKeys;
|
||||||
|
this.lastResortKey = lastResortKey;
|
||||||
|
this.signedPreKey = signedPreKey;
|
||||||
|
this.identityKey = identityKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.push;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class PreKeyStatus {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int count;
|
||||||
|
|
||||||
|
public PreKeyStatus() {}
|
||||||
|
|
||||||
|
public int getCount() {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package org.whispersystems.textsecure.internal.push;
|
||||||
|
|
||||||
|
public class ProvisioningMessage {
|
||||||
|
|
||||||
|
private String body;
|
||||||
|
|
||||||
|
public ProvisioningMessage(String body) {
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.push;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public class PushAttachmentData {
|
||||||
|
|
||||||
|
private final String contentType;
|
||||||
|
private final InputStream data;
|
||||||
|
private final long dataSize;
|
||||||
|
private final byte[] key;
|
||||||
|
|
||||||
|
public PushAttachmentData(String contentType, InputStream data, long dataSize, byte[] key) {
|
||||||
|
this.contentType = contentType;
|
||||||
|
this.data = data;
|
||||||
|
this.dataSize = dataSize;
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContentType() {
|
||||||
|
return contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getDataSize() {
|
||||||
|
return dataSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.push;
|
||||||
|
|
||||||
|
public class PushBody {
|
||||||
|
|
||||||
|
private final int type;
|
||||||
|
private final int remoteRegistrationId;
|
||||||
|
private final byte[] body;
|
||||||
|
|
||||||
|
public PushBody(int type, int remoteRegistrationId, byte[] body) {
|
||||||
|
this.type = type;
|
||||||
|
this.remoteRegistrationId = remoteRegistrationId;
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getBody() {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRemoteRegistrationId() {
|
||||||
|
return remoteRegistrationId;
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,570 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.push;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import org.apache.http.conn.ssl.StrictHostnameVerifier;
|
||||||
|
import org.whispersystems.libaxolotl.IdentityKey;
|
||||||
|
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
||||||
|
import org.whispersystems.libaxolotl.state.PreKeyBundle;
|
||||||
|
import org.whispersystems.libaxolotl.state.PreKeyRecord;
|
||||||
|
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
|
||||||
|
import org.whispersystems.textsecure.api.crypto.AttachmentCipherOutputStream;
|
||||||
|
import org.whispersystems.textsecure.api.push.ContactTokenDetails;
|
||||||
|
import org.whispersystems.textsecure.api.push.TextSecureAddress;
|
||||||
|
import org.whispersystems.textsecure.api.push.SignedPreKeyEntity;
|
||||||
|
import org.whispersystems.textsecure.api.push.TrustStore;
|
||||||
|
import org.whispersystems.textsecure.api.push.exceptions.AuthorizationFailedException;
|
||||||
|
import org.whispersystems.textsecure.api.push.exceptions.ExpectationFailedException;
|
||||||
|
import org.whispersystems.textsecure.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||||
|
import org.whispersystems.textsecure.api.push.exceptions.NotFoundException;
|
||||||
|
import org.whispersystems.textsecure.api.push.exceptions.PushNetworkException;
|
||||||
|
import org.whispersystems.textsecure.api.push.exceptions.RateLimitException;
|
||||||
|
import org.whispersystems.textsecure.api.push.exceptions.UnregisteredUserException;
|
||||||
|
import org.whispersystems.textsecure.api.util.CredentialsProvider;
|
||||||
|
import org.whispersystems.textsecure.internal.push.exceptions.MismatchedDevicesException;
|
||||||
|
import org.whispersystems.textsecure.internal.push.exceptions.StaleDevicesException;
|
||||||
|
import org.whispersystems.textsecure.internal.util.Base64;
|
||||||
|
import org.whispersystems.textsecure.internal.util.BlacklistingTrustManager;
|
||||||
|
import org.whispersystems.textsecure.internal.util.JsonUtil;
|
||||||
|
import org.whispersystems.textsecure.internal.util.Util;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Network interface to the TextSecure server API.
|
||||||
|
*
|
||||||
|
* @author Moxie Marlinspike
|
||||||
|
*/
|
||||||
|
public class PushServiceSocket {
|
||||||
|
|
||||||
|
private static final String CREATE_ACCOUNT_SMS_PATH = "/v1/accounts/sms/code/%s";
|
||||||
|
private static final String CREATE_ACCOUNT_VOICE_PATH = "/v1/accounts/voice/code/%s";
|
||||||
|
private static final String VERIFY_ACCOUNT_PATH = "/v1/accounts/code/%s";
|
||||||
|
private static final String REGISTER_GCM_PATH = "/v1/accounts/gcm/";
|
||||||
|
|
||||||
|
private static final String PREKEY_METADATA_PATH = "/v2/keys/";
|
||||||
|
private static final String PREKEY_PATH = "/v2/keys/%s";
|
||||||
|
private static final String PREKEY_DEVICE_PATH = "/v2/keys/%s/%s";
|
||||||
|
private static final String SIGNED_PREKEY_PATH = "/v2/keys/signed";
|
||||||
|
|
||||||
|
private static final String PROVISIONING_CODE_PATH = "/v1/devices/provisioning/code";
|
||||||
|
private static final String PROVISIONING_MESSAGE_PATH = "/v1/provisioning/%s";
|
||||||
|
|
||||||
|
private static final String DIRECTORY_TOKENS_PATH = "/v1/directory/tokens";
|
||||||
|
private static final String DIRECTORY_VERIFY_PATH = "/v1/directory/%s";
|
||||||
|
private static final String MESSAGE_PATH = "/v1/messages/%s";
|
||||||
|
private static final String RECEIPT_PATH = "/v1/receipt/%s/%d";
|
||||||
|
private static final String ATTACHMENT_PATH = "/v1/attachments/%s";
|
||||||
|
|
||||||
|
private static final boolean ENFORCE_SSL = true;
|
||||||
|
|
||||||
|
private final String serviceUrl;
|
||||||
|
private final TrustManager[] trustManagers;
|
||||||
|
private final CredentialsProvider credentialsProvider;
|
||||||
|
|
||||||
|
public PushServiceSocket(String serviceUrl, TrustStore trustStore, CredentialsProvider credentialsProvider)
|
||||||
|
{
|
||||||
|
this.serviceUrl = serviceUrl;
|
||||||
|
this.credentialsProvider = credentialsProvider;
|
||||||
|
this.trustManagers = BlacklistingTrustManager.createFor(trustStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createAccount(boolean voice) throws IOException {
|
||||||
|
String path = voice ? CREATE_ACCOUNT_VOICE_PATH : CREATE_ACCOUNT_SMS_PATH;
|
||||||
|
makeRequest(String.format(path, credentialsProvider.getUser()), "GET", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void verifyAccount(String verificationCode, String signalingKey,
|
||||||
|
boolean supportsSms, int registrationId)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, supportsSms, registrationId);
|
||||||
|
makeRequest(String.format(VERIFY_ACCOUNT_PATH, verificationCode),
|
||||||
|
"PUT", JsonUtil.toJson(signalingKeyEntity));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNewDeviceVerificationCode() throws IOException {
|
||||||
|
String responseText = makeRequest(PROVISIONING_CODE_PATH, "GET", null);
|
||||||
|
return JsonUtil.fromJson(responseText, DeviceCode.class).getVerificationCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendProvisioningMessage(String destination, byte[] body) throws IOException {
|
||||||
|
makeRequest(String.format(PROVISIONING_MESSAGE_PATH, destination), "PUT",
|
||||||
|
JsonUtil.toJson(new ProvisioningMessage(Base64.encodeBytes(body))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendReceipt(String destination, long messageId, String relay) throws IOException {
|
||||||
|
String path = String.format(RECEIPT_PATH, destination, messageId);
|
||||||
|
|
||||||
|
if (!Util.isEmpty(relay)) {
|
||||||
|
path += "?relay=" + relay;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeRequest(path, "PUT", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerGcmId(String gcmRegistrationId) throws IOException {
|
||||||
|
GcmRegistrationId registration = new GcmRegistrationId(gcmRegistrationId, true);
|
||||||
|
makeRequest(REGISTER_GCM_PATH, "PUT", JsonUtil.toJson(registration));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregisterGcmId() throws IOException {
|
||||||
|
makeRequest(REGISTER_GCM_PATH, "DELETE", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SendMessageResponse sendMessage(OutgoingPushMessageList bundle)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
String responseText = makeRequest(String.format(MESSAGE_PATH, bundle.getDestination()), "PUT", JsonUtil.toJson(bundle));
|
||||||
|
|
||||||
|
if (responseText == null) return new SendMessageResponse(false);
|
||||||
|
else return JsonUtil.fromJson(responseText, SendMessageResponse.class);
|
||||||
|
} catch (NotFoundException nfe) {
|
||||||
|
throw new UnregisteredUserException(bundle.getDestination(), nfe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerPreKeys(IdentityKey identityKey,
|
||||||
|
PreKeyRecord lastResortKey,
|
||||||
|
SignedPreKeyRecord signedPreKey,
|
||||||
|
List<PreKeyRecord> records)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
List<PreKeyEntity> entities = new LinkedList<>();
|
||||||
|
|
||||||
|
for (PreKeyRecord record : records) {
|
||||||
|
PreKeyEntity entity = new PreKeyEntity(record.getId(),
|
||||||
|
record.getKeyPair().getPublicKey());
|
||||||
|
|
||||||
|
entities.add(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
PreKeyEntity lastResortEntity = new PreKeyEntity(lastResortKey.getId(),
|
||||||
|
lastResortKey.getKeyPair().getPublicKey());
|
||||||
|
|
||||||
|
SignedPreKeyEntity signedPreKeyEntity = new SignedPreKeyEntity(signedPreKey.getId(),
|
||||||
|
signedPreKey.getKeyPair().getPublicKey(),
|
||||||
|
signedPreKey.getSignature());
|
||||||
|
|
||||||
|
makeRequest(String.format(PREKEY_PATH, ""), "PUT",
|
||||||
|
JsonUtil.toJson(new PreKeyState(entities, lastResortEntity,
|
||||||
|
signedPreKeyEntity, identityKey)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAvailablePreKeys() throws IOException {
|
||||||
|
String responseText = makeRequest(PREKEY_METADATA_PATH, "GET", null);
|
||||||
|
PreKeyStatus preKeyStatus = JsonUtil.fromJson(responseText, PreKeyStatus.class);
|
||||||
|
|
||||||
|
return preKeyStatus.getCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PreKeyBundle> getPreKeys(TextSecureAddress destination, int deviceIdInteger) throws IOException {
|
||||||
|
try {
|
||||||
|
String deviceId = String.valueOf(deviceIdInteger);
|
||||||
|
|
||||||
|
if (deviceId.equals("1"))
|
||||||
|
deviceId = "*";
|
||||||
|
|
||||||
|
String path = String.format(PREKEY_DEVICE_PATH, destination.getNumber(), deviceId);
|
||||||
|
|
||||||
|
if (!Util.isEmpty(destination.getRelay())) {
|
||||||
|
path = path + "?relay=" + destination.getRelay();
|
||||||
|
}
|
||||||
|
|
||||||
|
String responseText = makeRequest(path, "GET", null);
|
||||||
|
PreKeyResponse response = JsonUtil.fromJson(responseText, PreKeyResponse.class);
|
||||||
|
List<PreKeyBundle> bundles = new LinkedList<>();
|
||||||
|
|
||||||
|
for (PreKeyResponseItem device : response.getDevices()) {
|
||||||
|
ECPublicKey preKey = null;
|
||||||
|
ECPublicKey signedPreKey = null;
|
||||||
|
byte[] signedPreKeySignature = null;
|
||||||
|
int preKeyId = -1;
|
||||||
|
int signedPreKeyId = -1;
|
||||||
|
|
||||||
|
if (device.getSignedPreKey() != null) {
|
||||||
|
signedPreKey = device.getSignedPreKey().getPublicKey();
|
||||||
|
signedPreKeyId = device.getSignedPreKey().getKeyId();
|
||||||
|
signedPreKeySignature = device.getSignedPreKey().getSignature();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.getPreKey() != null) {
|
||||||
|
preKeyId = device.getPreKey().getKeyId();
|
||||||
|
preKey = device.getPreKey().getPublicKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
bundles.add(new PreKeyBundle(device.getRegistrationId(), device.getDeviceId(), preKeyId,
|
||||||
|
preKey, signedPreKeyId, signedPreKey, signedPreKeySignature,
|
||||||
|
response.getIdentityKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return bundles;
|
||||||
|
} catch (JsonUtil.JsonParseException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
} catch (NotFoundException nfe) {
|
||||||
|
throw new UnregisteredUserException(destination.getNumber(), nfe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreKeyBundle getPreKey(TextSecureAddress destination, int deviceId) throws IOException {
|
||||||
|
try {
|
||||||
|
String path = String.format(PREKEY_DEVICE_PATH, destination.getNumber(),
|
||||||
|
String.valueOf(deviceId));
|
||||||
|
|
||||||
|
if (!Util.isEmpty(destination.getRelay())) {
|
||||||
|
path = path + "?relay=" + destination.getRelay();
|
||||||
|
}
|
||||||
|
|
||||||
|
String responseText = makeRequest(path, "GET", null);
|
||||||
|
PreKeyResponse response = JsonUtil.fromJson(responseText, PreKeyResponse.class);
|
||||||
|
|
||||||
|
if (response.getDevices() == null || response.getDevices().size() < 1)
|
||||||
|
throw new IOException("Empty prekey list");
|
||||||
|
|
||||||
|
PreKeyResponseItem device = response.getDevices().get(0);
|
||||||
|
ECPublicKey preKey = null;
|
||||||
|
ECPublicKey signedPreKey = null;
|
||||||
|
byte[] signedPreKeySignature = null;
|
||||||
|
int preKeyId = -1;
|
||||||
|
int signedPreKeyId = -1;
|
||||||
|
|
||||||
|
if (device.getPreKey() != null) {
|
||||||
|
preKeyId = device.getPreKey().getKeyId();
|
||||||
|
preKey = device.getPreKey().getPublicKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.getSignedPreKey() != null) {
|
||||||
|
signedPreKeyId = device.getSignedPreKey().getKeyId();
|
||||||
|
signedPreKey = device.getSignedPreKey().getPublicKey();
|
||||||
|
signedPreKeySignature = device.getSignedPreKey().getSignature();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PreKeyBundle(device.getRegistrationId(), device.getDeviceId(), preKeyId, preKey,
|
||||||
|
signedPreKeyId, signedPreKey, signedPreKeySignature, response.getIdentityKey());
|
||||||
|
} catch (JsonUtil.JsonParseException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
} catch (NotFoundException nfe) {
|
||||||
|
throw new UnregisteredUserException(destination.getNumber(), nfe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignedPreKeyEntity getCurrentSignedPreKey() throws IOException {
|
||||||
|
try {
|
||||||
|
String responseText = makeRequest(SIGNED_PREKEY_PATH, "GET", null);
|
||||||
|
return JsonUtil.fromJson(responseText, SignedPreKeyEntity.class);
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
Log.w("PushServiceSocket", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentSignedPreKey(SignedPreKeyRecord signedPreKey) throws IOException {
|
||||||
|
SignedPreKeyEntity signedPreKeyEntity = new SignedPreKeyEntity(signedPreKey.getId(),
|
||||||
|
signedPreKey.getKeyPair().getPublicKey(),
|
||||||
|
signedPreKey.getSignature());
|
||||||
|
makeRequest(SIGNED_PREKEY_PATH, "PUT", JsonUtil.toJson(signedPreKeyEntity));
|
||||||
|
}
|
||||||
|
|
||||||
|
public long sendAttachment(PushAttachmentData attachment) throws IOException {
|
||||||
|
String response = makeRequest(String.format(ATTACHMENT_PATH, ""), "GET", null);
|
||||||
|
AttachmentDescriptor attachmentKey = JsonUtil.fromJson(response, AttachmentDescriptor.class);
|
||||||
|
|
||||||
|
if (attachmentKey == null || attachmentKey.getLocation() == null) {
|
||||||
|
throw new IOException("Server failed to allocate an attachment key!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.w("PushServiceSocket", "Got attachment content location: " + attachmentKey.getLocation());
|
||||||
|
|
||||||
|
uploadAttachment("PUT", attachmentKey.getLocation(), attachment.getData(),
|
||||||
|
attachment.getDataSize(), attachment.getKey());
|
||||||
|
|
||||||
|
return attachmentKey.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void retrieveAttachment(String relay, long attachmentId, File destination) throws IOException {
|
||||||
|
String path = String.format(ATTACHMENT_PATH, String.valueOf(attachmentId));
|
||||||
|
|
||||||
|
if (!Util.isEmpty(relay)) {
|
||||||
|
path = path + "?relay=" + relay;
|
||||||
|
}
|
||||||
|
|
||||||
|
String response = makeRequest(path, "GET", null);
|
||||||
|
AttachmentDescriptor descriptor = JsonUtil.fromJson(response, AttachmentDescriptor.class);
|
||||||
|
|
||||||
|
Log.w("PushServiceSocket", "Attachment: " + attachmentId + " is at: " + descriptor.getLocation());
|
||||||
|
|
||||||
|
downloadExternalFile(descriptor.getLocation(), destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ContactTokenDetails> retrieveDirectory(Set<String> contactTokens)
|
||||||
|
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||||
|
{
|
||||||
|
ContactTokenList contactTokenList = new ContactTokenList(new LinkedList<>(contactTokens));
|
||||||
|
String response = makeRequest(DIRECTORY_TOKENS_PATH, "PUT", JsonUtil.toJson(contactTokenList));
|
||||||
|
ContactTokenDetailsList activeTokens = JsonUtil.fromJson(response, ContactTokenDetailsList.class);
|
||||||
|
|
||||||
|
return activeTokens.getContacts();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContactTokenDetails getContactTokenDetails(String contactToken) throws IOException {
|
||||||
|
try {
|
||||||
|
String response = makeRequest(String.format(DIRECTORY_VERIFY_PATH, contactToken), "GET", null);
|
||||||
|
return JsonUtil.fromJson(response, ContactTokenDetails.class);
|
||||||
|
} catch (NotFoundException nfe) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void downloadExternalFile(String url, File localDestination)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
URL downloadUrl = new URL(url);
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) downloadUrl.openConnection();
|
||||||
|
connection.setRequestProperty("Content-Type", "application/octet-stream");
|
||||||
|
connection.setRequestMethod("GET");
|
||||||
|
connection.setDoInput(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (connection.getResponseCode() != 200) {
|
||||||
|
throw new NonSuccessfulResponseCodeException("Bad response: " + connection.getResponseCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
OutputStream output = new FileOutputStream(localDestination);
|
||||||
|
InputStream input = connection.getInputStream();
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int read;
|
||||||
|
|
||||||
|
while ((read = input.read(buffer)) != -1) {
|
||||||
|
output.write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
|
||||||
|
output.close();
|
||||||
|
Log.w("PushServiceSocket", "Downloaded: " + url + " to: " + localDestination.getAbsolutePath());
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new PushNetworkException(ioe);
|
||||||
|
} finally {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void uploadAttachment(String method, String url, InputStream data, long dataSize, byte[] key)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
URL uploadUrl = new URL(url);
|
||||||
|
HttpsURLConnection connection = (HttpsURLConnection) uploadUrl.openConnection();
|
||||||
|
connection.setDoOutput(true);
|
||||||
|
|
||||||
|
if (dataSize > 0) {
|
||||||
|
connection.setFixedLengthStreamingMode((int) AttachmentCipherOutputStream.getCiphertextLength(dataSize));
|
||||||
|
} else {
|
||||||
|
connection.setChunkedStreamingMode(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.setRequestMethod(method);
|
||||||
|
connection.setRequestProperty("Content-Type", "application/octet-stream");
|
||||||
|
connection.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
OutputStream stream = connection.getOutputStream();
|
||||||
|
AttachmentCipherOutputStream out = new AttachmentCipherOutputStream(key, stream);
|
||||||
|
|
||||||
|
Util.copy(data, out);
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
if (connection.getResponseCode() != 200) {
|
||||||
|
throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String makeRequest(String urlFragment, String method, String body)
|
||||||
|
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||||
|
{
|
||||||
|
HttpURLConnection connection = makeBaseRequest(urlFragment, method, body);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String response = Util.readFully(connection.getInputStream());
|
||||||
|
connection.disconnect();
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new PushNetworkException(ioe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpURLConnection makeBaseRequest(String urlFragment, String method, String body)
|
||||||
|
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||||
|
{
|
||||||
|
HttpURLConnection connection = getConnection(urlFragment, method, body);
|
||||||
|
int responseCode;
|
||||||
|
String responseMessage;
|
||||||
|
String response;
|
||||||
|
|
||||||
|
try {
|
||||||
|
responseCode = connection.getResponseCode();
|
||||||
|
responseMessage = connection.getResponseMessage();
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new PushNetworkException(ioe);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (responseCode) {
|
||||||
|
case 413:
|
||||||
|
connection.disconnect();
|
||||||
|
throw new RateLimitException("Rate limit exceeded: " + responseCode);
|
||||||
|
case 401:
|
||||||
|
case 403:
|
||||||
|
connection.disconnect();
|
||||||
|
throw new AuthorizationFailedException("Authorization failed!");
|
||||||
|
case 404:
|
||||||
|
connection.disconnect();
|
||||||
|
throw new NotFoundException("Not found");
|
||||||
|
case 409:
|
||||||
|
try {
|
||||||
|
response = Util.readFully(connection.getErrorStream());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new PushNetworkException(e);
|
||||||
|
}
|
||||||
|
throw new MismatchedDevicesException(JsonUtil.fromJson(response, MismatchedDevices.class));
|
||||||
|
case 410:
|
||||||
|
try {
|
||||||
|
response = Util.readFully(connection.getErrorStream());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new PushNetworkException(e);
|
||||||
|
}
|
||||||
|
throw new StaleDevicesException(JsonUtil.fromJson(response, StaleDevices.class));
|
||||||
|
case 417:
|
||||||
|
throw new ExpectationFailedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseCode != 200 && responseCode != 204) {
|
||||||
|
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " +
|
||||||
|
responseMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpURLConnection getConnection(String urlFragment, String method, String body)
|
||||||
|
throws PushNetworkException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
SSLContext context = SSLContext.getInstance("TLS");
|
||||||
|
context.init(null, trustManagers, null);
|
||||||
|
|
||||||
|
URL url = new URL(String.format("%s%s", serviceUrl, urlFragment));
|
||||||
|
Log.w("PushServiceSocket", "Push service URL: " + serviceUrl);
|
||||||
|
Log.w("PushServiceSocket", "Opening URL: " + url);
|
||||||
|
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
|
|
||||||
|
if (ENFORCE_SSL) {
|
||||||
|
((HttpsURLConnection) connection).setSSLSocketFactory(context.getSocketFactory());
|
||||||
|
((HttpsURLConnection) connection).setHostnameVerifier(new StrictHostnameVerifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.setRequestMethod(method);
|
||||||
|
connection.setRequestProperty("Content-Type", "application/json");
|
||||||
|
|
||||||
|
if (credentialsProvider.getPassword() != null) {
|
||||||
|
connection.setRequestProperty("Authorization", getAuthorizationHeader());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body != null) {
|
||||||
|
connection.setDoOutput(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.connect();
|
||||||
|
|
||||||
|
if (body != null) {
|
||||||
|
Log.w("PushServiceSocket", method + " -- " + body);
|
||||||
|
OutputStream out = connection.getOutputStream();
|
||||||
|
out.write(body.getBytes());
|
||||||
|
out.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return connection;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new PushNetworkException(e);
|
||||||
|
} catch (NoSuchAlgorithmException | KeyManagementException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getAuthorizationHeader() {
|
||||||
|
try {
|
||||||
|
return "Basic " + Base64.encodeBytes((credentialsProvider.getUser() + ":" + credentialsProvider.getPassword()).getBytes("UTF-8"));
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class GcmRegistrationId {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String gcmRegistrationId;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private boolean webSocketChannel;
|
||||||
|
|
||||||
|
public GcmRegistrationId() {}
|
||||||
|
|
||||||
|
public GcmRegistrationId(String gcmRegistrationId, boolean webSocketChannel) {
|
||||||
|
this.gcmRegistrationId = gcmRegistrationId;
|
||||||
|
this.webSocketChannel = webSocketChannel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class AttachmentDescriptor {
|
||||||
|
@JsonProperty
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String location;
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLocation() {
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2013 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.push;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
public class PushTransportDetails {
|
||||||
|
|
||||||
|
private final int messageVersion;
|
||||||
|
|
||||||
|
public PushTransportDetails(int messageVersion) {
|
||||||
|
this.messageVersion = messageVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getStrippedPaddingMessageBody(byte[] messageWithPadding) {
|
||||||
|
if (messageVersion < 2) throw new AssertionError("Unknown version: " + messageVersion);
|
||||||
|
else if (messageVersion == 2) return messageWithPadding;
|
||||||
|
|
||||||
|
int paddingStart = 0;
|
||||||
|
|
||||||
|
for (int i=messageWithPadding.length-1;i>=0;i--) {
|
||||||
|
if (messageWithPadding[i] == (byte)0x80) {
|
||||||
|
paddingStart = i;
|
||||||
|
break;
|
||||||
|
} else if (messageWithPadding[i] != (byte)0x00) {
|
||||||
|
Log.w("PushTransportDetails", "Padding byte is malformed, returning unstripped padding.");
|
||||||
|
return messageWithPadding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] strippedMessage = new byte[paddingStart];
|
||||||
|
System.arraycopy(messageWithPadding, 0, strippedMessage, 0, strippedMessage.length);
|
||||||
|
|
||||||
|
return strippedMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPaddedMessageBody(byte[] messageBody) {
|
||||||
|
if (messageVersion < 2) throw new AssertionError("Unknown version: " + messageVersion);
|
||||||
|
else if (messageVersion == 2) return messageBody;
|
||||||
|
|
||||||
|
// NOTE: This is dumb. We have our own padding scheme, but so does the cipher.
|
||||||
|
// The +1 -1 here is to make sure the Cipher has room to add one padding byte,
|
||||||
|
// otherwise it'll add a full 16 extra bytes.
|
||||||
|
byte[] paddedMessage = new byte[getPaddedMessageLength(messageBody.length + 1) - 1];
|
||||||
|
System.arraycopy(messageBody, 0, paddedMessage, 0, messageBody.length);
|
||||||
|
paddedMessage[messageBody.length] = (byte)0x80;
|
||||||
|
|
||||||
|
return paddedMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getPaddedMessageLength(int messageLength) {
|
||||||
|
int messageLengthWithTerminator = messageLength + 1;
|
||||||
|
int messagePartCount = messageLengthWithTerminator / 160;
|
||||||
|
|
||||||
|
if (messageLengthWithTerminator % 160 != 0) {
|
||||||
|
messagePartCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return messagePartCount * 160;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package org.whispersystems.textsecure.internal.push;
|
||||||
|
|
||||||
|
public class SendMessageResponse {
|
||||||
|
|
||||||
|
private boolean needsSync;
|
||||||
|
|
||||||
|
public SendMessageResponse() {}
|
||||||
|
|
||||||
|
public SendMessageResponse(boolean needsSync) {
|
||||||
|
this.needsSync = needsSync;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getNeedsSync() {
|
||||||
|
return needsSync;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.push;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class StaleDevices {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<Integer> staleDevices;
|
||||||
|
|
||||||
|
public List<Integer> getStaleDevices() {
|
||||||
|
return staleDevices;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.push.exceptions;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecure.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||||
|
import org.whispersystems.textsecure.internal.push.MismatchedDevices;
|
||||||
|
|
||||||
|
public class MismatchedDevicesException extends NonSuccessfulResponseCodeException {
|
||||||
|
|
||||||
|
private final MismatchedDevices mismatchedDevices;
|
||||||
|
|
||||||
|
public MismatchedDevicesException(MismatchedDevices mismatchedDevices) {
|
||||||
|
this.mismatchedDevices = mismatchedDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MismatchedDevices getMismatchedDevices() {
|
||||||
|
return mismatchedDevices;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.push.exceptions;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecure.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||||
|
import org.whispersystems.textsecure.internal.push.StaleDevices;
|
||||||
|
|
||||||
|
public class StaleDevicesException extends NonSuccessfulResponseCodeException {
|
||||||
|
|
||||||
|
private final StaleDevices staleDevices;
|
||||||
|
|
||||||
|
public StaleDevicesException(StaleDevices staleDevices) {
|
||||||
|
this.staleDevices = staleDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StaleDevices getStaleDevices() {
|
||||||
|
return staleDevices;
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.util;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecure.api.push.TrustStore;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.KeyStoreException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trust manager that defers to a system X509 trust manager, and
|
||||||
|
* additionally rejects certificates if they have a blacklisted
|
||||||
|
* serial.
|
||||||
|
*
|
||||||
|
* @author Moxie Marlinspike
|
||||||
|
*/
|
||||||
|
public class BlacklistingTrustManager implements X509TrustManager {
|
||||||
|
|
||||||
|
private static final List<BigInteger> BLACKLIST = new LinkedList<BigInteger>() {{
|
||||||
|
add(new BigInteger("4098"));
|
||||||
|
}};
|
||||||
|
|
||||||
|
public static TrustManager[] createFor(TrustManager[] trustManagers) {
|
||||||
|
for (TrustManager trustManager : trustManagers) {
|
||||||
|
if (trustManager instanceof X509TrustManager) {
|
||||||
|
TrustManager[] results = new BlacklistingTrustManager[1];
|
||||||
|
results[0] = new BlacklistingTrustManager((X509TrustManager)trustManager);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AssertionError("No X509 Trust Managers!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TrustManager[] createFor(TrustStore trustStore) {
|
||||||
|
try {
|
||||||
|
InputStream keyStoreInputStream = trustStore.getKeyStoreInputStream();
|
||||||
|
KeyStore keyStore = KeyStore.getInstance("BKS");
|
||||||
|
|
||||||
|
keyStore.load(keyStoreInputStream, trustStore.getKeyStorePassword().toCharArray());
|
||||||
|
|
||||||
|
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509");
|
||||||
|
trustManagerFactory.init(keyStore);
|
||||||
|
|
||||||
|
return BlacklistingTrustManager.createFor(trustManagerFactory.getTrustManagers());
|
||||||
|
} catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final X509TrustManager trustManager;
|
||||||
|
|
||||||
|
public BlacklistingTrustManager(X509TrustManager trustManager) {
|
||||||
|
this.trustManager = trustManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkClientTrusted(X509Certificate[] chain, String authType)
|
||||||
|
throws CertificateException
|
||||||
|
{
|
||||||
|
trustManager.checkClientTrusted(chain, authType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkServerTrusted(X509Certificate[] chain, String authType)
|
||||||
|
throws CertificateException
|
||||||
|
{
|
||||||
|
trustManager.checkServerTrusted(chain, authType);
|
||||||
|
|
||||||
|
for (X509Certificate certificate : chain) {
|
||||||
|
for (BigInteger blacklistedSerial : BLACKLIST) {
|
||||||
|
if (certificate.getSerialNumber().equals(blacklistedSerial)) {
|
||||||
|
throw new CertificateException("Blacklisted Serial: " + certificate.getSerialNumber());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public X509Certificate[] getAcceptedIssuers() {
|
||||||
|
return trustManager.getAcceptedIssuers();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2011 Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for generating hex dumps.
|
||||||
|
*/
|
||||||
|
public class Hex {
|
||||||
|
|
||||||
|
private final static int HEX_DIGITS_START = 10;
|
||||||
|
private final static int ASCII_TEXT_START = HEX_DIGITS_START + (16*2 + (16/2));
|
||||||
|
|
||||||
|
final static String EOL = System.getProperty("line.separator");
|
||||||
|
|
||||||
|
private final static char[] HEX_DIGITS = {
|
||||||
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
|
||||||
|
};
|
||||||
|
|
||||||
|
public static String toString(byte[] bytes) {
|
||||||
|
return toString(bytes, 0, bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toString(byte[] bytes, int offset, int length) {
|
||||||
|
StringBuffer buf = new StringBuffer();
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
appendHexChar(buf, bytes[offset + i]);
|
||||||
|
buf.append(' ');
|
||||||
|
}
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toStringCondensed(byte[] bytes) {
|
||||||
|
StringBuffer buf = new StringBuffer();
|
||||||
|
for (int i=0;i<bytes.length;i++) {
|
||||||
|
appendHexChar(buf, bytes[i]);
|
||||||
|
}
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] fromStringCondensed(String encoded) throws IOException {
|
||||||
|
final char[] data = encoded.toCharArray();
|
||||||
|
final int len = data.length;
|
||||||
|
|
||||||
|
if ((len & 0x01) != 0) {
|
||||||
|
throw new IOException("Odd number of characters.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] out = new byte[len >> 1];
|
||||||
|
|
||||||
|
// two characters form the hex value.
|
||||||
|
for (int i = 0, j = 0; j < len; i++) {
|
||||||
|
int f = Character.digit(data[j], 16) << 4;
|
||||||
|
j++;
|
||||||
|
f = f | Character.digit(data[j], 16);
|
||||||
|
j++;
|
||||||
|
out[i] = (byte) (f & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String dump(byte[] bytes) {
|
||||||
|
return dump(bytes, 0, bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String dump(byte[] bytes, int offset, int length) {
|
||||||
|
StringBuffer buf = new StringBuffer();
|
||||||
|
int lines = ((length - 1) / 16) + 1;
|
||||||
|
int lineOffset;
|
||||||
|
int lineLength;
|
||||||
|
|
||||||
|
for (int i = 0; i < lines; i++) {
|
||||||
|
lineOffset = (i * 16) + offset;
|
||||||
|
lineLength = Math.min(16, (length - (i * 16)));
|
||||||
|
appendDumpLine(buf, i, bytes, lineOffset, lineLength);
|
||||||
|
buf.append(EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendDumpLine(StringBuffer buf, int line, byte[] bytes, int lineOffset, int lineLength) {
|
||||||
|
buf.append(HEX_DIGITS[(line >> 28) & 0xf]);
|
||||||
|
buf.append(HEX_DIGITS[(line >> 24) & 0xf]);
|
||||||
|
buf.append(HEX_DIGITS[(line >> 20) & 0xf]);
|
||||||
|
buf.append(HEX_DIGITS[(line >> 16) & 0xf]);
|
||||||
|
buf.append(HEX_DIGITS[(line >> 12) & 0xf]);
|
||||||
|
buf.append(HEX_DIGITS[(line >> 8) & 0xf]);
|
||||||
|
buf.append(HEX_DIGITS[(line >> 4) & 0xf]);
|
||||||
|
buf.append(HEX_DIGITS[(line ) & 0xf]);
|
||||||
|
buf.append(": ");
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
int idx = i + lineOffset;
|
||||||
|
if (i < lineLength) {
|
||||||
|
int b = bytes[idx];
|
||||||
|
appendHexChar(buf, b);
|
||||||
|
} else {
|
||||||
|
buf.append(" ");
|
||||||
|
}
|
||||||
|
if ((i % 2) == 1) {
|
||||||
|
buf.append(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 16 && i < lineLength; i++) {
|
||||||
|
int idx = i + lineOffset;
|
||||||
|
int b = bytes[idx];
|
||||||
|
if (b >= 0x20 && b <= 0x7e) {
|
||||||
|
buf.append((char)b);
|
||||||
|
} else {
|
||||||
|
buf.append('.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendHexChar(StringBuffer buf, int b) {
|
||||||
|
buf.append(HEX_DIGITS[(b >> 4) & 0xf]);
|
||||||
|
buf.append(HEX_DIGITS[b & 0xf]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
package org.whispersystems.textsecure.internal.util;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||||
|
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
|
||||||
|
import org.whispersystems.libaxolotl.IdentityKey;
|
||||||
|
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class JsonUtil {
|
||||||
|
|
||||||
|
private static final String TAG = JsonUtil.class.getSimpleName();
|
||||||
|
|
||||||
|
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
static {
|
||||||
|
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toJson(Object object) {
|
||||||
|
try {
|
||||||
|
return objectMapper.writeValueAsString(object);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T fromJson(String json, Class<T> clazz) {
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(json, clazz);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
throw new JsonParseException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class JsonParseException extends RuntimeException {
|
||||||
|
public JsonParseException(Exception e) {
|
||||||
|
super(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class IdentityKeySerializer extends JsonSerializer<IdentityKey> {
|
||||||
|
@Override
|
||||||
|
public void serialize(IdentityKey value, JsonGenerator gen, SerializerProvider serializers)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
gen.writeString(Base64.encodeBytesWithoutPadding(value.serialize()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class IdentityKeyDeserializer extends JsonDeserializer<IdentityKey> {
|
||||||
|
@Override
|
||||||
|
public IdentityKey deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||||
|
try {
|
||||||
|
return new IdentityKey(Base64.decodeWithoutPadding(p.getValueAsString()), 0);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package org.whispersystems.textsecure.internal.util;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecure.api.util.CredentialsProvider;
|
||||||
|
|
||||||
|
public class StaticCredentialsProvider implements CredentialsProvider {
|
||||||
|
|
||||||
|
private final String user;
|
||||||
|
private final String password;
|
||||||
|
private final String signalingKey;
|
||||||
|
|
||||||
|
public StaticCredentialsProvider(String user, String password, String signalingKey) {
|
||||||
|
this.user = user;
|
||||||
|
this.password = password;
|
||||||
|
this.signalingKey = signalingKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSignalingKey() {
|
||||||
|
return signalingKey;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* 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.whispersystems.textsecure.internal.util;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
public class Util {
|
||||||
|
|
||||||
|
public static byte[] join(byte[]... input) {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
for (byte[] part : input) {
|
||||||
|
baos.write(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
return baos.toByteArray();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[][] split(byte[] input, int firstLength, int secondLength) {
|
||||||
|
byte[][] parts = new byte[2][];
|
||||||
|
|
||||||
|
parts[0] = new byte[firstLength];
|
||||||
|
System.arraycopy(input, 0, parts[0], 0, firstLength);
|
||||||
|
|
||||||
|
parts[1] = new byte[secondLength];
|
||||||
|
System.arraycopy(input, firstLength, parts[1], 0, secondLength);
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] trim(byte[] input, int length) {
|
||||||
|
byte[] result = new byte[length];
|
||||||
|
System.arraycopy(input, 0, result, 0, result.length);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isEmpty(String value) {
|
||||||
|
return value == null || value.trim().length() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] getSecretBytes(int size) {
|
||||||
|
try {
|
||||||
|
byte[] secret = new byte[size];
|
||||||
|
SecureRandom.getInstance("SHA1PRNG").nextBytes(secret);
|
||||||
|
return secret;
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String readFully(InputStream in) throws IOException {
|
||||||
|
ByteArrayOutputStream bout = new ByteArrayOutputStream();
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int read;
|
||||||
|
|
||||||
|
while ((read = in.read(buffer)) != -1) {
|
||||||
|
bout.write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
|
||||||
|
in.close();
|
||||||
|
|
||||||
|
return new String(bout.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void readFully(InputStream in, byte[] buffer) throws IOException {
|
||||||
|
int offset = 0;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
int read = in.read(buffer, offset, buffer.length - offset);
|
||||||
|
|
||||||
|
if (read + offset < buffer.length) offset += read;
|
||||||
|
else return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void copy(InputStream in, OutputStream out) throws IOException {
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int read;
|
||||||
|
|
||||||
|
while ((read = in.read(buffer)) != -1) {
|
||||||
|
out.write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
|
||||||
|
in.close();
|
||||||
|
out.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void sleep(long millis) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(millis);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void wait(Object lock, long millis) {
|
||||||
|
try {
|
||||||
|
lock.wait(millis);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,142 @@
|
|||||||
|
package org.whispersystems.textsecure.internal.websocket;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.squareup.okhttp.OkHttpClient;
|
||||||
|
import com.squareup.okhttp.Request;
|
||||||
|
import com.squareup.okhttp.Response;
|
||||||
|
import com.squareup.okhttp.internal.ws.WebSocket;
|
||||||
|
import com.squareup.okhttp.internal.ws.WebSocketListener;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecure.api.push.TrustStore;
|
||||||
|
import org.whispersystems.textsecure.api.util.CredentialsProvider;
|
||||||
|
import org.whispersystems.textsecure.internal.util.BlacklistingTrustManager;
|
||||||
|
import org.whispersystems.textsecure.internal.util.Util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
|
||||||
|
import okio.Buffer;
|
||||||
|
import okio.BufferedSource;
|
||||||
|
|
||||||
|
public class OkHttpClientWrapper implements WebSocketListener {
|
||||||
|
|
||||||
|
private static final String TAG = OkHttpClientWrapper.class.getSimpleName();
|
||||||
|
|
||||||
|
private final String uri;
|
||||||
|
private final TrustStore trustStore;
|
||||||
|
private final CredentialsProvider credentialsProvider;
|
||||||
|
private final WebSocketEventListener listener;
|
||||||
|
|
||||||
|
private WebSocket webSocket;
|
||||||
|
private boolean closed;
|
||||||
|
private boolean connected;
|
||||||
|
|
||||||
|
public OkHttpClientWrapper(String uri, TrustStore trustStore,
|
||||||
|
CredentialsProvider credentialsProvider,
|
||||||
|
WebSocketEventListener listener)
|
||||||
|
{
|
||||||
|
Log.w(TAG, "Connecting to: " + uri);
|
||||||
|
|
||||||
|
this.uri = uri;
|
||||||
|
this.trustStore = trustStore;
|
||||||
|
this.credentialsProvider = credentialsProvider;
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void connect() {
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
int attempt = 0;
|
||||||
|
|
||||||
|
while ((webSocket = newSocket()) != null) {
|
||||||
|
try {
|
||||||
|
Response response = webSocket.connect(OkHttpClientWrapper.this);
|
||||||
|
|
||||||
|
if (response.code() == 101) {
|
||||||
|
synchronized (OkHttpClientWrapper.this) {
|
||||||
|
if (closed) webSocket.close(1000, "OK");
|
||||||
|
else connected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.onConnected();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.w(TAG, "WebSocket Response: " + response.code());
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Util.sleep(Math.min(++attempt * 200, TimeUnit.SECONDS.toMillis(15)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void disconnect() {
|
||||||
|
Log.w(TAG, "Calling disconnect()...");
|
||||||
|
try {
|
||||||
|
closed = true;
|
||||||
|
if (webSocket != null && connected) {
|
||||||
|
webSocket.close(1000, "OK");
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMessage(byte[] message) throws IOException {
|
||||||
|
webSocket.sendMessage(WebSocket.PayloadType.BINARY, new Buffer().write(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(BufferedSource payload, WebSocket.PayloadType type) throws IOException {
|
||||||
|
Log.w(TAG, "onMessage: " + type);
|
||||||
|
if (type.equals(WebSocket.PayloadType.BINARY)) {
|
||||||
|
listener.onMessage(payload.readByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClose(int code, String reason) {
|
||||||
|
Log.w(TAG, String.format("onClose(%d, %s)", code, reason));
|
||||||
|
listener.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(IOException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
listener.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized WebSocket newSocket() {
|
||||||
|
if (closed) return null;
|
||||||
|
|
||||||
|
String filledUri = String.format(uri, credentialsProvider.getUser(), credentialsProvider.getPassword());
|
||||||
|
SSLSocketFactory socketFactory = createTlsSocketFactory(trustStore);
|
||||||
|
|
||||||
|
return WebSocket.newWebSocket(new OkHttpClient().setSslSocketFactory(socketFactory),
|
||||||
|
new Request.Builder().url(filledUri).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SSLSocketFactory createTlsSocketFactory(TrustStore trustStore) {
|
||||||
|
try {
|
||||||
|
SSLContext context = SSLContext.getInstance("TLS");
|
||||||
|
context.init(null, BlacklistingTrustManager.createFor(trustStore), null);
|
||||||
|
|
||||||
|
return context.getSocketFactory();
|
||||||
|
} catch (NoSuchAlgorithmException | KeyManagementException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,174 @@
|
|||||||
|
package org.whispersystems.textsecure.internal.websocket;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecure.api.push.TrustStore;
|
||||||
|
import org.whispersystems.textsecure.api.util.CredentialsProvider;
|
||||||
|
import org.whispersystems.textsecure.internal.util.Util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import static org.whispersystems.textsecure.internal.websocket.WebSocketProtos.WebSocketMessage;
|
||||||
|
import static org.whispersystems.textsecure.internal.websocket.WebSocketProtos.WebSocketRequestMessage;
|
||||||
|
import static org.whispersystems.textsecure.internal.websocket.WebSocketProtos.WebSocketResponseMessage;
|
||||||
|
|
||||||
|
public class WebSocketConnection implements WebSocketEventListener {
|
||||||
|
|
||||||
|
private static final String TAG = WebSocketConnection.class.getSimpleName();
|
||||||
|
|
||||||
|
private final LinkedList<WebSocketRequestMessage> incomingRequests = new LinkedList<>();
|
||||||
|
|
||||||
|
private final String wsUri;
|
||||||
|
private final TrustStore trustStore;
|
||||||
|
private final CredentialsProvider credentialsProvider;
|
||||||
|
|
||||||
|
private OkHttpClientWrapper client;
|
||||||
|
private KeepAliveSender keepAliveSender;
|
||||||
|
|
||||||
|
public WebSocketConnection(String httpUri, TrustStore trustStore, CredentialsProvider credentialsProvider) {
|
||||||
|
this.trustStore = trustStore;
|
||||||
|
this.credentialsProvider = credentialsProvider;
|
||||||
|
this.wsUri = httpUri.replace("https://", "wss://")
|
||||||
|
.replace("http://", "ws://") + "/v1/websocket/?login=%s&password=%s";
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void connect() {
|
||||||
|
Log.w(TAG, "WSC connect()...");
|
||||||
|
|
||||||
|
if (client == null) {
|
||||||
|
client = new OkHttpClientWrapper(wsUri, trustStore, credentialsProvider, this);
|
||||||
|
client.connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void disconnect() {
|
||||||
|
Log.w(TAG, "WSC disconnect()...");
|
||||||
|
|
||||||
|
if (client != null) {
|
||||||
|
client.disconnect();
|
||||||
|
client = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keepAliveSender != null) {
|
||||||
|
keepAliveSender.shutdown();
|
||||||
|
keepAliveSender = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized WebSocketRequestMessage readRequest(long timeoutMillis)
|
||||||
|
throws TimeoutException, IOException
|
||||||
|
{
|
||||||
|
if (client == null) {
|
||||||
|
throw new IOException("Connection closed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
while (client != null && incomingRequests.isEmpty() && elapsedTime(startTime) < timeoutMillis) {
|
||||||
|
Util.wait(this, Math.max(1, timeoutMillis - elapsedTime(startTime)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (incomingRequests.isEmpty() && client == null) throw new IOException("Connection closed!");
|
||||||
|
else if (incomingRequests.isEmpty()) throw new TimeoutException("Timeout exceeded");
|
||||||
|
else return incomingRequests.removeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void sendResponse(WebSocketResponseMessage response) throws IOException {
|
||||||
|
if (client == null) {
|
||||||
|
throw new IOException("Connection closed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketMessage message = WebSocketMessage.newBuilder()
|
||||||
|
.setType(WebSocketMessage.Type.RESPONSE)
|
||||||
|
.setResponse(response)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
client.sendMessage(message.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void sendKeepAlive() throws IOException {
|
||||||
|
if (keepAliveSender != null) {
|
||||||
|
client.sendMessage(WebSocketMessage.newBuilder()
|
||||||
|
.setType(WebSocketMessage.Type.REQUEST)
|
||||||
|
.setRequest(WebSocketRequestMessage.newBuilder()
|
||||||
|
.setId(System.currentTimeMillis())
|
||||||
|
.setPath("/v1/keepalive")
|
||||||
|
.setVerb("GET")
|
||||||
|
.build()).build()
|
||||||
|
.toByteArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void onMessage(byte[] payload) {
|
||||||
|
Log.w(TAG, "WSC onMessage()");
|
||||||
|
try {
|
||||||
|
WebSocketMessage message = WebSocketMessage.parseFrom(payload);
|
||||||
|
|
||||||
|
Log.w(TAG, "Message Type: " + message.getType().getNumber());
|
||||||
|
|
||||||
|
if (message.getType().getNumber() == WebSocketMessage.Type.REQUEST_VALUE) {
|
||||||
|
incomingRequests.add(message.getRequest());
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyAll();
|
||||||
|
} catch (InvalidProtocolBufferException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void onClose() {
|
||||||
|
Log.w(TAG, "onClose()...");
|
||||||
|
|
||||||
|
if (client != null) {
|
||||||
|
client.disconnect();
|
||||||
|
client = null;
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keepAliveSender != null) {
|
||||||
|
keepAliveSender.shutdown();
|
||||||
|
keepAliveSender = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void onConnected() {
|
||||||
|
if (client != null && keepAliveSender == null) {
|
||||||
|
keepAliveSender = new KeepAliveSender();
|
||||||
|
keepAliveSender.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long elapsedTime(long startTime) {
|
||||||
|
return System.currentTimeMillis() - startTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class KeepAliveSender extends Thread {
|
||||||
|
|
||||||
|
private AtomicBoolean stop = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
while (!stop.get()) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(TimeUnit.SECONDS.toMillis(15));
|
||||||
|
|
||||||
|
Log.w(TAG, "Sending keep alive...");
|
||||||
|
sendKeepAlive();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
stop.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package org.whispersystems.textsecure.internal.websocket;
|
||||||
|
|
||||||
|
public interface WebSocketEventListener {
|
||||||
|
|
||||||
|
public void onMessage(byte[] payload);
|
||||||
|
public void onClose();
|
||||||
|
public void onConnected();
|
||||||
|
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user