Compare commits

...

43 Commits

Author SHA1 Message Date
Alan Evans
fde96d2200 Bump version to 2.8.1 2019-11-01 18:24:51 -04:00
Alan Evans
ca627780b8 Replace jcenter with maven. 2019-11-01 18:24:41 -04:00
Alan Evans
a3cff019f3 Bump version to 2.8.0 2019-11-01 16:54:17 -04:00
Alan Evans
6c907e9163 Update to protobuf 3.10 lite. 2019-11-01 16:53:36 -04:00
Alan Evans
74ebb401b1 Version 2 tests. 2019-11-01 16:42:14 -04:00
Greyson Parrelli
3662b6d705 Update fingerprint generation interface.
With the introduction of UUIDs, we need to be able to generate
fingerprints from raw bytes, not just strings. In addition, we need to
be able to conditionally set the fingerprint version based on whether or
not we're using UUIDs.
2019-11-01 16:31:20 -04:00
Sebastian
f1a44edd6b Fix some Javadoc typos. 2019-11-01 13:11:32 -04:00
Alan Evans
d400dd8154
Readme Syntax highlighting 2019-11-01 13:09:04 -04:00
Xashyar
dd067d7514 Copyright Year Update. 2019-11-01 13:07:16 -04:00
HansM
46ff14ed3c Updated readme to use java code tag. 2019-11-01 13:06:56 -04:00
Alan Evans
f371478e17
Update gradle. 2019-10-31 16:56:53 -04:00
Moxie Marlinspike
9518b127ce Bump version to 2.7.1 2019-05-02 13:15:09 -07:00
Moxie Marlinspike
3c1a8ee4b9 Bump curve25519 version to 0.5.0 2019-04-04 09:28:02 -07:00
Moxie Marlinspike
ea0ffa41ec Bump version to 2.7.0 2018-09-21 13:54:58 -07:00
Moxie Marlinspike
179ecc71b3 Add some checks and public method 2018-09-21 13:54:29 -07:00
Moxie Marlinspike
4f5e1ff299 Bump version to 2.6.2
// FREEBIE
2017-07-12 16:58:02 -07:00
Moxie Marlinspike
c920fefbb0 Sanity check length when deserializing
// FREEBIE
2017-07-12 16:48:58 -07:00
Moxie Marlinspike
4692ac10ab Bump version to 2.6.1
// FREEBIE
2017-06-23 09:21:07 -07:00
Moxie Marlinspike
62de00febb Update curve25519 to 0.4.1
// FREEBIE
2017-06-23 09:20:51 -07:00
Moxie Marlinspike
8408469776 Sort device consistency messages on vrf rather than signature
// FREEBIE
2017-06-23 09:11:47 -07:00
Moxie Marlinspike
c6b61ae607 Bump version to 2.6.0
// FREEBIE
2017-06-22 09:24:49 -07:00
Moxie Marlinspike
fabf2bee29 Update curve25519-java to 0.4.0
// FREEBIE
2017-06-22 09:24:28 -07:00
Moxie Marlinspike
df1c957acb Bump version to 2.5.3
// FREEBIE
2017-06-02 09:01:40 -07:00
Moxie Marlinspike
370406c6f4 Add return value to save interface
// FREEBIE
2017-06-02 09:01:07 -07:00
Moxie Marlinspike
bc56f07aaa Bump version to 2.5.2
// FREEBIE
2017-05-30 15:28:23 -07:00
Moxie Marlinspike
7d94f31a99 We no longer need to remove previous session states on identity
// FREEBIE
2017-05-30 11:27:25 -07:00
Moxie Marlinspike
aa7d05f9b5 Bump version to 2.5.1
// FREEBIE
2017-05-24 17:52:46 -07:00
Moxie Marlinspike
1932e1a1ed Remote vestigial v2 cases
// FREEBIE
2017-05-24 17:50:43 -07:00
Moxie Marlinspike
6935c70081 Add identity trust checks to encrypt/decrypt
Now that we support interfaces for directional trust, this will
allow for more complicated trust decisions that might not only
happen at the moment of session creation.

// FREEBIE
2017-05-24 17:09:17 -07:00
Moxie Marlinspike
899e11bf88 Bump version to 2.5.0
// FREEBIE
2017-05-19 11:30:45 -07:00
Moxie Marlinspike
111df84ac0 Remove vestigial "last resort" prekey stuff that's no longer used
// FREEBIE
2017-05-19 11:30:09 -07:00
Moxie Marlinspike
51db4578ae Update IdentityKeyStore interface to support directional trust
1) Allow IdentityKeyStore to make trust decisions based on
   direction of received identity key (incoming vs outgoing)

2) Have IdentityKeyStore reflect whether a save operation reflects
   a replacement, and build in previous session cleanup to the
   library rather than leaving it as an application concern.

// FREEBIE
2017-05-19 11:23:00 -07:00
Moxie Marlinspike
e787f540cf Update scannable fingerprint test for new regime
// FREEBIE
2017-05-19 11:22:09 -07:00
Moxie Marlinspike
5c784ead7f Bump version to 2.4.0
// FREEBIE
2016-11-13 09:16:09 -08:00
Moxie Marlinspike
dbbdb1f5d8 Remove identifiers from ScannableFingerprint
Although it helps to eliminate confusion when people inadvertantly
scan the wrong code, people might share the QR code image publicly
and inadvertantly publish their identifier.

// FREEBIE
2016-11-13 09:15:55 -08:00
Moxie Marlinspike
1b2be846be Allow fingerprint generation for collections of identity keys
// FREEBIE
2016-10-18 14:42:51 -07:00
Moxie Marlinspike
98e88b749a Bump version to 2.3.0
// FREEBIE
2016-10-18 13:47:13 -07:00
Moxie Marlinspike
d84e141714 Add distinguishing commitment prefix and version
// FREEBIE
2016-10-18 13:37:49 -07:00
Moxie Marlinspike
9e66504e59 Update curve25519-java
// FREEBIE
2016-10-18 12:41:31 -07:00
Moxie Marlinspike
a71a4bc15a Update gradle
// FREEBIE
2016-10-17 15:39:21 -07:00
Moxie Marlinspike
93f7989dae Clarify comment
// FREEBIE
2016-10-17 15:28:16 -07:00
Moxie Marlinspike
87fae0f983 Add explicit vector for numericfingerprintgeneratortest
// FREEBIE
2016-08-23 15:24:31 -07:00
Moxie Marlinspike
8a00482fbd Support for device consistency messages
// FREEBIE
2016-08-20 09:59:47 -07:00
43 changed files with 997 additions and 16961 deletions

View File

