Compare commits

...

58 Commits

Author SHA1 Message Date
Moxie Marlinspike
31eddbf346 Bump version to 2.3.0
// FREEBIE
2014-11-21 14:49:14 -08:00
Moxie Marlinspike
e9b383d277 Add jobs for pending push messages during migration.
// FREEBIE
2014-11-21 12:53:40 -08:00
Moxie Marlinspike
eeafb81c90 Fix escaping in danish translation.
// FREEBIE
2014-11-21 12:53:24 -08:00
Moxie Marlinspike
02e27c94f2 Update language translations.
// FREEBIE
2014-11-21 11:49:35 -08:00
Jake McGinty
167386ea49 make the light theme light
Fixes #2111
Tested on GB and LP
// FREEBIE
2014-11-20 21:59:15 -08:00
Jake McGinty
e31994ac77 proguard has guarded its last pro
// FREEBIE
2014-11-20 17:45:31 -08:00
Moxie Marlinspike
ea9a5decac Update gradle-witness and actually use spongycastle.
// FREEBIE
2014-11-20 16:46:35 -08:00
Moxie Marlinspike
1cb191a6ee Move targetSdkVersion back down to 19.
// FREEBIE
2014-11-20 15:58:32 -08:00
Jake McGinty
60737bdd7b proguard: don't warn for test classes
// FREEBIE
2014-11-19 14:52:02 -08:00
Moxie Marlinspike
2dded4888e No more push encoding pretense.
// FREEBIE
2014-11-19 14:45:12 -08:00
Jake McGinty
eaf89735b8 encapsulated delivery icon to separate from lock
and as a bonus some tweaked text sizes and colors
// FREEBIE
2014-11-19 12:56:44 -08:00
Jake McGinty
8fa2f92a91 use latest appcompat and build tools, add dagger pin
// FREEBIE
2014-11-18 17:03:10 -08:00
Moxie Marlinspike
174324e2a0 Potential fix for BroadcastReceiver crash.
// FREEBIE
2014-11-18 14:55:16 -08:00
Jake McGinty
bc3686058a optimize new assets
Thanks to @pejakm for pointing out the emoji optimization.
// FREEBIE
2014-11-18 11:27:10 -08:00
Jake McGinty
0110015c8b remove unused image resources
// FREEBIE
2014-11-18 11:27:10 -08:00
Jake McGinty
5ca8a6d421 materialize conversation item indicators
// FREEBIE
2014-11-18 11:27:08 -08:00
Jake McGinty
0fc9ff7490 add "elevation" window overlay for android 4.x too
// FREEBIE
2014-11-17 12:14:09 -08:00
Jake McGinty
08d939b010 fix AppCompat theme crash
Fixes #2084
// FREEBIE
2014-11-17 11:51:49 -08:00
Jake McGinty
020920d988 silly actionbar shadow issue
// FREEBIE
2014-11-17 11:28:57 -08:00
Moxie Marlinspike
bd3d9ac533 Update JobManager README.md
// FREEBIE
2014-11-17 09:17:14 -08:00
agrajaghh
85670d95ee Fix README.md formatting
// FREEBIE

Closes #2101
2014-11-17 08:57:21 -08:00
Moxie Marlinspike
3d1007d101 Added README
// FREEBIE
2014-11-16 17:24:05 -08:00
Moxie Marlinspike
35821d444e Move responsibility for Context injection out of JavaSerializer.
// FREEBIE
2014-11-16 17:23:33 -08:00
Moxie Marlinspike
3cd7c2d8e5 libjobqueue javadoc and scoping.
// FREEBIE
2014-11-16 15:53:51 -08:00
Moxie Marlinspike
5b08791086 Fix regression with providers being registered.
// FREEBIE
2014-11-14 15:44:49 -08:00
Jake McGinty
31b9dcb5eb stop crashes when sending sms
// FREEBIE
2014-11-15 02:14:37 +03:00
Jake McGinty
43adc75428 add icon back
// FREEBIE
2014-11-15 02:04:17 +03:00
Jake McGinty
67a4523ca7 update gradle+plugins, update gradle-witness
// FREEBIE
2014-11-14 13:07:04 +03:00
Moxie Marlinspike
b35b9be0c8 Add missing copyright headers from libtextsecure.
// FREEBIE
2014-11-12 18:37:16 -08:00
Moxie Marlinspike
9215322846 Abstract out TrustStore interface.
// FREEBIE
2014-11-12 17:09:59 -08:00
Moxie Marlinspike
cbebc040cc Make ProGuard with with Dagger 2014-11-12 16:03:58 -08:00
Moxie Marlinspike
bea26e83da Correctly process push messages with identity key conflicts. 2014-11-12 15:42:43 -08:00
Moxie Marlinspike
a85dbce041 Correctly handle PKWM via SMS. 2014-11-12 15:42:43 -08:00
Moxie Marlinspike
baaa3514d4 Fix delivery receipts in group messages.
Fixes #2056
Fixes #2067
Fixes #2087
2014-11-12 15:42:43 -08:00
Moxie Marlinspike
71fdaac1b2 Fix regressions for registration. 2014-11-12 15:42:43 -08:00
Moxie Marlinspike
fb31319e52 Put everything under either internal or api. 2014-11-12 15:42:43 -08:00
Moxie Marlinspike
08ed90c5ec Split out Util functions. 2014-11-12 15:42:43 -08:00
Moxie Marlinspike
0d102f76cc Move ListenableFutureTask up to parent. 2014-11-12 15:38:23 -08:00
Moxie Marlinspike
28cb1ed85b Move DirectoryUtil up to parent. 2014-11-12 15:29:59 -08:00
Moxie Marlinspike
cd9b20dc9d Move dependency up to parent. 2014-11-12 15:29:59 -08:00
Moxie Marlinspike
f09abff407 Refactor out old classes. 2014-11-12 15:29:59 -08:00
Moxie Marlinspike
f9934bd8e5 Modernize libtextsecure layout 2014-11-12 15:29:59 -08:00
Moxie Marlinspike
1182052c7f Rename library to libtextsecure 2014-11-12 15:29:58 -08:00
Moxie Marlinspike
0d06d50a65 Let's have JobManager only deal with checked exceptions.
Also, switch to Builder for JobManager construction.
2014-11-12 15:29:58 -08:00
Moxie Marlinspike
d9d4ec9d9d Fix some bugs with PKWM padding and attachment detection. 2014-11-12 15:29:58 -08:00
Moxie Marlinspike
9a6f65988f Add support for dependency injection, and accompanying tests. 2014-11-12 15:29:58 -08:00
Moxie Marlinspike
601e233d47 Add account management interface to libtextsecure api 2014-11-12 15:28:08 -08:00
Moxie Marlinspike
ae178fc4ec Move API around a little, eliminate TransportDetails interface. 2014-11-12 15:26:25 -08:00
Moxie Marlinspike
cafe03a70a Transition the outbound pipeline to JobManager jobs. 2014-11-12 15:26:25 -08:00
Moxie Marlinspike
99f42e2ee1 Move API around. 2014-11-12 15:21:32 -08:00
Moxie Marlinspike
a3f1d9cdfd Beginning of libtextsecure refactor.
1) Break out appropriate components.

2) Switch the incoming pipeline from SendReceiveService to
   the JobManager.
2014-11-12 15:21:32 -08:00
Jake McGinty
4cab657ebe clear pending slides when attachment reselected
Fixes #2012

// FREEBIE
2014-10-29 18:50:11 -07:00
Jake McGinty
db6f8618e6 padding workaround for bug in appcompat-v7 21.0.0
bug: https://code.google.com/p/android/issues/detail?id=77982

// FREEBIE
2014-10-29 18:28:20 -07:00
Jake McGinty
98af1fb6ee shorten blocks of text
// FREEBIE
2014-10-29 16:53:25 -07:00
Jake McGinty
ad1d55f12d enable proguard
// FREEBIE
2014-10-29 16:53:21 -07:00
Jake McGinty
7df49811b7 replace ABS with AppCompat
// FREEBIE
2014-10-29 16:51:55 -07:00
Jake McGinty
ff2ac8a66e refactor ListenableFutureTask and make saves async
// FREEBIE
2014-10-28 02:25:41 -05:00
Jake McGinty
53da1f849a in-app image media preview
// FREEBIE
2014-10-28 00:50:01 -05:00
479 changed files with 25587 additions and 8571 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.ai binary

View File

@@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.thoughtcrime.securesms"
android:versionCode="83"
android:versionName="2.2.0">
android:versionCode="84"
android:versionName="2.3.0">
<permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"
android:label="Access to TextSecure Secrets"
@@ -114,12 +114,12 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DatabaseMigrationActivity"
android:theme="@style/NoAnimation.Theme.Sherlock.Light.DarkActionBar"
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DatabaseUpgradeActivity"
android:theme="@style/NoAnimation.Theme.Sherlock.Light.DarkActionBar"
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -192,6 +192,11 @@
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".MediaPreviewActivity"
android:label="@string/AndroidManifest__media_preview"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DummyActivity"
android:theme="@android:style/Theme.NoDisplay"
android:enabled="true"

View File

@@ -0,0 +1,153 @@
package org.thoughtcrime.securesms.jobs;
import android.test.AndroidTestCase;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.dependencies.AxolotlStorageModule;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyStore;
import org.whispersystems.textsecure.api.TextSecureAccountManager;
import org.whispersystems.textsecure.api.push.SignedPreKeyEntity;
import org.whispersystems.textsecure.api.push.exceptions.PushNetworkException;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import dagger.Module;
import dagger.ObjectGraph;
import dagger.Provides;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
public class CleanPreKeysJobTest extends AndroidTestCase {
public void testSignedPreKeyRotationNotRegistered() throws IOException, MasterSecretJob.RequirementNotMetException {
TextSecureAccountManager accountManager = mock(TextSecureAccountManager.class);
SignedPreKeyStore signedPreKeyStore = mock(SignedPreKeyStore.class);
MasterSecret masterSecret = mock(MasterSecret.class);
when(accountManager.getSignedPreKey()).thenReturn(null);
CleanPreKeysJob cleanPreKeysJob = new CleanPreKeysJob(getContext());
ObjectGraph objectGraph = ObjectGraph.create(new TestModule(accountManager, signedPreKeyStore));
objectGraph.inject(cleanPreKeysJob);
cleanPreKeysJob.onRun(masterSecret);
verify(accountManager).getSignedPreKey();
verifyNoMoreInteractions(signedPreKeyStore);
}
public void testSignedPreKeyEviction() throws Exception {
SignedPreKeyStore signedPreKeyStore = mock(SignedPreKeyStore.class);
TextSecureAccountManager accountManager = mock(TextSecureAccountManager.class);
SignedPreKeyEntity currentSignedPreKeyEntity = mock(SignedPreKeyEntity.class);
MasterSecret masterSecret = mock(MasterSecret.class);
when(currentSignedPreKeyEntity.getKeyId()).thenReturn(3133);
when(accountManager.getSignedPreKey()).thenReturn(currentSignedPreKeyEntity);
final SignedPreKeyRecord currentRecord = new SignedPreKeyRecord(3133, System.currentTimeMillis(), Curve.generateKeyPair(), new byte[64]);
List<SignedPreKeyRecord> records = new LinkedList<SignedPreKeyRecord>() {{
add(new SignedPreKeyRecord(2, 11, Curve.generateKeyPair(), new byte[32]));
add(new SignedPreKeyRecord(4, System.currentTimeMillis() - 100, Curve.generateKeyPair(), new byte[64]));
add(currentRecord);
add(new SignedPreKeyRecord(3, System.currentTimeMillis() - 90, Curve.generateKeyPair(), new byte[64]));
add(new SignedPreKeyRecord(1, 10, Curve.generateKeyPair(), new byte[32]));
}};
when(signedPreKeyStore.loadSignedPreKeys()).thenReturn(records);
when(signedPreKeyStore.loadSignedPreKey(eq(3133))).thenReturn(currentRecord);
CleanPreKeysJob cleanPreKeysJob = new CleanPreKeysJob(getContext());
ObjectGraph objectGraph = ObjectGraph.create(new TestModule(accountManager, signedPreKeyStore));
objectGraph.inject(cleanPreKeysJob);
cleanPreKeysJob.onRun(masterSecret);
verify(signedPreKeyStore).removeSignedPreKey(eq(1));
verify(signedPreKeyStore, times(1)).removeSignedPreKey(anyInt());
}
public void testSignedPreKeyNoEviction() throws Exception {
SignedPreKeyStore signedPreKeyStore = mock(SignedPreKeyStore.class);
TextSecureAccountManager accountManager = mock(TextSecureAccountManager.class);
SignedPreKeyEntity currentSignedPreKeyEntity = mock(SignedPreKeyEntity.class);
when(currentSignedPreKeyEntity.getKeyId()).thenReturn(3133);
when(accountManager.getSignedPreKey()).thenReturn(currentSignedPreKeyEntity);
final SignedPreKeyRecord currentRecord = new SignedPreKeyRecord(3133, System.currentTimeMillis(), Curve.generateKeyPair(), new byte[64]);
List<SignedPreKeyRecord> records = new LinkedList<SignedPreKeyRecord>() {{
add(currentRecord);
}};
when(signedPreKeyStore.loadSignedPreKeys()).thenReturn(records);
when(signedPreKeyStore.loadSignedPreKey(eq(3133))).thenReturn(currentRecord);
CleanPreKeysJob cleanPreKeysJob = new CleanPreKeysJob(getContext());
ObjectGraph objectGraph = ObjectGraph.create(new TestModule(accountManager, signedPreKeyStore));
objectGraph.inject(cleanPreKeysJob);
verify(signedPreKeyStore, never()).removeSignedPreKey(anyInt());
}
public void testConnectionError() throws Exception {
SignedPreKeyStore signedPreKeyStore = mock(SignedPreKeyStore.class);
TextSecureAccountManager accountManager = mock(TextSecureAccountManager.class);
MasterSecret masterSecret = mock(MasterSecret.class);
when(accountManager.getSignedPreKey()).thenThrow(new PushNetworkException("Connectivity error!"));
CleanPreKeysJob cleanPreKeysJob = new CleanPreKeysJob(getContext());
ObjectGraph objectGraph = ObjectGraph.create(new TestModule(accountManager, signedPreKeyStore));
objectGraph.inject(cleanPreKeysJob);
try {
cleanPreKeysJob.onRun(masterSecret);
throw new AssertionError("should have failed!");
} catch (IOException e) {
assertTrue(cleanPreKeysJob.onShouldRetry(e));
}
}
@Module(injects = {CleanPreKeysJob.class})
public static class TestModule {
private final TextSecureAccountManager accountManager;
private final SignedPreKeyStore signedPreKeyStore;
private TestModule(TextSecureAccountManager accountManager, SignedPreKeyStore signedPreKeyStore) {
this.accountManager = accountManager;
this.signedPreKeyStore = signedPreKeyStore;
}
@Provides TextSecureAccountManager provideTextSecureAccountManager() {
return accountManager;
}
@Provides
AxolotlStorageModule.SignedPreKeyStoreFactory provideSignedPreKeyStore() {
return new AxolotlStorageModule.SignedPreKeyStoreFactory() {
@Override
public SignedPreKeyStore create(MasterSecret masterSecret) {
return signedPreKeyStore;
}
};
}
}
}

