Break out libtextsecure

// FREEBIE

Closes #2542
This commit is contained in:
Moxie Marlinspike 2015-03-02 08:25:19 -08:00
parent 7bf7acb1ff
commit ed5b3f8679
76 changed files with 21 additions and 16210 deletions

View File

@ -27,6 +27,7 @@ repositories {
url "https://raw.github.com/whispersystems/maven/master/shortcutbadger/releases/" url "https://raw.github.com/whispersystems/maven/master/shortcutbadger/releases/"
} }
jcenter() jcenter()
mavenLocal()
} }
dependencies { dependencies {
@ -56,6 +57,7 @@ dependencies {
compile 'org.whispersystems:jobmanager:0.10.0' compile 'org.whispersystems:jobmanager:0.10.0'
compile 'org.whispersystems:libpastelog:1.0.4' compile 'org.whispersystems:libpastelog:1.0.4'
compile 'org.whispersystems:textsecure-android:1.0.0'
androidTestCompile 'com.google.dexmaker:dexmaker:1.2' androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
@ -70,8 +72,6 @@ dependencies {
exclude group: 'javax.inject' exclude group: 'javax.inject'
} }
androidTestCompile 'com.android.support.test:testing-support-lib:0.1' androidTestCompile 'com.android.support.test:testing-support-lib:0.1'
compile project(':libtextsecure')
} }
dependencyVerification { dependencyVerification {
@ -96,23 +96,27 @@ dependencyVerification {
'com.madgag.spongycastle:prov:b8c3fec3a59aac1aa04ccf4dad7179351e54ef7672f53f508151b614c131398a', 'com.madgag.spongycastle:prov:b8c3fec3a59aac1aa04ccf4dad7179351e54ef7672f53f508151b614c131398a',
'org.whispersystems:jobmanager:01f35586c43aa3806f1c18d3d6a5a972def98103ba1a5a9ca3eec08d15f974b7', 'org.whispersystems:jobmanager:01f35586c43aa3806f1c18d3d6a5a972def98103ba1a5a9ca3eec08d15f974b7',
'org.whispersystems:libpastelog:3ccf00fe1597eb8ca1e5de99b17fc225387a1b80b5bbc00ec1bc4d4f3ea9cdde', 'org.whispersystems:libpastelog:3ccf00fe1597eb8ca1e5de99b17fc225387a1b80b5bbc00ec1bc4d4f3ea9cdde',
'org.whispersystems:textsecure-android:af67cb5d8770de6a2ae9c3d64b5ad06b63b24d2d9fb6dc3c4298d7acc60caa2a',
'com.android.support:support-annotations:fdee2354787ef66b268e75958de3f7f6c4f8f325510a6dac9f49c929f83a63de', 'com.android.support:support-annotations:fdee2354787ef66b268e75958de3f7f6c4f8f325510a6dac9f49c929f83a63de',
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a', 'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff', 'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f', 'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f',
'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74', 'org.whispersystems:axolotl-android:9cc3328b5a6ca6d407543f3072fd0fbf9080e1ca13ba9d7745f6475d076c2856',
'org.whispersystems:textsecure-java:bf101047ef41ece9e4dcffdbb0506e185a888c42693d9cb78754d93f01d0c4c7',
'org.whispersystems:axolotl-java:793556b1397da323ad61d5b6a3efaba464972315d4e3f2e771a9183415aeceaf',
'org.whispersystems:curve25519-android:3c29a4131a69b0d16baaa3d707678deb39602c3a3ffd75805ce7f9db252e5d0d',
'com.googlecode.libphonenumber:libphonenumber:eba17eae81dd622ea89a00a3a8c025b2f25d342e0d9644c5b62e16f15687c3ab', 'com.googlecode.libphonenumber:libphonenumber:eba17eae81dd622ea89a00a3a8c025b2f25d342e0d9644c5b62e16f15687c3ab',
'com.fasterxml.jackson.core:jackson-databind:835097bcdd11f5bc8a08378c70d4c8054dfa4b911691cc2752063c75534d198d', 'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74',
'org.whispersystems:axolotl-android:7617256d05aaecd7b5475cd55e42773d7079167a22ca48512bcb0f84f8473cc9',
'com.squareup.okhttp:okhttp:89b7f63e2e5b6c410266abc14f50fe52ea8d2d8a57260829e499b1cd9f0e61af', 'com.squareup.okhttp:okhttp:89b7f63e2e5b6c410266abc14f50fe52ea8d2d8a57260829e499b1cd9f0e61af',
'com.fasterxml.jackson.core:jackson-databind:835097bcdd11f5bc8a08378c70d4c8054dfa4b911691cc2752063c75534d198d',
'org.whispersystems:curve25519-java:9ccef8f5aba05d9942336f023c589d6278b4f9135bdc34a7bade1f4e7ad65fa3',
'com.squareup.okio:okio:5e1098bd3fdee4c3347f5ab815b40ba851e4ab1b348c5e49a5b0362f0ce6e978',
'com.fasterxml.jackson.core:jackson-annotations:0ca408c24202a7626ec8b861e99d85eca5e38b73311dd6dd12e3e9deecc3fe94', 'com.fasterxml.jackson.core:jackson-annotations:0ca408c24202a7626ec8b861e99d85eca5e38b73311dd6dd12e3e9deecc3fe94',
'com.fasterxml.jackson.core:jackson-core:cbf4604784b4de226262845447a1ad3bb38a6728cebe86562e2c5afada8be2c0', 'com.fasterxml.jackson.core:jackson-core:cbf4604784b4de226262845447a1ad3bb38a6728cebe86562e2c5afada8be2c0',
'com.squareup.okio:okio:5e1098bd3fdee4c3347f5ab815b40ba851e4ab1b348c5e49a5b0362f0ce6e978',
'com.android.support:support-v4:703572d3015a088cc5604b7e38885af3d307c829d0c5ceaf8654ff41c71cd160', 'com.android.support:support-v4:703572d3015a088cc5604b7e38885af3d307c829d0c5ceaf8654ff41c71cd160',
] ]
} }
android { android {
compileSdkVersion 21 compileSdkVersion 21
buildToolsVersion '21.1.2' buildToolsVersion '21.1.2'

View File

@ -1,98 +0,0 @@
# 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();
}
`````

View File

@ -1,53 +0,0 @@
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())
}
}
}

View File

@ -1,59 +0,0 @@
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;
}

View File

@ -1,3 +0,0 @@
all:
protoc --java_out=../src/main/java/ IncomingPushMessageSignal.proto Provisioning.proto WebSocketResources.proto

View File

@ -1,16 +0,0 @@
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;
}

View File

@ -1,46 +0,0 @@
/**
* 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;
}

View File

@ -1,35 +0,0 @@
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);
}
}
}

View File

@ -1,8 +0,0 @@
<?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>

View File

@ -1,256 +0,0 @@
/**
* 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;
}
}

View File

@ -1,129 +0,0 @@
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) {}
}
}

View File

@ -1,105 +0,0 @@
/**
* 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);
}
}

View File

@ -1,393 +0,0 @@
/**
* 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);
}
}

View File

@ -1,220 +0,0 @@
/**
* 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;
}
}
}

View File

@ -1,121 +0,0 @@
/**
* 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);
}
}
}

View File

@ -1,167 +0,0 @@
/**
* 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());
}
}

View File

@ -1,44 +0,0 @@
/**
* 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;
}
}

View File

@ -1,41 +0,0 @@
/**
* 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;
}
}

View File

@ -1,62 +0,0 @@
/**
* 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;
}
}

View File

@ -1,52 +0,0 @@
/**
* 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;
}
}

View File

@ -1,252 +0,0 @@
/**
* 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");
}
}

View File

@ -1,99 +0,0 @@
/**
* 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;
}
}

View File

@ -1,136 +0,0 @@
/**
* 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;
}
}

View File

@ -1,62 +0,0 @@
/**
* 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;
}
}

View File

@ -1,68 +0,0 @@
/**
* 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());
}
}
}

View File

@ -1,80 +0,0 @@
/**
* 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);
}
}

View File

@ -1,29 +0,0 @@
/**
* 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();
}

View File

@ -1,23 +0,0 @@
/**
* 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);
}
}

View File

@ -1,49 +0,0 @@
/**
* 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;
}
}

View File

@ -1,20 +0,0 @@
/**
* 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 {
}

View File

@ -1,15 +0,0 @@
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;
}
}

View File

@ -1,30 +0,0 @@
/**
* 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);
}
}

View File

@ -1,23 +0,0 @@
/**
* 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);
}
}

View File

@ -1,31 +0,0 @@
/**
* 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);
}
}

View File

@ -1,23 +0,0 @@
/**
* 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);
}
}

View File

@ -1,33 +0,0 @@
/**
* 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;
}
}

View File

@ -1,7 +0,0 @@
package org.whispersystems.textsecure.api.util;
public interface CredentialsProvider {
public String getUser();
public String getPassword();
public String getSignalingKey();
}

View File

@ -1,23 +0,0 @@
/**
* 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);
}
}

View File

@ -1,137 +0,0 @@
/**
* 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;
}
}
}

View File

@ -1,75 +0,0 @@
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);
}
}
}

View File

@ -1,51 +0,0 @@
/**
* 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;
}
}

View File

@ -1,32 +0,0 @@
/**
* 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;
}
}

View File

@ -1,34 +0,0 @@
/**
* 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;
}
}

View File

@ -1,13 +0,0 @@
package org.whispersystems.textsecure.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
public class DeviceCode {
@JsonProperty
private String verificationCode;
public String getVerificationCode() {
return verificationCode;
}
}

View File

@ -1,37 +0,0 @@
/**
* 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;
}
}

View File

@ -1,58 +0,0 @@
/**
* 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;
}
}

View File

@ -1,61 +0,0 @@
/**
* 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;
}
}

View File

@ -1,78 +0,0 @@
/**
* 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);
}
}
}
}

View File

@ -1,56 +0,0 @@
/**
* 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;
}
}

View File

@ -1,53 +0,0 @@
/**
* 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;
}
}

View File

@ -1,39 +0,0 @@
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;
}
}

View File

@ -1,31 +0,0 @@
/**
* 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;
}
}

View File

@ -1,11 +0,0 @@
package org.whispersystems.textsecure.internal.push;
public class ProvisioningMessage {
private String body;
public ProvisioningMessage(String body) {
this.body = body;
}
}

View File

@ -1,50 +0,0 @@
/**
* 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;
}
}

View File

@ -1,42 +0,0 @@
/**
* 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;
}
}

View File

@ -1,570 +0,0 @@
/**
* 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;
}
}
}

View File

@ -1,75 +0,0 @@
/**
* 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;
}
}

View File

@ -1,16 +0,0 @@
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;
}
}

View File

@ -1,31 +0,0 @@
/**
* 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;
}
}

View File

@ -1,33 +0,0 @@
/**
* 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;
}
}

View File

@ -1,33 +0,0 @@
/**
* 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;
}
}

View File

@ -1,111 +0,0 @@
/**
* 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();
}
}

View File

@ -1,138 +0,0 @@
/**
* 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]);
}
}

View File

@ -1,75 +0,0 @@
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);
}
}
}
}

View File

@ -1,31 +0,0 @@
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;
}
}

View File

@ -1,128 +0,0 @@
/**
* 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);
}
}
}

View File

@ -1,142 +0,0 @@
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);
}
}
}

View File

@ -1,174 +0,0 @@
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);
}
}
}

View File

@ -1,9 +0,0 @@
package org.whispersystems.textsecure.internal.websocket;
public interface WebSocketEventListener {
public void onMessage(byte[] payload);
public void onClose();
public void onConnected();
}

View File

@ -1 +0,0 @@
include ':libtextsecure'

View File

@ -31,6 +31,8 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.jobqueue.JobManager; import org.whispersystems.jobqueue.JobManager;
import org.whispersystems.jobqueue.dependencies.DependencyInjector; import org.whispersystems.jobqueue.dependencies.DependencyInjector;
import org.whispersystems.jobqueue.requirements.NetworkRequirementProvider; import org.whispersystems.jobqueue.requirements.NetworkRequirementProvider;
import org.whispersystems.libaxolotl.logging.AxolotlLoggerProvider;
import org.whispersystems.libaxolotl.util.AndroidAxolotlLogger;
import java.security.Security; import java.security.Security;
@ -56,6 +58,7 @@ public class ApplicationContext extends Application implements DependencyInjecto
@Override @Override
public void onCreate() { public void onCreate() {
initializeRandomNumberFix(); initializeRandomNumberFix();
initializeLogging();
initializeDependencyInjection(); initializeDependencyInjection();
initializeJobManager(); initializeJobManager();
initializeGcmCheck(); initializeGcmCheck();
@ -76,6 +79,10 @@ public class ApplicationContext extends Application implements DependencyInjecto
PRNGFixes.apply(); PRNGFixes.apply();
} }
private void initializeLogging() {
AxolotlLoggerProvider.setProvider(new AndroidAxolotlLogger());
}
private void initializeJobManager() { private void initializeJobManager() {
this.jobManager = JobManager.newBuilder(this) this.jobManager = JobManager.newBuilder(this)
.withName("TextSecureJobs") .withName("TextSecureJobs")

View File

@ -29,7 +29,6 @@ import org.whispersystems.libaxolotl.IdentityKeyPair;
import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.InvalidKeyIdException; import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.ecc.Curve; import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.Curve25519;
import org.whispersystems.libaxolotl.ecc.ECKeyPair; import org.whispersystems.libaxolotl.ecc.ECKeyPair;
import org.whispersystems.libaxolotl.state.PreKeyRecord; import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.PreKeyStore; import org.whispersystems.libaxolotl.state.PreKeyStore;
@ -56,7 +55,7 @@ public class PreKeyUtil {
for (int i=0;i<BATCH_SIZE;i++) { for (int i=0;i<BATCH_SIZE;i++) {
int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE; int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
ECKeyPair keyPair = Curve25519.generateKeyPair(); ECKeyPair keyPair = Curve.generateKeyPair();
PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair); PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
preKeyStore.storePreKey(preKeyId, record); preKeyStore.storePreKey(preKeyId, record);
@ -73,7 +72,7 @@ public class PreKeyUtil {
try { try {
SignedPreKeyStore signedPreKeyStore = new TextSecurePreKeyStore(context, masterSecret); SignedPreKeyStore signedPreKeyStore = new TextSecurePreKeyStore(context, masterSecret);
int signedPreKeyId = getNextSignedPreKeyId(context); int signedPreKeyId = getNextSignedPreKeyId(context);
ECKeyPair keyPair = Curve25519.generateKeyPair(); ECKeyPair keyPair = Curve.generateKeyPair();
byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize()); byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
SignedPreKeyRecord record = new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature); SignedPreKeyRecord record = new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
@ -98,7 +97,7 @@ public class PreKeyUtil {
} }
} }
ECKeyPair keyPair = Curve25519.generateKeyPair(); ECKeyPair keyPair = Curve.generateKeyPair();
PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair); PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair);
preKeyStore.storePreKey(Medium.MAX_VALUE, record); preKeyStore.storePreKey(Medium.MAX_VALUE, record);