@ -43,7 +43,7 @@ State is kept in the following places:
On Android:
```
```groovy
dependencies {
compile 'org.whispersystems:signal-protocol-android:(latest version number)'
}
@ -51,7 +51,7 @@ dependencies {
For pure Java apps:
```
```xml
<dependency>
<groupId>org.whispersystems</groupId>
<artifactId>signal-protocol-java</artifactId>
@ -64,17 +64,19 @@ For pure Java apps:
At install time, a libsignal client needs to generate its identity keys, registration id, and
prekeys.
IdentityKeyPair identityKeyPair = KeyHelper.generateIdentityKeyPair();
int registrationId = KeyHelper.generateRegistrationId();
List<PreKeyRecord> preKeys = KeyHelper.generatePreKeys(startId, 100);
SignedPreKeyRecord signedPreKey = KeyHelper.generateSignedPreKey(identityKeyPair, 5);
```java
IdentityKeyPair identityKeyPair = KeyHelper.generateIdentityKeyPair();
int registrationId = KeyHelper.generateRegistrationId();
List<PreKeyRecord> preKeys = KeyHelper.generatePreKeys(startId, 100);
SignedPreKeyRecord signedPreKey = KeyHelper.generateSignedPreKey(identityKeyPair, 5);
// Store identityKeyPair somewhere durable and safe.
// Store registrationId somewhere durable and safe.
// Store preKeys in PreKeyStore.
// Store signed prekey in SignedPreKeyStore.
// Store identityKeyPair somewhere durable and safe.
// Store registrationId somewhere durable and safe.
// Store preKeys in PreKeyStore.
// Store signed prekey in SignedPreKeyStore.
```
## Building a session
A libsignal client needs to implement four interfaces: IdentityKeyStore, PreKeyStore,
@ -83,23 +85,25 @@ prekeys, signed prekeys, and session state.
Once those are implemented, building a session is fairly straightforward:
SessionStore sessionStore = new MySessionStore();
PreKeyStore preKeyStore = new MyPreKeyStore();
SignedPreKeyStore signedPreKeyStore = new MySignedPreKeyStore();
IdentityKeyStore identityStore = new MyIdentityKeyStore();
```java
SessionStore sessionStore = new MySessionStore();
PreKeyStore preKeyStore = new MyPreKeyStore();
SignedPreKeyStore signedPreKeyStore = new MySignedPreKeyStore();
IdentityKeyStore identityStore = new MyIdentityKeyStore();
// Instantiate a SessionBuilder for a remote recipientId + deviceId tuple.
SessionBuilder sessionBuilder = new SessionBuilder(sessionStore, preKeyStore, signedPreKeyStore,
identityStore, recipientId, deviceId);
// Instantiate a SessionBuilder for a remote recipientId + deviceId tuple.
SessionBuilder sessionBuilder = new SessionBuilder(sessionStore, preKeyStore, signedPreKeyStore,
identityStore, recipientId, deviceId);
// Build a session with a PreKey retrieved from the server.
sessionBuilder.process(retrievedPreKey);
// Build a session with a PreKey retrieved from the server.
sessionBuilder.process(retrievedPreKey);
SessionCipher sessionCipher = new SessionCipher(sessionStore, recipientId, deviceId);
CiphertextMessage message = sessionCipher.encrypt("Hello world!".getBytes("UTF-8"));
deliver(message.serialize());
SessionCipher sessionCipher = new SessionCipher(sessionStore, recipientId, deviceId);
CiphertextMessage message = sessionCipher.encrypt("Hello world!".getBytes("UTF-8"));
deliver(message.serialize());
```
# Legal things
## Cryptography Notice
@ -112,7 +116,7 @@ The form and manner of this distribution makes it eligible for export under the
## License
Copyright 2013-2016 Open Whisper Systems
Copyright 2013-2019 Open Whisper Systems
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html

View File

@ -1,10 +1,16 @@
buildscript {
repositories {
google()
mavenCentral()
jcenter {
content {
includeVersion 'org.jetbrains.trove4j', 'trove4j', '20160824'
}
}
}
dependencies {
classpath 'com.android.tools.build:gradle:1.0.0'
classpath 'com.android.tools.build:gradle:3.5.1'
}
}
@ -17,29 +23,35 @@ version = version_number
group = group_info
android {
compileSdkVersion 21
buildToolsVersion "21.1.2"
compileSdkVersion 28
buildToolsVersion '28.0.3'
defaultConfig {
minSdkVersion 19
targetSdkVersion 28
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
sourceSets {
androidTest {
java.srcDirs = ['src/androidTest/java', project(':tests').file('src/test/java')]
}
}
libraryVariants.all { variant ->
variant.outputs.each { output ->
def outputFile = output.outputFile
if (outputFile != null && outputFile.name.endsWith('.aar')) {
def fileName = "${archivesBaseName}-${version}.aar"
output.outputFile = new File(outputFile.parent, fileName)
}
}
}
}
repositories {
google()
mavenCentral()
mavenLocal()
jcenter {
content {
includeVersion 'org.jetbrains.trove4j', 'trove4j', '20160824'
}
}
}
dependencies {
@ -49,8 +61,25 @@ dependencies {
}
}
def isReleaseBuild() {
return version.contains("SNAPSHOT") == false
}
def getReleaseRepositoryUrl() {
return hasProperty('sonatypeRepo') ? sonatypeRepo
: "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
}
def getRepositoryUsername() {
return hasProperty('whisperSonatypeUsername') ? whisperSonatypeUsername : ""
}
def getRepositoryPassword() {
return hasProperty('whisperSonatypePassword') ? whisperSonatypePassword : ""
}
signing {
required { has("release") && gradle.taskGraph.hasTask("uploadArchives") }
required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
sign configurations.archives
}
@ -59,8 +88,8 @@ uploadArchives {
repositories.mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
repository(url: sonatypeRepo) {
authentication(userName: whisperSonatypeUsername, password: whisperSonatypePassword)
repository(url: getReleaseRepositoryUrl()) {
authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
}
pom.project {

View File

@ -1,7 +1,7 @@
subprojects {
ext.version_number = "2.2.0"
ext.version_number = "2.8.1"
ext.group_info = "org.whispersystems"
ext.curve25519_version = "0.2.4"
ext.curve25519_version = "0.5.0"
if (JavaVersion.current().isJava8Compatible()) {
allprojects {

Binary file not shown.

View File

@ -1,6 +1,7 @@
#Tue Dec 09 10:15:39 PST 2014
# Note: Check https://gradle.org/release-checksums/ before updating wrapper or distribution
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=027fdd265d277bae65a0d349b6b8da02135b0b8e14ba891e26281fa877fe37a2
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip

130
gradlew vendored
View File

@ -1,4 +1,20 @@
#!/usr/bin/env bash
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
@ -6,47 +22,6 @@
##
##############################################################################
# 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"
@ -61,9 +36,49 @@ while [ -h "$PRG" ] ; do
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# 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
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@ -90,7 +105,7 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@ -110,10 +125,11 @@ 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
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
@ -154,11 +170,19 @@ if $cygwin ; then
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=("$@")
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
APP_ARGS=$(save "$@")
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

30
gradlew.bat vendored
View File

@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@ -8,14 +24,14 @@
@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 Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
@ -46,10 +62,9 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
@rem Get command-line arguments, handling Windows 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.
@ -60,11 +75,6 @@ set _SKIP=2
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

View File

@ -1,4 +1,16 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
}
}
apply plugin: 'java'
apply plugin: 'com.google.protobuf'
apply plugin: 'maven'
apply plugin: 'signing'
@ -22,11 +34,30 @@ sourceSets {
dependencies {
compile "org.whispersystems:curve25519-java:${curve25519_version}"
compile 'com.google.protobuf:protobuf-java:2.5.0'
compile 'com.google.protobuf:protobuf-javalite:3.10.0'
testCompile ('junit:junit:3.8.2')
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.10.0'
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option "lite"
}
}
}
}
}
sourceSets {
main.proto.srcDir '../protobuf'
}
test {
testLogging {
@ -37,8 +68,25 @@ test {
include 'org/whispersystems/**'
}
def isReleaseBuild() {
return version.contains("SNAPSHOT") == false
}
def getReleaseRepositoryUrl() {
return hasProperty('sonatypeRepo') ? sonatypeRepo
: "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
}
def getRepositoryUsername() {
return hasProperty('whisperSonatypeUsername') ? whisperSonatypeUsername : ""
}
def getRepositoryPassword() {
return hasProperty('whisperSonatypePassword') ? whisperSonatypePassword : ""
}
signing {
required { has("release") && gradle.taskGraph.hasTask("uploadArchives") }
required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
sign configurations.archives
}
@ -47,8 +95,8 @@ uploadArchives {
repositories.mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
repository(url: sonatypeRepo) {
authentication(userName: whisperSonatypeUsername, password: whisperSonatypePassword)
repository(url: getReleaseRepositoryUrl()) {
authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
}
pom.project {

View File

@ -34,7 +34,6 @@ import org.whispersystems.libsignal.util.guava.Optional;
* <ol>
* <li>A {@link org.whispersystems.libsignal.state.PreKeyBundle} retrieved from a server.</li>
* <li>A {@link PreKeySignalMessage} received from a client.</li>
* <li>A {@link org.whispersystems.libsignal.protocol.KeyExchangeMessage} sent to or received from a client.</li>
* </ol>
*
* Sessions are constructed per recipientId + deviceId tuple. Remote logical users are identified
@ -101,13 +100,14 @@ public class SessionBuilder {
{
IdentityKey theirIdentityKey = message.getIdentityKey();
if (!identityKeyStore.isTrustedIdentity(remoteAddress, theirIdentityKey)) {
if (!identityKeyStore.isTrustedIdentity(remoteAddress, theirIdentityKey, IdentityKeyStore.Direction.RECEIVING)) {
throw new UntrustedIdentityException(remoteAddress.getName(), theirIdentityKey);
}
Optional<Integer> unsignedPreKeyId = processV3(sessionRecord, message);
identityKeyStore.saveIdentity(remoteAddress, theirIdentityKey);
return unsignedPreKeyId;
}
@ -144,7 +144,7 @@ public class SessionBuilder {
sessionRecord.getSessionState().setRemoteRegistrationId(message.getRegistrationId());
sessionRecord.getSessionState().setAliceBaseKey(message.getBaseKey().serialize());
if (message.getPreKeyId().isPresent() && message.getPreKeyId().get() != Medium.MAX_VALUE) {
if (message.getPreKeyId().isPresent()) {
return message.getPreKeyId();
} else {
return Optional.absent();
@ -164,7 +164,7 @@ public class SessionBuilder {
*/
public void process(PreKeyBundle preKey) throws InvalidKeyException, UntrustedIdentityException {
synchronized (SessionCipher.SESSION_LOCK) {
if (!identityKeyStore.isTrustedIdentity(remoteAddress, preKey.getIdentityKey())) {
if (!identityKeyStore.isTrustedIdentity(remoteAddress, preKey.getIdentityKey(), IdentityKeyStore.Direction.SENDING)) {
throw new UntrustedIdentityException(remoteAddress.getName(), preKey.getIdentityKey());
}
@ -205,8 +205,8 @@ public class SessionBuilder {
sessionRecord.getSessionState().setRemoteRegistrationId(preKey.getRegistrationId());
sessionRecord.getSessionState().setAliceBaseKey(ourBaseKey.getPublicKey().serialize());
sessionStore.storeSession(remoteAddress, sessionRecord);
identityKeyStore.saveIdentity(remoteAddress, preKey.getIdentityKey());
sessionStore.storeSession(remoteAddress, sessionRecord);
}
}

View File

@ -54,6 +54,7 @@ public class SessionCipher {
public static final Object SESSION_LOCK = new Object();
private final SessionStore sessionStore;
private final IdentityKeyStore identityKeyStore;
private final SessionBuilder sessionBuilder;
private final PreKeyStore preKeyStore;
private final SignalProtocolAddress remoteAddress;
@ -70,11 +71,12 @@ public class SessionCipher {
SignedPreKeyStore signedPreKeyStore, IdentityKeyStore identityKeyStore,
SignalProtocolAddress remoteAddress)
{
this.sessionStore = sessionStore;
this.preKeyStore = preKeyStore;
this.remoteAddress = remoteAddress;
this.sessionBuilder = new SessionBuilder(sessionStore, preKeyStore, signedPreKeyStore,
identityKeyStore, remoteAddress);
this.sessionStore = sessionStore;
this.preKeyStore = preKeyStore;
this.identityKeyStore = identityKeyStore;
this.remoteAddress = remoteAddress;
this.sessionBuilder = new SessionBuilder(sessionStore, preKeyStore, signedPreKeyStore,
identityKeyStore, remoteAddress);
}
public SessionCipher(SignalProtocolStore store, SignalProtocolAddress remoteAddress) {
@ -87,7 +89,7 @@ public class SessionCipher {
* @param paddedMessage The plaintext message bytes, optionally padded to a constant multiple.
* @return A ciphertext message encrypted to the recipient+device tuple.
*/
public CiphertextMessage encrypt(byte[] paddedMessage) {
public CiphertextMessage encrypt(byte[] paddedMessage) throws UntrustedIdentityException {
synchronized (SESSION_LOCK) {
SessionRecord sessionRecord = sessionStore.loadSession(remoteAddress);
SessionState sessionState = sessionRecord.getSessionState();
@ -97,7 +99,7 @@ public class SessionCipher {
int previousCounter = sessionState.getPreviousCounter();
int sessionVersion = sessionState.getSessionVersion();
byte[] ciphertextBody = getCiphertext(sessionVersion, messageKeys, paddedMessage);
byte[] ciphertextBody = getCiphertext(messageKeys, paddedMessage);
CiphertextMessage ciphertextMessage = new SignalMessage(sessionVersion, messageKeys.getMacKey(),
senderEphemeral, chainKey.getIndex(),
previousCounter, ciphertextBody,
@ -115,6 +117,12 @@ public class SessionCipher {
}
sessionState.setSenderChainKey(chainKey.getNextChainKey());
if (!identityKeyStore.isTrustedIdentity(remoteAddress, sessionState.getRemoteIdentityKey(), IdentityKeyStore.Direction.SENDING)) {
throw new UntrustedIdentityException(remoteAddress.getName(), sessionState.getRemoteIdentityKey());
}
identityKeyStore.saveIdentity(remoteAddress, sessionState.getRemoteIdentityKey());
sessionStore.storeSession(remoteAddress, sessionRecord);
return ciphertextMessage;
}
@ -198,7 +206,7 @@ public class SessionCipher {
*/
public byte[] decrypt(SignalMessage ciphertext)
throws InvalidMessageException, DuplicateMessageException, LegacyMessageException,
NoSessionException
NoSessionException, UntrustedIdentityException
{
return decrypt(ciphertext, new NullDecryptionCallback());
}
@ -223,7 +231,7 @@ public class SessionCipher {
*/
public byte[] decrypt(SignalMessage ciphertext, DecryptionCallback callback)
throws InvalidMessageException, DuplicateMessageException, LegacyMessageException,
NoSessionException
NoSessionException, UntrustedIdentityException
{
synchronized (SESSION_LOCK) {
@ -234,6 +242,12 @@ public class SessionCipher {
SessionRecord sessionRecord = sessionStore.loadSession(remoteAddress);
byte[] plaintext = decrypt(sessionRecord, ciphertext);
if (!identityKeyStore.isTrustedIdentity(remoteAddress, sessionRecord.getSessionState().getRemoteIdentityKey(), IdentityKeyStore.Direction.RECEIVING)) {
throw new UntrustedIdentityException(remoteAddress.getName(), sessionRecord.getSessionState().getRemoteIdentityKey());
}
identityKeyStore.saveIdentity(remoteAddress, sessionRecord.getSessionState().getRemoteIdentityKey());
callback.handlePlaintext(plaintext);
sessionStore.storeSession(remoteAddress, sessionRecord);
@ -290,19 +304,17 @@ public class SessionCipher {
sessionState.getSessionVersion()));
}
int messageVersion = ciphertextMessage.getMessageVersion();
ECPublicKey theirEphemeral = ciphertextMessage.getSenderRatchetKey();
int counter = ciphertextMessage.getCounter();
ChainKey chainKey = getOrCreateChainKey(sessionState, theirEphemeral);
MessageKeys messageKeys = getOrCreateMessageKeys(sessionState, theirEphemeral,
chainKey, counter);
ciphertextMessage.verifyMac(messageVersion,
sessionState.getRemoteIdentityKey(),
ciphertextMessage.verifyMac(sessionState.getRemoteIdentityKey(),
sessionState.getLocalIdentityKey(),
messageKeys.getMacKey());
byte[] plaintext = getPlaintext(messageVersion, messageKeys, ciphertextMessage.getBody());
byte[] plaintext = getPlaintext(messageKeys, ciphertextMessage.getBody());
sessionState.clearUnacknowledgedPreKeyMessage();
@ -380,58 +392,26 @@ public class SessionCipher {
return chainKey.getMessageKeys();
}
private byte[] getCiphertext(int version, MessageKeys messageKeys, byte[] plaintext) {
private byte[] getCiphertext(MessageKeys messageKeys, byte[] plaintext) {
try {
Cipher cipher;
if (version >= 3) {
cipher = getCipher(Cipher.ENCRYPT_MODE, messageKeys.getCipherKey(), messageKeys.getIv());
} else {
cipher = getCipher(Cipher.ENCRYPT_MODE, messageKeys.getCipherKey(), messageKeys.getCounter());
}
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, messageKeys.getCipherKey(), messageKeys.getIv());
return cipher.doFinal(plaintext);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
private byte[] getPlaintext(int version, MessageKeys messageKeys, byte[] cipherText)
private byte[] getPlaintext(MessageKeys messageKeys, byte[] cipherText)
throws InvalidMessageException
{
try {
Cipher cipher;
if (version >= 3) {
cipher = getCipher(Cipher.DECRYPT_MODE, messageKeys.getCipherKey(), messageKeys.getIv());
} else {
cipher = getCipher(Cipher.DECRYPT_MODE, messageKeys.getCipherKey(), messageKeys.getCounter());
}
Cipher cipher = getCipher(Cipher.DECRYPT_MODE, messageKeys.getCipherKey(), messageKeys.getIv());
return cipher.doFinal(cipherText);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new InvalidMessageException(e);
}
}
private Cipher getCipher(int mode, SecretKeySpec key, int counter) {
try {
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
byte[] ivBytes = new byte[16];
ByteUtil.intToByteArray(ivBytes, 0, counter);
IvParameterSpec iv = new IvParameterSpec(ivBytes);
cipher.init(mode, key, iv);
return cipher;
} catch (NoSuchAlgorithmException | NoSuchPaddingException | java.security.InvalidKeyException |
InvalidAlgorithmParameterException e)
{
throw new AssertionError(e);
}
}
private Cipher getCipher(int mode, SecretKeySpec key, IvParameterSpec iv) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

View File

@ -0,0 +1,54 @@
package org.whispersystems.libsignal.devices;
import org.whispersystems.libsignal.util.ByteArrayComparator;
import org.whispersystems.libsignal.util.ByteUtil;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class DeviceConsistencyCodeGenerator {
private static final int CODE_VERSION = 0;
public static String generateFor(DeviceConsistencyCommitment commitment,
List<DeviceConsistencySignature> signatures)
{
try {
ArrayList<DeviceConsistencySignature> sortedSignatures = new ArrayList<>(signatures);
Collections.sort(sortedSignatures, new SignatureComparator());
MessageDigest messageDigest = MessageDigest.getInstance("SHA-512");
messageDigest.update(ByteUtil.shortToByteArray(CODE_VERSION));
messageDigest.update(commitment.toByteArray());
for (DeviceConsistencySignature signature : sortedSignatures) {
messageDigest.update(signature.getVrfOutput());
}
byte[] hash = messageDigest.digest();
String digits = getEncodedChunk(hash, 0) + getEncodedChunk(hash, 5);
return digits.substring(0, 6);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private static String getEncodedChunk(byte[] hash, int offset) {
long chunk = ByteUtil.byteArray5ToLong(hash, offset) % 100000;
return String.format("%05d", chunk);
}
private static class SignatureComparator extends ByteArrayComparator implements Comparator<DeviceConsistencySignature> {
@Override
public int compare(DeviceConsistencySignature first, DeviceConsistencySignature second) {
return compare(first.getVrfOutput(), second.getVrfOutput());
}
}
}

View File

@ -0,0 +1,49 @@
package org.whispersystems.libsignal.devices;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.util.ByteUtil;
import org.whispersystems.libsignal.util.IdentityKeyComparator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class DeviceConsistencyCommitment {
private static final String VERSION = "DeviceConsistencyCommitment_V0";
private final int generation;
private final byte[] serialized;
public DeviceConsistencyCommitment(int generation, List<IdentityKey> identityKeys) {
try {
ArrayList<IdentityKey> sortedIdentityKeys = new ArrayList<>(identityKeys);
Collections.sort(sortedIdentityKeys, new IdentityKeyComparator());
MessageDigest messageDigest = MessageDigest.getInstance("SHA-512");
messageDigest.update(VERSION.getBytes());
messageDigest.update(ByteUtil.intToByteArray(generation));
for (IdentityKey commitment : sortedIdentityKeys) {
messageDigest.update(commitment.getPublicKey().serialize());
}
this.generation = generation;
this.serialized = messageDigest.digest();
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
public byte[] toByteArray() {
return serialized;
}
public int getGeneration() {
return generation;
}
}

View File

@ -0,0 +1,21 @@
package org.whispersystems.libsignal.devices;
public class DeviceConsistencySignature {
private final byte[] signature;
private final byte[] vrfOutput;
public DeviceConsistencySignature(byte[] signature, byte[] vrfOutput) {
this.signature = signature;
this.vrfOutput = vrfOutput;
}
public byte[] getVrfOutput() {
return vrfOutput;
}
public byte[] getSignature() {
return signature;
}
}

View File

@ -7,7 +7,7 @@ package org.whispersystems.libsignal.ecc;
import org.whispersystems.curve25519.Curve25519;
import org.whispersystems.curve25519.Curve25519KeyPair;
import org.whispersystems.curve25519.NoSuchProviderException;
import org.whispersystems.curve25519.VrfSignatureVerificationFailedException;
import org.whispersystems.libsignal.InvalidKeyException;
import static org.whispersystems.curve25519.Curve25519.BEST;
@ -30,10 +30,18 @@ public class Curve {
public static ECPublicKey decodePoint(byte[] bytes, int offset)
throws InvalidKeyException
{
if (bytes == null || bytes.length - offset < 1) {
throw new InvalidKeyException("No key type identifier");
}
int type = bytes[offset] & 0xFF;
switch (type) {
case Curve.DJB_TYPE:
if (bytes.length - offset < 33) {
throw new InvalidKeyException("Bad key length: " + bytes.length);
}
byte[] keyBytes = new byte[32];
System.arraycopy(bytes, offset+1, keyBytes, 0, keyBytes.length);
return new DjbECPublicKey(keyBytes);
@ -49,6 +57,14 @@ public class Curve {
public static byte[] calculateAgreement(ECPublicKey publicKey, ECPrivateKey privateKey)
throws InvalidKeyException
{
if (publicKey == null) {
throw new InvalidKeyException("public value is null");
}
if (privateKey == null) {
throw new InvalidKeyException("private value is null");
}
if (publicKey.getType() != privateKey.getType()) {
throw new InvalidKeyException("Public and private keys must be of the same type!");
}
@ -65,6 +81,10 @@ public class Curve {
public static boolean verifySignature(ECPublicKey signingKey, byte[] message, byte[] signature)
throws InvalidKeyException
{
if (signingKey == null || message == null || signature == null) {
throw new InvalidKeyException("Values must not be null");
}
if (signingKey.getType() == DJB_TYPE) {
return Curve25519.getInstance(BEST)
.verifySignature(((DjbECPublicKey) signingKey).getPublicKey(), message, signature);
@ -76,6 +96,10 @@ public class Curve {
public static byte[] calculateSignature(ECPrivateKey signingKey, byte[] message)
throws InvalidKeyException
{
if (signingKey == null || message == null) {
throw new InvalidKeyException("Values must not be null");
}
if (signingKey.getType() == DJB_TYPE) {
return Curve25519.getInstance(BEST)
.calculateSignature(((DjbECPrivateKey) signingKey).getPrivateKey(), message);
@ -83,4 +107,35 @@ public class Curve {
throw new InvalidKeyException("Unknown type: " + signingKey.getType());
}
}
public static byte[] calculateVrfSignature(ECPrivateKey signingKey, byte[] message)
throws InvalidKeyException
{
if (signingKey == null || message == null) {
throw new InvalidKeyException("Values must not be null");
}
if (signingKey.getType() == DJB_TYPE) {
return Curve25519.getInstance(BEST)
.calculateVrfSignature(((DjbECPrivateKey)signingKey).getPrivateKey(), message);
} else {
throw new InvalidKeyException("Unknown type: " + signingKey.getType());
}
}
public static byte[] verifyVrfSignature(ECPublicKey signingKey, byte[] message, byte[] signature)
throws InvalidKeyException, VrfSignatureVerificationFailedException
{
if (signingKey == null || message == null || signature == null) {
throw new InvalidKeyException("Values must not be null");
}
if (signingKey.getType() == DJB_TYPE) {
return Curve25519.getInstance(BEST)
.verifyVrfSignature(((DjbECPublicKey) signingKey).getPublicKey(), message, signature);
} else {
throw new InvalidKeyException("Unknown type: " + signingKey.getType());
}
}
}

View File

@ -5,21 +5,39 @@
*/
package org.whispersystems.libsignal.fingerprint;
import org.whispersystems.libsignal.util.ByteUtil;
public class DisplayableFingerprint {
private final String localFingerprint;
private final String remoteFingerprint;
private final String localFingerprintNumbers;
private final String remoteFingerprintNumbers;
public DisplayableFingerprint(String localFingerprint, String remoteFingerprint) {
this.localFingerprint = localFingerprint;
this.remoteFingerprint = remoteFingerprint;
DisplayableFingerprint(byte[] localFingerprint, byte[] remoteFingerprint)
{
this.localFingerprintNumbers = getDisplayStringFor(localFingerprint);
this.remoteFingerprintNumbers = getDisplayStringFor(remoteFingerprint);
}
public String getDisplayText() {
if (localFingerprint.compareTo(remoteFingerprint) <= 0) {
return localFingerprint + remoteFingerprint;
if (localFingerprintNumbers.compareTo(remoteFingerprintNumbers) <= 0) {
return localFingerprintNumbers + remoteFingerprintNumbers;
} else {
return remoteFingerprint + localFingerprint;
return remoteFingerprintNumbers + localFingerprintNumbers;
}
}
private String getDisplayStringFor(byte[] fingerprint) {
return getEncodedChunk(fingerprint, 0) +
getEncodedChunk(fingerprint, 5) +
getEncodedChunk(fingerprint, 10) +
getEncodedChunk(fingerprint, 15) +
getEncodedChunk(fingerprint, 20) +
getEncodedChunk(fingerprint, 25);
}
private String getEncodedChunk(byte[] hash, int offset) {
long chunk = ByteUtil.byteArray5ToLong(hash, offset) % 100000;
return String.format("%05d", chunk);
}
}

View File

@ -25,7 +25,7 @@ public class Fingerprint {
}
/**
* @return A scannable fingerprint that can be scanned anc compared locally.
* @return A scannable fingerprint that can be scanned and compared locally.
*/
public ScannableFingerprint getScannableFingerprint() {
return scannableFingerprint;

View File

@ -7,7 +7,18 @@ package org.whispersystems.libsignal.fingerprint;
import org.whispersystems.libsignal.IdentityKey;
import java.util.List;
public interface FingerprintGenerator {
public Fingerprint createFor(String localStableIdentifier, IdentityKey localIdentityKey,
String remoteStableIdentifier, IdentityKey remoteIdentityKey);
public Fingerprint createFor(int version,
byte[] localStableIdentifier,
IdentityKey localIdentityKey,
byte[] remoteStableIdentifier,
IdentityKey remoteIdentityKey);
public Fingerprint createFor(int version,
byte[] localStableIdentifier,
List<IdentityKey> localIdentityKey,
byte[] remoteStableIdentifier,
List<IdentityKey> remoteIdentityKey);
}

View File

@ -7,11 +7,20 @@ package org.whispersystems.libsignal.fingerprint;
public class FingerprintVersionMismatchException extends Exception {
public FingerprintVersionMismatchException() {
private final int theirVersion;
private final int ourVersion;
public FingerprintVersionMismatchException(int theirVersion, int ourVersion) {
super();
this.theirVersion = theirVersion;
this.ourVersion = ourVersion;
}
public FingerprintVersionMismatchException(Exception e) {
super(e);
public int getTheirVersion() {
return theirVersion;
}
public int getOurVersion() {
return ourVersion;
}
}

View File

@ -7,15 +7,21 @@ package org.whispersystems.libsignal.fingerprint;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.util.ByteUtil;
import org.whispersystems.libsignal.util.IdentityKeyComparator;
import java.io.ByteArrayOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class NumericFingerprintGenerator implements FingerprintGenerator {
private static final int VERSION = 0;
private static final int FINGERPRINT_VERSION = 0;
private final long iterations;
private final int iterations;
/**
* Construct a fingerprint generator for 60 digit numerics.
@ -30,13 +36,14 @@ public class NumericFingerprintGenerator implements FingerprintGenerator {
* - 1400 > 110 bits
* - 5200 > 112 bits
*/
public NumericFingerprintGenerator(long iterations) {
public NumericFingerprintGenerator(int iterations) {
this.iterations = iterations;
}
/**
* Generate a scannable and displayble fingerprint.
* Generate a scannable and displayable fingerprint.
*
* @param version The version of fingerprint you are generating.
* @param localStableIdentifier The client's "stable" identifier.
* @param localIdentityKey The client's identity key.
* @param remoteStableIdentifier The remote party's "stable" identifier.
@ -44,45 +51,87 @@ public class NumericFingerprintGenerator implements FingerprintGenerator {
* @return A unique fingerprint for this conversation.
*/
@Override
public Fingerprint createFor(String localStableIdentifier, IdentityKey localIdentityKey,
String remoteStableIdentifier, IdentityKey remoteIdentityKey)
public Fingerprint createFor(int version,
byte[] localStableIdentifier,
final IdentityKey localIdentityKey,
byte[] remoteStableIdentifier,
final IdentityKey remoteIdentityKey)
{
DisplayableFingerprint displayableFingerprint = new DisplayableFingerprint(getDisplayStringFor(localStableIdentifier, localIdentityKey),
getDisplayStringFor(remoteStableIdentifier, remoteIdentityKey));
return createFor(version,
localStableIdentifier,
new LinkedList<IdentityKey>() {{
add(localIdentityKey);
}},
remoteStableIdentifier,
new LinkedList<IdentityKey>() {{
add(remoteIdentityKey);
}});
}
ScannableFingerprint scannableFingerprint = new ScannableFingerprint(VERSION,
localStableIdentifier, localIdentityKey,
remoteStableIdentifier, remoteIdentityKey);
/**
* Generate a scannable and displayable fingerprint for logical identities that have multiple
* physical keys.
*
* Do not trust the output of this unless you've been through the device consistency process
* for the provided localIdentityKeys.
*
* @param version The version of fingerprint you are generating.
* @param localStableIdentifier The client's "stable" identifier.
* @param localIdentityKeys The client's collection of physical identity keys.
* @param remoteStableIdentifier The remote party's "stable" identifier.
* @param remoteIdentityKeys The remote party's collection of physical identity key.
* @return A unique fingerprint for this conversation.
*/
public Fingerprint createFor(int version,
byte[] localStableIdentifier,
List<IdentityKey> localIdentityKeys,
byte[] remoteStableIdentifier,
List<IdentityKey> remoteIdentityKeys)
{
byte[] localFingerprint = getFingerprint(iterations, localStableIdentifier, localIdentityKeys);
byte[] remoteFingerprint = getFingerprint(iterations, remoteStableIdentifier, remoteIdentityKeys);
DisplayableFingerprint displayableFingerprint = new DisplayableFingerprint(localFingerprint,
remoteFingerprint);
ScannableFingerprint scannableFingerprint = new ScannableFingerprint(version,
localFingerprint,
remoteFingerprint);
return new Fingerprint(displayableFingerprint, scannableFingerprint);
}
private String getDisplayStringFor(String stableIdentifier, IdentityKey identityKey) {
private byte[] getFingerprint(int iterations, byte[] stableIdentifier, List<IdentityKey> unsortedIdentityKeys) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-512");
byte[] publicKey = identityKey.getPublicKey().serialize();
byte[] hash = ByteUtil.combine(ByteUtil.shortToByteArray(VERSION),
publicKey, stableIdentifier.getBytes());
byte[] publicKey = getLogicalKeyBytes(unsortedIdentityKeys);
byte[] hash = ByteUtil.combine(ByteUtil.shortToByteArray(FINGERPRINT_VERSION),
publicKey, stableIdentifier);
for (int i=0;i<iterations;i++) {
digest.update(hash);
hash = digest.digest(publicKey);
}
return getEncodedChunk(hash, 0) +
getEncodedChunk(hash, 5) +
getEncodedChunk(hash, 10) +
getEncodedChunk(hash, 15) +
getEncodedChunk(hash, 20) +
getEncodedChunk(hash, 25);
return hash;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private String getEncodedChunk(byte[] hash, int offset) {
long chunk = ByteUtil.byteArray5ToLong(hash, offset) % 100000;
return String.format("%05d", chunk);
private byte[] getLogicalKeyBytes(List<IdentityKey> identityKeys) {
ArrayList<IdentityKey> sortedIdentityKeys = new ArrayList<>(identityKeys);
Collections.sort(sortedIdentityKeys, new IdentityKeyComparator());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
for (IdentityKey identityKey : sortedIdentityKeys) {
byte[] publicKeyBytes = identityKey.getPublicKey().serialize();
baos.write(publicKeyBytes, 0, publicKeyBytes.length);
}
return baos.toByteArray();
}
}

View File

@ -8,71 +8,64 @@ package org.whispersystems.libsignal.fingerprint;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.fingerprint.FingerprintProtos.CombinedFingerprint;
import org.whispersystems.libsignal.fingerprint.FingerprintProtos.FingerprintData;
import org.whispersystems.libsignal.fingerprint.FingerprintProtos.CombinedFingerprints;
import org.whispersystems.libsignal.fingerprint.FingerprintProtos.LogicalFingerprint;
import org.whispersystems.libsignal.util.ByteUtil;
import java.security.MessageDigest;
public class ScannableFingerprint {
private final CombinedFingerprint combinedFingerprint;
private final int version;
private final CombinedFingerprints fingerprints;
public ScannableFingerprint(int version,
String localStableIdentifier, IdentityKey localIdentityKey,
String remoteStableIdentifier, IdentityKey remoteIdentityKey)
ScannableFingerprint(int version, byte[] localFingerprintData, byte[] remoteFingerprintData)
{
this.combinedFingerprint = CombinedFingerprint.newBuilder()
.setVersion(version)
.setLocalFingerprint(FingerprintData.newBuilder()
.setIdentifier(ByteString.copyFrom(localStableIdentifier.getBytes()))
.setPublicKey(ByteString.copyFrom(localIdentityKey.serialize())))
.setRemoteFingerprint(FingerprintData.newBuilder()
.setIdentifier(ByteString.copyFrom(remoteStableIdentifier.getBytes()))
.setPublicKey(ByteString.copyFrom(remoteIdentityKey.serialize())))
.build();
LogicalFingerprint localFingerprint = LogicalFingerprint.newBuilder()
.setContent(ByteString.copyFrom(ByteUtil.trim(localFingerprintData, 32)))
.build();
LogicalFingerprint remoteFingerprint = LogicalFingerprint.newBuilder()
.setContent(ByteString.copyFrom(ByteUtil.trim(remoteFingerprintData, 32)))
.build();
this.version = version;
this.fingerprints = CombinedFingerprints.newBuilder()
.setVersion(version)
.setLocalFingerprint(localFingerprint)
.setRemoteFingerprint(remoteFingerprint)
.build();
}
/**
* @return A byte string to be displayed in a QR code.
*/
public byte[] getSerialized() {
return combinedFingerprint.toByteArray();
return fingerprints.toByteArray();
}
/**
* Compare a scanned QR code with what we expect.
*
* @param scannedFingerprintData The scanned data
* @return True if matching, otehrwise false.
* @return True if matching, otherwise false.
* @throws FingerprintVersionMismatchException if the scanned fingerprint is the wrong version.
* @throws FingerprintIdentifierMismatchException if the scanned fingerprint is for the wrong stable identifier.
*/
public boolean compareTo(byte[] scannedFingerprintData)
throws FingerprintVersionMismatchException,
FingerprintIdentifierMismatchException,
FingerprintParsingException
{
try {
CombinedFingerprint scannedFingerprint = CombinedFingerprint.parseFrom(scannedFingerprintData);
CombinedFingerprints scanned = CombinedFingerprints.parseFrom(scannedFingerprintData);
if (!scannedFingerprint.hasRemoteFingerprint() || !scannedFingerprint.hasLocalFingerprint() ||
!scannedFingerprint.hasVersion() || scannedFingerprint.getVersion() != combinedFingerprint.getVersion())
if (!scanned.hasRemoteFingerprint() || !scanned.hasLocalFingerprint() ||
!scanned.hasVersion() || scanned.getVersion() != version)
{
throw new FingerprintVersionMismatchException();
throw new FingerprintVersionMismatchException(scanned.getVersion(), version);
}
if (!combinedFingerprint.getLocalFingerprint().getIdentifier().equals(scannedFingerprint.getRemoteFingerprint().getIdentifier()) ||
!combinedFingerprint.getRemoteFingerprint().getIdentifier().equals(scannedFingerprint.getLocalFingerprint().getIdentifier()))
{
throw new FingerprintIdentifierMismatchException(new String(combinedFingerprint.getLocalFingerprint().getIdentifier().toByteArray()),
new String(combinedFingerprint.getRemoteFingerprint().getIdentifier().toByteArray()),
new String(scannedFingerprint.getLocalFingerprint().getIdentifier().toByteArray()),
new String(scannedFingerprint.getRemoteFingerprint().getIdentifier().toByteArray()));
}
return MessageDigest.isEqual(combinedFingerprint.getLocalFingerprint().toByteArray(), scannedFingerprint.getRemoteFingerprint().toByteArray()) &&
MessageDigest.isEqual(combinedFingerprint.getRemoteFingerprint().toByteArray(), scannedFingerprint.getLocalFingerprint().toByteArray());
return MessageDigest.isEqual(fingerprints.getLocalFingerprint().getContent().toByteArray(), scanned.getRemoteFingerprint().getContent().toByteArray()) &&
MessageDigest.isEqual(fingerprints.getRemoteFingerprint().getContent().toByteArray(), scanned.getLocalFingerprint().getContent().toByteArray());
} catch (InvalidProtocolBufferException e) {
throw new FingerprintParsingException(e);
}

View File

@ -20,7 +20,7 @@ import static org.whispersystems.libsignal.state.StorageProtos.SenderKeyRecordSt
* A durable representation of a set of SenderKeyStates for a specific
* SenderKeyName.
*
* @author Moxie Marlisnpike
* @author Moxie Marlinspike
*/
public class SenderKeyRecord {

View File

@ -7,7 +7,6 @@ package org.whispersystems.libsignal.protocol;
public interface CiphertextMessage {
public static final int UNSUPPORTED_VERSION = 1;
public static final int CURRENT_VERSION = 3;
public static final int WHISPER_TYPE = 2;

View File

@ -0,0 +1,62 @@
package org.whispersystems.libsignal.protocol;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.whispersystems.curve25519.VrfSignatureVerificationFailedException;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.devices.DeviceConsistencyCommitment;
import org.whispersystems.libsignal.devices.DeviceConsistencySignature;
import org.whispersystems.libsignal.ecc.Curve;
public class DeviceConsistencyMessage {
private final DeviceConsistencySignature signature;
private final int generation;
private final byte[] serialized;
public DeviceConsistencyMessage(DeviceConsistencyCommitment commitment, IdentityKeyPair identityKeyPair) {
try {
byte[] signatureBytes = Curve.calculateVrfSignature(identityKeyPair.getPrivateKey(), commitment.toByteArray());
byte[] vrfOutputBytes = Curve.verifyVrfSignature(identityKeyPair.getPublicKey().getPublicKey(), commitment.toByteArray(), signatureBytes);
this.generation = commitment.getGeneration();
this.signature = new DeviceConsistencySignature(signatureBytes, vrfOutputBytes);
this.serialized = SignalProtos.DeviceConsistencyCodeMessage.newBuilder()
.setGeneration(commitment.getGeneration())
.setSignature(ByteString.copyFrom(signature.getSignature()))
.build()
.toByteArray();
} catch (InvalidKeyException | VrfSignatureVerificationFailedException e) {
throw new AssertionError(e);
}
}
public DeviceConsistencyMessage(DeviceConsistencyCommitment commitment, byte[] serialized, IdentityKey identityKey) throws InvalidMessageException {
try {
SignalProtos.DeviceConsistencyCodeMessage message = SignalProtos.DeviceConsistencyCodeMessage.parseFrom(serialized);
byte[] vrfOutputBytes = Curve.verifyVrfSignature(identityKey.getPublicKey(), commitment.toByteArray(), message.getSignature().toByteArray());
this.generation = message.getGeneration();
this.signature = new DeviceConsistencySignature(message.getSignature().toByteArray(), vrfOutputBytes);
this.serialized = serialized;
} catch (InvalidProtocolBufferException | InvalidKeyException | VrfSignatureVerificationFailedException e) {
throw new InvalidMessageException(e);
}
}
public byte[] getSerialized() {
return serialized;
}
public DeviceConsistencySignature getSignature() {
return signature;
}
public int getGeneration() {
return generation;
}
}

View File

@ -41,7 +41,7 @@ public class SignalMessage implements CiphertextMessage {
byte[] message = messageParts[1];
byte[] mac = messageParts[2];
if (ByteUtil.highBitsToInt(version) <= CiphertextMessage.UNSUPPORTED_VERSION) {
if (ByteUtil.highBitsToInt(version) < CURRENT_VERSION) {
throw new LegacyMessageException("Legacy message: " + ByteUtil.highBitsToInt(version));
}
@ -82,8 +82,7 @@ public class SignalMessage implements CiphertextMessage {
.setCiphertext(ByteString.copyFrom(ciphertext))
.build().toByteArray();
byte[] mac = getMac(messageVersion, senderIdentityKey, receiverIdentityKey, macKey,
ByteUtil.combine(version, message));
byte[] mac = getMac(senderIdentityKey, receiverIdentityKey, macKey, ByteUtil.combine(version, message));
this.serialized = ByteUtil.combine(version, message, mac);
this.senderRatchetKey = senderRatchetKey;
@ -109,12 +108,11 @@ public class SignalMessage implements CiphertextMessage {
return ciphertext;
}
public void verifyMac(int messageVersion, IdentityKey senderIdentityKey,
IdentityKey receiverIdentityKey, SecretKeySpec macKey)
public void verifyMac(IdentityKey senderIdentityKey, IdentityKey receiverIdentityKey, SecretKeySpec macKey)
throws InvalidMessageException
{
byte[][] parts = ByteUtil.split(serialized, serialized.length - MAC_LENGTH, MAC_LENGTH);
byte[] ourMac = getMac(messageVersion, senderIdentityKey, receiverIdentityKey, macKey, parts[0]);
byte[] ourMac = getMac(senderIdentityKey, receiverIdentityKey, macKey, parts[0]);
byte[] theirMac = parts[1];
if (!MessageDigest.isEqual(ourMac, theirMac)) {
@ -122,8 +120,7 @@ public class SignalMessage implements CiphertextMessage {
}
}
private byte[] getMac(int messageVersion,
IdentityKey senderIdentityKey,
private byte[] getMac(IdentityKey senderIdentityKey,
IdentityKey receiverIdentityKey,
SecretKeySpec macKey, byte[] serialized)
{
@ -131,10 +128,8 @@ public class SignalMessage implements CiphertextMessage {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(macKey);
if (messageVersion >= 3) {
mac.update(senderIdentityKey.getPublicKey().serialize());
mac.update(receiverIdentityKey.getPublicKey().serialize());
}
mac.update(senderIdentityKey.getPublicKey().serialize());
mac.update(receiverIdentityKey.getPublicKey().serialize());
byte[] fullMac = mac.doFinal(serialized);
return ByteUtil.trim(fullMac, MAC_LENGTH);
@ -155,7 +150,7 @@ public class SignalMessage implements CiphertextMessage {
public static boolean isLegacy(byte[] message) {
return message != null && message.length >= 1 &&
ByteUtil.highBitsToInt(message[0]) <= CiphertextMessage.UNSUPPORTED_VERSION;
ByteUtil.highBitsToInt(message[0]) != CiphertextMessage.CURRENT_VERSION;
}
}

View File

@ -16,6 +16,10 @@ import org.whispersystems.libsignal.SignalProtocolAddress;
*/
public interface IdentityKeyStore {
public enum Direction {
SENDING, RECEIVING
}
/**
* Get the local client's identity key pair.
*
@ -40,24 +44,39 @@ public interface IdentityKeyStore {
*
* @param address The address of the remote client.
* @param identityKey The remote client's identity key.
* @return True if the identity key replaces a previous identity, false if not
*/
public void saveIdentity(SignalProtocolAddress address, IdentityKey identityKey);
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey);
/**
* Verify a remote client's identity key.
* <p>
* Determine whether a remote client's identity is trusted. Convention is
* that the TextSecure protocol is 'trust on first use.' This means that
* that the Signal Protocol is 'trust on first use.' This means that
* an identity key is considered 'trusted' if there is no entry for the recipient
* in the local store, or if it matches the saved key for a recipient in the local
* store. Only if it mismatches an entry in the local store is it considered
* 'untrusted.'
*
* Clients may wish to make a distinction as to how keys are trusted based on the
* direction of travel. For instance, clients may wish to accept all 'incoming' identity
* key changes, while only blocking identity key changes when sending a message.
*
* @param address The address of the remote client.
* @param identityKey The identity key to verify.
* @param direction The direction (sending or receiving) this identity is being used for.
* @return true if trusted, false if untrusted.
*/
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey);
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction);
/**
* Return the saved public identity key for a remote client
*
* @param address The address of the remote client
* @return The public identity key, or null if absent
*/
public IdentityKey getIdentity(SignalProtocolAddress address);
}

View File

@ -74,6 +74,9 @@ public class SessionRecord {
return previousStates;
}
public void removePreviousSessionStates() {
previousStates.clear();
}
public boolean isFresh() {
return fresh;

View File

@ -36,13 +36,25 @@ public class InMemoryIdentityKeyStore implements IdentityKeyStore {
}
@Override
public void saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
trustedKeys.put(address, identityKey);
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
IdentityKey existing = trustedKeys.get(address);
if (!identityKey.equals(existing)) {
trustedKeys.put(address, identityKey);
return true;
} else {
return false;
}
}
@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
IdentityKey trusted = trustedKeys.get(address);
return (trusted == null || trusted.equals(identityKey));
}
@Override
public IdentityKey getIdentity(SignalProtocolAddress address) {
return trustedKeys.get(address);
}
}

View File

@ -39,13 +39,18 @@ public class InMemorySignalProtocolStore implements SignalProtocolStore {
}
@Override
public void saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
identityKeyStore.saveIdentity(address, identityKey);
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return identityKeyStore.saveIdentity(address, identityKey);
}
@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return identityKeyStore.isTrustedIdentity(address, identityKey);
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
return identityKeyStore.isTrustedIdentity(address, identityKey, direction);
}
@Override
public IdentityKey getIdentity(SignalProtocolAddress address) {
return identityKeyStore.getIdentity(address);
}
@Override

View File

@ -0,0 +1,18 @@
package org.whispersystems.libsignal.util;
public abstract class ByteArrayComparator {
protected int compare(byte[] left, byte[] right) {
for (int i = 0, j = 0; i < left.length && j < right.length; i++, j++) {
int a = (left[i] & 0xff);
int b = (right[j] & 0xff);
if (a != b) {
return a - b;
}
}
return left.length - right.length;
}
}

View File

@ -0,0 +1,13 @@
package org.whispersystems.libsignal.util;
import org.whispersystems.libsignal.IdentityKey;
import java.util.Comparator;
public class IdentityKeyComparator extends ByteArrayComparator implements Comparator<IdentityKey> {
@Override
public int compare(IdentityKey first, IdentityKey second) {
return compare(first.getPublicKey().serialize(), second.getPublicKey().serialize());
}
}

View File

@ -92,17 +92,6 @@ public class KeyHelper {
return results;
}
/**
* Generate the last resort PreKey. Clients should do this only once, at install
* time, and durably store it for the length of the install.
*
* @return the generated last resort PreKeyRecord.
*/
public static PreKeyRecord generateLastResortPreKey() {
ECKeyPair keyPair = Curve.generateKeyPair();
return new PreKeyRecord(Medium.MAX_VALUE, keyPair);
}
/**
* Generate a signed PreKey
*

View File

@ -1,15 +1,17 @@
syntax = "proto2";
package textsecure;
option java_package = "org.whispersystems.libsignal.fingerprint";
option java_package = "org.whispersystems.libsignal.fingerprint";
option java_outer_classname = "FingerprintProtos";
message FingerprintData {
optional bytes publicKey = 1;
optional bytes identifier = 2;
message LogicalFingerprint {
optional bytes content = 1;
// optional bytes identifier = 2;
}
message CombinedFingerprint {
optional uint32 version = 1;
optional FingerprintData localFingerprint = 2;
optional FingerprintData remoteFingerprint = 3;
message CombinedFingerprints {
optional uint32 version = 1;
optional LogicalFingerprint localFingerprint = 2;
optional LogicalFingerprint remoteFingerprint = 3;
}

View File

@ -1,6 +1,8 @@
syntax = "proto2";
package textsecure;
option java_package = "org.whispersystems.libsignal.state";
option java_package = "org.whispersystems.libsignal.state";
option java_outer_classname = "StorageProtos";
message SessionStructure {

View File

@ -1,3 +0,0 @@
all:
protoc --java_out=../java/src/main/java/ WhisperTextProtocol.proto LocalStorageProtocol.proto FingerprintProtocol.proto

View File

@ -1,6 +1,8 @@
syntax = "proto2";
package textsecure;
option java_package = "org.whispersystems.libsignal.protocol";
option java_package = "org.whispersystems.libsignal.protocol";
option java_outer_classname = "SignalProtos";
message SignalMessage {
@ -16,7 +18,7 @@ message PreKeySignalMessage {
optional uint32 signedPreKeyId = 6;
optional bytes baseKey = 2;
optional bytes identityKey = 3;
optional bytes message = 4; // WhisperMessage
optional bytes message = 4; // SignalMessage
}
message KeyExchangeMessage {
@ -38,4 +40,9 @@ message SenderKeyDistributionMessage {
optional uint32 iteration = 2;
optional bytes chainKey = 3;
optional bytes signingKey = 4;
}
message DeviceConsistencyCodeMessage {
optional uint32 generation = 1;
optional bytes signature = 2;
}

View File

@ -357,7 +357,7 @@ public class SessionBuilderTest extends TestCase {
private void runInteraction(SignalProtocolStore aliceStore, SignalProtocolStore bobStore)
throws DuplicateMessageException, LegacyMessageException, InvalidMessageException, NoSessionException
throws DuplicateMessageException, LegacyMessageException, InvalidMessageException, NoSessionException, UntrustedIdentityException
{
SessionCipher aliceSessionCipher = new SessionCipher(aliceStore, BOB_ADDRESS);
SessionCipher bobSessionCipher = new SessionCipher(bobStore, ALICE_ADDRESS);

View File

@ -28,7 +28,7 @@ public class SessionCipherTest extends TestCase {
public void testBasicSessionV3()
throws InvalidKeyException, DuplicateMessageException,
LegacyMessageException, InvalidMessageException, NoSuchAlgorithmException, NoSessionException
LegacyMessageException, InvalidMessageException, NoSuchAlgorithmException, NoSessionException, UntrustedIdentityException
{
SessionRecord aliceSessionRecord = new SessionRecord();
SessionRecord bobSessionRecord = new SessionRecord();
@ -70,7 +70,7 @@ public class SessionCipherTest extends TestCase {
}
private void runInteraction(SessionRecord aliceSessionRecord, SessionRecord bobSessionRecord)
throws DuplicateMessageException, LegacyMessageException, InvalidMessageException, NoSuchAlgorithmException, NoSessionException {
throws DuplicateMessageException, LegacyMessageException, InvalidMessageException, NoSuchAlgorithmException, NoSessionException, UntrustedIdentityException {
SignalProtocolStore aliceStore = new TestInMemorySignalProtocolStore();
SignalProtocolStore bobStore = new TestInMemorySignalProtocolStore();

View File

@ -0,0 +1,74 @@
package org.whispersystems.libsignal.devices;
import junit.framework.TestCase;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.protocol.DeviceConsistencyMessage;
import org.whispersystems.libsignal.util.KeyHelper;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class DeviceConsistencyTest extends TestCase {
public void testDeviceConsistency() throws InvalidMessageException {
final IdentityKeyPair deviceOne = KeyHelper.generateIdentityKeyPair();
final IdentityKeyPair deviceTwo = KeyHelper.generateIdentityKeyPair();
final IdentityKeyPair deviceThree = KeyHelper.generateIdentityKeyPair();
List<IdentityKey> keyList = new LinkedList<IdentityKey>() {{
add(deviceOne.getPublicKey());
add(deviceTwo.getPublicKey());
add(deviceThree.getPublicKey());
}};
Collections.shuffle(keyList);
DeviceConsistencyCommitment deviceOneCommitment = new DeviceConsistencyCommitment(1, keyList);
Collections.shuffle(keyList);
DeviceConsistencyCommitment deviceTwoCommitment = new DeviceConsistencyCommitment(1, keyList);
Collections.shuffle(keyList);
DeviceConsistencyCommitment deviceThreeCommitment = new DeviceConsistencyCommitment(1, keyList);
assertTrue(Arrays.equals(deviceOneCommitment.toByteArray(), deviceTwoCommitment.toByteArray()));
assertTrue(Arrays.equals(deviceTwoCommitment.toByteArray(), deviceThreeCommitment.toByteArray()));
DeviceConsistencyMessage deviceOneMessage = new DeviceConsistencyMessage(deviceOneCommitment, deviceOne);
DeviceConsistencyMessage deviceTwoMessage = new DeviceConsistencyMessage(deviceOneCommitment, deviceTwo);
DeviceConsistencyMessage deviceThreeMessage = new DeviceConsistencyMessage(deviceOneCommitment, deviceThree);
DeviceConsistencyMessage receivedDeviceOneMessage = new DeviceConsistencyMessage(deviceOneCommitment, deviceOneMessage.getSerialized(), deviceOne.getPublicKey());
DeviceConsistencyMessage receivedDeviceTwoMessage = new DeviceConsistencyMessage(deviceOneCommitment, deviceTwoMessage.getSerialized(), deviceTwo.getPublicKey());
DeviceConsistencyMessage receivedDeviceThreeMessage = new DeviceConsistencyMessage(deviceOneCommitment, deviceThreeMessage.getSerialized(), deviceThree.getPublicKey());
assertTrue(Arrays.equals(deviceOneMessage.getSignature().getVrfOutput(), receivedDeviceOneMessage.getSignature().getVrfOutput()));
assertTrue(Arrays.equals(deviceTwoMessage.getSignature().getVrfOutput(), receivedDeviceTwoMessage.getSignature().getVrfOutput()));
assertTrue(Arrays.equals(deviceThreeMessage.getSignature().getVrfOutput(), receivedDeviceThreeMessage.getSignature().getVrfOutput()));
String codeOne = generateCode(deviceOneCommitment, deviceOneMessage, receivedDeviceTwoMessage, receivedDeviceThreeMessage);
String codeTwo = generateCode(deviceTwoCommitment, deviceTwoMessage, receivedDeviceThreeMessage, receivedDeviceOneMessage);
String codeThree = generateCode(deviceThreeCommitment, deviceThreeMessage, receivedDeviceTwoMessage, receivedDeviceOneMessage);
assertEquals(codeOne, codeTwo);
assertEquals(codeTwo, codeThree);
}
private String generateCode(DeviceConsistencyCommitment commitment,
DeviceConsistencyMessage... messages)
{
List<DeviceConsistencySignature> signatures = new LinkedList<>();
for (DeviceConsistencyMessage message : messages) {
signatures.add(message.getSignature());
}
return DeviceConsistencyCodeGenerator.generateFor(commitment, signatures);
}
}

View File

@ -133,4 +133,48 @@ public class Curve25519Test extends TestCase {
}
}
}
public void testDecodeSize() throws InvalidKeyException {
ECKeyPair keyPair = Curve.generateKeyPair();
byte[] serializedPublic = keyPair.getPublicKey().serialize();
ECPublicKey justRight = Curve.decodePoint(serializedPublic, 0);
try {
ECPublicKey tooSmall = Curve.decodePoint(serializedPublic, 1);
throw new AssertionError("Shouldn't decode");
} catch (InvalidKeyException e) {
// good
}
try {
ECPublicKey empty = Curve.decodePoint(new byte[0], 0);
throw new AssertionError("Shouldn't parse");
} catch (InvalidKeyException e) {
// good
}
try {
byte[] badKeyType = new byte[33];
System.arraycopy(serializedPublic, 0, badKeyType, 0, serializedPublic.length);
badKeyType[0] = 0x01;
Curve.decodePoint(badKeyType, 0);
throw new AssertionError("Should be bad key type");
} catch (InvalidKeyException e) {
// good
}
byte[] extraSpace = new byte[serializedPublic.length + 1];
System.arraycopy(serializedPublic, 0, extraSpace, 0, serializedPublic.length);
ECPublicKey extra = Curve.decodePoint(extraSpace, 0);
byte[] offsetSpace = new byte[serializedPublic.length + 1];
System.arraycopy(serializedPublic, 0, offsetSpace, 1, serializedPublic.length);
ECPublicKey offset = Curve.decodePoint(offsetSpace, 1);
assertTrue(Arrays.equals(serializedPublic, justRight.serialize()));
assertTrue(Arrays.equals(extra.serialize(), serializedPublic));
assertTrue(Arrays.equals(offset.serialize(), serializedPublic));
}
}

View File

@ -6,8 +6,69 @@ import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECKeyPair;
import java.util.Arrays;
public class NumericFingerprintGeneratorTest extends TestCase {
private static final byte[] ALICE_IDENTITY = {(byte) 0x05, (byte) 0x06, (byte) 0x86, (byte) 0x3b, (byte) 0xc6, (byte) 0x6d, (byte) 0x02, (byte) 0xb4, (byte) 0x0d, (byte) 0x27, (byte) 0xb8, (byte) 0xd4, (byte) 0x9c, (byte) 0xa7, (byte) 0xc0, (byte) 0x9e, (byte) 0x92, (byte) 0x39, (byte) 0x23, (byte) 0x6f, (byte) 0x9d, (byte) 0x7d, (byte) 0x25, (byte) 0xd6, (byte) 0xfc, (byte) 0xca, (byte) 0x5c, (byte) 0xe1, (byte) 0x3c, (byte) 0x70, (byte) 0x64, (byte) 0xd8, (byte) 0x68};
private static final byte[] BOB_IDENTITY = {(byte) 0x05, (byte) 0xf7, (byte) 0x81, (byte) 0xb6, (byte) 0xfb, (byte) 0x32, (byte) 0xfe, (byte) 0xd9, (byte) 0xba, (byte) 0x1c, (byte) 0xf2, (byte) 0xde, (byte) 0x97, (byte) 0x8d, (byte) 0x4d, (byte) 0x5d, (byte) 0xa2, (byte) 0x8d, (byte) 0xc3, (byte) 0x40, (byte) 0x46, (byte) 0xae, (byte) 0x81, (byte) 0x44, (byte) 0x02, (byte) 0xb5, (byte) 0xc0, (byte) 0xdb, (byte) 0xd9, (byte) 0x6f, (byte) 0xda, (byte) 0x90, (byte) 0x7b};
private static final int VERSION_1 = 1;
private static final String DISPLAYABLE_FINGERPRINT_V1 = "300354477692869396892869876765458257569162576843440918079131";
private static final byte[] ALICE_SCANNABLE_FINGERPRINT_V1 = new byte[]{(byte)0x08, (byte)0x01, (byte)0x12, (byte)0x22, (byte)0x0a, (byte)0x20, (byte)0x1e, (byte)0x30, (byte)0x1a, (byte)0x03, (byte)0x53, (byte)0xdc, (byte)0xe3, (byte)0xdb, (byte)0xe7, (byte)0x68, (byte)0x4c, (byte)0xb8, (byte)0x33, (byte)0x6e, (byte)0x85, (byte)0x13, (byte)0x6c, (byte)0xdc, (byte)0x0e, (byte)0xe9, (byte)0x62, (byte)0x19, (byte)0x49, (byte)0x4a, (byte)0xda, (byte)0x30, (byte)0x5d, (byte)0x62, (byte)0xa7, (byte)0xbd, (byte)0x61, (byte)0xdf, (byte)0x1a, (byte)0x22, (byte)0x0a, (byte)0x20, (byte)0xd6, (byte)0x2c, (byte)0xbf, (byte)0x73, (byte)0xa1, (byte)0x15, (byte)0x92, (byte)0x01, (byte)0x5b, (byte)0x6b, (byte)0x9f, (byte)0x16, (byte)0x82, (byte)0xac, (byte)0x30, (byte)0x6f, (byte)0xea, (byte)0x3a, (byte)0xaf, (byte)0x38, (byte)0x85, (byte)0xb8, (byte)0x4d, (byte)0x12, (byte)0xbc, (byte)0xa6, (byte)0x31, (byte)0xe9, (byte)0xd4, (byte)0xfb, (byte)0x3a, (byte)0x4d};
private static final byte[] BOB_SCANNABLE_FINGERPRINT_V1 = new byte[]{(byte)0x08, (byte)0x01, (byte)0x12, (byte)0x22, (byte)0x0a, (byte)0x20, (byte)0xd6, (byte)0x2c, (byte)0xbf, (byte)0x73, (byte)0xa1, (byte)0x15, (byte)0x92, (byte)0x01, (byte)0x5b, (byte)0x6b, (byte)0x9f, (byte)0x16, (byte)0x82, (byte)0xac, (byte)0x30, (byte)0x6f, (byte)0xea, (byte)0x3a, (byte)0xaf, (byte)0x38, (byte)0x85, (byte)0xb8, (byte)0x4d, (byte)0x12, (byte)0xbc, (byte)0xa6, (byte)0x31, (byte)0xe9, (byte)0xd4, (byte)0xfb, (byte)0x3a, (byte)0x4d, (byte)0x1a, (byte)0x22, (byte)0x0a, (byte)0x20, (byte)0x1e, (byte)0x30, (byte)0x1a, (byte)0x03, (byte)0x53, (byte)0xdc, (byte)0xe3, (byte)0xdb, (byte)0xe7, (byte)0x68, (byte)0x4c, (byte)0xb8, (byte)0x33, (byte)0x6e, (byte)0x85, (byte)0x13, (byte)0x6c, (byte)0xdc, (byte)0x0e, (byte)0xe9, (byte)0x62, (byte)0x19, (byte)0x49, (byte)0x4a, (byte)0xda, (byte)0x30, (byte)0x5d, (byte)0x62, (byte)0xa7, (byte)0xbd, (byte)0x61, (byte)0xdf};
private static final int VERSION_2 = 2;
private static final String DISPLAYABLE_FINGERPRINT_V2 = DISPLAYABLE_FINGERPRINT_V1;
private static final byte[] ALICE_SCANNABLE_FINGERPRINT_V2 = new byte[]{(byte)0x08, (byte)0x02, (byte)0x12, (byte)0x22, (byte)0x0a, (byte)0x20, (byte)0x1e, (byte)0x30, (byte)0x1a, (byte)0x03, (byte)0x53, (byte)0xdc, (byte)0xe3, (byte)0xdb, (byte)0xe7, (byte)0x68, (byte)0x4c, (byte)0xb8, (byte)0x33, (byte)0x6e, (byte)0x85, (byte)0x13, (byte)0x6c, (byte)0xdc, (byte)0x0e, (byte)0xe9, (byte)0x62, (byte)0x19, (byte)0x49, (byte)0x4a, (byte)0xda, (byte)0x30, (byte)0x5d, (byte)0x62, (byte)0xa7, (byte)0xbd, (byte)0x61, (byte)0xdf, (byte)0x1a, (byte)0x22, (byte)0x0a, (byte)0x20, (byte)0xd6, (byte)0x2c, (byte)0xbf, (byte)0x73, (byte)0xa1, (byte)0x15, (byte)0x92, (byte)0x01, (byte)0x5b, (byte)0x6b, (byte)0x9f, (byte)0x16, (byte)0x82, (byte)0xac, (byte)0x30, (byte)0x6f, (byte)0xea, (byte)0x3a, (byte)0xaf, (byte)0x38, (byte)0x85, (byte)0xb8, (byte)0x4d, (byte)0x12, (byte)0xbc, (byte)0xa6, (byte)0x31, (byte)0xe9, (byte)0xd4, (byte)0xfb, (byte)0x3a, (byte)0x4d};
private static final byte[] BOB_SCANNABLE_FINGERPRINT_V2 = new byte[]{(byte)0x08, (byte)0x02, (byte)0x12, (byte)0x22, (byte)0x0a, (byte)0x20, (byte)0xd6, (byte)0x2c, (byte)0xbf, (byte)0x73, (byte)0xa1, (byte)0x15, (byte)0x92, (byte)0x01, (byte)0x5b, (byte)0x6b, (byte)0x9f, (byte)0x16, (byte)0x82, (byte)0xac, (byte)0x30, (byte)0x6f, (byte)0xea, (byte)0x3a, (byte)0xaf, (byte)0x38, (byte)0x85, (byte)0xb8, (byte)0x4d, (byte)0x12, (byte)0xbc, (byte)0xa6, (byte)0x31, (byte)0xe9, (byte)0xd4, (byte)0xfb, (byte)0x3a, (byte)0x4d, (byte)0x1a, (byte)0x22, (byte)0x0a, (byte)0x20, (byte)0x1e, (byte)0x30, (byte)0x1a, (byte)0x03, (byte)0x53, (byte)0xdc, (byte)0xe3, (byte)0xdb, (byte)0xe7, (byte)0x68, (byte)0x4c, (byte)0xb8, (byte)0x33, (byte)0x6e, (byte)0x85, (byte)0x13, (byte)0x6c, (byte)0xdc, (byte)0x0e, (byte)0xe9, (byte)0x62, (byte)0x19, (byte)0x49, (byte)0x4a, (byte)0xda, (byte)0x30, (byte)0x5d, (byte)0x62, (byte)0xa7, (byte)0xbd, (byte)0x61, (byte)0xdf};
public void testVectorsVersion1() throws Exception {
IdentityKey aliceIdentityKey = new IdentityKey(ALICE_IDENTITY, 0);
IdentityKey bobIdentityKey = new IdentityKey(BOB_IDENTITY, 0);
byte[] aliceStableId = "+14152222222".getBytes();
byte[] bobStableId = "+14153333333".getBytes();
NumericFingerprintGenerator generator = new NumericFingerprintGenerator(5200);
Fingerprint aliceFingerprint = generator.createFor(VERSION_1,
aliceStableId, aliceIdentityKey,
bobStableId, bobIdentityKey);
Fingerprint bobFingerprint = generator.createFor(VERSION_1,
bobStableId, bobIdentityKey,
aliceStableId, aliceIdentityKey);
assertEquals(aliceFingerprint.getDisplayableFingerprint().getDisplayText(), DISPLAYABLE_FINGERPRINT_V1);
assertEquals(bobFingerprint.getDisplayableFingerprint().getDisplayText(), DISPLAYABLE_FINGERPRINT_V1);
assertTrue(Arrays.equals(aliceFingerprint.getScannableFingerprint().getSerialized(), ALICE_SCANNABLE_FINGERPRINT_V1));
assertTrue(Arrays.equals(bobFingerprint.getScannableFingerprint().getSerialized(), BOB_SCANNABLE_FINGERPRINT_V1));
}
public void testVectorsVersion2() throws Exception {
IdentityKey aliceIdentityKey = new IdentityKey(ALICE_IDENTITY, 0);
IdentityKey bobIdentityKey = new IdentityKey(BOB_IDENTITY, 0);
byte[] aliceStableId = "+14152222222".getBytes();
byte[] bobStableId = "+14153333333".getBytes();
NumericFingerprintGenerator generator = new NumericFingerprintGenerator(5200);
Fingerprint aliceFingerprint = generator.createFor(VERSION_2,
aliceStableId, aliceIdentityKey,
bobStableId, bobIdentityKey);
Fingerprint bobFingerprint = generator.createFor(VERSION_2,
bobStableId, bobIdentityKey,
aliceStableId, aliceIdentityKey);
assertEquals(aliceFingerprint.getDisplayableFingerprint().getDisplayText(), DISPLAYABLE_FINGERPRINT_V2);
assertEquals(bobFingerprint.getDisplayableFingerprint().getDisplayText(), DISPLAYABLE_FINGERPRINT_V2);
assertTrue(Arrays.equals(aliceFingerprint.getScannableFingerprint().getSerialized(), ALICE_SCANNABLE_FINGERPRINT_V2));
assertTrue(Arrays.equals(bobFingerprint.getScannableFingerprint().getSerialized(), BOB_SCANNABLE_FINGERPRINT_V2));
}
public void testMatchingFingerprints() throws FingerprintVersionMismatchException, FingerprintIdentifierMismatchException, FingerprintParsingException {
ECKeyPair aliceKeyPair = Curve.generateKeyPair();
ECKeyPair bobKeyPair = Curve.generateKeyPair();
@ -16,11 +77,13 @@ public class NumericFingerprintGeneratorTest extends TestCase {
IdentityKey bobIdentityKey = new IdentityKey(bobKeyPair.getPublicKey());
NumericFingerprintGenerator generator = new NumericFingerprintGenerator(1024);
Fingerprint aliceFingerprint = generator.createFor("+14152222222", aliceIdentityKey,
"+14153333333", bobIdentityKey);
Fingerprint aliceFingerprint = generator.createFor(VERSION_1,
"+14152222222".getBytes(), aliceIdentityKey,
"+14153333333".getBytes(), bobIdentityKey);
Fingerprint bobFingerprint = generator.createFor("+14153333333", bobIdentityKey,
"+14152222222", aliceIdentityKey);
Fingerprint bobFingerprint = generator.createFor(VERSION_1,
"+14153333333".getBytes(), bobIdentityKey,
"+14152222222".getBytes(), aliceIdentityKey);
assertEquals(aliceFingerprint.getDisplayableFingerprint().getDisplayText(),
bobFingerprint.getDisplayableFingerprint().getDisplayText());
@ -41,14 +104,16 @@ public class NumericFingerprintGeneratorTest extends TestCase {
IdentityKey mitmIdentityKey = new IdentityKey(mitmKeyPair.getPublicKey());
NumericFingerprintGenerator generator = new NumericFingerprintGenerator(1024);
Fingerprint aliceFingerprint = generator.createFor("+14152222222", aliceIdentityKey,
"+14153333333", mitmIdentityKey);
Fingerprint aliceFingerprint = generator.createFor(VERSION_1,
"+14152222222".getBytes(), aliceIdentityKey,
"+14153333333".getBytes(), mitmIdentityKey);
Fingerprint bobFingerprint = generator.createFor("+14153333333", bobIdentityKey,
"+14152222222", aliceIdentityKey);
Fingerprint bobFingerprint = generator.createFor(VERSION_1,
"+14153333333".getBytes(), bobIdentityKey,
"+14152222222".getBytes(), aliceIdentityKey);
assertNotSame(aliceFingerprint.getDisplayableFingerprint().getDisplayText(),
bobFingerprint.getDisplayableFingerprint().getDisplayText());
assertFalse(aliceFingerprint.getDisplayableFingerprint().getDisplayText().equals(
bobFingerprint.getDisplayableFingerprint().getDisplayText()));
assertFalse(aliceFingerprint.getScannableFingerprint().compareTo(bobFingerprint.getScannableFingerprint().getSerialized()));
assertFalse(bobFingerprint.getScannableFingerprint().compareTo(aliceFingerprint.getScannableFingerprint().getSerialized()));
@ -62,28 +127,43 @@ public class NumericFingerprintGeneratorTest extends TestCase {
IdentityKey bobIdentityKey = new IdentityKey(bobKeyPair.getPublicKey());
NumericFingerprintGenerator generator = new NumericFingerprintGenerator(1024);
Fingerprint aliceFingerprint = generator.createFor("+141512222222", aliceIdentityKey,
"+14153333333", bobIdentityKey);
Fingerprint aliceFingerprint = generator.createFor(VERSION_1,
"+141512222222".getBytes(), aliceIdentityKey,
"+14153333333".getBytes(), bobIdentityKey);
Fingerprint bobFingerprint = generator.createFor("+14153333333", bobIdentityKey,
"+14152222222", aliceIdentityKey);
Fingerprint bobFingerprint = generator.createFor(VERSION_1,
"+14153333333".getBytes(), bobIdentityKey,
"+14152222222".getBytes(), aliceIdentityKey);
assertNotSame(aliceFingerprint.getDisplayableFingerprint().getDisplayText(),
bobFingerprint.getDisplayableFingerprint().getDisplayText());
assertFalse(aliceFingerprint.getDisplayableFingerprint().getDisplayText().equals(
bobFingerprint.getDisplayableFingerprint().getDisplayText()));
try {;
aliceFingerprint.getScannableFingerprint().compareTo(bobFingerprint.getScannableFingerprint().getSerialized());
throw new AssertionError("Should mismatch!");
} catch (FingerprintIdentifierMismatchException e) {
// good
}
assertFalse(aliceFingerprint.getScannableFingerprint().compareTo(bobFingerprint.getScannableFingerprint().getSerialized()));
assertFalse(bobFingerprint.getScannableFingerprint().compareTo(aliceFingerprint.getScannableFingerprint().getSerialized()));
}
try {
bobFingerprint.getScannableFingerprint().compareTo(aliceFingerprint.getScannableFingerprint().getSerialized());
throw new AssertionError("Should mismatch!");
} catch (FingerprintIdentifierMismatchException e) {
// good
}
public void testDifferentVersionsMakeSameFingerPrintsButDifferentScannable() throws Exception {
IdentityKey aliceIdentityKey = new IdentityKey(ALICE_IDENTITY, 0);
IdentityKey bobIdentityKey = new IdentityKey(BOB_IDENTITY, 0);
byte[] aliceStableId = "+14152222222".getBytes();
byte[] bobStableId = "+14153333333".getBytes();
NumericFingerprintGenerator generator = new NumericFingerprintGenerator(5200);
Fingerprint aliceFingerprintV1 = generator.createFor(VERSION_1,
aliceStableId, aliceIdentityKey,
bobStableId, bobIdentityKey);
Fingerprint aliceFingerprintV2 = generator.createFor(VERSION_2,
aliceStableId, aliceIdentityKey,
bobStableId, bobIdentityKey);
assertTrue(aliceFingerprintV1.getDisplayableFingerprint().getDisplayText().equals(
aliceFingerprintV2.getDisplayableFingerprint().getDisplayText()));
assertFalse(Arrays.equals(aliceFingerprintV1.getScannableFingerprint().getSerialized(),
aliceFingerprintV2.getScannableFingerprint().getSerialized()));
}
}