View File

@@ -0,0 +1,101 @@
package org.thoughtcrime.securesms.jobs;
import android.test.AndroidTestCase;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.whispersystems.textsecure.api.TextSecureMessageSender;
import org.whispersystems.textsecure.api.push.PushAddress;
import org.whispersystems.textsecure.api.push.exceptions.NotFoundException;
import org.whispersystems.textsecure.api.push.exceptions.PushNetworkException;
import java.io.IOException;
import dagger.Module;
import dagger.ObjectGraph;
import dagger.Provides;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.thoughtcrime.securesms.dependencies.TextSecureCommunicationModule.TextSecureMessageSenderFactory;
public class DeliveryReceiptJobTest extends AndroidTestCase {
public void testDelivery() throws IOException {
TextSecureMessageSender textSecureMessageSender = mock(TextSecureMessageSender.class);
long timestamp = System.currentTimeMillis();
DeliveryReceiptJob deliveryReceiptJob = new DeliveryReceiptJob(getContext(),
"+14152222222",
timestamp, "foo");
ObjectGraph objectGraph = ObjectGraph.create(new TestModule(textSecureMessageSender));
objectGraph.inject(deliveryReceiptJob);
deliveryReceiptJob.onRun();
ArgumentCaptor<PushAddress> captor = ArgumentCaptor.forClass(PushAddress.class);
verify(textSecureMessageSender).sendDeliveryReceipt(captor.capture(), eq(timestamp));
assertTrue(captor.getValue().getRelay().equals("foo"));
assertTrue(captor.getValue().getNumber().equals("+14152222222"));
}
public void testNetworkError() throws IOException {
TextSecureMessageSender textSecureMessageSender = mock(TextSecureMessageSender.class);
long timestamp = System.currentTimeMillis();
Mockito.doThrow(new PushNetworkException("network error"))
.when(textSecureMessageSender)
.sendDeliveryReceipt(any(PushAddress.class), eq(timestamp));
DeliveryReceiptJob deliveryReceiptJob = new DeliveryReceiptJob(getContext(),
"+14152222222",
timestamp, "foo");
ObjectGraph objectGraph = ObjectGraph.create(new TestModule(textSecureMessageSender));
objectGraph.inject(deliveryReceiptJob);
try {
deliveryReceiptJob.onRun();
throw new AssertionError();
} catch (IOException e) {
assertTrue(deliveryReceiptJob.onShouldRetry(e));
}
Mockito.doThrow(new NotFoundException("not found"))
.when(textSecureMessageSender)
.sendDeliveryReceipt(any(PushAddress.class), eq(timestamp));
try {
deliveryReceiptJob.onRun();
throw new AssertionError();
} catch (IOException e) {
assertFalse(deliveryReceiptJob.onShouldRetry(e));
}
}
@Module(injects = DeliveryReceiptJob.class)
public static class TestModule {
private final TextSecureMessageSender textSecureMessageSender;
public TestModule(TextSecureMessageSender textSecureMessageSender) {
this.textSecureMessageSender = textSecureMessageSender;
}
@Provides TextSecureMessageSenderFactory provideTextSecureMessageSenderFactory() {
return new TextSecureMessageSenderFactory() {
@Override
public TextSecureMessageSender create(MasterSecret masterSecret) {
return textSecureMessageSender;
}
};
}
}
}

View File

@@ -1,92 +0,0 @@
package org.thoughtcrime.securesms.service;
import android.test.AndroidTestCase;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyStore;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.SignedPreKeyEntity;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
public class PreKeyServiceTest extends AndroidTestCase {
public void testSignedPreKeyRotationNotRegistered() throws IOException {
SignedPreKeyStore signedPreKeyStore = mock(SignedPreKeyStore.class);
PushServiceSocket pushServiceSocket = mock(PushServiceSocket.class);
when(pushServiceSocket.getCurrentSignedPreKey()).thenReturn(null);
PreKeyService.CleanSignedPreKeysTask cleanTask = new PreKeyService.CleanSignedPreKeysTask(signedPreKeyStore,
pushServiceSocket);
cleanTask.run();
verify(pushServiceSocket).getCurrentSignedPreKey();
verifyNoMoreInteractions(signedPreKeyStore);
}
public void testSignedPreKeyEviction() throws Exception {
SignedPreKeyStore signedPreKeyStore = mock(SignedPreKeyStore.class);
PushServiceSocket pushServiceSocket = mock(PushServiceSocket.class);
SignedPreKeyEntity currentSignedPreKeyEntity = mock(SignedPreKeyEntity.class);
when(currentSignedPreKeyEntity.getKeyId()).thenReturn(3133);
when(pushServiceSocket.getCurrentSignedPreKey()).thenReturn(currentSignedPreKeyEntity);
final SignedPreKeyRecord currentRecord = new SignedPreKeyRecord(3133, System.currentTimeMillis(), Curve.generateKeyPair(true), new byte[64]);
List<SignedPreKeyRecord> records = new LinkedList<SignedPreKeyRecord>() {{
add(new SignedPreKeyRecord(1, 10, Curve.generateKeyPair(true), new byte[32]));
add(new SignedPreKeyRecord(2, 11, Curve.generateKeyPair(true), new byte[32]));
add(new SignedPreKeyRecord(3, System.currentTimeMillis() - 90, Curve.generateKeyPair(true), new byte[64]));
add(new SignedPreKeyRecord(4, System.currentTimeMillis() - 100, Curve.generateKeyPair(true), new byte[64]));
add(currentRecord);
}};
when(signedPreKeyStore.loadSignedPreKeys()).thenReturn(records);
when(signedPreKeyStore.loadSignedPreKey(eq(3133))).thenReturn(currentRecord);
PreKeyService.CleanSignedPreKeysTask cleanTask = new PreKeyService.CleanSignedPreKeysTask(signedPreKeyStore, pushServiceSocket);
cleanTask.run();
verify(signedPreKeyStore).removeSignedPreKey(eq(1));
verify(signedPreKeyStore).removeSignedPreKey(eq(2));
verify(signedPreKeyStore, times(2)).removeSignedPreKey(anyInt());
}
public void testSignedPreKeyNoEviction() throws Exception {
SignedPreKeyStore signedPreKeyStore = mock(SignedPreKeyStore.class);
PushServiceSocket pushServiceSocket = mock(PushServiceSocket.class);
SignedPreKeyEntity currentSignedPreKeyEntity = mock(SignedPreKeyEntity.class);
when(currentSignedPreKeyEntity.getKeyId()).thenReturn(3133);
when(pushServiceSocket.getCurrentSignedPreKey()).thenReturn(currentSignedPreKeyEntity);
final SignedPreKeyRecord currentRecord = new SignedPreKeyRecord(3133, System.currentTimeMillis(), Curve.generateKeyPair(true), new byte[64]);
List<SignedPreKeyRecord> records = new LinkedList<SignedPreKeyRecord>() {{
add(currentRecord);
}};
when(signedPreKeyStore.loadSignedPreKeys()).thenReturn(records);
when(signedPreKeyStore.loadSignedPreKey(eq(3133))).thenReturn(currentRecord);
PreKeyService.CleanSignedPreKeysTask cleanTask = new PreKeyService.CleanSignedPreKeysTask(signedPreKeyStore, pushServiceSocket);
cleanTask.run();
verify(signedPreKeyStore, never()).removeSignedPreKey(anyInt());
}
}

View File

