Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31eddbf346 | ||
|
|
e9b383d277 | ||
|
|
eeafb81c90 | ||
|
|
02e27c94f2 | ||
|
|
167386ea49 | ||
|
|
e31994ac77 | ||
|
|
ea9a5decac | ||
|
|
1cb191a6ee | ||
|
|
60737bdd7b | ||
|
|
2dded4888e | ||
|
|
eaf89735b8 | ||
|
|
8fa2f92a91 | ||
|
|
174324e2a0 | ||
|
|
bc3686058a | ||
|
|
0110015c8b | ||
|
|
5ca8a6d421 | ||
|
|
0fc9ff7490 | ||
|
|
08d939b010 | ||
|
|
020920d988 | ||
|
|
bd3d9ac533 | ||
|
|
85670d95ee | ||
|
|
3d1007d101 | ||
|
|
35821d444e | ||
|
|
3cd7c2d8e5 | ||
|
|
5b08791086 | ||
|
|
31b9dcb5eb | ||
|
|
43adc75428 | ||
|
|
67a4523ca7 | ||
|
|
b35b9be0c8 | ||
|
|
9215322846 | ||
|
|
cbebc040cc | ||
|
|
bea26e83da | ||
|
|
a85dbce041 | ||
|
|
baaa3514d4 | ||
|
|
71fdaac1b2 | ||
|
|
fb31319e52 | ||
|
|
08ed90c5ec | ||
|
|
0d102f76cc | ||
|
|
28cb1ed85b | ||
|
|
cd9b20dc9d | ||
|
|
f09abff407 | ||
|
|
f9934bd8e5 | ||
|
|
1182052c7f | ||
|
|
0d06d50a65 | ||
|
|
d9d4ec9d9d | ||
|
|
9a6f65988f | ||
|
|
601e233d47 | ||
|
|
ae178fc4ec | ||
|
|
cafe03a70a | ||
|
|
99f42e2ee1 | ||
|
|
a3f1d9cdfd | ||
|
|
4cab657ebe | ||
|
|
db6f8618e6 | ||
|
|
98af1fb6ee | ||
|
|
ad1d55f12d | ||
|
|
7df49811b7 | ||
|
|
ff2ac8a66e | ||
|
|
53da1f849a |
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.ai binary
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
6425
artwork/ic_delivery_delivered.ai
Normal file
6323
artwork/ic_delivery_pending.ai
Normal file
BIN
artwork/ic_lock_black_18dp.psd
Normal file
BIN
artwork/ic_lock_white_18dp.psd
Normal file
|
Before Width: | Height: | Size: 445 KiB After Width: | Height: | Size: 323 KiB |
|
Before Width: | Height: | Size: 547 KiB After Width: | Height: | Size: 394 KiB |
|
Before Width: | Height: | Size: 660 KiB After Width: | Height: | Size: 477 KiB |
|
Before Width: | Height: | Size: 369 KiB After Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 363 KiB After Width: | Height: | Size: 278 KiB |
47
build.gradle
@@ -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'
|
||||
|
||||
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
@@ -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.
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ public class PersistentResult {
|
||||
}
|
||||
}
|
||||
|
||||
public void onRun() throws Throwable {
|
||||
public void onRun() throws Exception {
|
||||
synchronized (RAN_LOCK) {
|
||||
this.ran = true;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
@@ -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 *;
|
||||
#}
|
||||
@@ -1,3 +0,0 @@
|
||||
|
||||
all:
|
||||
protoc --java_out=../src/ IncomingPushMessageSignal.proto
|
||||
@@ -1,3 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
</resources>
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
public class PreKeyStatus {
|
||||
|
||||
private int count;
|
||||
|
||||
public PreKeyStatus() {}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package org.whispersystems.textsecure.push.exceptions;
|
||||
|
||||
public class AuthorizationFailedException extends NonSuccessfulResponseCodeException {
|
||||
public AuthorizationFailedException(String s) {
|
||||
super(s);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package org.whispersystems.textsecure.push.exceptions;
|
||||
|
||||
public class ExpectationFailedException extends NonSuccessfulResponseCodeException {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
public interface CanonicalRecipient {
|
||||
// public String getNumber();
|
||||
public long getRecipientId();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package org.whispersystems.textsecure.util;
|
||||
|
||||
public interface FutureTaskListener<V> {
|
||||
public void onSuccess(V result);
|
||||
public void onFailure(Throwable error);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package org.whispersystems.textsecure.util;
|
||||
|
||||
public class InvalidNumberException extends Throwable {
|
||||
public InvalidNumberException(String s) {
|
||||
super(s);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
3
libtextsecure/protobuf/Makefile
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
all:
|
||||
protoc --java_out=../src/main/java/ IncomingPushMessageSignal.proto
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.api.crypto;
|
||||
|
||||
import org.whispersystems.textsecure.internal.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class AttachmentCipherOutputStream extends OutputStream {
|
||||
|
||||
private final Cipher cipher;
|
||||
private final Mac mac;
|
||||
private final OutputStream outputStream;
|
||||
|
||||
private long ciphertextLength = 0;
|
||||
|
||||
public AttachmentCipherOutputStream(byte[] combinedKeyMaterial,
|
||||
OutputStream outputStream)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
this.outputStream = outputStream;
|
||||
this.cipher = initializeCipher();
|
||||
this.mac = initializeMac();
|
||||
|
||||
byte[][] keyParts = Util.split(combinedKeyMaterial, 32, 32);
|
||||
|
||||
this.cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(keyParts[0], "AES"));
|
||||
this.mac.init(new SecretKeySpec(keyParts[1], "HmacSHA256"));
|
||||
|
||||
mac.update(cipher.getIV());
|
||||
outputStream.write(cipher.getIV());
|
||||
ciphertextLength += cipher.getIV().length;
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer) throws IOException {
|
||||
write(buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int length) throws IOException {
|
||||
byte[] ciphertext = cipher.update(buffer, offset, length);
|
||||
|
||||
if (ciphertext != null) {
|
||||
mac.update(ciphertext);
|
||||
outputStream.write(ciphertext);
|
||||
ciphertextLength += ciphertext.length;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) {
|
||||
throw new AssertionError("NYI");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
try {
|
||||
byte[] ciphertext = cipher.doFinal();
|
||||
byte[] auth = mac.doFinal(ciphertext);
|
||||
|
||||
outputStream.write(ciphertext);
|
||||
outputStream.write(auth);
|
||||
|
||||
ciphertextLength += ciphertext.length;
|
||||
ciphertextLength += auth.length;
|
||||
|
||||
outputStream.flush();
|
||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static long getCiphertextLength(long plaintextLength) {
|
||||
return 16 + (((plaintextLength / 16) +1) * 16) + 32;
|
||||
}
|
||||
|
||||
private Mac initializeMac() {
|
||||
try {
|
||||
return Mac.getInstance("HmacSHA256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Cipher initializeCipher() {
|
||||
try {
|
||||
return Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.api.crypto;
|
||||
|
||||
import org.whispersystems.libaxolotl.IdentityKey;
|
||||
|
||||
public class UntrustedIdentityException extends Exception {
|
||||
|
||||
private final IdentityKey identityKey;
|
||||
private final String e164number;
|
||||
|
||||
public UntrustedIdentityException(String s, String e164number, IdentityKey identityKey) {
|
||||
super(s);
|
||||
this.e164number = e164number;
|
||||
this.identityKey = identityKey;
|
||||
}
|
||||
|
||||
public UntrustedIdentityException(UntrustedIdentityException e) {
|
||||
this(e.getMessage(), e.getE164Number(), e.getIdentityKey());
|
||||
}
|
||||
|
||||
public IdentityKey getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public String getE164Number() {
|
||||
return e164number;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.api.messages;
|
||||
|
||||
public abstract class TextSecureAttachment {
|
||||
|
||||
private final String contentType;
|
||||
|
||||
protected TextSecureAttachment(String contentType) {
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public abstract boolean isStream();
|
||||
public abstract boolean isPointer();
|
||||
|
||||
public TextSecureAttachmentStream asStream() {
|
||||
return (TextSecureAttachmentStream)this;
|
||||
}
|
||||
|
||||
public TextSecureAttachmentPointer asPointer() {
|
||||
return (TextSecureAttachmentPointer)this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||