@@ -4,8 +4,8 @@ import android.test.AndroidTestCase;
import junit.framework.AssertionFailedError;
import org.whispersystems.textsecure.util.InvalidNumberException;
import org.whispersystems.textsecure.util.PhoneNumberFormatter;
import org.whispersystems.textsecure.api.util.InvalidNumberException;
import org.whispersystems.textsecure.api.util.PhoneNumberFormatter;
import static org.fest.assertions.api.Assertions.assertThat;
public class PhoneNumberFormatterTest extends AndroidTestCase {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 445 KiB

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 547 KiB

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 660 KiB

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 KiB

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 363 KiB

After

Width:  |  Height:  |  Size: 278 KiB

View File

@@ -5,12 +5,13 @@ buildscript {
}
}
dependencies {
classpath 'com.android.tools.build:gradle:0.12.2'
classpath 'com.android.tools.build:gradle:0.14.2'
classpath files('libs/gradle-witness.jar')
}
}
apply plugin: 'com.android.application'
apply from: 'strip_play_services.gradle'
apply plugin: 'witness'
repositories {
@@ -18,7 +19,7 @@ repositories {
url "https://repo1.maven.org/maven2"
}
maven {
url "https://raw.github.com/whispersystems/maven/master/gcm-client/releases/"
url "https://raw.github.com/whispersystems/maven/master/preferencefragment/releases/"
}
maven {
url "https://raw.github.com/whispersystems/maven/master/gson/releases/"
@@ -29,42 +30,53 @@ repositories {
}
dependencies {
compile 'com.actionbarsherlock:actionbarsherlock:4.4.0@aar'
compile 'com.android.support:support-v4:20.0.0'
compile 'se.emilsjolander:stickylistheaders:2.2.0'
compile 'com.google.android.gms:play-services:5.0.89'
compile 'com.google.android.gms:play-services:6.1.71'
compile 'com.astuetz:pagerslidingtabstrip:1.0.1'
compile 'org.w3c:smil:1.0.0'
compile 'org.apache.httpcomponents:httpclient-android:4.3.5'
compile 'com.github.chrisbanes.photoview:library:1.2.3'
compile 'com.android.support:appcompat-v7:21.0.2'
compile 'com.madgag.spongycastle:prov:1.51.0.0'
compile ('com.android.support:support-v4-preferencefragment:1.0.0@aar'){
exclude module: 'support-v4'
}
compile 'com.squareup.dagger:dagger:1.2.2'
provided 'com.squareup.dagger:dagger-compiler:1.2.2'
androidTestCompile 'com.squareup:fest-android:1.0.8'
androidTestCompile 'com.google.dexmaker:dexmaker:1.1'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.1'
compile project(':library')
compile project(':libtextsecure')
compile project(':jobqueue')
}
dependencyVerification {
verify = [
'com.actionbarsherlock:actionbarsherlock:5ab04d74101f70024b222e3ff9c87bee151ec43331b4a2134b6cc08cf8565819',
'com.android.support:support-v4:81f2b1c2c94efd5a4ec7fcd97b6cdcd00e87a933905c5c86103c7319eb024572',
'se.emilsjolander:stickylistheaders:89146b46c96fea0e40200474a2625cda10fe94891e4128f53cdb42375091b9b6',
'com.google.android.gms:play-services:38f326e525830f1d70f60f594ceafcbdf5b312287ddbecd338fd1ed7958a4b1e',
'com.google.android.gms:play-services:32e7d1834a1cf8fa4b17e8d359db580c286e26c1eefbf84fdb9996eac8d74919',
'com.astuetz:pagerslidingtabstrip:f1641396732c7132a7abb837e482e5ee2b0ebb8d10813fc52bbaec2c15c184c2',
'org.w3c:smil:085dc40f2bb249651578bfa07499fd08b16ad0886dbe2c4078586a408da62f9b',
'org.apache.httpcomponents:httpclient-android:6f56466a9bd0d42934b90bfbfe9977a8b654c058bf44a12bdc2877c4e1f033f1',
'com.android.support:support-annotations:1aa96ef0cc4a445bfc2f93ccf762305bc57fa107b12afe9d11f3863ae8a11036',
'com.github.chrisbanes.photoview:library:8b5344e206f125e7ba9d684008f36c4992d03853c57e5814125f88496126e3cc',
'com.android.support:appcompat-v7:b760fd3d0b0b0547a1bcef9031b40939f31049ba955f04c8fdc5aa09a25d19e9',
'com.madgag.spongycastle:prov:b8c3fec3a59aac1aa04ccf4dad7179351e54ef7672f53f508151b614c131398a',
'com.android.support:support-v4-preferencefragment:5470f5872514a6226fa1fc6f4e000991f38805691c534cf0bd2778911fc773ad',
'com.squareup.dagger:dagger:789aca24537022e49f91fc6444078d9de8f1dd99e1bfb090f18491b186967883',
'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f',
'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74',
'com.madgag:sc-light-jdk15on:931f39d351429fb96c2f749e7ecb1a256a8ebbf5edca7995c9cc085b94d1841d',
'com.googlecode.libphonenumber:libphonenumber:eba17eae81dd622ea89a00a3a8c025b2f25d342e0d9644c5b62e16f15687c3ab',
'org.whispersystems:gson:08f4f7498455d1539c9233e5aac18e9b1805815ef29221572996508eb512fe51',
'com.android.support:support-v4:126a4c291f41f75f3fff4968e9d397bc8454cdff4d8f994cbe0524e3bad76e72',
'com.android.support:support-annotations:7e37f00e3d932c4d9a6dd84604a99133bb5ae7d81cbd1aee69861fcf2f91e5e9',
]
}
android {
compileSdkVersion 19
buildToolsVersion '19.1.0'
compileSdkVersion 21
buildToolsVersion '21.1.1'
defaultConfig {
minSdkVersion 9
@@ -77,6 +89,15 @@ android {
}
android {
buildTypes {
debug {
minifyEnabled false
}
release {
minifyEnabled false
}
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'

View File

@@ -1,6 +1,6 @@
#Mon Jun 09 23:26:49 PDT 2014
#Fri Nov 14 10:44:11 MSK 2014
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-all.zip

238
jobqueue/README.md Normal file
View File

@@ -0,0 +1,238 @@
# JobManager
An Android library that facilitates scheduling persistent jobs which are executed when their
prerequisites have been met. Similar to Path's android-priority-queue.
## The JobManager Way
Android apps often need to perform blocking operations. A messaging app might need to make REST
API calls over a network, send SMS messages, download attachments, and interact with a database.
The standard Android way to do these things are with Services, AsyncTasks, or a dedicated Thread.
However, some of an app's operations might need to wait until certain dependencies are available
(such as a network connection), and some of the operations might need to be durable (complete even if the
app restarts before they have a chance to run). The standard Android way can result in
a lot of retry logic, timers for monitoring dependencies, and one-off code for making operations
durable.
By contrast, the JobManager way allows operations to be broken up into Jobs. A Job represents a
unit of work to be done, the prerequisites that need to be met (such as network access) before the
work can execute, and the characteristics of the job (such as durable persistence).
Applications construct a `JobManager` at initialization time:
```
public class ApplicationContext extends Application {
private JobManager jobManager;
@Override
public void onCreate() {
initializeJobManager();
}
private void initializeJobManager() {
this.jobManager = JobManager.newBuilder(this)
.withName("SampleJobManager")
.withConsumerThreads(5)
.build();
}
...
}
```
This constructs a new `JobManager` with 5 consumer threads dedicated to executing Jobs. A
Job looks like this:
```
public class SampleJob extends Job {
public SampleJob() {
super(JobParameters.newBuilder().create());
}
@Override
public onAdded() {
// Called after the Job has been added to the queue.
}
@Override
public void onRun() {
// Here's where we execute our work.
Log.w("SampleJob", "Hello, world!");
}
@Override
public void onCanceled() {
// This would be called if the job had failed.
}
@Override
public boolean onShouldRetry(Exception exception) {
// Called if onRun() had thrown an exception to determine whether
// onRun() should be called again.
return false;
}
}
```
A Job is scheduled simply by adding it to the JobManager:
```
this.jobManager.add(new SampleJob());
```
## Persistence
To create durable Jobs, the JobManager needs to be given an interface responsible for serializing
and deserializing Job objects. A `JavaJobSerializer` is included with JobManager that uses Java
Serialization, but you can specify your own serializer if you wish:
```
public class ApplicationContext extends Application {
private JobManager jobManager;
@Override
public void onCreate() {
initializeJobManager();
}
private void initializeJobManager() {
this.jobManager = JobManager.newBuilder(this)
.withName("SampleJobManager")
.withConsumerThreads(5)
.withJobSerializer(new JavaJobSerializer())
.build();
}
...
}
```
The Job simply needs to declare itself as durable when constructed:
```
public class SampleJob extends Job {
public SampleJob() {
super(JobParameters.newBuilder()
.withPersistence()
.create());
}
...
```
Persistent jobs that are enqueued will be serialized to disk to ensure that they run even if
the App restarts first. A Job's onAdded() method is called after the commit to disk is complete.
## Requirements
A Job might have certain requirements that need to be met before it can run. A requirement is
represented by the `Requirement` interface. Each `Requirement` must also have a corresponding
`RequirementProvider` that is registered with the JobManager.
A `Requirement` tells you whether it is present when queried, while a `RequirementProvider`
broadcasts to a listener when a Requirement's status might have changed. `Requirement` is attached
to Job, while `RequirementProvider` is attached to JobManager.
One common `Requirement` a `Job` might depend on is the presence of network connectivity.
A `NetworkRequirement` is bundled with JobManager:
```
public class ApplicationContext extends Application {
private JobManager jobManager;
@Override
public void onCreate() {
initializeJobManager();
}
private void initializeJobManager() {
this.jobManager = JobManager.newBuilder(this)
.withName("SampleJobManager")
.withConsumerThreads(5)
.withJobSerializer(new JavaJobSerializer())
.withRequirementProviders(new NetworkRequirementProvider(this))
.build();
}
...
}
```
The Job declares itself as having a `Requirement` when constructed:
```
public class SampleJob extends Job {
public SampleJob(Context context) {
super(JobParameters.newBuilder()
.withPersistence()
.withRequirement(new NetworkRequirement(context))
.create());
}
...
```
## Dependency Injection
It is possible that Jobs (and Requirements) might require dependency injection. A simple example
is `Context`, which many Jobs might require, but can't be persisted to disk for durable Jobs. Or
maybe Jobs require more complex DI through libraries such as Dagger.
JobManager has an extremely primitive DI mechanism strictly for injecting `Context` objects into
Jobs and Requirements after they're deserialized, and includes support for plugging in more complex
DI systems such as Dagger.
The JobManager `Context` injection works by having your `Job` and/or `Requirement` implement the
`ContextDependent` interface. `Job`s and `Requirement`s implementing that interface will get a
`setContext(Context context)` call immediately after the persistent `Job` or `Requirement` is
deserialized.
To plugin a more complex DI mechanism, simply pass an instance of the `DependencyInjector` interface
to the `JobManager`:
```
public class ApplicationContext extends Application implements DependencyInjector {
private JobManager jobManager;
@Override
public void onCreate() {
initializeJobManager();
}
private void initializeJobManager() {
this.jobManager = JobManager.newBuilder(this)
.withName("SampleJobManager")
.withConsumerThreads(5)
.withJobSerializer(new JavaJobSerializer())
.withRequirementProviders(new NetworkRequirementProvider(this))
.withDependencyInjector(this)
.build();
}
@Override
public void injectDependencies(Object object) {
// And here we do our DI magic.
}
...
}
```
`injectDependencies(Object object)` will be called for a `Job` before the job's `onAdded()` method
is called, or after a persistent job is deserialized.

View File

@@ -1,8 +1,8 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion 20
buildToolsVersion "20.0.0"
compileSdkVersion 21
buildToolsVersion '21.1.1'
defaultConfig {
applicationId "org.whispersystems.jobqueue"

View File

@@ -7,6 +7,7 @@ import org.whispersystems.jobqueue.jobs.RequirementDeferringTestJob;
import org.whispersystems.jobqueue.jobs.RequirementTestJob;
import org.whispersystems.jobqueue.jobs.TestJob;
import org.whispersystems.jobqueue.persistence.JavaJobSerializer;
import org.whispersystems.jobqueue.requirements.RequirementProvider;
import org.whispersystems.jobqueue.util.MockRequirement;
import org.whispersystems.jobqueue.util.MockRequirementProvider;
import org.whispersystems.jobqueue.util.PersistentMockRequirement;
@@ -15,12 +16,17 @@ import org.whispersystems.jobqueue.util.PersistentResult;
import org.whispersystems.jobqueue.util.RunnableThrowable;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
public class JobManagerTest extends AndroidTestCase {
public void testTransientJobExecution() throws InterruptedException {
TestJob testJob = new TestJob();
JobManager jobManager = new JobManager(getContext(), "transient-test", null, null, 1);
JobManager jobManager = JobManager.newBuilder(getContext())
.withName("transient-test")
.withConsumerThreads(1)
.build();
jobManager.add(testJob);
@@ -32,8 +38,12 @@ public class JobManagerTest extends AndroidTestCase {
MockRequirementProvider provider = new MockRequirementProvider();
MockRequirement requirement = new MockRequirement(false);
TestJob testJob = new RequirementTestJob(requirement);
JobManager jobManager = new JobManager(getContext(), "transient-requirement-test",
provider, null, 1);
JobManager jobManager = JobManager.newBuilder(getContext())
.withName("transient-requirement-test")
.withRequirementProviders(provider)
.withConsumerThreads(1)
.build();
jobManager.add(testJob);
@@ -75,8 +85,12 @@ public class JobManagerTest extends AndroidTestCase {
MockRequirementProvider provider = new MockRequirementProvider();
MockRequirement requirement = new MockRequirement(false);
RequirementDeferringTestJob testJob = new RequirementDeferringTestJob(requirement, 5, waitRunnable);
JobManager jobManager = new JobManager(getContext(), "transient-requirement-test",
provider, null, 1);
JobManager jobManager = JobManager.newBuilder(getContext())
.withName("transient-requirement-test")
.withRequirementProviders(provider)
.withConsumerThreads(1)
.build();
jobManager.add(testJob);
@@ -106,8 +120,11 @@ public class JobManagerTest extends AndroidTestCase {
public void testPersistentJobExecuton() throws InterruptedException {
PersistentMockRequirement requirement = new PersistentMockRequirement();
PersistentTestJob testJob = new PersistentTestJob(requirement);
JobManager jobManager = new JobManager(getContext(), "persistent-requirement-test3",
null, new JavaJobSerializer(getContext()), 1);
JobManager jobManager = JobManager.newBuilder(getContext())
.withName("persistent-requirement-test3")
.withJobSerializer(new JavaJobSerializer())
.withConsumerThreads(1)
.build();
PersistentResult.getInstance().reset();
PersistentRequirement.getInstance().setPresent(false);
@@ -118,8 +135,12 @@ public class JobManagerTest extends AndroidTestCase {
assertTrue(!PersistentResult.getInstance().isRan());
PersistentRequirement.getInstance().setPresent(true);
jobManager = new JobManager(getContext(), "persistent-requirement-test3", null,
new JavaJobSerializer(getContext()), 1);
jobManager = JobManager.newBuilder(getContext())
.withName("persistent-requirement-test3")
.withJobSerializer(new JavaJobSerializer())
.withConsumerThreads(1)
.build();
assertTrue(PersistentResult.getInstance().isRan());
}
@@ -128,8 +149,12 @@ public class JobManagerTest extends AndroidTestCase {
EncryptionKeys keys = new EncryptionKeys(new byte[30]);
PersistentMockRequirement requirement = new PersistentMockRequirement();
PersistentTestJob testJob = new PersistentTestJob(requirement, keys);
JobManager jobManager = new JobManager(getContext(), "persistent-requirement-test4",
null, new JavaJobSerializer(getContext()), 1);
JobManager jobManager = JobManager.newBuilder(getContext())
.withName("persistent-requirement-test4")
.withJobSerializer(new JavaJobSerializer())
.withConsumerThreads(1)
.build();
jobManager.setEncryptionKeys(keys);
PersistentResult.getInstance().reset();
@@ -141,7 +166,11 @@ public class JobManagerTest extends AndroidTestCase {
assertTrue(!PersistentResult.getInstance().isRan());
PersistentRequirement.getInstance().setPresent(true);
jobManager = new JobManager(getContext(), "persistent-requirement-test4", null, new JavaJobSerializer(getContext()), 1);
jobManager = JobManager.newBuilder(getContext())
.withName("persistent-requirement-test4")
.withJobSerializer(new JavaJobSerializer())
.withConsumerThreads(1)
.build();
assertTrue(!PersistentResult.getInstance().isRan());
@@ -169,7 +198,10 @@ public class JobManagerTest extends AndroidTestCase {
TestJob testJobOne = new TestJob(JobParameters.newBuilder().withGroupId("foo").create(), waitRunnable);
TestJob testJobTwo = new TestJob(JobParameters.newBuilder().withGroupId("foo").create());
TestJob testJobThree = new TestJob(JobParameters.newBuilder().withGroupId("bar").create());
JobManager jobManager = new JobManager(getContext(), "transient-test", null, null, 3);
JobManager jobManager = JobManager.newBuilder(getContext())
.withName("transient-test")
.withConsumerThreads(3)
.build();
jobManager.add(testJobOne);
jobManager.add(testJobTwo);

View File

@@ -19,11 +19,11 @@ public class PersistentTestJob extends Job {
@Override
public void onAdded() {
PersistentResult.getInstance().onAdded();;
PersistentResult.getInstance().onAdded();
}
@Override
public void onRun() throws Throwable {
public void onRun() throws Exception {
PersistentResult.getInstance().onRun();
}
@@ -33,7 +33,7 @@ public class PersistentTestJob extends Job {
}
@Override
public boolean onShouldRetry(Throwable throwable) {
public boolean onShouldRetry(Exception exception) {
return false;
}
}

View File

@@ -20,7 +20,7 @@ public class RequirementDeferringTestJob extends TestJob {
}
@Override
public void onRun() throws Throwable {
public void onRun() throws Exception {
synchronized (RAN_LOCK) {
this.ran = true;
}
@@ -34,8 +34,8 @@ public class RequirementDeferringTestJob extends TestJob {
}
@Override
public boolean onShouldRetry(Throwable throwable) {
if (throwable instanceof Exception) {
public boolean onShouldRetry(Exception exception) {
if (exception instanceof Exception) {
return true;
}
return false;

View File

@@ -37,7 +37,7 @@ public class TestJob extends Job {
}
@Override
public void onRun() throws Throwable {
public void onRun() throws Exception {
synchronized (RAN_LOCK) {
this.ran = true;
}
@@ -54,7 +54,7 @@ public class TestJob extends Job {
}
@Override
public boolean onShouldRetry(Throwable throwable) {
public boolean onShouldRetry(Exception exception) {
return false;
}

View File

@@ -15,4 +15,9 @@ public class MockRequirementProvider implements RequirementProvider {
public void setListener(RequirementListener listener) {
this.listener = listener;
}
@Override
public String getName() {
return "mock-requirement-provider";
}
}

View File

@@ -23,7 +23,7 @@ public class PersistentResult {
}
}
public void onRun() throws Throwable {
public void onRun() throws Exception {
synchronized (RAN_LOCK) {
this.ran = true;
}

View File

@@ -2,7 +2,7 @@ package org.whispersystems.jobqueue.util;
public interface RunnableThrowable {
public void run() throws Throwable;
public void run() throws Exception;
public void shouldThrow(Boolean value);
}

View File

@@ -21,6 +21,10 @@ import org.whispersystems.jobqueue.requirements.Requirement;
import java.io.Serializable;
import java.util.List;
/**
* An abstract class representing a unit of work that can be scheduled with
* the JobManager. This should be extended to implement tasks.
*/
public abstract class Job implements Serializable {
private final JobParameters parameters;
@@ -80,10 +84,33 @@ public abstract class Job implements Serializable {
this.runIteration = runIteration;
}
/**
* Called after a job has been added to the JobManager queue. If it's a persistent job,
* the state has been persisted to disk before this method is called.
*/
public abstract void onAdded();
public abstract void onRun() throws Throwable;
/**
* Called to actually execute the job.
* @throws Exception
*/
public abstract void onRun() throws Exception;
/**
* If onRun() throws an exception, this method will be called to determine whether the
* job should be retried.
*
* @param exception The exception onRun() threw.
* @return true if onRun() should be called again, false otherwise.
*/
public abstract boolean onShouldRetry(Exception exception);
/**
* Called if a job fails to run (onShouldRetry returned false, or the number of retries exceeded
* the job's configured retry count.
*/
public abstract void onCanceled();
public abstract boolean onShouldRetry(Throwable throwable);
}

View File

@@ -16,9 +16,13 @@
*/
package org.whispersystems.jobqueue;
import android.util.Log;
import org.whispersystems.jobqueue.persistence.PersistentStorage;
public class JobConsumer extends Thread {
class JobConsumer extends Thread {
private static final String TAG = JobConsumer.class.getSimpleName();
enum JobResult {
SUCCESS,
@@ -38,11 +42,12 @@ public class JobConsumer extends Thread {
@Override
public void run() {
while (true) {
Job job = jobQueue.getNext();
Job job = jobQueue.getNext();
JobResult result = runJob(job);
JobResult result;
if ((result = runJob(job)) != JobResult.DEFERRED) {
if (result == JobResult.DEFERRED) {
jobQueue.push(job);
} else {
if (result == JobResult.FAILURE) {
job.onCanceled();
}
@@ -50,8 +55,6 @@ public class JobConsumer extends Thread {
if (job.isPersistent()) {
persistentStorage.remove(job.getPersistentId());
}
} else {
jobQueue.add(job);
}
if (job.getGroupId() != null) {
@@ -68,8 +71,11 @@ public class JobConsumer extends Thread {
try {
job.onRun();
return JobResult.SUCCESS;
} catch (Throwable throwable) {
if (!job.onShouldRetry(throwable)) {
} catch (Exception exception) {
Log.w(TAG, exception);
if (exception instanceof RuntimeException) {
throw (RuntimeException)exception;
} else if (!job.onShouldRetry(exception)) {
return JobResult.FAILURE;
} else if (!job.isRequirementsMet()) {
job.setRunIteration(runIteration+1);

View File

@@ -19,34 +19,50 @@ package org.whispersystems.jobqueue;
import android.content.Context;
import android.util.Log;
import org.whispersystems.jobqueue.dependencies.DependencyInjector;
import org.whispersystems.jobqueue.persistence.JobSerializer;
import org.whispersystems.jobqueue.persistence.PersistentStorage;
import org.whispersystems.jobqueue.requirements.RequirementListener;
import org.whispersystems.jobqueue.requirements.RequirementProvider;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A JobManager allows you to enqueue {@link org.whispersystems.jobqueue.Job} tasks
* that are executed once a Job's {@link org.whispersystems.jobqueue.requirements.Requirement}s
* are met.
*/
public class JobManager implements RequirementListener {
private final JobQueue jobQueue = new JobQueue();
private final Executor eventExecutor = Executors.newSingleThreadExecutor();
private final AtomicBoolean hasLoadedEncrypted = new AtomicBoolean(false);
private final PersistentStorage persistentStorage;
private final PersistentStorage persistentStorage;
private final List<RequirementProvider> requirementProviders;
private final DependencyInjector dependencyInjector;
public JobManager(Context context, String name,
RequirementProvider requirementProvider,
JobSerializer jobSerializer, int consumers)
private JobManager(Context context, String name,
List<RequirementProvider> requirementProviders,
DependencyInjector dependencyInjector,
JobSerializer jobSerializer, int consumers)
{
this.persistentStorage = new PersistentStorage(context, name, jobSerializer);
this.persistentStorage = new PersistentStorage(context, name, jobSerializer, dependencyInjector);
this.requirementProviders = requirementProviders;
this.dependencyInjector = dependencyInjector;
eventExecutor.execute(new LoadTask(null));
if (requirementProvider != null) {
requirementProvider.setListener(this);
if (requirementProviders != null && !requirementProviders.isEmpty()) {
for (RequirementProvider provider : requirementProviders) {
provider.setListener(this);
}
}
for (int i=0;i<consumers;i++) {
@@ -54,12 +70,42 @@ public class JobManager implements RequirementListener {
}
}
/**
* @param context An Android {@link android.content.Context}.
* @return a {@link org.whispersystems.jobqueue.JobManager.Builder} used to construct a JobManager.
*/
public static Builder newBuilder(Context context) {
return new Builder(context);
}
/**
* Returns a {@link org.whispersystems.jobqueue.requirements.RequirementProvider} registered with
* the JobManager by name.
*
* @param name The name of the registered {@link org.whispersystems.jobqueue.requirements.RequirementProvider}
* @return The RequirementProvider, or null if no provider is registered with that name.
*/
public RequirementProvider getRequirementProvider(String name) {
for (RequirementProvider provider : requirementProviders) {
if (provider.getName().equals(name)) {
return provider;
}
}
return null;
}
public void setEncryptionKeys(EncryptionKeys keys) {
if (hasLoadedEncrypted.compareAndSet(false, true)) {
eventExecutor.execute(new LoadTask(keys));
}
}
/**
* Queue a {@link org.whispersystems.jobqueue.Job} to be executed.
*
* @param job The Job to be executed.
*/
public void add(final Job job) {
eventExecutor.execute(new Runnable() {
@Override
@@ -69,6 +115,10 @@ public class JobManager implements RequirementListener {
persistentStorage.store(job);
}
if (dependencyInjector != null) {
dependencyInjector.injectDependencies(job);
}
job.onAdded();
jobQueue.add(job);
} catch (IOException e) {
@@ -108,4 +158,96 @@ public class JobManager implements RequirementListener {
}
}
public static class Builder {
private final Context context;
private String name;
private List<RequirementProvider> requirementProviders;
private DependencyInjector dependencyInjector;
private JobSerializer jobSerializer;
private int consumerThreads;
Builder(Context context) {
this.context = context;
this.consumerThreads = 5;
}
/**
* A name for the {@link org.whispersystems.jobqueue.JobManager}. This is a required parameter,
* and is linked to the durable queue used by persistent jobs.
*
* @param name The name for the JobManager to build.
* @return The builder.
*/
public Builder withName(String name) {
this.name = name;
return this;
}
/**
* The {@link org.whispersystems.jobqueue.requirements.RequirementProvider}s to register with this
* JobManager. Optional. Each {@link org.whispersystems.jobqueue.requirements.Requirement} an
* enqueued Job depends on should have a matching RequirementProvider registered here.
*
* @param requirementProviders The RequirementProviders
* @return The builder.
*/
public Builder withRequirementProviders(RequirementProvider... requirementProviders) {
this.requirementProviders = Arrays.asList(requirementProviders);
return this;
}
/**
* The {@link org.whispersystems.jobqueue.dependencies.DependencyInjector} to use for injecting
* dependencies into {@link Job}s. Optional. Injection occurs just before a Job's onAdded() callback, or
* after deserializing a persistent job.
*
* @param dependencyInjector The injector to use.
* @return The builder.
*/
public Builder withDependencyInjector(DependencyInjector dependencyInjector) {
this.dependencyInjector = dependencyInjector;
return this;
}
/**
* The {@link org.whispersystems.jobqueue.persistence.JobSerializer} to use for persistent Jobs.
* Required if persistent Jobs are used.
*
* @param jobSerializer The serializer to use.
* @return The builder.
*/
public Builder withJobSerializer(JobSerializer jobSerializer) {
this.jobSerializer = jobSerializer;
return this;
}
/**
* Set the number of threads dedicated to consuming Jobs from the queue and executing them.
*
* @param consumerThreads The number of threads.
* @return The builder.
*/
public Builder withConsumerThreads(int consumerThreads) {
this.consumerThreads = consumerThreads;
return this;
}
/**
* @return A constructed JobManager.
*/
public JobManager build() {
if (name == null) {
throw new IllegalArgumentException("You must specify a name!");
}
if (requirementProviders == null) {
requirementProviders = new LinkedList<>();
}
return new JobManager(context, name, requirementProviders,
dependencyInjector, jobSerializer,
consumerThreads);
}
}
}

View File

@@ -22,6 +22,9 @@ import java.io.Serializable;
import java.util.LinkedList;
import java.util.List;
/**
* The set of parameters that describe a {@link org.whispersystems.jobqueue.Job}.
*/
public class JobParameters implements Serializable {
private transient EncryptionKeys encryptionKeys;
@@ -63,6 +66,9 @@ public class JobParameters implements Serializable {
return retryCount;
}
/**
* @return a builder used to construct JobParameters.
*/
public static Builder newBuilder() {
return new Builder();
}
@@ -78,31 +84,64 @@ public class JobParameters implements Serializable {
private int retryCount = 100;
private String groupId = null;
/**
* Specify a {@link org.whispersystems.jobqueue.requirements.Requirement }that must be met
* before the Job is executed. May be called multiple times to register multiple requirements.
* @param requirement The Requirement that must be met.
* @return the builder.
*/
public Builder withRequirement(Requirement requirement) {
this.requirements.add(requirement);
return this;
}
/**
* Specify that the Job should be durably persisted to disk, so that it remains in the queue
* across application restarts.
* @return The builder.
*/
public Builder withPersistence() {
this.isPersistent = true;
return this;
}
/**
* Specify that the job should use encryption when durably persisted to disk.
* @param encryptionKeys The keys to encrypt the serialized job with before persisting.
* @return the builder.
*/
public Builder withEncryption(EncryptionKeys encryptionKeys) {
this.encryptionKeys = encryptionKeys;
return this;
}
/**
* Specify how many times the job should be retried if execution fails but onShouldRetry() returns
* true.
*
* @param retryCount The number of times the job should be retried.
* @return the builder.
*/
public Builder withRetryCount(int retryCount) {
this.retryCount = retryCount;
return this;
}
/**
* Specify a groupId the job should belong to. Jobs with the same groupId are guaranteed to be
* executed serially.
*
* @param groupId The job's groupId.
* @return the builder.
*/
public Builder withGroupId(String groupId) {
this.groupId = groupId;
return this;
}
/**
* @return the JobParameters instance that describes a Job.
*/
public JobParameters create() {
return new JobParameters(requirements, isPersistent, groupId, encryptionKeys, retryCount);
}

View File

@@ -16,34 +16,36 @@
*/
package org.whispersystems.jobqueue;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
public class JobQueue {
class JobQueue {
private final Set<String> activeGroupIds = new HashSet<>();
private final LinkedList<Job> jobQueue = new LinkedList<>();
public synchronized void onRequirementStatusChanged() {
synchronized void onRequirementStatusChanged() {
notifyAll();
}
public synchronized void add(Job job) {
synchronized void add(Job job) {
jobQueue.add(job);
notifyAll();
}
public synchronized void addAll(List<Job> jobs) {
synchronized void addAll(List<Job> jobs) {
jobQueue.addAll(jobs);
notifyAll();
}
public synchronized Job getNext() {
synchronized void push(Job job) {
jobQueue.push(job);
}
synchronized Job getNext() {
try {
Job nextAvailableJob;
@@ -57,7 +59,7 @@ public class JobQueue {
}
}
public synchronized void setGroupIdAvailable(String groupId) {
synchronized void setGroupIdAvailable(String groupId) {
if (groupId != null) {
activeGroupIds.remove(groupId);
notifyAll();

View File

@@ -1,23 +1,27 @@
/**
* 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/>.
*/
* 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.jobqueue.dependencies;
import android.content.Context;
/**
* Any Job or Requirement that depends on {@link android.content.Context} can implement this
* interface to receive a Context after being deserialized.
*/
public interface ContextDependent {
public void setContext(Context context);
}

View File

@@ -0,0 +1,24 @@
/**
* 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.jobqueue.dependencies;
/**
* Interface responsible for injecting dependencies into Jobs.
*/
public interface DependencyInjector {
public void injectDependencies(Object object);
}

View File

@@ -16,13 +16,10 @@
*/
package org.whispersystems.jobqueue.persistence;
import android.content.Context;
import android.util.Base64;
import org.whispersystems.jobqueue.EncryptionKeys;
import org.whispersystems.jobqueue.Job;
import org.whispersystems.jobqueue.dependencies.ContextDependent;
import org.whispersystems.jobqueue.requirements.Requirement;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -30,13 +27,13 @@ import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
/**
* An implementation of {@link org.whispersystems.jobqueue.persistence.JobSerializer} that uses
* Java Serialization.
*/
public class JavaJobSerializer implements JobSerializer {
private final Context context;
public JavaJobSerializer(Context context) {
this.context = context;
}
public JavaJobSerializer() {}
@Override
public String serialize(Job job) throws IOException {
@@ -53,21 +50,7 @@ public class JavaJobSerializer implements JobSerializer {
ByteArrayInputStream bais = new ByteArrayInputStream(Base64.decode(serialized, Base64.NO_WRAP));
ObjectInputStream ois = new ObjectInputStream(bais);
Job job = (Job)ois.readObject();
if (job instanceof ContextDependent) {
((ContextDependent)job).setContext(context);
}
for (Requirement requirement : job.getRequirements()) {
if (requirement instanceof ContextDependent) {
((ContextDependent)requirement).setContext(context);
}
}
job.setEncryptionKeys(keys);
return job;
return (Job)ois.readObject();
} catch (ClassNotFoundException e) {
throw new IOException(e);
}

View File

@@ -21,9 +21,27 @@ import org.whispersystems.jobqueue.Job;
import java.io.IOException;
/**
* A JobSerializer is responsible for serializing and deserializing persistent jobs.
*/
public interface JobSerializer {
/**
* Serialize a job object into a string.
* @param job The Job to serialize.
* @return The serialized Job.
* @throws IOException if serialization fails.
*/
public String serialize(Job job) throws IOException;
/**
* Deserialize a String into a Job.
* @param keys Optional encryption keys that could have been used.
* @param encrypted True if the job was encrypted using the encryption keys.
* @param serialized The serialized Job.
* @return The deserialized Job.
* @throws IOException If the Job deserialization fails.
*/
public Job deserialize(EncryptionKeys keys, boolean encrypted, String serialized) throws IOException;
}

View File

@@ -25,6 +25,9 @@ import android.util.Log;
import org.whispersystems.jobqueue.EncryptionKeys;
import org.whispersystems.jobqueue.Job;
import org.whispersystems.jobqueue.dependencies.ContextDependent;
import org.whispersystems.jobqueue.dependencies.DependencyInjector;
import org.whispersystems.jobqueue.requirements.Requirement;
import java.io.IOException;
import java.util.LinkedList;
@@ -42,12 +45,19 @@ public class PersistentStorage {
private static final String DATABASE_CREATE = String.format("CREATE TABLE %s (%s INTEGER PRIMARY KEY, %s TEXT NOT NULL, %s INTEGER DEFAULT 0);",
TABLE_NAME, ID, ITEM, ENCRYPTED);
private final DatabaseHelper databaseHelper;
private final JobSerializer jobSerializer;
private final Context context;
private final DatabaseHelper databaseHelper;
private final JobSerializer jobSerializer;
private final DependencyInjector dependencyInjector;
public PersistentStorage(Context context, String name, JobSerializer serializer) {
this.databaseHelper = new DatabaseHelper(context, "_jobqueue-" + name);
this.jobSerializer = serializer;
public PersistentStorage(Context context, String name,
JobSerializer serializer,
DependencyInjector dependencyInjector)
{
this.databaseHelper = new DatabaseHelper(context, "_jobqueue-" + name);
this.context = context;
this.jobSerializer = serializer;
this.dependencyInjector = dependencyInjector;
}
public void store(Job job) throws IOException {
@@ -59,10 +69,6 @@ public class PersistentStorage {
job.setPersistentId(id);
}
// public List<Job> getAll(EncryptionKeys keys) {
// return getJobs(keys, null);
// }
public List<Job> getAllUnencrypted() {
return getJobs(null, ENCRYPTED + " = 0");
}
@@ -88,6 +94,9 @@ public class PersistentStorage {
Job job = jobSerializer.deserialize(keys, encrypted, item);
job.setPersistentId(id);
job.setEncryptionKeys(keys);
injectDependencies(job);
results.add(job);
} catch (IOException e) {
Log.w("PersistentStore", e);
@@ -102,12 +111,27 @@ public class PersistentStorage {
return results;
}
public void remove(long id) {
databaseHelper.getWritableDatabase()
.delete(TABLE_NAME, ID + " = ?", new String[] {String.valueOf(id)});
}
private void injectDependencies(Job job) {
if (job instanceof ContextDependent) {
((ContextDependent)job).setContext(context);
}
for (Requirement requirement : job.getRequirements()) {
if (requirement instanceof ContextDependent) {
((ContextDependent)requirement).setContext(context);
}
}
if (dependencyInjector != null) {
dependencyInjector.injectDependencies(job);
}
}
private static class DatabaseHelper extends SQLiteOpenHelper {
public DatabaseHelper(Context context, String name) {

View File

@@ -22,6 +22,9 @@ import android.net.NetworkInfo;
import org.whispersystems.jobqueue.dependencies.ContextDependent;
/**
* A requirement that is satisfied when a network connection is present.
*/
public class NetworkRequirement implements Requirement, ContextDependent {
private transient Context context;

View File

@@ -46,6 +46,11 @@ public class NetworkRequirementProvider implements RequirementProvider {
}, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
}
@Override
public String getName() {
return "network";
}
@Override
public void setListener(RequirementListener listener) {
this.listener = listener;

View File

@@ -18,6 +18,12 @@ package org.whispersystems.jobqueue.requirements;
import java.io.Serializable;
/**
* A Requirement that must be satisfied before a Job can run.
*/
public interface Requirement extends Serializable {
/**
* @return true if the requirement is satisfied, false otherwise.
*/
public boolean isPresent();
}

View File

@@ -16,6 +16,22 @@
*/
package org.whispersystems.jobqueue.requirements;
/**
* Notifies listeners when a {@link org.whispersystems.jobqueue.requirements.Requirement}'s
* state is likely to have changed.
*/
public interface RequirementProvider {
/**
* @return The name of the provider.
*/
public String getName();
/**
* The {@link org.whispersystems.jobqueue.requirements.RequirementListener} to call when
* a {@link org.whispersystems.jobqueue.requirements.Requirement}'s status is likely to
* have changed.
*
* @param listener The listener to call.
*/
public void setListener(RequirementListener listener);
}

View File

@@ -4,7 +4,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:0.12.+'
classpath 'com.android.tools.build:gradle:0.14.2'
}
}
@@ -19,8 +19,8 @@ dependencies {
}
android {
compileSdkVersion 19
buildToolsVersion '19.1.0'
compileSdkVersion 21
buildToolsVersion '21.1.1'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7

View File

@@ -274,6 +274,17 @@ public class SessionCipher {
}
}
public int getSessionVersion() {
synchronized (SESSION_LOCK) {
if (!sessionStore.containsSession(recipientId, deviceId)) {
throw new IllegalStateException(String.format("No session for (%d, %d)!", recipientId, deviceId));
}
SessionRecord record = sessionStore.loadSession(recipientId, deviceId);
return record.getSessionState().getSessionVersion();
}
}
private ChainKey getOrCreateChainKey(SessionState sessionState, ECPublicKey theirEphemeral)
throws InvalidMessageException
{

View File

@@ -1,92 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="library" default="help">
<!-- The local.properties file is created and updated by the 'android' tool.
It contains the path to the SDK. It should *NOT* be checked into
Version Control Systems. -->
<property file="local.properties"/>
<!-- The ant.properties file can be created by you. It is only edited by the
'android' tool to add properties to it.
This is the place to change some Ant specific build properties.
Here are some properties you may want to change/update:
source.dir
The name of the source directory. Default is 'src'.
out.dir
The name of the output directory. Default is 'bin'.
For other overridable properties, look at the beginning of the rules
files in the SDK, at tools/ant/build.xml
Properties related to the SDK location or the project target should
be updated using the 'android' tool with the 'update' action.
This file is an integral part of the build system for your
application and should be checked into Version Control Systems.
-->
<property file="ant.properties"/>
<!-- if sdk.dir was not set from one of the property file, then
get it from the ANDROID_HOME env var.
This must be done before we load project.properties since
the proguard config can use sdk.dir -->
<property environment="env"/>
<condition property="sdk.dir" value="${env.ANDROID_HOME}">
<isset property="env.ANDROID_HOME"/>
</condition>
<!-- The project.properties file is created and updated by the 'android'
tool, as well as ADT.
This contains project specific properties such as project target, and library
dependencies. Lower level build properties are stored in ant.properties
(or in .classpath for Eclipse projects).
This file is an integral part of the build system for your
application and should be checked into Version Control Systems. -->
<loadproperties srcFile="project.properties"/>
<!-- quick check on sdk.dir -->
<fail
message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
unless="sdk.dir"
/>
<!--
Import per project custom build rules if present at the root of the project.
This is the place to put custom intermediary targets such as:
-pre-build
-pre-compile
-post-compile (This is typically used for code obfuscation.
Compiled code location: ${out.classes.absolute.dir}
If this is not done in place, override ${out.dex.input.absolute.dir})
-post-package
-post-build
-pre-clean
-->
<import file="custom_rules.xml" optional="true"/>
<!-- Import the actual build file.
To customize existing targets, there are two options:
- Customize only one target:
- copy/paste the target into this file, *before* the
<import> task.
- customize it to your needs.
- Customize the whole content of build.xml
- copy/paste the content of the rules files (minus the top node)
into this file, replacing the <import> task.
- customize to your needs.
***********************
****** IMPORTANT ******
***********************
In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
in order to avoid having your file be overridden by tools such as "android update project"
-->
<!-- version-tag: 1 -->
<import file="${sdk.dir}/tools/ant/build.xml"/>
</project>

View File

@@ -1,20 +0,0 @@
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View File

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

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -1,161 +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.crypto;
import android.util.Log;
import org.whispersystems.libaxolotl.InvalidMacException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util;
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;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.Arrays;
/**
* Encrypts push attachments.
*
* @author Moxie Marlinspike
*/
public class AttachmentCipher {
static final int CIPHER_KEY_SIZE = 32;
static final int MAC_KEY_SIZE = 32;
private final SecretKeySpec cipherKey;
private final SecretKeySpec macKey;
private final Cipher cipher;
private final Mac mac;
public AttachmentCipher() {
this.cipherKey = initializeRandomCipherKey();
this.macKey = initializeRandomMacKey();
this.cipher = initializeCipher();
this.mac = initializeMac();
}
public AttachmentCipher(byte[] combinedKeyMaterial) {
byte[][] parts = Util.split(combinedKeyMaterial, CIPHER_KEY_SIZE, MAC_KEY_SIZE);
this.cipherKey = new SecretKeySpec(parts[0], "AES");
this.macKey = new SecretKeySpec(parts[1], "HmacSHA256");
this.cipher = initializeCipher();
this.mac = initializeMac();
}
public byte[] getCombinedKeyMaterial() {
return Util.combine(this.cipherKey.getEncoded(), this.macKey.getEncoded());
}
public byte[] encrypt(byte[] plaintext) {
try {
this.cipher.init(Cipher.ENCRYPT_MODE, this.cipherKey);
this.mac.init(this.macKey);
byte[] ciphertext = this.cipher.doFinal(plaintext);
byte[] iv = this.cipher.getIV();
byte[] mac = this.mac.doFinal(Util.combine(iv, ciphertext));
return Util.combine(iv, ciphertext, mac);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
public byte[] decrypt(byte[] ciphertext)
throws InvalidMacException, InvalidMessageException
{
try {
if (ciphertext.length <= cipher.getBlockSize() + mac.getMacLength()) {
throw new InvalidMessageException("Message too short!");
}
byte[][] ciphertextParts = Util.split(ciphertext,
this.cipher.getBlockSize(),
ciphertext.length - this.cipher.getBlockSize() - this.mac.getMacLength(),
this.mac.getMacLength());
this.mac.update(ciphertext, 0, ciphertext.length - mac.getMacLength());
byte[] ourMac = this.mac.doFinal();
if (!Arrays.equals(ourMac, ciphertextParts[2])) {
throw new InvalidMacException("Mac doesn't match!");
}
this.cipher.init(Cipher.DECRYPT_MODE, this.cipherKey,
new IvParameterSpec(ciphertextParts[0]));
return cipher.doFinal(ciphertextParts[1]);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
} catch (InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new InvalidMessageException(e);
} catch (ParseException e) {
throw new InvalidMessageException(e);
}
}
private Mac initializeMac() {
try {
Mac mac = Mac.getInstance("HmacSHA256");
return mac;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private Cipher initializeCipher() {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
return cipher;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (NoSuchPaddingException e) {
throw new AssertionError(e);
}
}
private SecretKeySpec initializeRandomCipherKey() {
byte[] key = new byte[CIPHER_KEY_SIZE];
Util.getSecureRandom().nextBytes(key);
return new SecretKeySpec(key, "AES");
}
private SecretKeySpec initializeRandomMacKey() {
byte[] key = new byte[MAC_KEY_SIZE];
Util.getSecureRandom().nextBytes(key);
return new SecretKeySpec(key, "HmacSHA256");
}
}

View File

@@ -1,86 +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.directory;
import org.whispersystems.textsecure.util.Conversions;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* A simple bloom filter implementation that backs the RedPhone directory.
*
* @author Moxie Marlinspike
*
*/
public class BloomFilter {
private final MappedByteBuffer buffer;
private final long length;
private final int hashCount;
public BloomFilter(File bloomFilter, int hashCount)
throws IOException
{
this.length = bloomFilter.length();
this.buffer = new FileInputStream(bloomFilter).getChannel()
.map(FileChannel.MapMode.READ_ONLY, 0, length);
this.hashCount = hashCount;
}
public int getHashCount() {
return hashCount;
}
private boolean isBitSet(long bitIndex) {
int byteInQuestion = this.buffer.get((int)(bitIndex / 8));
int bitOffset = (0x01 << (bitIndex % 8));
return (byteInQuestion & bitOffset) > 0;
}
public boolean contains(String entity) {
try {
for (int i=0;i<this.hashCount;i++) {
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(new SecretKeySpec((i+"").getBytes(), "HmacSHA1"));
byte[] hashValue = mac.doFinal(entity.getBytes());
long bitIndex = Math.abs(Conversions.byteArrayToLong(hashValue, 0)) % (this.length * 8);
if (!isBitSet(bitIndex))
return false;
}
return true;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -1,26 +0,0 @@
package org.whispersystems.textsecure.directory;
public class DirectoryDescriptor {
private String version;
private long capacity;
private int hashCount;
public String getUrl() {
return url;
}
public int getHashCount() {
return hashCount;
}
public long getCapacity() {
return capacity;
}
public String getVersion() {
return version;
}
private String url;
}

View File

@@ -1,225 +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.directory;
import android.content.Context;
import android.util.Log;
import com.google.thoughtcrimegson.Gson;
import com.google.thoughtcrimegson.JsonParseException;
import com.google.thoughtcrimegson.annotations.SerializedName;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.List;
import java.util.zip.GZIPInputStream;
/**
* Handles providing lookups, serializing, and deserializing the RedPhone directory.
*
* @author Moxie Marlinspike
*
*/
public class NumberFilter {
private static NumberFilter instance;
public synchronized static NumberFilter getInstance(Context context) {
if (instance == null)
instance = NumberFilter.deserializeFromFile(context);
return instance;
}
private static final String DIRECTORY_META_FILE = "directory.stat";
private File bloomFilter;
private String version;
private long capacity;
private int hashCount;
private Context context;
private NumberFilter(Context context, File bloomFilter, long capacity,
int hashCount, String version)
{
this.context = context.getApplicationContext();
this.bloomFilter = bloomFilter;
this.capacity = capacity;
this.hashCount = hashCount;
this.version = version;
}
public synchronized boolean containsNumber(String number) {
try {
if (bloomFilter == null) return false;
else if (number == null || number.length() == 0) return false;
return new BloomFilter(bloomFilter, hashCount).contains(number);
} catch (IOException ioe) {
Log.w("NumberFilter", ioe);
return false;
}
}
public synchronized boolean containsNumbers(List<String> numbers) {
try {
if (bloomFilter == null) return false;
if (numbers == null || numbers.size() == 0) return false;
BloomFilter filter = new BloomFilter(bloomFilter, hashCount);
for (String number : numbers) {
if (!filter.contains(number)) {
return false;
}
}
return true;
} catch (IOException ioe) {
Log.w("NumberFilter", ioe);
return false;
}
}
public synchronized void update(DirectoryDescriptor descriptor, File compressedData) {
try {
File uncompressed = File.createTempFile("directory", ".dat", context.getFilesDir());
FileInputStream fin = new FileInputStream (compressedData);
GZIPInputStream gin = new GZIPInputStream(fin);
FileOutputStream out = new FileOutputStream(uncompressed);
byte[] buffer = new byte[4096];
int read;
while ((read = gin.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
out.close();
compressedData.delete();
update(uncompressed, descriptor.getCapacity(), descriptor.getHashCount(), descriptor.getVersion());
} catch (IOException ioe) {
Log.w("NumberFilter", ioe);
}
}
private synchronized void update(File bloomFilter, long capacity, int hashCount, String version)
{
if (this.bloomFilter != null)
this.bloomFilter.delete();
this.bloomFilter = bloomFilter;
this.capacity = capacity;
this.hashCount = hashCount;
this.version = version;
serializeToFile(context);
}
private void serializeToFile(Context context) {
if (this.bloomFilter == null)
return;
try {
FileOutputStream fout = context.openFileOutput(DIRECTORY_META_FILE, 0);
NumberFilterStorage storage = new NumberFilterStorage(bloomFilter.getAbsolutePath(),
capacity, hashCount, version);
storage.serializeToStream(fout);
fout.close();
} catch (IOException ioe) {
Log.w("NumberFilter", ioe);
}
}
private static NumberFilter deserializeFromFile(Context context) {
try {
FileInputStream fis = context.openFileInput(DIRECTORY_META_FILE);
NumberFilterStorage storage = NumberFilterStorage.fromStream(fis);
if (storage == null) return new NumberFilter(context, null, 0, 0, "0");
else return new NumberFilter(context,
new File(storage.getDataPath()),
storage.getCapacity(),
storage.getHashCount(),
storage.getVersion());
} catch (IOException ioe) {
Log.w("NumberFilter", ioe);
return new NumberFilter(context, null, 0, 0, "0");
}
}
private static class NumberFilterStorage {
@SerializedName("data_path")
private String dataPath;
@SerializedName("capacity")
private long capacity;
@SerializedName("hash_count")
private int hashCount;
@SerializedName("version")
private String version;
public NumberFilterStorage(String dataPath, long capacity, int hashCount, String version) {
this.dataPath = dataPath;
this.capacity = capacity;
this.hashCount = hashCount;
this.version = version;
}
public String getDataPath() {
return dataPath;
}
public long getCapacity() {
return capacity;
}
public int getHashCount() {
return hashCount;
}
public String getVersion() {
return version;
}
public void serializeToStream(OutputStream out) throws IOException {
out.write(new Gson().toJson(this).getBytes());
}
public static NumberFilterStorage fromStream(InputStream in) throws IOException {
try {
return new Gson().fromJson(new BufferedReader(new InputStreamReader(in)),
NumberFilterStorage.class);
} catch (JsonParseException jpe) {
Log.w("NumberFilter", jpe);
throw new IOException("JSON Parse Exception");
}
}
}
}

View File

@@ -1,28 +0,0 @@
package org.whispersystems.textsecure.push;
public class AccountAttributes {
private String signalingKey;
private boolean supportsSms;
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,33 +0,0 @@
package org.whispersystems.textsecure.push;
import com.google.thoughtcrimegson.Gson;
public class ContactTokenDetails {
private String token;
private String relay;
private String number;
private boolean supportsSms;
public ContactTokenDetails() {}
public String getToken() {
return token;
}
public String getRelay() {
return relay;
}
public boolean isSupportsSms() {
return supportsSms;
}
public void setNumber(String number) {
this.number = number;
}
public String getNumber() {
return number;
}
}

View File

@@ -1,14 +0,0 @@
package org.whispersystems.textsecure.push;
import java.util.List;
public class ContactTokenDetailsList {
private List<ContactTokenDetails> contacts;
public ContactTokenDetailsList() {}
public List<ContactTokenDetails> getContacts() {
return contacts;
}
}

View File

@@ -1,18 +0,0 @@
package org.whispersystems.textsecure.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,149 +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.push;
import android.os.Parcel;
import android.os.Parcelable;
import org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal;
public class IncomingPushMessage implements Parcelable {
public static final Parcelable.Creator<IncomingPushMessage> CREATOR = new Parcelable.Creator<IncomingPushMessage>() {
@Override
public IncomingPushMessage createFromParcel(Parcel in) {
return new IncomingPushMessage(in);
}
@Override
public IncomingPushMessage[] newArray(int size) {
return new IncomingPushMessage[size];
}
};
private int type;
private String source;
private int sourceDevice;
private byte[] message;
private long timestamp;
private String relay;
private IncomingPushMessage(IncomingPushMessage message, byte[] body) {
this.type = message.type;
this.source = message.source;
this.sourceDevice = message.sourceDevice;
this.timestamp = message.timestamp;
this.relay = message.relay;
this.message = body;
}
public IncomingPushMessage(IncomingPushMessageSignal signal) {
this.type = signal.getType().getNumber();
this.source = signal.getSource();
this.sourceDevice = signal.getSourceDevice();
this.message = signal.getMessage().toByteArray();
this.timestamp = signal.getTimestamp();
this.relay = signal.getRelay();
}
public IncomingPushMessage(Parcel in) {
this.type = in.readInt();
this.source = in.readString();
this.sourceDevice = in.readInt();
if (in.readInt() == 1) {
this.relay = in.readString();
}
this.message = new byte[in.readInt()];
in.readByteArray(this.message);
this.timestamp = in.readLong();
}
public IncomingPushMessage(int type, String source, int sourceDevice,
byte[] body, long timestamp)
{
this.type = type;
this.source = source;
this.sourceDevice = sourceDevice;
this.message = body;
this.timestamp = timestamp;
}
public String getRelay() {
return relay;
}
public long getTimestampMillis() {
return timestamp;
}
public String getSource() {
return source;
}
public int getSourceDevice() {
return sourceDevice;
}
public byte[] getBody() {
return message;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(type);
dest.writeString(source);
dest.writeInt(sourceDevice);
dest.writeInt(relay == null ? 0 : 1);
if (relay != null) {
dest.writeString(relay);
}
dest.writeInt(message.length);
dest.writeByteArray(message);
dest.writeLong(timestamp);
}
public IncomingPushMessage withBody(byte[] body) {
return new IncomingPushMessage(this, body);
}
public int getType() {
return type;
}
public boolean isSecureMessage() {
return getType() == IncomingPushMessageSignal.Type.CIPHERTEXT_VALUE;
}
public boolean isPreKeyBundle() {
return getType() == IncomingPushMessageSignal.Type.PREKEY_BUNDLE_VALUE;
}
public boolean isReceipt() {
return getType() == IncomingPushMessageSignal.Type.RECEIPT_VALUE;
}
public boolean isPlaintext() {
return getType() == IncomingPushMessageSignal.Type.PLAINTEXT_VALUE;
}
}

View File

@@ -1,17 +0,0 @@
package org.whispersystems.textsecure.push;
import java.util.List;
public class MismatchedDevices {
private List<Integer> missingDevices;
private List<Integer> extraDevices;
public List<Integer> getMissingDevices() {
return missingDevices;
}
public List<Integer> getExtraDevices() {
return extraDevices;
}
}

View File

@@ -1,39 +0,0 @@
package org.whispersystems.textsecure.push;
import java.util.List;
public class OutgoingPushMessageList {
private String destination;
private String relay;
private long timestamp;
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,31 +0,0 @@
package org.whispersystems.textsecure.push;
import com.google.thoughtcrimegson.GsonBuilder;
public class PreKeyResponseItem {
private int deviceId;
private int registrationId;
private SignedPreKeyEntity signedPreKey;
private PreKeyEntity preKey;
public int getDeviceId() {
return deviceId;
}
public int getRegistrationId() {
return registrationId;
}
public SignedPreKeyEntity getSignedPreKey() {
return signedPreKey;
}
public PreKeyEntity getPreKey() {
return preKey;
}
public static GsonBuilder forBuilder(GsonBuilder builder) {
return SignedPreKeyEntity.forBuilder(builder);
}
}

View File

@@ -1,12 +0,0 @@
package org.whispersystems.textsecure.push;
public class PreKeyStatus {
private int count;
public PreKeyStatus() {}
public int getCount() {
return count;
}
}

View File

@@ -1,32 +0,0 @@
package org.whispersystems.textsecure.push;
import android.content.Context;
import org.whispersystems.textsecure.directory.Directory;
import org.whispersystems.textsecure.storage.RecipientDevice;
public class PushAddress extends RecipientDevice {
private final String e164number;
private final String relay;
private PushAddress(long recipientId, String e164number, int deviceId, String relay) {
super(recipientId, deviceId);
this.e164number = e164number;
this.relay = relay;
}
public String getNumber() {
return e164number;
}
public String getRelay() {
return relay;
}
public static PushAddress create(Context context, long recipientId, String e164number, int deviceId) {
String relay = Directory.getInstance(context).getRelay(e164number);
return new PushAddress(recipientId, e164number, deviceId, relay);
}
}

View File

@@ -1,21 +0,0 @@
package org.whispersystems.textsecure.push;
public class PushAttachmentData {
private final String contentType;
private final byte[] data;
public PushAttachmentData(String contentType, byte[] data) {
this.contentType = contentType;
this.data = data;
}
public String getContentType() {
return contentType;
}
public byte[] getData() {
return data;
}
}

View File

@@ -1,63 +0,0 @@
package org.whispersystems.textsecure.push;
import android.os.Parcel;
import android.os.Parcelable;
public class PushAttachmentPointer implements Parcelable {
public static final Parcelable.Creator<PushAttachmentPointer> CREATOR = new Parcelable.Creator<PushAttachmentPointer>() {
@Override
public PushAttachmentPointer createFromParcel(Parcel in) {
return new PushAttachmentPointer(in);
}
@Override
public PushAttachmentPointer[] newArray(int size) {
return new PushAttachmentPointer[size];
}
};
private final String contentType;
private final long id;
private final byte[] key;
public PushAttachmentPointer(String contentType, long id, byte[] key) {
this.contentType = contentType;
this.id = id;
this.key = key;
}
public PushAttachmentPointer(Parcel in) {
this.contentType = in.readString();
this.id = in.readLong();
int keyLength = in.readInt();
this.key = new byte[keyLength];
in.readByteArray(this.key);
}
public String getContentType() {
return contentType;
}
public long getId() {
return id;
}
public byte[] getKey() {
return key;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(contentType);
dest.writeLong(id);
dest.writeInt(this.key.length);
dest.writeByteArray(this.key);
}
}

View File

@@ -1,26 +0,0 @@
package org.whispersystems.textsecure.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,18 +0,0 @@
package org.whispersystems.textsecure.push;
import java.util.List;
public class PushMessageResponse {
private List<String> success;
private List<String> failure;
public List<String> getSuccess() {
return success;
}
public List<String> getFailure() {
return failure;
}
}

View File

@@ -1,27 +0,0 @@
package org.whispersystems.textsecure.push;
import org.whispersystems.textsecure.crypto.TransportDetails;
import java.io.IOException;
public class RawTransportDetails implements TransportDetails {
@Override
public byte[] getStrippedPaddingMessageBody(byte[] messageWithPadding) {
return messageWithPadding;
}
@Override
public byte[] getPaddedMessageBody(byte[] messageBody) {
return messageBody;
}
@Override
public byte[] getEncodedMessage(byte[] messageWithMac) {
return messageWithMac;
}
@Override
public byte[] getDecodedMessage(byte[] encodedMessageBytes) throws IOException {
return encodedMessageBytes;
}
}

View File

@@ -1,12 +0,0 @@
package org.whispersystems.textsecure.push;
import java.util.List;
public class StaleDevices {
private List<Integer> staleDevices;
public List<Integer> getStaleDevices() {
return staleDevices;
}
}

View File

@@ -1,18 +0,0 @@
package org.whispersystems.textsecure.push;
import java.io.IOException;
import java.util.List;
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.push.exceptions;
public class AuthorizationFailedException extends NonSuccessfulResponseCodeException {
public AuthorizationFailedException(String s) {
super(s);
}
}

View File

@@ -1,4 +0,0 @@
package org.whispersystems.textsecure.push.exceptions;
public class ExpectationFailedException extends NonSuccessfulResponseCodeException {
}

View File

@@ -1,16 +0,0 @@
package org.whispersystems.textsecure.push.exceptions;
import org.whispersystems.textsecure.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,14 +0,0 @@
package org.whispersystems.textsecure.push.exceptions;
import java.io.IOException;
public class NonSuccessfulResponseCodeException extends IOException {
public NonSuccessfulResponseCodeException() {
super();
}
public NonSuccessfulResponseCodeException(String s) {
super(s);
}
}

View File

@@ -1,9 +0,0 @@
package org.whispersystems.textsecure.push.exceptions;
import org.whispersystems.textsecure.push.exceptions.NonSuccessfulResponseCodeException;
public class NotFoundException extends NonSuccessfulResponseCodeException {
public NotFoundException(String s) {
super(s);
}
}

View File

@@ -1,9 +0,0 @@
package org.whispersystems.textsecure.push.exceptions;
import java.io.IOException;
public class PushNetworkException extends IOException {
public PushNetworkException(Exception exception) {
super(exception);
}
}

View File

@@ -1,10 +0,0 @@
package org.whispersystems.textsecure.push.exceptions;
import org.whispersystems.textsecure.push.exceptions.NonSuccessfulResponseCodeException;
public class RateLimitException extends NonSuccessfulResponseCodeException {
public RateLimitException(String s) {
super(s);
}
}

View File

@@ -1,17 +0,0 @@
package org.whispersystems.textsecure.push.exceptions;
import org.whispersystems.textsecure.push.StaleDevices;
import org.whispersystems.textsecure.push.exceptions.NonSuccessfulResponseCodeException;
public class StaleDevicesException extends NonSuccessfulResponseCodeException {
private final StaleDevices staleDevices;
public StaleDevicesException(StaleDevices staleDevices) {
this.staleDevices = staleDevices;
}
public StaleDevices getStaleDevices() {
return staleDevices;
}
}

View File

@@ -1,6 +0,0 @@
package org.whispersystems.textsecure.storage;
public interface CanonicalRecipient {
// public String getNumber();
public long getRecipientId();
}

View File

@@ -1,31 +0,0 @@
package org.whispersystems.textsecure.storage;
public class RecipientDevice {
public static final int DEFAULT_DEVICE_ID = 1;
private final long recipientId;
private final int deviceId;
public RecipientDevice(long recipientId, int deviceId) {
this.recipientId = recipientId;
this.deviceId = deviceId;
}
public long getRecipientId() {
return recipientId;
}
public int getDeviceId() {
return deviceId;
}
public CanonicalRecipient getRecipient() {
return new CanonicalRecipient() {
@Override
public long getRecipientId() {
return recipientId;
}
};
}
}

View File

@@ -1,41 +0,0 @@
package org.whispersystems.textsecure.storage;
import android.content.Context;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.textsecure.crypto.MasterSecret;
public class SessionUtil {
public static int getSessionVersion(Context context,
MasterSecret masterSecret,
RecipientDevice recipient)
{
return
new TextSecureSessionStore(context, masterSecret)
.loadSession(recipient.getRecipientId(), recipient.getDeviceId())
.getSessionState()
.getSessionVersion();
}
public static boolean hasEncryptCapableSession(Context context,
MasterSecret masterSecret,
CanonicalRecipient recipient)
{
return hasEncryptCapableSession(context, masterSecret,
new RecipientDevice(recipient.getRecipientId(),
RecipientDevice.DEFAULT_DEVICE_ID));
}
public static boolean hasEncryptCapableSession(Context context,
MasterSecret masterSecret,
RecipientDevice recipientDevice)
{
long recipientId = recipientDevice.getRecipientId();
int deviceId = recipientDevice.getDeviceId();
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
return sessionStore.containsSession(recipientId, deviceId);
}
}

View File

@@ -1,6 +0,0 @@
package org.whispersystems.textsecure.util;
public interface FutureTaskListener<V> {
public void onSuccess(V result);
public void onFailure(Throwable error);
}

View File

@@ -1,7 +0,0 @@
package org.whispersystems.textsecure.util;
public class InvalidNumberException extends Throwable {
public InvalidNumberException(String s) {
super(s);
}
}

View File

@@ -1,52 +0,0 @@
package org.whispersystems.textsecure.util;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ListenableFutureTask<V> extends FutureTask<V> {
// private WeakReference<FutureTaskListener<V>> listener;
private FutureTaskListener<V> listener;
public ListenableFutureTask(Callable<V> callable, FutureTaskListener<V> listener) {
super(callable);
this.listener = listener;
// if (listener == null) {
// this.listener = null;
// } else {
// this.listener = new WeakReference<FutureTaskListener<V>>(listener);
// }
}
public synchronized void setListener(FutureTaskListener<V> listener) {
// if (listener != null) this.listener = new WeakReference<FutureTaskListener<V>>(listener);
// else this.listener = null;
this.listener = listener;
if (this.isDone()) {
callback();
}
}
@Override
protected synchronized void done() {
callback();
}
private void callback() {
if (this.listener != null) {
FutureTaskListener<V> nestedListener = this.listener;
// FutureTaskListener<V> nestedListener = this.listener.get();
if (nestedListener != null) {
try {
nestedListener.onSuccess(get());
} catch (ExecutionException ee) {
nestedListener.onFailure(ee);
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
}
}
}

View File

@@ -1,214 +0,0 @@
package org.whispersystems.textsecure.util;
import android.content.Context;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.DrawableContainer;
import android.graphics.drawable.StateListDrawable;
import android.telephony.TelephonyManager;
import android.view.View;
import android.widget.EditText;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.text.ParseException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
public class Util {
public static byte[] combine(byte[]... elements) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
for (byte[] element : elements) {
baos.write(element);
}
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[][] split(byte[] input, int firstLength, int secondLength, int thirdLength)
throws ParseException
{
if (input == null || firstLength < 0 || secondLength < 0 || thirdLength < 0 ||
input.length < firstLength + secondLength + thirdLength)
{
throw new ParseException("Input too small: " + (input == null ? null : Hex.toString(input)), 0);
}
byte[][] parts = new byte[3][];
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);
parts[2] = new byte[thirdLength];
System.arraycopy(input, firstLength + secondLength, parts[2], 0, thirdLength);
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 boolean isEmpty(EditText value) {
return value == null || value.getText() == null || isEmpty(value.getText().toString());
}
public static boolean isEmpty(CharSequence value) {
return value == null || value.length() == 0;
}
public static String getSecret(int size) {
try {
byte[] secret = new byte[size];
SecureRandom.getInstance("SHA1PRNG").nextBytes(secret);
return Base64.encodeBytes(secret);
} catch (NoSuchAlgorithmException nsae) {
throw new AssertionError(nsae);
}
}
public static String readFully(File file) throws IOException {
return readFully(new FileInputStream(file));
}
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 String join(Collection<String> list, String delimiter) {
StringBuilder result = new StringBuilder();
int i=0;
for (String item : list) {
result.append(item);
if (++i < list.size())
result.append(delimiter);
}
return result.toString();
}
public static List<String> split(String source, String delimiter) {
List<String> results = new LinkedList<String>();
if (isEmpty(source)) {
return results;
}
String[] elements = source.split(delimiter);
for (String element : elements) {
results.add(element);
}
return results;
}
public static String getDeviceE164Number(Context context) {
String localNumber = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE))
.getLine1Number();
if (!org.whispersystems.textsecure.util.Util.isEmpty(localNumber) &&
!localNumber.startsWith("+"))
{
if (localNumber.length() == 10) localNumber = "+1" + localNumber;
else localNumber = "+" + localNumber;
return localNumber;
}
return null;
}
public static SecureRandom getSecureRandom() {
try {
return SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
/*
* source: http://stackoverflow.com/a/9500334
*/
public static void fixBackgroundRepeat(Drawable bg) {
if (bg != null) {
if (bg instanceof BitmapDrawable) {
BitmapDrawable bmp = (BitmapDrawable) bg;
bmp.mutate();
bmp.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
}
}
}
}

Binary file not shown.

View File

@@ -4,7 +4,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:0.12.2'
classpath 'com.android.tools.build:gradle:0.14.2'
}
}
@@ -20,7 +20,7 @@ repositories {
dependencies {
compile 'com.google.protobuf:protobuf-java:2.5.0'
compile 'com.madgag:sc-light-jdk15on:1.47.0.2'
// compile 'com.madgag:sc-light-jdk15on:1.47.0.2'
compile 'com.googlecode.libphonenumber:libphonenumber:6.1'
compile 'org.whispersystems:gson:2.2.4'
@@ -28,34 +28,13 @@ dependencies {
}
android {
compileSdkVersion 19
buildToolsVersion '19.1.0'
compileSdkVersion 21
buildToolsVersion '21.1.1'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
android {
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
resources.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
jniLibs.srcDirs = ['libs']
}
androidTest {
java.srcDirs = ['androidTest/java']
resources.srcDirs = ['androidTest/java']
aidl.srcDirs = ['androidTest/java']
renderscript.srcDirs = ['androidTest/java']
}
}
}
}
tasks.whenTaskAdded { task ->
@@ -66,7 +45,7 @@ tasks.whenTaskAdded { task ->
version '0.1'
group 'org.whispersystems.textsecure'
archivesBaseName = 'textsecure-library'
archivesBaseName = 'libtextsecure'
uploadArchives {
repositories {

View File

@@ -1,6 +1,6 @@
package textsecure;
option java_package = "org.whispersystems.textsecure.push";
option java_package = "org.whispersystems.textsecure.internal.push";
option java_outer_classname = "PushMessageProtos";
message IncomingPushMessageSignal {

View File

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

View File

@@ -1,11 +1,8 @@
package org.whispersystems.textsecure.push;
import android.test.AndroidTestCase;
import android.util.Base64;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import org.whispersystems.textsecure.internal.push.PushTransportDetails;
public class PushTransportDetailsTest extends AndroidTestCase {
@@ -35,23 +32,4 @@ public class PushTransportDetailsTest extends AndroidTestCase {
assertTrue(transportV2.getPaddedMessageBody(message).length == message.length);
}
}
public void testV3Encoding() throws NoSuchAlgorithmException {
byte[] message = new byte[501];
SecureRandom.getInstance("SHA1PRNG").nextBytes(message);
byte[] padded = transportV3.getEncodedMessage(message);
assertTrue(Arrays.equals(padded, message));
}
public void testV2Encoding() throws NoSuchAlgorithmException {
byte[] message = new byte[501];
SecureRandom.getInstance("SHA1PRNG").nextBytes(message);
byte[] padded = transportV2.getEncodedMessage(message);
assertTrue(Arrays.equals(padded, message));
}
}

View File

@@ -0,0 +1,93 @@
/**
* 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.IdentityKey;
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.TrustStore;
import org.whispersystems.textsecure.internal.push.PushServiceSocket;
import org.whispersystems.textsecure.api.push.SignedPreKeyEntity;
import java.io.IOException;
import java.util.List;
import java.util.Set;
public class TextSecureAccountManager {
private final PushServiceSocket pushServiceSocket;
public TextSecureAccountManager(String url, TrustStore trustStore,
String user, String password)
{
this.pushServiceSocket = new PushServiceSocket(url, trustStore, user, password);
}
public void setGcmId(Optional<String> gcmRegistrationId) throws IOException {
if (gcmRegistrationId.isPresent()) {
this.pushServiceSocket.registerGcmId(gcmRegistrationId.get());
} else {
this.pushServiceSocket.unregisterGcmId();
}
}
public void requestSmsVerificationCode() throws IOException {
this.pushServiceSocket.createAccount(false);
}
public void requestVoiceVerificationCode() throws IOException {
this.pushServiceSocket.createAccount(true);
}
public void verifyAccount(String verificationCode, String signalingKey,
boolean supportsSms, int axolotlRegistrationId)
throws IOException
{
this.pushServiceSocket.verifyAccount(verificationCode, signalingKey,
supportsSms, axolotlRegistrationId);
}
public void setPreKeys(IdentityKey identityKey, PreKeyRecord lastResortKey,
SignedPreKeyRecord signedPreKey, List<PreKeyRecord> oneTimePreKeys)
throws IOException
{
this.pushServiceSocket.registerPreKeys(identityKey, lastResortKey, signedPreKey, oneTimePreKeys);
}
public int getPreKeysCount() throws IOException {
return this.pushServiceSocket.getAvailablePreKeys();
}
public void setSignedPreKey(SignedPreKeyRecord signedPreKey) throws IOException {
this.pushServiceSocket.setCurrentSignedPreKey(signedPreKey);
}
public SignedPreKeyEntity getSignedPreKey() throws IOException {
return this.pushServiceSocket.getCurrentSignedPreKey();
}
public Optional<ContactTokenDetails> getContact(String contactToken) throws IOException {
return Optional.fromNullable(this.pushServiceSocket.getContactTokenDetails(contactToken));
}
public List<ContactTokenDetails> getContacts(Set<String> contactTokens) {
return this.pushServiceSocket.retrieveDirectory(contactTokens);
}
}

View File

@@ -0,0 +1,46 @@
/**
* 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.internal.push.PushServiceSocket;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
public class TextSecureMessageReceiver {
private final PushServiceSocket socket;
public TextSecureMessageReceiver(String url, TrustStore trustStore,
String user, String password)
{
this.socket = new PushServiceSocket(url, trustStore, user, password);
}
public InputStream retrieveAttachment(TextSecureAttachmentPointer pointer, File destination)
throws IOException, InvalidMessageException
{
socket.retrieveAttachment(pointer.getRelay().orNull(), pointer.getId(), destination);
return new AttachmentCipherInputStream(destination, pointer.getKey());
}
}

View File

@@ -0,0 +1,323 @@
/**
* 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 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.PushAddress;
import org.whispersystems.textsecure.api.push.TrustStore;
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.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.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;
public class TextSecureMessageSender {
private static final String TAG = TextSecureMessageSender.class.getSimpleName();
private final PushServiceSocket socket;
private final AxolotlStore store;
private final Optional<EventListener> eventListener;
public TextSecureMessageSender(String url, TrustStore trustStore,
String user, String password, AxolotlStore store,
Optional<EventListener> eventListener)
{
this.socket = new PushServiceSocket(url, trustStore, user, password);
this.store = store;
this.eventListener = eventListener;
}
public void sendDeliveryReceipt(PushAddress recipient, long messageId) throws IOException {
this.socket.sendReceipt(recipient.getNumber(), messageId, recipient.getRelay());
}
public void sendMessage(PushAddress recipient, TextSecureMessage message)
throws UntrustedIdentityException, IOException
{
byte[] content = createMessageContent(message);
sendMessage(recipient, message.getTimestamp(), content);
if (message.isEndSession()) {
store.deleteAllSessions(recipient.getRecipientId());
if (eventListener.isPresent()) {
eventListener.get().onSecurityEvent(recipient.getRecipientId());
}
}
}
public void sendMessage(List<PushAddress> 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 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<PushAddress> recipients, long timestamp, byte[] content)
throws IOException, EncapsulatedExceptions
{
List<UntrustedIdentityException> untrustedIdentities = new LinkedList<>();
List<UnregisteredUserException> unregisteredUsers = new LinkedList<>();
for (PushAddress 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);
}
}
if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty()) {
throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers);
}
}
private void sendMessage(PushAddress recipient, long timestamp, byte[] content)
throws UntrustedIdentityException, IOException
{
for (int i=0;i<3;i++) {
try {
OutgoingPushMessageList messages = getEncryptedMessages(socket, recipient, timestamp, content);
socket.sendMessage(messages);
return;
} catch (MismatchedDevicesException mde) {
Log.w(TAG, mde);
handleMismatchedDevices(socket, recipient, mde.getMismatchedDevices());
} catch (StaleDevicesException ste) {
Log.w(TAG, ste);
handleStaleDevices(recipient, ste.getStaleDevices());
}
}
}
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,
PushAddress recipient,
long timestamp,
byte[] plaintext)
throws IOException, UntrustedIdentityException
{
PushBody masterBody = getEncryptedMessage(socket, recipient, plaintext);
List<OutgoingPushMessage> messages = new LinkedList<>();
messages.add(new OutgoingPushMessage(recipient, masterBody));
for (int deviceId : store.getSubDeviceSessions(recipient.getRecipientId())) {
PushAddress device = new PushAddress(recipient.getRecipientId(), recipient.getNumber(), deviceId, recipient.getRelay());
PushBody body = getEncryptedMessage(socket, device, plaintext);
messages.add(new OutgoingPushMessage(device, body));
}
return new OutgoingPushMessageList(recipient.getNumber(), timestamp, recipient.getRelay(), messages);
}
private PushBody getEncryptedMessage(PushServiceSocket socket, PushAddress recipient, byte[] plaintext)
throws IOException, UntrustedIdentityException
{
if (!store.containsSession(recipient.getRecipientId(), recipient.getDeviceId())) {
try {
List<PreKeyBundle> preKeys = socket.getPreKeys(recipient);
for (PreKeyBundle preKey : preKeys) {
try {
SessionBuilder sessionBuilder = new SessionBuilder(store, recipient.getRecipientId(), recipient.getDeviceId());
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(), recipient.getDeviceId());
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, PushAddress recipient,
MismatchedDevices mismatchedDevices)
throws IOException, UntrustedIdentityException
{
try {
for (int extraDeviceId : mismatchedDevices.getExtraDevices()) {
store.deleteSession(recipient.getRecipientId(), extraDeviceId);
}
for (int missingDeviceId : mismatchedDevices.getMissingDevices()) {
PushAddress device = new PushAddress(recipient.getRecipientId(), recipient.getNumber(),
missingDeviceId, recipient.getRelay());
PreKeyBundle preKey = socket.getPreKey(device);
try {
SessionBuilder sessionBuilder = new SessionBuilder(store, device.getRecipientId(), device.getDeviceId());
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(PushAddress 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,5 +1,5 @@
/**
* Copyright (C) 2013 Open Whisper Systems
* 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
@@ -14,13 +14,13 @@
* 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.crypto;
package org.whispersystems.textsecure.api.crypto;
import android.util.Log;
import org.whispersystems.libaxolotl.InvalidMacException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.util.Util;
import org.whispersystems.textsecure.internal.util.Util;
import java.io.File;
import java.io.FileInputStream;
@@ -48,13 +48,15 @@ import javax.crypto.spec.SecretKeySpec;
public class AttachmentCipherInputStream extends FileInputStream {
private static final int BLOCK_SIZE = 16;
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;
private byte[] overflowBuffer;
public AttachmentCipherInputStream(File file, byte[] combinedKeyMaterial)
throws IOException, InvalidMessageException
@@ -62,11 +64,9 @@ public class AttachmentCipherInputStream extends FileInputStream {
super(file);
try {
byte[][] parts = Util.split(combinedKeyMaterial,
AttachmentCipher.CIPHER_KEY_SIZE,
AttachmentCipher.MAC_KEY_SIZE);
byte[][] parts = Util.split(combinedKeyMaterial, CIPHER_KEY_SIZE, MAC_KEY_SIZE);
Mac mac = Mac.getInstance("HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(parts[1], "HmacSHA256"));
if (file.length() <= BLOCK_SIZE + mac.getMacLength()) {
@@ -84,16 +84,10 @@ public class AttachmentCipherInputStream extends FileInputStream {
this.done = false;
this.totalRead = 0;
this.totalDataSize = file.length() - cipher.getBlockSize() - mac.getMacLength();
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
} catch (InvalidMacException e) {
throw new InvalidMessageException(e);
} catch (NoSuchPaddingException e) {
throw new AssertionError(e);
} catch (InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
}
}

View File

@@ -0,0 +1,121 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.crypto;
import org.whispersystems.textsecure.internal.util.Util;
import java.io.IOException;
import java.io.OutputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
public class AttachmentCipherOutputStream extends OutputStream {
private final Cipher cipher;
private final Mac mac;
private final OutputStream outputStream;
private long ciphertextLength = 0;
public AttachmentCipherOutputStream(byte[] combinedKeyMaterial,
OutputStream outputStream)
throws IOException
{
try {
this.outputStream = outputStream;
this.cipher = initializeCipher();
this.mac = initializeMac();
byte[][] keyParts = Util.split(combinedKeyMaterial, 32, 32);
this.cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(keyParts[0], "AES"));
this.mac.init(new SecretKeySpec(keyParts[1], "HmacSHA256"));
mac.update(cipher.getIV());
outputStream.write(cipher.getIV());
ciphertextLength += cipher.getIV().length;
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
@Override
public void write(byte[] buffer) throws IOException {
write(buffer, 0, buffer.length);
}
@Override
public void write(byte[] buffer, int offset, int length) throws IOException {
byte[] ciphertext = cipher.update(buffer, offset, length);
if (ciphertext != null) {
mac.update(ciphertext);
outputStream.write(ciphertext);
ciphertextLength += ciphertext.length;
}
}
@Override
public void write(int b) {
throw new AssertionError("NYI");
}
@Override
public void flush() throws IOException {
try {
byte[] ciphertext = cipher.doFinal();
byte[] auth = mac.doFinal(ciphertext);
outputStream.write(ciphertext);
outputStream.write(auth);
ciphertextLength += ciphertext.length;
ciphertextLength += auth.length;
outputStream.flush();
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
public static long getCiphertextLength(long plaintextLength) {
return 16 + (((plaintextLength / 16) +1) * 16) + 32;
}
private Mac initializeMac() {
try {
return Mac.getInstance("HmacSHA256");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private Cipher initializeCipher() {
try {
return Cipher.getInstance("AES/CBC/PKCS5Padding");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new AssertionError(e);
}
}
}

View File

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

@@ -0,0 +1,44 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.crypto;
import org.whispersystems.libaxolotl.IdentityKey;
public class UntrustedIdentityException extends Exception {
private final IdentityKey identityKey;
private final String e164number;
public UntrustedIdentityException(String s, String e164number, IdentityKey identityKey) {
super(s);
this.e164number = e164number;
this.identityKey = identityKey;
}
public UntrustedIdentityException(UntrustedIdentityException e) {
this(e.getMessage(), e.getE164Number(), e.getIdentityKey());
}
public IdentityKey getIdentityKey() {
return identityKey;
}
public String getE164Number() {
return e164number;
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.messages;
public abstract class TextSecureAttachment {
private final String contentType;
protected TextSecureAttachment(String contentType) {
this.contentType = contentType;
}
public String getContentType() {
return contentType;
}
public abstract boolean isStream();
public abstract boolean isPointer();
public TextSecureAttachmentStream asStream() {
return (TextSecureAttachmentStream)this;
}
public TextSecureAttachmentPointer asPointer() {
return (TextSecureAttachmentPointer)this;
}
}

View File

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

@@ -0,0 +1,49 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.messages;
import java.io.InputStream;
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,11 +1,35 @@
package org.whispersystems.textsecure.push;
/**
* 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.util.Base64;
import org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal;
import org.whispersystems.textsecure.util.Hex;
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;
@@ -14,13 +38,10 @@ import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class IncomingEncryptedPushMessage {
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;
@@ -33,9 +54,9 @@ public class IncomingEncryptedPushMessage {
private static final int IV_LENGTH = 16;
private static final int CIPHERTEXT_OFFSET = IV_OFFSET + IV_LENGTH;
private final IncomingPushMessage incomingPushMessage;
private final IncomingPushMessageSignal signal;
public IncomingEncryptedPushMessage(String message, String signalingKey)
public TextSecureEnvelope(String message, String signalingKey)
throws IOException, InvalidVersionException
{
byte[] ciphertext = Base64.decode(message);
@@ -48,14 +69,60 @@ public class IncomingEncryptedPushMessage {
verifyMac(ciphertext, macKey);
byte[] plaintext = getPlaintext(ciphertext, cipherKey);
IncomingPushMessageSignal signal = IncomingPushMessageSignal.parseFrom(plaintext);
this.incomingPushMessage = new IncomingPushMessage(signal);
this.signal = IncomingPushMessageSignal.parseFrom(getPlaintext(ciphertext, cipherKey));
}
public IncomingPushMessage getIncomingPushMessage() {
return incomingPushMessage;
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();
}
public String getSource() {
return signal.getSource();
}
public int getSourceDevice() {
return signal.getSourceDevice();
}
public int getType() {
return signal.getType().getNumber();
}
public String getRelay() {
return signal.getRelay();
}
public long getTimestamp() {
return signal.getTimestamp();
}
public byte[] getMessage() {
return signal.getMessage().toByteArray();
}
public boolean isWhisperMessage() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.CIPHERTEXT_VALUE;
}
public boolean isPreKeyWhisperMessage() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.PREKEY_BUNDLE_VALUE;
}
public boolean isPlaintext() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.PLAINTEXT_VALUE;
}
public boolean isReceipt() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.RECEIPT_VALUE;
}
private byte[] getPlaintext(byte[] ciphertext, SecretKeySpec cipherKey) throws IOException {
@@ -69,18 +136,10 @@ public class IncomingEncryptedPushMessage {
return cipher.doFinal(ciphertext, CIPHERTEXT_OFFSET,
ciphertext.length - VERSION_LENGTH - IV_LENGTH - MAC_SIZE);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (NoSuchPaddingException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
} catch (InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
} catch (IllegalBlockSizeException e) {
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
Log.w("IncomingEncryptedPushMessage", e);
Log.w(TAG, e);
throw new IOException("Bad padding?");
}
}
@@ -102,15 +161,13 @@ public class IncomingEncryptedPushMessage {
byte[] theirMacBytes = new byte[MAC_SIZE];
System.arraycopy(ciphertext, ciphertext.length-MAC_SIZE, theirMacBytes, 0, theirMacBytes.length);
Log.w("IncomingEncryptedPushMessage", "Our MAC: " + Hex.toString(ourMacBytes));
Log.w("IncomingEncryptedPushMessage", "Thr MAC: " + Hex.toString(theirMacBytes));
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 e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}

View File

@@ -0,0 +1,74 @@
/**
* 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;
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;
public TextSecureGroup(byte[] groupId) {
this(Type.DELIVER, groupId, null, null, null);
}
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;
}
}

Some files were not shown because too many files have changed in this diff Show More