mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-08 09:18:39 +01:00
Compare commits
280 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f8a110428 | |||
| 07b4279d0b | |||
| 6a33b231e3 | |||
| b38d02061d | |||
| f832a36a5e | |||
| 911ca7c29d | |||
| 544b75a2a7 | |||
| 56e8d4fb06 | |||
| 36a2278aef | |||
| 0c785b85b8 | |||
| a0ecba147e | |||
| 977591ac82 | |||
| fe1838d3fe | |||
| ede06cf97d | |||
| 93deee6824 | |||
| db19077834 | |||
| 6f91f62db2 | |||
| 1fc63b7597 | |||
| a079e479ec | |||
| e90705b459 | |||
| 244db437cb | |||
| 6558eae032 | |||
| 88b54a262b | |||
| 4fa8b8a4bd | |||
| cc0ced9a81 | |||
| 52447f5e97 | |||
| bceb69b284 | |||
| a8d826020d | |||
| 2ada7f87f2 | |||
| 7f8ca58762 | |||
| 058c25808b | |||
| c1e6b6b086 | |||
| b0a6bb79f6 | |||
| de52bf50a2 | |||
| 207dd23c86 | |||
| 7e0de29dd7 | |||
| eaa8f1ee8f | |||
| fb494c1151 | |||
| 49ecd9ef5d | |||
| 8772214fd4 | |||
| 5b74d0cbeb | |||
| 1e7c93007d | |||
| 608815a69b | |||
| fb49efa34d | |||
| b20e8616ec | |||
| acf78b6b63 | |||
| d68fe928b8 | |||
| db4d561d9e | |||
| 6933ca50a7 | |||
| 88c0e6f8ab | |||
| 6cd4728e3c | |||
| 97cc82837c | |||
| 5f0b0e8495 | |||
| c837d590ab | |||
| 6108a32631 | |||
| 6d78e1760d | |||
| c7b7242eff | |||
| 77c687efcf | |||
| 274af2f010 | |||
| 6cd5100530 | |||
| 115a408b0b | |||
| d983016137 | |||
| a8309b6b2b | |||
| e241589c92 | |||
| dcb6240a6a | |||
| 599bb5ab0f | |||
| 739fe584c6 | |||
| 920d1c6207 | |||
| c1d3d26a15 | |||
| e3b66dc7e8 | |||
| dfa08d1356 | |||
| 6da734c2f9 | |||
| a9f3141a77 | |||
| ee267d4702 | |||
| c39174ed8a | |||
| 9c872b6da6 | |||
| 2924a09936 | |||
| b9da012cc4 | |||
| 8626d41787 | |||
| c58705722a | |||
| c702ff676a | |||
| 995569dd50 | |||
| a0dfd42527 | |||
| a7dd78cce6 | |||
| e2e9cd40b3 | |||
| e4fc6f41b8 | |||
| 778b2a0d27 | |||
| 64cd15ca0b | |||
| 1e46c3c0ba | |||
| 91772b4e11 | |||
| af5c7cb7ca | |||
| e760474fa9 | |||
| 28bb88e347 | |||
| 17a1fe97ca | |||
| 7348237862 | |||
| 4e66db5dc1 | |||
| ffa6c9acd1 | |||
| b3e66a9259 | |||
| 5c0cf8c1f0 | |||
| 89c716fca4 | |||
| aae9830dfa | |||
| 48c72697c1 | |||
| aac7e9ea53 | |||
| a1b10b3222 | |||
| 2ab8db33e3 | |||
| 2b1386232f | |||
| 7bb1caa22e | |||
| 72c14b8651 | |||
| f85c3bb1e6 | |||
| d8714fe3b4 | |||
| c54c1907b6 | |||
| 55257155dd | |||
| 55a8bd86de | |||
| c60909272b | |||
| 3dcc2d8171 | |||
| 0ccbb22e4c | |||
| 0ffa10eaea | |||
| 9824073023 | |||
| d2adc11a2d | |||
| 4d0b870c88 | |||
| 840997cb7d | |||
| e4406621ed | |||
| 7f96ee0b62 | |||
| 17f850b31a | |||
| 5d87ad0301 | |||
| a33eeed6d3 | |||
| 554ce29337 | |||
| 6054d0b36f | |||
| 8ba15cf3b4 | |||
| b15e5b4867 | |||
| 544511905a | |||
| 02e3f511bd | |||
| 0539b071e7 | |||
| 36e400650c | |||
| d8930daf4b | |||
| 17b5816d5b | |||
| e574c81ea2 | |||
| 6799a2f841 | |||
| 61fb20f412 | |||
| 47f648e7bc | |||
| 064c0ddb82 | |||
| b42c42007d | |||
| 807cdfce2e | |||
| 43dc3aeebd | |||
| 0369c5ee16 | |||
| 3df2390fe4 | |||
| 9ec4a7af0f | |||
| 70636fb4a7 | |||
| 3c4efdd8f9 | |||
| 80deb301e5 | |||
| 5eabfdc34c | |||
| 5e96832666 | |||
| 1ccce24cf8 | |||
| d59e4f2da7 | |||
| a254c1a7b2 | |||
| 82cc610938 | |||
| becf7c40e8 | |||
| ed21c3ca03 | |||
| 631005e565 | |||
| b57fab8c75 | |||
| 4260a8436b | |||
| 23d478191f | |||
| d075a33d4e | |||
| 7507dadbe7 | |||
| bfd0363390 | |||
| cfb22825f4 | |||
| d37fafbfe7 | |||
| 2dc2eb5835 | |||
| 019d036f69 | |||
| f1ca5fc8e2 | |||
| 9089fc4001 | |||
| b281b817ba | |||
| cee6736656 | |||
| 350ca059b9 | |||
| a7cc5bdc5e | |||
| 2893dfff60 | |||
| 1c14f06734 | |||
| ff053437e3 | |||
| 03dc220cea | |||
| 097f97b5e4 | |||
| 95d3db3260 | |||
| 0485ae603b | |||
| 2f93da6f1a | |||
| 78f3e28974 | |||
| fecd8300c3 | |||
| 262f90dbe7 | |||
| ed271c6f3e | |||
| 3fa5843c93 | |||
| 7f544cb3e5 | |||
| 9f4d40a364 | |||
| 09f0c5b63f | |||
| 53e0dc5dee | |||
| 8b06051e7c | |||
| 660ec88202 | |||
| b9057a1c11 | |||
| 1f85b1f3d2 | |||
| 71f78f03f4 | |||
| 4b3d129097 | |||
| daeb823399 | |||
| c7f76c5d1c | |||
| 9ba1391a1e | |||
| 5d137465e8 | |||
| 78cbb3c073 | |||
| 097d95428a | |||
| 2cfb6ede7f | |||
| 695663a5b1 | |||
| a2204c8370 | |||
| 8c4757ea02 | |||
| 85821274fa | |||
| a8b1ec8e52 | |||
| adbdaebd69 | |||
| c2da4fcd7d | |||
| 46ebff3659 | |||
| 452ccd0350 | |||
| 6e6c809690 | |||
| 7b5c1904cf | |||
| c0aa9d7587 | |||
| 5d03e3d516 | |||
| 2bc3a4417f | |||
| 59d03cbeb2 | |||
| f9f9ae68f5 | |||
| 88f2b67984 | |||
| 44dba4bd98 | |||
| ef585eba42 | |||
| 42967dab13 | |||
| 4f75d1a5db | |||
| debacbdf58 | |||
| 12c4283c30 | |||
| 096c9e0ff7 | |||
| 2f23a13a6f | |||
| bdadbaaac4 | |||
| 16f41555ba | |||
| 8c037456e7 | |||
| feccee557e | |||
| 67f5605bc4 | |||
| ccb18cd46c | |||
| 5b1d91016c | |||
| 27ab04ab41 | |||
| 9432a45b39 | |||
| 9e3475ed94 | |||
| 86d088bce2 | |||
| a135e7efa2 | |||
| c247935f1a | |||
| 31657f5c15 | |||
| bcecc30d33 | |||
| 7193252d77 | |||
| 5b682a3a3d | |||
| 6b8659a393 | |||
| 14557d3dc1 | |||
| 03cbee0277 | |||
| adc0907906 | |||
| f14a71a076 | |||
| 12c2b53f7c | |||
| ff60b5b731 | |||
| 43954a176a | |||
| d698d3bd6f | |||
| 50a81c0e60 | |||
| b0b8377a8e | |||
| 7d02bb8487 | |||
| 8449d75684 | |||
| b89163bb14 | |||
| b7ce220600 | |||
| 948079a42e | |||
| de1c6cdd0c | |||
| 37147fb0a5 | |||
| 33334f80c3 | |||
| 36286da9bd | |||
| 2248acb9f2 | |||
| 3632a2cc95 | |||
| 9447ea12cb | |||
| 89c2329fdf | |||
| 84012c7adb | |||
| d7395af774 | |||
| be4daff0f3 | |||
| c2459d0a31 | |||
| 5f993ed0f7 | |||
| acea24c19c | |||
| 82b5d58d04 | |||
| af2990fa08 | |||
| 0fa48540e1 |
@@ -0,0 +1,17 @@
|
|||||||
|
name: Android CI
|
||||||
|
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: set up JDK 1.8
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 1.8
|
||||||
|
- name: Build with Gradle
|
||||||
|
run: ./gradlew qa
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
.classpath
|
.classpath
|
||||||
|
captures/
|
||||||
project.properties
|
project.properties
|
||||||
.project
|
.project
|
||||||
.settings
|
.settings
|
||||||
|
|||||||
+27
-47
@@ -27,6 +27,7 @@
|
|||||||
tools:ignore="ProtectedPermissions"/>
|
tools:ignore="ProtectedPermissions"/>
|
||||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
|
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
|
||||||
<uses-permission android:name="android.permission.RECEIVE_MMS"/>
|
<uses-permission android:name="android.permission.RECEIVE_MMS"/>
|
||||||
@@ -113,22 +114,20 @@
|
|||||||
<meta-data android:name="firebase_messaging_auto_init_enabled" android:value="false" />
|
<meta-data android:name="firebase_messaging_auto_init_enabled" android:value="false" />
|
||||||
|
|
||||||
<activity android:name="org.thoughtcrime.securesms.WebRtcCallActivity"
|
<activity android:name="org.thoughtcrime.securesms.WebRtcCallActivity"
|
||||||
|
android:theme="@style/TextSecure.LightTheme.WebRTCCall"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|screenSize|fontScale"
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|screenSize|fontScale"
|
||||||
android:launchMode="singleTask"/>
|
android:launchMode="singleTask"/>
|
||||||
|
|
||||||
<activity android:name=".CountrySelectionActivity"
|
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
|
||||||
|
|
||||||
<activity android:name=".InviteActivity"
|
<activity android:name=".InviteActivity"
|
||||||
android:theme="@style/TextSecure.HighlightTheme"
|
android:theme="@style/Signal.Light.NoActionBar.Invite"
|
||||||
android:windowSoftInputMode="stateHidden"
|
android:windowSoftInputMode="stateHidden"
|
||||||
android:parentActivityName=".ConversationListActivity"
|
android:parentActivityName=".MainActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="org.thoughtcrime.securesms.ConversationListActivity" />
|
android:value="org.thoughtcrime.securesms.MainActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name=".PromptMmsActivity"
|
<activity android:name=".PromptMmsActivity"
|
||||||
@@ -191,16 +190,8 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
|
||||||
<activity android:name=".ConversationListActivity"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:theme="@style/TextSecure.LightNoActionBar"
|
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
|
||||||
android:exported="true" />
|
|
||||||
|
|
||||||
<activity-alias android:name=".RoutingActivity"
|
<activity-alias android:name=".RoutingActivity"
|
||||||
android:targetActivity=".ConversationListActivity"
|
android:targetActivity=".MainActivity"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -216,24 +207,14 @@
|
|||||||
|
|
||||||
</activity-alias>
|
</activity-alias>
|
||||||
|
|
||||||
<activity android:name=".ConversationListArchiveActivity"
|
|
||||||
android:label="@string/AndroidManifest_archived_conversations"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
|
||||||
android:parentActivityName=".ConversationListActivity">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
|
||||||
android:value="org.thoughtcrime.securesms.ConversationListActivity" />
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity android:name=".conversation.ConversationActivity"
|
<activity android:name=".conversation.ConversationActivity"
|
||||||
android:windowSoftInputMode="stateUnchanged"
|
android:windowSoftInputMode="stateUnchanged"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:parentActivityName=".ConversationListActivity">
|
android:parentActivityName=".MainActivity">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="org.thoughtcrime.securesms.ConversationListActivity" />
|
android:value="org.thoughtcrime.securesms.MainActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name=".longmessage.LongMessageActivity" />
|
<activity android:name=".longmessage.LongMessageActivity" />
|
||||||
@@ -298,10 +279,11 @@
|
|||||||
android:windowSoftInputMode="stateHidden"
|
android:windowSoftInputMode="stateHidden"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".mediasend.MediaSendActivity"
|
<activity android:name=".mediasend.MediaSendActivity"
|
||||||
android:theme="@style/TextSecure.FullScreenMedia"
|
android:theme="@style/TextSecure.FullScreenMedia"
|
||||||
android:windowSoftInputMode="stateHidden"
|
android:windowSoftInputMode="stateHidden"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:launchMode="singleTop"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".PassphraseChangeActivity"
|
<activity android:name=".PassphraseChangeActivity"
|
||||||
android:label="@string/AndroidManifest__change_passphrase"
|
android:label="@string/AndroidManifest__change_passphrase"
|
||||||
@@ -318,24 +300,12 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name=".registration.WelcomeActivity"
|
<activity android:name=".registration.RegistrationNavigationActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||||
android:windowSoftInputMode="stateUnchanged"
|
android:windowSoftInputMode="stateUnchanged"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".RegistrationActivity"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
|
||||||
android:windowSoftInputMode="stateUnchanged"
|
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
|
||||||
|
|
||||||
<activity android:name=".registration.CaptchaActivity"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:theme="@style/TextSecure.LightNoActionBar"
|
|
||||||
android:windowSoftInputMode="stateUnchanged"
|
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
|
||||||
|
|
||||||
<activity android:name=".revealable.ViewOnceMessageActivity"
|
<activity android:name=".revealable.ViewOnceMessageActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@style/TextSecure.FullScreenMedia"
|
android:theme="@style/TextSecure.FullScreenMedia"
|
||||||
@@ -364,10 +334,9 @@
|
|||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".MediaOverviewActivity"
|
<activity android:name=".mediaoverview.MediaOverviewActivity"
|
||||||
android:theme="@style/TextSecure.LightNoActionBar"
|
android:theme="@style/TextSecure.LightNoActionBar"
|
||||||
android:windowSoftInputMode="stateHidden"
|
android:windowSoftInputMode="stateHidden"
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".DummyActivity"
|
<activity android:name=".DummyActivity"
|
||||||
@@ -473,7 +442,18 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".maps.PlacePickerActivity"
|
android:name=".maps.PlacePickerActivity"
|
||||||
android:label="@string/PlacePickerActivity_title"
|
android:label="@string/PlacePickerActivity_title"
|
||||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
android:theme="@style/TextSecure.LightNoActionBar"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".usernames.ProfileEditActivityV2"
|
||||||
|
android:theme="@style/TextSecure.LightNoActionBar"
|
||||||
|
android:windowSoftInputMode="adjustResize"/>
|
||||||
|
|
||||||
|
<activity android:name=".MainActivity"
|
||||||
|
android:theme="@style/TextSecure.LightNoActionBar"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||||
|
|
||||||
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
||||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||||
|
|||||||
@@ -0,0 +1,462 @@
|
|||||||
|
import org.signal.signing.ApkSignerUtil
|
||||||
|
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
maven {
|
||||||
|
url "https://repo1.maven.org/maven2"
|
||||||
|
}
|
||||||
|
jcenter {
|
||||||
|
content {
|
||||||
|
includeVersion 'org.jetbrains.trove4j', 'trove4j', '20160824'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:3.5.1'
|
||||||
|
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0'
|
||||||
|
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'com.google.protobuf'
|
||||||
|
apply plugin: 'androidx.navigation.safeargs'
|
||||||
|
apply plugin: 'witness'
|
||||||
|
apply from: 'translations.gradle'
|
||||||
|
apply from: 'witness-verifications.gradle'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
url "https://raw.github.com/signalapp/maven/master/photoview/releases/"
|
||||||
|
content {
|
||||||
|
includeGroupByRegex "com\\.github\\.chrisbanes.*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
url "https://raw.github.com/signalapp/maven/master/shortcutbadger/releases/"
|
||||||
|
content {
|
||||||
|
includeGroupByRegex "me\\.leolin.*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
url "https://raw.github.com/signalapp/maven/master/circular-progress-button/releases/"
|
||||||
|
content {
|
||||||
|
includeGroupByRegex "com\\.github\\.dmytrodanylyk\\.circular-progress-button\\.*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
url "https://raw.github.com/signalapp/maven/master/sqlcipher/release/"
|
||||||
|
content {
|
||||||
|
includeGroupByRegex "org\\.signal.*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maven { // textdrawable
|
||||||
|
url 'https://dl.bintray.com/amulyakhare/maven'
|
||||||
|
content {
|
||||||
|
includeGroupByRegex "com\\.amulyakhare.*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
jcenter()
|
||||||
|
mavenLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
protobuf {
|
||||||
|
protoc {
|
||||||
|
artifact = 'com.google.protobuf:protoc:3.10.0'
|
||||||
|
}
|
||||||
|
generateProtoTasks {
|
||||||
|
all().each { task ->
|
||||||
|
task.builtins {
|
||||||
|
java {
|
||||||
|
option "lite"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.1.0-beta01'
|
||||||
|
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||||
|
implementation 'com.google.android.material:material:1.0.0'
|
||||||
|
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||||
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
|
implementation 'androidx.preference:preference:1.0.0'
|
||||||
|
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||||
|
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||||
|
implementation 'androidx.exifinterface:exifinterface:1.0.0'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
|
implementation 'androidx.navigation:navigation-fragment:2.1.0'
|
||||||
|
implementation 'androidx.navigation:navigation-ui:2.1.0'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
|
||||||
|
implementation "androidx.camera:camera-core:1.0.0-alpha06"
|
||||||
|
implementation "androidx.camera:camera-camera2:1.0.0-alpha06"
|
||||||
|
|
||||||
|
implementation('com.google.firebase:firebase-messaging:17.3.4') {
|
||||||
|
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||||
|
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||||
|
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||||
|
}
|
||||||
|
|
||||||
|
implementation 'com.google.android.gms:play-services-maps:16.1.0'
|
||||||
|
implementation 'com.google.android.gms:play-services-auth:16.0.1'
|
||||||
|
|
||||||
|
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
|
||||||
|
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
||||||
|
|
||||||
|
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
||||||
|
implementation 'org.signal:aesgcmprovider:0.0.3'
|
||||||
|
|
||||||
|
implementation project(':libsignal-service')
|
||||||
|
|
||||||
|
implementation 'org.signal:ringrtc-android:0.2.0'
|
||||||
|
|
||||||
|
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||||
|
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||||
|
implementation 'com.jpardogo.materialtabstrip:library:1.0.9'
|
||||||
|
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
|
||||||
|
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
||||||
|
implementation 'com.github.bumptech.glide:glide:4.9.0'
|
||||||
|
annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
|
||||||
|
annotationProcessor 'androidx.annotation:annotation:1.1.0'
|
||||||
|
implementation 'com.makeramen:roundedimageview:2.1.0'
|
||||||
|
implementation 'com.pnikosis:materialish-progress:1.5'
|
||||||
|
implementation 'org.greenrobot:eventbus:3.0.0'
|
||||||
|
implementation 'pl.tajchert:waitingdots:0.1.0'
|
||||||
|
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
|
||||||
|
implementation 'com.melnykov:floatingactionbutton:1.3.0'
|
||||||
|
implementation 'com.google.zxing:android-integration:3.1.0'
|
||||||
|
implementation 'mobi.upod:time-duration-picker:1.1.3'
|
||||||
|
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||||
|
implementation 'com.google.zxing:core:3.2.1'
|
||||||
|
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
|
||||||
|
exclude group: 'com.android.support', module: 'support-annotations'
|
||||||
|
}
|
||||||
|
implementation ('cn.carbswang.android:NumberPickerView:1.0.9') {
|
||||||
|
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||||
|
}
|
||||||
|
implementation ('com.tomergoldst.android:tooltips:1.0.6') {
|
||||||
|
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||||
|
}
|
||||||
|
implementation ('com.klinkerapps:android-smsmms:4.0.1') {
|
||||||
|
exclude group: 'com.squareup.okhttp', module: 'okhttp'
|
||||||
|
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
|
||||||
|
}
|
||||||
|
implementation 'com.annimon:stream:1.1.8'
|
||||||
|
implementation ('com.takisoft.fix:colorpicker:0.9.1') {
|
||||||
|
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||||
|
exclude group: 'com.android.support', module: 'recyclerview-v7'
|
||||||
|
}
|
||||||
|
|
||||||
|
implementation 'com.airbnb.android:lottie:3.0.7'
|
||||||
|
|
||||||
|
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
|
||||||
|
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
|
||||||
|
implementation 'org.signal:android-database-sqlcipher:3.5.9-S3'
|
||||||
|
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
|
||||||
|
exclude group: 'com.fasterxml.jackson.core'
|
||||||
|
exclude group: 'org.freemarker'
|
||||||
|
}
|
||||||
|
|
||||||
|
testImplementation 'junit:junit:4.12'
|
||||||
|
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||||
|
testImplementation 'org.mockito:mockito-core:1.9.5'
|
||||||
|
testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
|
||||||
|
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
|
||||||
|
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
|
||||||
|
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
|
||||||
|
|
||||||
|
testImplementation 'androidx.test:core:1.2.0'
|
||||||
|
androidTestImplementation 'androidx.multidex:multidex:2.0.1'
|
||||||
|
androidTestImplementation 'androidx.multidex:multidex-instrumentation:2.0.0'
|
||||||
|
androidTestImplementation 'com.google.dexmaker:dexmaker:1.2'
|
||||||
|
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:1.2'
|
||||||
|
androidTestImplementation ('org.assertj:assertj-core:1.7.1') {
|
||||||
|
exclude group: 'org.hamcrest', module: 'hamcrest-core'
|
||||||
|
}
|
||||||
|
androidTestImplementation ('com.squareup.assertj:assertj-android:1.1.1') {
|
||||||
|
exclude group: 'org.hamcrest', module: 'hamcrest-core'
|
||||||
|
exclude group: 'com.android.support', module: 'support-annotations'
|
||||||
|
}
|
||||||
|
testImplementation 'org.robolectric:robolectric:4.2'
|
||||||
|
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyVerification {
|
||||||
|
configuration = '(play|website)(Debug|Release)RuntimeClasspath'
|
||||||
|
}
|
||||||
|
|
||||||
|
def canonicalVersionCode = 575
|
||||||
|
def canonicalVersionName = "4.51.2"
|
||||||
|
|
||||||
|
def postFixSize = 10
|
||||||
|
def abiPostFix = ['universal' : 0,
|
||||||
|
'armeabi-v7a' : 1,
|
||||||
|
'arm64-v8a' : 2,
|
||||||
|
'x86' : 3,
|
||||||
|
'x86_64' : 4]
|
||||||
|
|
||||||
|
android {
|
||||||
|
flavorDimensions "none"
|
||||||
|
compileSdkVersion 28
|
||||||
|
buildToolsVersion '28.0.3'
|
||||||
|
useLibrary 'org.apache.http.legacy'
|
||||||
|
|
||||||
|
dexOptions {
|
||||||
|
javaMaxHeapSize "4g"
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
versionCode canonicalVersionCode * postFixSize
|
||||||
|
versionName canonicalVersionName
|
||||||
|
|
||||||
|
minSdkVersion 19
|
||||||
|
targetSdkVersion 28
|
||||||
|
multiDexEnabled true
|
||||||
|
|
||||||
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
project.ext.set("archivesBaseName", "Signal");
|
||||||
|
|
||||||
|
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
|
||||||
|
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\""
|
||||||
|
buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
|
||||||
|
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
|
||||||
|
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
|
||||||
|
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
|
||||||
|
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
|
||||||
|
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||||
|
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||||
|
buildConfigField "String", "USER_AGENT", "\"OWA\""
|
||||||
|
buildConfigField "boolean", "DEV_BUILD", "false"
|
||||||
|
buildConfigField "String", "MRENCLAVE", "\"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9\""
|
||||||
|
buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"f2e2a5004794a6c1bac5c4949eadbc243dd02e02d1a93f10fe24584fb70815d8\""
|
||||||
|
buildConfigField "String", "KEY_BACKUP_MRENCLAVE", "\"f51f435802ada769e67aaf5744372bb7e7d519eecf996d335eb5b46b872b5789\""
|
||||||
|
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
|
||||||
|
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||||
|
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||||
|
|
||||||
|
ndk {
|
||||||
|
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||||
|
}
|
||||||
|
|
||||||
|
resConfigs autoResConfig()
|
||||||
|
|
||||||
|
splits {
|
||||||
|
abi {
|
||||||
|
enable true
|
||||||
|
reset()
|
||||||
|
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||||
|
universalApk true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
exclude 'LICENSE.txt'
|
||||||
|
exclude 'LICENSE'
|
||||||
|
exclude 'NOTICE'
|
||||||
|
exclude 'asm-license.txt'
|
||||||
|
exclude 'META-INF/LICENSE'
|
||||||
|
exclude 'META-INF/NOTICE'
|
||||||
|
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
minifyEnabled true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'),
|
||||||
|
'proguard/proguard-firebase-messaging.pro',
|
||||||
|
'proguard/proguard-google-play-services.pro',
|
||||||
|
'proguard/proguard-jackson.pro',
|
||||||
|
'proguard/proguard-sqlite.pro',
|
||||||
|
'proguard/proguard-appcompat-v7.pro',
|
||||||
|
'proguard/proguard-square-okhttp.pro',
|
||||||
|
'proguard/proguard-square-okio.pro',
|
||||||
|
'proguard/proguard-spongycastle.pro',
|
||||||
|
'proguard/proguard-rounded-image-view.pro',
|
||||||
|
'proguard/proguard-glide.pro',
|
||||||
|
'proguard/proguard-shortcutbadger.pro',
|
||||||
|
'proguard/proguard-retrofit.pro',
|
||||||
|
'proguard/proguard-webrtc.pro',
|
||||||
|
'proguard/proguard-klinker.pro',
|
||||||
|
'proguard/proguard-retrolambda.pro',
|
||||||
|
'proguard/proguard-okhttp.pro',
|
||||||
|
'proguard/proguard-ez-vcard.pro',
|
||||||
|
'proguard/proguard.cfg'
|
||||||
|
testProguardFiles 'proguard/proguard-automation.pro',
|
||||||
|
'proguard/proguard.cfg'
|
||||||
|
}
|
||||||
|
staging {
|
||||||
|
initWith debug
|
||||||
|
|
||||||
|
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
|
||||||
|
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
|
||||||
|
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
|
||||||
|
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
|
||||||
|
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||||
|
buildConfigField "String", "MRENCLAVE", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\""
|
||||||
|
buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"b5a865941f95887018c86725cc92308d34a3084dc2b4e7bd2de5e5e1690b50c6\""
|
||||||
|
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||||
|
}
|
||||||
|
release {
|
||||||
|
minifyEnabled true
|
||||||
|
proguardFiles = buildTypes.debug.proguardFiles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
productFlavors {
|
||||||
|
play {
|
||||||
|
dimension "none"
|
||||||
|
ext.websiteUpdateUrl = "null"
|
||||||
|
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||||
|
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||||
|
}
|
||||||
|
|
||||||
|
website {
|
||||||
|
dimension "none"
|
||||||
|
ext.websiteUpdateUrl = "https://updates.signal.org/android"
|
||||||
|
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
|
||||||
|
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android.applicationVariants.all { variant ->
|
||||||
|
variant.outputs.each { output ->
|
||||||
|
output.outputFileName = output.outputFileName.replace(".apk", "-${variant.versionName}.apk")
|
||||||
|
def abiName = output.getFilter("ABI") ?: 'universal'
|
||||||
|
def postFix = abiPostFix.get(abiName, 0)
|
||||||
|
|
||||||
|
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
|
||||||
|
|
||||||
|
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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']
|
||||||
|
proto.srcDir '../protobuf'
|
||||||
|
}
|
||||||
|
androidTest {
|
||||||
|
java.srcDirs = ['../test/androidTest/java']
|
||||||
|
}
|
||||||
|
test {
|
||||||
|
java.srcDirs = ['../test/unitTest/java']
|
||||||
|
resources.srcDirs = ['../test/unitTest/resources']
|
||||||
|
}
|
||||||
|
staging {
|
||||||
|
res.srcDirs = ['../staging/res']
|
||||||
|
}
|
||||||
|
|
||||||
|
website.manifest.srcFile '../website/AndroidManifest.xml'
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
abortOnError true
|
||||||
|
baseline file("lint-baseline.xml")
|
||||||
|
disable "LintError"
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
includeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def assembleWebsiteDescriptor = { variant, file ->
|
||||||
|
if (file.exists()) {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
|
file.eachByte 4096, {bytes, size ->
|
||||||
|
md.update(bytes, 0, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
String digest = md.digest().collect {String.format "%02x", it}.join();
|
||||||
|
String url = variant.productFlavors.get(0).ext.websiteUpdateUrl
|
||||||
|
String apkName = file.getName()
|
||||||
|
|
||||||
|
String descriptor = "{" +
|
||||||
|
"\"versionCode\" : ${canonicalVersionCode * postFixSize + abiPostFix['universal']}," +
|
||||||
|
"\"versionName\" : \"$canonicalVersionName\"," +
|
||||||
|
"\"sha256sum\" : \"$digest\"," +
|
||||||
|
"\"url\" : \"$url/$apkName\"" +
|
||||||
|
"}"
|
||||||
|
|
||||||
|
File descriptorFile = new File(file.getParent(), apkName.replace(".apk", ".json"))
|
||||||
|
|
||||||
|
descriptorFile.write(descriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def signProductionRelease = { variant ->
|
||||||
|
variant.outputs.collect { output ->
|
||||||
|
String apkName = output.outputFile.name
|
||||||
|
File inputFile = new File(output.outputFile.path)
|
||||||
|
File outputFile = new File(output.outputFile.parent, apkName.replace('-unsigned', ''))
|
||||||
|
|
||||||
|
new ApkSignerUtil('sun.security.pkcs11.SunPKCS11',
|
||||||
|
'pkcs11.config',
|
||||||
|
'PKCS11',
|
||||||
|
'file:pkcs11.password').calculateSignature(inputFile.getAbsolutePath(),
|
||||||
|
outputFile.getAbsolutePath())
|
||||||
|
|
||||||
|
inputFile.delete()
|
||||||
|
outputFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task signProductionPlayRelease {
|
||||||
|
doLast {
|
||||||
|
signProductionRelease(android.applicationVariants.find { (it.name == 'playRelease') })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task signProductionWebsiteRelease {
|
||||||
|
doLast {
|
||||||
|
def variant = android.applicationVariants.find { (it.name == 'websiteRelease') }
|
||||||
|
File signedRelease = signProductionRelease(variant).find { it.name.contains('universal') }
|
||||||
|
assembleWebsiteDescriptor(variant, signedRelease)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.whenTaskAdded { task ->
|
||||||
|
if (task.name.equals("assemblePlayRelease")) {
|
||||||
|
task.finalizedBy signProductionPlayRelease
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.name.equals("assembleWebsiteRelease")) {
|
||||||
|
task.finalizedBy signProductionWebsiteRelease
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getLastCommitTimestamp() {
|
||||||
|
new ByteArrayOutputStream().withStream { os ->
|
||||||
|
def result = exec {
|
||||||
|
executable = 'git'
|
||||||
|
args = ['log', '-1', '--pretty=format:%ct']
|
||||||
|
standardOutput = os
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.toString() + "000"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<issues format="5" by="lint 3.3.2" client="gradle" variant="playRelease" version="3.3.2">
|
<issues format="5" by="lint 3.3.2" client="gradle" variant="playRelease" version="3.3.2">
|
||||||
|
|
||||||
<issue
|
|
||||||
id="LintError"
|
|
||||||
message="Unexpected failure during lint analysis of JobManager.java (this is a bug in lint or one of the libraries it depends on)

Stack: `NullPointerException:ClsFileImpl.getMirror(ClsFileImpl.java:343)←ClsElementImpl.getMirror(ClsElementImpl.java:159)←ClsElementImpl.getText(ClsElementImpl.java:202)←InferenceSession.argConstraints(InferenceSession.java:1817)←InferenceSession.isFunctionalTypeMoreSpecific(InferenceSession.java:1748)←InferenceSession.isFunctionalTypeMoreSpecificOnExpression(InferenceSession.java:1729)←JavaMethodsConflictResolver.isFunctionalTypeMoreSpecific(JavaMethodsConflictResolver.java:779)←JavaMethodsConflictResolver.isTypeMoreSpecific(JavaMethodsConflictResolver.java:673)`

You can set environment variable `LINT_PRINT_STACKTRACE=true` to dump a full stacktrace to stdout.">
|
|
||||||
<location
|
|
||||||
file="src/org/thoughtcrime/securesms/jobmanager/JobManager.java"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="MissingPermission"
|
id="MissingPermission"
|
||||||
message="Call requires permission which may be rejected by user: code should explicitly check to see if permission is available (with `checkPermission`) or explicitly handle a potential `SecurityException`"
|
message="Call requires permission which may be rejected by user: code should explicitly check to see if permission is available (with `checkPermission`) or explicitly handle a potential `SecurityException`"
|
||||||
@@ -12,5 +12,14 @@
|
|||||||
<issue id="ImpliedQuantity" severity="warning" />
|
<issue id="ImpliedQuantity" severity="warning" />
|
||||||
|
|
||||||
<issue id="CanvasSize" severity="error" />
|
<issue id="CanvasSize" severity="error" />
|
||||||
|
<issue id="HardcodedText" severity="error" />
|
||||||
|
<issue id="VectorRaster" severity="error" />
|
||||||
|
<issue id="ButtonOrder" severity="error" />
|
||||||
|
<issue id="ExtraTranslation" severity="warning" />
|
||||||
|
|
||||||
|
<issue id="RestrictedApi" severity="error">
|
||||||
|
<ignore path="*/org/thoughtcrime/securesms/mediasend/camerax/VideoCapture.java" />
|
||||||
|
<ignore path="*/org/thoughtcrime/securesms/mediasend/camerax/CameraXModule.java" />
|
||||||
|
</issue>
|
||||||
|
|
||||||
</lint>
|
</lint>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
-dontoptimize
|
-dontoptimize
|
||||||
|
-dontobfuscate
|
||||||
-keepattributes SourceFile,LineNumberTable
|
-keepattributes SourceFile,LineNumberTable
|
||||||
-keep class org.whispersystems.** { *; }
|
-keep class org.whispersystems.** { *; }
|
||||||
-keep class org.thoughtcrime.securesms.** { *; }
|
-keep class org.thoughtcrime.securesms.** { *; }
|
||||||
@@ -6,3 +7,5 @@
|
|||||||
public void onEvent*(**);
|
public void onEvent*(**);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Protobuf lite
|
||||||
|
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import groovy.io.FileType
|
||||||
|
import groovy.transform.stc.ClosureParams
|
||||||
|
import groovy.transform.stc.SimpleType
|
||||||
|
|
||||||
|
ext {
|
||||||
|
autoResConfig = this.&autoResConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
def allStringsResourceFiles(@ClosureParams(value = SimpleType.class, options = ['java.io.File']) Closure c) {
|
||||||
|
file('../res').eachFileRecurse(FileType.FILES) { f ->
|
||||||
|
if (f.name == 'strings.xml') {
|
||||||
|
c(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discovers supported languages listed as under the res/values- directory.
|
||||||
|
*/
|
||||||
|
def autoResConfig() {
|
||||||
|
def files = []
|
||||||
|
allStringsResourceFiles { f ->
|
||||||
|
files.add(f.parentFile.name)
|
||||||
|
}
|
||||||
|
['en'] + files.collect { f -> f =~ /^values-([a-z]{2}(-r[A-Z]{2})?)$/ }
|
||||||
|
.findAll { matcher -> matcher.find() }
|
||||||
|
.collect { matcher -> matcher.group(1) }
|
||||||
|
.sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
task pullTranslations(type: Exec) {
|
||||||
|
group 'Translate'
|
||||||
|
description 'Pull translations, requires transifex client and api key.'
|
||||||
|
commandLine 'tx', 'pull', '-a', '--minimum-perc=80', '--force'
|
||||||
|
}
|
||||||
|
|
||||||
|
task replaceEllipsis {
|
||||||
|
group 'Translate'
|
||||||
|
description 'Process strings for ellipsis characters.'
|
||||||
|
doLast {
|
||||||
|
allStringsResourceFiles { f ->
|
||||||
|
def before = f.text
|
||||||
|
def after = f.text.replace('...', '…')
|
||||||
|
if (before != after) {
|
||||||
|
f.text = after
|
||||||
|
logger.info("$f.parentFile.name/$f.name...updated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mustRunAfter pullTranslations
|
||||||
|
}
|
||||||
|
|
||||||
|
task cleanApostropheErrors {
|
||||||
|
group 'Translate'
|
||||||
|
description 'Fix transifex apostrophe string errors.'
|
||||||
|
doLast {
|
||||||
|
allStringsResourceFiles { f ->
|
||||||
|
def before = f.text
|
||||||
|
def after = before.replaceAll(/([^\\=08])(')/, '$1\\\\\'')
|
||||||
|
if (before != after) {
|
||||||
|
f.text = after
|
||||||
|
logger.info("$f.parentFile.name/$f.name...updated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mustRunAfter replaceEllipsis
|
||||||
|
}
|
||||||
|
|
||||||
|
task excludeNonTranslatables {
|
||||||
|
group 'Translate'
|
||||||
|
description 'Remove strings that are marked "translatable"="false" or are ExtraTranslations.'
|
||||||
|
doLast {
|
||||||
|
def englishFile = file('../res/values/strings.xml')
|
||||||
|
|
||||||
|
def english = new XmlParser().parse(englishFile)
|
||||||
|
def nonTranslatable = english
|
||||||
|
.findAll { it['@translatable'] == 'false' }
|
||||||
|
.collect { it['@name'] }
|
||||||
|
.toSet()
|
||||||
|
def all = english.collect { it['@name'] }.toSet()
|
||||||
|
def translatable = all - nonTranslatable
|
||||||
|
|
||||||
|
allStringsResourceFiles { f ->
|
||||||
|
if (f != englishFile) {
|
||||||
|
def newLines = f.readLines()
|
||||||
|
.collect { line ->
|
||||||
|
def matcher = line =~ /name="([^"]*)".*<\//
|
||||||
|
if (matcher.find()) {
|
||||||
|
def name = matcher.group(1)
|
||||||
|
if (!line.contains('excludeNonTranslatables') && !translatable.contains(name)) {
|
||||||
|
return " <!-- Removed by excludeNonTranslatables ${line.trim()} -->"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
f.write(newLines.join("\n") + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mustRunAfter cleanApostropheErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
task translate {
|
||||||
|
group 'Translate'
|
||||||
|
description 'Pull translations and post-process for ellipsis, apostrophes and non-translatables.'
|
||||||
|
dependsOn pullTranslations, replaceEllipsis, cleanApostropheErrors, excludeNonTranslatables
|
||||||
|
}
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
// Auto-generated, use ./gradlew calculateChecksums to regenerate
|
||||||
|
|
||||||
|
dependencyVerification {
|
||||||
|
verify = [
|
||||||
|
|
||||||
|
['androidx.activity:activity:1.0.0',
|
||||||
|
'd1bc9842455c2e534415d88c44df4d52413b478db9093a1ba36324f705f44c3d'],
|
||||||
|
|
||||||
|
['androidx.annotation:annotation:1.1.0',
|
||||||
|
'd38d63edb30f1467818d50aaf05f8a692dea8b31392a049bfa991b159ad5b692'],
|
||||||
|
|
||||||
|
['androidx.appcompat:appcompat-resources:1.1.0-beta01',
|
||||||
|
'53c0a33d07c4bab48d4c8169bf30053aa14965af4a775b56092a9fc7079802b1'],
|
||||||
|
|
||||||
|
['androidx.appcompat:appcompat:1.1.0-beta01',
|
||||||
|
'49ad229add44f822fcb3c8405c3fddbd72660da6a839ce29e13158f5980514fd'],
|
||||||
|
|
||||||
|
['androidx.arch.core:core-common:2.1.0',
|
||||||
|
'fe1237bf029d063e7f29fe39aeaf73ef74c8b0a3658486fc29d3c54326653889'],
|
||||||
|
|
||||||
|
['androidx.arch.core:core-runtime:2.1.0',
|
||||||
|
'dd77615bd3dd275afb11b62df25bae46b10b4a117cd37943af45bdcbf8755852'],
|
||||||
|
|
||||||
|
['androidx.asynclayoutinflater:asynclayoutinflater:1.0.0',
|
||||||
|
'f7eab60c57addd94bb06275832fe7600611beaaae1a1ec597c231956faf96c8b'],
|
||||||
|
|
||||||
|
['androidx.camera:camera-camera2:1.0.0-alpha06',
|
||||||
|
'e50f20deb950ffebcd4d1de5408ef7a5404bec80ec77119e05663c890739b903'],
|
||||||
|
|
||||||
|
['androidx.camera:camera-core:1.0.0-alpha06',
|
||||||
|
'0096cabe539d9b4288f406acfb44264b137ebd600e38e33504ff425c979016c9'],
|
||||||
|
|
||||||
|
['androidx.cardview:cardview:1.0.0',
|
||||||
|
'1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7'],
|
||||||
|
|
||||||
|
['androidx.collection:collection:1.1.0',
|
||||||
|
'632a0e5407461de774409352940e292a291037724207a787820c77daf7d33b72'],
|
||||||
|
|
||||||
|
['androidx.concurrent:concurrent-futures:1.0.0-alpha03',
|
||||||
|
'50812a53912255e3e0f2147d13bbbb81937c3726fda2e984e77a27c7207d96a1'],
|
||||||
|
|
||||||
|
['androidx.constraintlayout:constraintlayout-solver:1.1.3',
|
||||||
|
'965c177e64fbd81bd1d27b402b66ef9d7bc7b5cb5f718044bf7a453abc542045'],
|
||||||
|
|
||||||
|
['androidx.constraintlayout:constraintlayout:1.1.3',
|
||||||
|
'5ff864def9d41cd04e08348d69591143bae3ceff4284cf8608bceb98c36ac830'],
|
||||||
|
|
||||||
|
['androidx.coordinatorlayout:coordinatorlayout:1.0.0',
|
||||||
|
'e508c695489493374d942bf7b4ee02abf7571d25aac4c622e57d6cd5cd29eb73'],
|
||||||
|
|
||||||
|
['androidx.core:core:1.1.0',
|
||||||
|
'76c7cfbe596fe3c09a6983bf1c89e889299c08ac9a3b52ce5182a088d056647e'],
|
||||||
|
|
||||||
|
['androidx.cursoradapter:cursoradapter:1.0.0',
|
||||||
|
'a81c8fe78815fa47df5b749deb52727ad11f9397da58b16017f4eb2c11e28564'],
|
||||||
|
|
||||||
|
['androidx.customview:customview:1.0.0',
|
||||||
|
'20e5b8f6526a34595a604f56718da81167c0b40a7a94a57daa355663f2594df2'],
|
||||||
|
|
||||||
|
['androidx.documentfile:documentfile:1.0.0',
|
||||||
|
'865a061ef2fad16522f8433536b8d47208c46ff7c7745197dfa1eeb481869487'],
|
||||||
|
|
||||||
|
['androidx.drawerlayout:drawerlayout:1.0.0',
|
||||||
|
'9402442cdc5a43cf62fb14f8cf98c63342d4d9d9b805c8033c6cf7e802749ac1'],
|
||||||
|
|
||||||
|
['androidx.exifinterface:exifinterface:1.0.0',
|
||||||
|
'ee48be10aab8f54efff4c14b77d11e10b9eeee4379d5ef6bf297a2923c55cc11'],
|
||||||
|
|
||||||
|
['androidx.fragment:fragment:1.1.0',
|
||||||
|
'a14c8b8f2153f128e800fbd266a6beab1c283982a29ec570d2cc05d307d81496'],
|
||||||
|
|
||||||
|
['androidx.gridlayout:gridlayout:1.0.0',
|
||||||
|
'a7e5dc6f39dbc3dc6ac6d57b02a9c6fd792e80f0e45ddb3bb08e8f03d23c8755'],
|
||||||
|
|
||||||
|
['androidx.interpolator:interpolator:1.0.0',
|
||||||
|
'33193135a64fe21fa2c35eec6688f1a76e512606c0fc83dc1b689e37add7732a'],
|
||||||
|
|
||||||
|
['androidx.legacy:legacy-preference-v14:1.0.0',
|
||||||
|
'd6d11913e56b8f2d14fd560bd1ad6d7fd5624a15dd4ec073b2d9188205f86280'],
|
||||||
|
|
||||||
|
['androidx.legacy:legacy-support-core-ui:1.0.0',
|
||||||
|
'0d1260c6e7e6a337f875df71b516931e703f716e90889817cd3a20fa5ac3d947'],
|
||||||
|
|
||||||
|
['androidx.legacy:legacy-support-core-utils:1.0.0',
|
||||||
|
'a7edcf01d5b52b3034073027bc4775b78a4764bb6202bb91d61c829add8dd1c7'],
|
||||||
|
|
||||||
|
['androidx.legacy:legacy-support-v13:1.0.0',
|
||||||
|
'65f5fcb57644d381d471a00fdf50f90b808be6b48a8ae57fb4ea39b7da8cca86'],
|
||||||
|
|
||||||
|
['androidx.legacy:legacy-support-v4:1.0.0',
|
||||||
|
'78fec1485f0f388a4749022dd51416857127cd2544ae1c3fd0b16589055480b0'],
|
||||||
|
|
||||||
|
['androidx.lifecycle:lifecycle-common-java8:2.1.0',
|
||||||
|
'a1ec63c1bb973443cb731d78ec336c5e20e7ee35c89cbb32d36f92c55bb02542'],
|
||||||
|
|
||||||
|
['androidx.lifecycle:lifecycle-common:2.2.0-alpha05',
|
||||||
|
'63898dabf7cfe5ec5d7ed8b8c2564c1427be876e1496ead95c2703cf59d3734b'],
|
||||||
|
|
||||||
|
['androidx.lifecycle:lifecycle-extensions:2.1.0',
|
||||||
|
'bd53c64b038585215b4959c1a388437a3ad525608a31c58e4283c3e371727d4d'],
|
||||||
|
|
||||||
|
['androidx.lifecycle:lifecycle-livedata-core:2.2.0-alpha05',
|
||||||
|
'6df2bcbf3be50a5fa29e9aa09d39437f2d61d17c2c46ef618d65bbac4a4a99fc'],
|
||||||
|
|
||||||
|
['androidx.lifecycle:lifecycle-livedata:2.1.0',
|
||||||
|
'242e446bed3db36f0df0aab0cb7f91060bd2dab7adcad1117adf54e724cd1d26'],
|
||||||
|
|
||||||
|
['androidx.lifecycle:lifecycle-process:2.1.0',
|
||||||
|
'8cddd0c7f4927bbf71fb71fca000786df82cc597c99463d6916ccbe4a205a9ac'],
|
||||||
|
|
||||||
|
['androidx.lifecycle:lifecycle-runtime:2.1.0',
|
||||||
|
'e5173897b965e870651e83d9d5af1742d3f532d58863223a390ce3a194c8312b'],
|
||||||
|
|
||||||
|
['androidx.lifecycle:lifecycle-service:2.1.0',
|
||||||
|
'23516745f34f16ff7850bb1eadd55cf193dd789cba428de4bca120433e3bfd69'],
|
||||||
|
|
||||||
|
['androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05',
|
||||||
|
'f503b53f50c4e6c1f9a3d698c4733df6e7a44049fe477ad0b85cc2f460401fbc'],
|
||||||
|
|
||||||
|
['androidx.lifecycle:lifecycle-viewmodel:2.2.0-alpha05',
|
||||||
|
'7725715491963440ee483e46526cd4f83af1c758e072e97b3eab2115c6f4db35'],
|
||||||
|
|
||||||
|
['androidx.loader:loader:1.0.0',
|
||||||
|
'11f735cb3b55c458d470bed9e25254375b518b4b1bad6926783a7026db0f5025'],
|
||||||
|
|
||||||
|
['androidx.localbroadcastmanager:localbroadcastmanager:1.0.0',
|
||||||
|
'e71c328ceef5c4a7d76f2d86df1b65d65fe2acf868b1a4efd84a3f34336186d8'],
|
||||||
|
|
||||||
|
['androidx.media:media:1.0.0',
|
||||||
|
'b23b527b2bac870c4a7451e6982d7132e413e88d7f27dbeb1fc7640a720cd9ee'],
|
||||||
|
|
||||||
|
['androidx.multidex:multidex:2.0.1',
|
||||||
|
'42dd32ff9f97f85771b82a20003a8d70f68ab7b4ba328964312ce0732693db09'],
|
||||||
|
|
||||||
|
['androidx.navigation:navigation-common:2.1.0',
|
||||||
|
'f968fcaa2fd94b0d1275ce175ecfb4773678732ead9b4d81993ffd5bc3fe3c7c'],
|
||||||
|
|
||||||
|
['androidx.navigation:navigation-fragment:2.1.0',
|
||||||
|
'776ba1be826f8de7cb262f55ece262c5eb9947758cbd2e902298750521404dd2'],
|
||||||
|
|
||||||
|
['androidx.navigation:navigation-runtime:2.1.0',
|
||||||
|
'499029c016345a2a2130ee7a32670871757e5fc7e6d1b93be8962bb59fa5ce9d'],
|
||||||
|
|
||||||
|
['androidx.navigation:navigation-ui:2.1.0',
|
||||||
|
'1ec0558d692982c5bcfcca6de5b5972723e6b4a9870aa7fc1eddf5e869f116ed'],
|
||||||
|
|
||||||
|
['androidx.preference:preference:1.0.0',
|
||||||
|
'ea9fde25606eb456210ffe9f7e51048abd776b55a34c0cc6608282b5699122d1'],
|
||||||
|
|
||||||
|
['androidx.print:print:1.0.0',
|
||||||
|
'1d5c7f3135a1bba661fc373fd72e11eb0a4adbb3396787826dd8e4190d5d9edd'],
|
||||||
|
|
||||||
|
['androidx.recyclerview:recyclerview:1.0.0',
|
||||||
|
'06956fb1ac014027ca9d2b40469a4b42aa61b4957bb11848e1ff352701ab4548'],
|
||||||
|
|
||||||
|
['androidx.savedstate:savedstate:1.0.0',
|
||||||
|
'2510a5619c37579c9ce1a04574faaf323cd0ffe2fc4e20fa8f8f01e5bb402e83'],
|
||||||
|
|
||||||
|
['androidx.slidingpanelayout:slidingpanelayout:1.0.0',
|
||||||
|
'76bffb7cefbf780794d8817002dad1562f3e27c0a9f746d62401c8edb30aeede'],
|
||||||
|
|
||||||
|
['androidx.swiperefreshlayout:swiperefreshlayout:1.0.0',
|
||||||
|
'9761b3a809c9b093fd06a3c4bbc645756dec0e95b5c9da419bc9f2a3f3026e8d'],
|
||||||
|
|
||||||
|
['androidx.transition:transition:1.0.1',
|
||||||
|
'c374bef04f01580ba76447e759ea560079727779ff882ad55735fd445edca8b4'],
|
||||||
|
|
||||||
|
['androidx.vectordrawable:vectordrawable-animated:1.1.0-beta02',
|
||||||
|
'f1613c47f1e6d2cd02ec9a42925f1a964fa63d1d028d34d884364cc3b9ffcb8f'],
|
||||||
|
|
||||||
|
['androidx.vectordrawable:vectordrawable:1.1.0-beta02',
|
||||||
|
'b632152304edb506bf7eacb329ef41e43b80164bf5be4c7bb132a249a65cbc26'],
|
||||||
|
|
||||||
|
['androidx.versionedparcelable:versionedparcelable:1.1.0',
|
||||||
|
'9a1d77140ac222b7866b5054ee7d159bc1800987ed2d46dd6afdd145abb710c1'],
|
||||||
|
|
||||||
|
['androidx.viewpager:viewpager:1.0.0',
|
||||||
|
'147af4e14a1984010d8f155e5e19d781f03c1d70dfed02a8e0d18428b8fc8682'],
|
||||||
|
|
||||||
|
['cn.carbswang.android:NumberPickerView:1.0.9',
|
||||||
|
'18b3c316d62c7c277978a8d4ed57a5b8f4e943762264960f579a8a549c756729'],
|
||||||
|
|
||||||
|
['com.airbnb.android:lottie:3.0.7',
|
||||||
|
'6819ff968eb768096133c7873d63351705fd4ac424a0917d86c4145f5035097d'],
|
||||||
|
|
||||||
|
['com.amulyakhare:com.amulyakhare.textdrawable:1.0.1',
|
||||||
|
'54c92b5fba38cfd316a07e5a30528068f45ce8515a6890f1297df4c401af5dcb'],
|
||||||
|
|
||||||
|
['com.annimon:stream:1.1.8',
|
||||||
|
'5da6e2e3e0551d61a3ea7014f04312276549e3dd739cf637996e4cf43c5535b9'],
|
||||||
|
|
||||||
|
['com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4',
|
||||||
|
'5b4aa6a52a957cfd55f60f4220c11c0c371385a3cb9786cae03c260dcdef5794'],
|
||||||
|
|
||||||
|
['com.davemorrissey.labs:subsampling-scale-image-view:3.6.0',
|
||||||
|
'550c5baa07e0bb4ff0a18b705e96d34436d22619248bd8c08c08c730b1f55cfe'],
|
||||||
|
|
||||||
|
['com.fasterxml.jackson.core:jackson-annotations:2.9.0',
|
||||||
|
'45d32ac61ef8a744b464c54c2b3414be571016dd46bfc2bec226761cf7ae457a'],
|
||||||
|
|
||||||
|
['com.fasterxml.jackson.core:jackson-core:2.9.9',
|
||||||
|
'3083079be6088db2ed0a0c6ff92204e0aa48fa1de9db5b59c468f35acf882c2c'],
|
||||||
|
|
||||||
|
['com.fasterxml.jackson.core:jackson-databind:2.9.9.2',
|
||||||
|
'fb262d42ea2de98044b62d393950a5aa050435fec38bbcadf2325cf7dc41b848'],
|
||||||
|
|
||||||
|
['com.github.bumptech.glide:annotations:4.9.0',
|
||||||
|
'702a7521cb3f6d7e55edd66e90bda1a1975baf971d25f75b75638579f86bc69b'],
|
||||||
|
|
||||||
|
['com.github.bumptech.glide:disklrucache:4.9.0',
|
||||||
|
'4696a81340eb6beee21ab93f703ed6e7ae49fb4ce3bc2fbc546e5bacd21b96b9'],
|
||||||
|
|
||||||
|
['com.github.bumptech.glide:gifdecoder:4.9.0',
|
||||||
|
'7ee9402ae1c48fac9232b67e81f881c217b907b3252e49ce57bdb97937ebb270'],
|
||||||
|
|
||||||
|
['com.github.bumptech.glide:glide:4.9.0',
|
||||||
|
'1bf482442fce81aa9065a5e97e721039d921cc45f727a987be5f1a69f844d955'],
|
||||||
|
|
||||||
|
['com.github.chrisbanes:PhotoView:2.1.3',
|
||||||
|
'ed06775308da260e1fd86d1d3288988fcd3d80db24ce0d7c9fcfedc39e622292'],
|
||||||
|
|
||||||
|
['com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2',
|
||||||
|
'8dc6a29a5a8db7b2ad5a9a7fda1dc9ae0893f4c8f0545732b2c63854ea693e8e'],
|
||||||
|
|
||||||
|
['com.google.android.exoplayer:exoplayer-core:2.9.1',
|
||||||
|
'b6ab34abac36bc2bc6934b7a50008162feca2c0fde91aaf1e8c1c22f2c16e2c0'],
|
||||||
|
|
||||||
|
['com.google.android.exoplayer:exoplayer-ui:2.9.1',
|
||||||
|
'7a942afcc402ff01e9bf48e8d3942850986710f06562d50a1408aaf04a683151'],
|
||||||
|
|
||||||
|
['com.google.android.gms:play-services-auth-api-phone:16.0.0',
|
||||||
|
'19365818b9ceb048ef48db12b5ffadd5eb86dbeb2c7c7b823bfdd89c665f42e5'],
|
||||||
|
|
||||||
|
['com.google.android.gms:play-services-auth-base:16.0.0',
|
||||||
|
'51dc02ad2f8d1d9dff7b5b52c4df2c6c12ef7df55d752e919d5cb4dd6002ecd0'],
|
||||||
|
|
||||||
|
['com.google.android.gms:play-services-auth:16.0.1',
|
||||||
|
'aec9e1c584d442cb9f59481a50b2c66dc191872607c04d97ecb82dd0eb5149ec'],
|
||||||
|
|
||||||
|
['com.google.android.gms:play-services-base:16.0.1',
|
||||||
|
'aca10c780c3219bc50f3db06734f4ab88badd3113c564c0a3156ff8ff674655b'],
|
||||||
|
|
||||||
|
['com.google.android.gms:play-services-basement:16.0.1',
|
||||||
|
'e08bfd1e87c4e50ef76161d7ac76b873aeb975367eeb3afa4abe62ea1887c7c6'],
|
||||||
|
|
||||||
|
['com.google.android.gms:play-services-maps:16.1.0',
|
||||||
|
'ff50cae9e4059416202375597d99cdc8ddefd9cea3f1dc2ff53779a3a12eb480'],
|
||||||
|
|
||||||
|
['com.google.android.gms:play-services-stats:16.0.1',
|
||||||
|
'5b2d8281adbfd6e74d2295c94bab9ea80fc9a84dfbb397995673f5af4d4c6368'],
|
||||||
|
|
||||||
|
['com.google.android.gms:play-services-tasks:16.0.1',
|
||||||
|
'b31c18d8d1cc8d9814f295ee7435471333f370ba5bd904ca14f8f2bec4f35c35'],
|
||||||
|
|
||||||
|
['com.google.android.material:material:1.0.0',
|
||||||
|
'7680e381a3c03798d999b2e441caadd8a56a0a808e108024a67af9fe26c11adc'],
|
||||||
|
|
||||||
|
['com.google.android:flexbox:0.3.0',
|
||||||
|
'a9989fd13ae2ee42765dfc515fe362edf4f326e74925d02a10369df8092a4935'],
|
||||||
|
|
||||||
|
['com.google.auto.value:auto-value-annotations:1.6.3',
|
||||||
|
'0e951fee8c31f60270bc46553a8586001b7b93dbb12aec06373aa99a150392c0'],
|
||||||
|
|
||||||
|
['com.google.firebase:firebase-common:16.0.3',
|
||||||
|
'3db6bfd4c6f758551e5f9acdeada2050577277e6da1aefb2412de23829759bcf'],
|
||||||
|
|
||||||
|
['com.google.firebase:firebase-iid-interop:16.0.1',
|
||||||
|
'2a86322b9346fd4836219206d249e85803311655e96036a8e4b714ce7e79693b'],
|
||||||
|
|
||||||
|
['com.google.firebase:firebase-iid:17.0.4',
|
||||||
|
'bb42774e309d5eac1aa493d19711032bee4f677a409639b6a5cfa93089af93eb'],
|
||||||
|
|
||||||
|
['com.google.firebase:firebase-messaging:17.3.4',
|
||||||
|
'e42288e7950d7d3b033d3395a5ac9365d230da3e439a2794ec13e2ef0fbaf078'],
|
||||||
|
|
||||||
|
['com.google.guava:listenablefuture:1.0',
|
||||||
|
'e4ad7607e5c0477c6f890ef26a49cb8d1bb4dffb650bab4502afee64644e3069'],
|
||||||
|
|
||||||
|
['com.google.protobuf:protobuf-javalite:3.10.0',
|
||||||
|
'215a94dbe100130295906b531bb72a26965c7ac8fcd9a75bf8054a8ac2abf4b4'],
|
||||||
|
|
||||||
|
['com.google.zxing:android-integration:3.1.0',
|
||||||
|
'89e56aadf1164bd71e57949163c53abf90af368b51669c0d4a47a163335f95c4'],
|
||||||
|
|
||||||
|
['com.google.zxing:core:3.2.1',
|
||||||
|
'b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259'],
|
||||||
|
|
||||||
|
['com.googlecode.ez-vcard:ez-vcard:0.9.11',
|
||||||
|
'7e24ad50b222d2f70ac91bdccfa3c0f6200b078d797cb784837f75e77bb4210f'],
|
||||||
|
|
||||||
|
['com.googlecode.libphonenumber:libphonenumber:8.11.0',
|
||||||
|
'833ec343e55a6910daca392af76f8e09caaab9c4e3f41ded83f4746e7ef389c7'],
|
||||||
|
|
||||||
|
['com.jpardogo.materialtabstrip:library:1.0.9',
|
||||||
|
'c6ef812fba4f74be7dc4a905faa4c2908cba261a94c13d4f96d5e67e4aad4aaa'],
|
||||||
|
|
||||||
|
['com.klinkerapps:android-smsmms:4.0.1',
|
||||||
|
'e7c3328a0f3a8dd44daa8129de4e99996f3057a4546e47891b036b81e0ebf1d1'],
|
||||||
|
|
||||||
|
['com.klinkerapps:logger:1.0.3',
|
||||||
|
'177e325259a8b111ad6745ec10db5861723c99f402222b80629f576f49408541'],
|
||||||
|
|
||||||
|
['com.makeramen:roundedimageview:2.1.0',
|
||||||
|
'1f5a1865796b308c6cdd114acc6e78408b110f0a62fc63553278fbeacd489cd1'],
|
||||||
|
|
||||||
|
['com.melnykov:floatingactionbutton:1.3.0',
|
||||||
|
'15d58d4fac0f7a288d0e5301bbaf501a146f5b3f5921277811bf99bd3b397263'],
|
||||||
|
|
||||||
|
['com.nineoldandroids:library:2.4.0',
|
||||||
|
'68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a'],
|
||||||
|
|
||||||
|
['com.pnikosis:materialish-progress:1.5',
|
||||||
|
'd71d80e00717a096784482aee21001a9d299fec3833e4ebd87739ed36cf77c54'],
|
||||||
|
|
||||||
|
['com.squareup.okhttp3:okhttp:3.12.1',
|
||||||
|
'07c3d82ca7eaf4722f00b2da807dc7860f6169ae60cfedcf5d40218f90880a46'],
|
||||||
|
|
||||||
|
['com.squareup.okio:okio:1.17.4',
|
||||||
|
'd78fac588458fc099e6c82e91fe5f0375c67434626451a3a77772c65d9eee85b'],
|
||||||
|
|
||||||
|
['com.takisoft.fix:colorpicker:0.9.1',
|
||||||
|
'f5d0dbabe406a1800498ca9c1faf34db36e021d8488bf10360f29961fe3ab0d1'],
|
||||||
|
|
||||||
|
['com.theartofdev.edmodo:android-image-cropper:2.8.0',
|
||||||
|
'5516ea87672e478b3d0ed5c274a7df27d22c02e66f899388f9b8bee93669e176'],
|
||||||
|
|
||||||
|
['com.tomergoldst.android:tooltips:1.0.6',
|
||||||
|
'4c56697dd1ad64b8066535c61f961a6d901e7ae5d97ae27084ba40ad620349b6'],
|
||||||
|
|
||||||
|
['me.leolin:ShortcutBadger:1.1.16',
|
||||||
|
'e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774'],
|
||||||
|
|
||||||
|
['mobi.upod:time-duration-picker:1.1.3',
|
||||||
|
'db469ce0f48dd96b892eac424ed76870e54bf00fe0a28cdcddfbe5f2a226a0e1'],
|
||||||
|
|
||||||
|
['org.apache.httpcomponents:httpclient-android:4.3.5',
|
||||||
|
'6f56466a9bd0d42934b90bfbfe9977a8b654c058bf44a12bdc2877c4e1f033f1'],
|
||||||
|
|
||||||
|
['org.conscrypt:conscrypt-android:2.0.0',
|
||||||
|
'400ca559a49b860a82862b22cee0e3110764bdcf7ee7c79e7479895c25cdfc09'],
|
||||||
|
|
||||||
|
['org.greenrobot:eventbus:3.0.0',
|
||||||
|
'180d4212467df06f2fbc9c8d8a2984533ac79c87769ad883bc421612f0b4e17c'],
|
||||||
|
|
||||||
|
['org.jsoup:jsoup:1.8.3',
|
||||||
|
'abeaf34795a4de70f72aed6de5966d2955ec7eb348eeb813324f23c999575473'],
|
||||||
|
|
||||||
|
['org.signal:aesgcmprovider:0.0.3',
|
||||||
|
'6eb4422e8a618b3b76cb2096a3619d251f9e27989dc68307a1e5414c3710f2d1'],
|
||||||
|
|
||||||
|
['org.signal:android-database-sqlcipher:3.5.9-S3',
|
||||||
|
'33d4063336893af00b9d68b418e7b290cace74c20ce8aacffddc0911010d3d73'],
|
||||||
|
|
||||||
|
['org.signal:ringrtc-android:0.2.0',
|
||||||
|
'bce32dfe6c4fe107ccd4b9f155dcfaefe574e7740ef57190614b7e34c914f5f0'],
|
||||||
|
|
||||||
|
['org.signal:signal-metadata-java:0.1.0',
|
||||||
|
'f3faa23b7d9b5096d12979c35679d1e3b5e007522d8bef167a28e456f2a7c7d9'],
|
||||||
|
|
||||||
|
['org.threeten:threetenbp:1.3.6',
|
||||||
|
'f4c23ffaaed717c3b99c003e0ee02d6d66377fd47d866fec7d971bd8644fc1a7'],
|
||||||
|
|
||||||
|
['org.whispersystems:curve25519-java:0.5.0',
|
||||||
|
'0aadd43cf01d11e9b58f867b3c4f25c3194e8b0623d1953d32dfbfbee009e38d'],
|
||||||
|
|
||||||
|
['org.whispersystems:signal-protocol-java:2.8.1',
|
||||||
|
'b19db36839ab008fdccefc7f8c005f2ea43dc7c7298a209bc424e6f9b6d5617b'],
|
||||||
|
|
||||||
|
['pl.tajchert:waitingdots:0.1.0',
|
||||||
|
'2835d49e0787dbcb606c5a60021ced66578503b1e9fddcd7a5ef0cd5f095ba2c'],
|
||||||
|
|
||||||
|
['se.emilsjolander:stickylistheaders:2.7.0',
|
||||||
|
'a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb'],
|
||||||
|
]
|
||||||
|
}
|
||||||
-558
@@ -1,558 +0,0 @@
|
|||||||
import org.signal.signing.ApkSignerUtil
|
|
||||||
|
|
||||||
import java.security.MessageDigest
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
maven {
|
|
||||||
url "https://repo1.maven.org/maven2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath 'com.android.tools.build:gradle:3.3.2'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'witness'
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
maven {
|
|
||||||
url "https://raw.github.com/signalapp/maven/master/photoview/releases/"
|
|
||||||
content {
|
|
||||||
includeGroupByRegex "com\\.github\\.chrisbanes.*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
maven {
|
|
||||||
url "https://raw.github.com/signalapp/maven/master/shortcutbadger/releases/"
|
|
||||||
content {
|
|
||||||
includeGroupByRegex "me\\.leolin.*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
maven {
|
|
||||||
url "https://raw.github.com/signalapp/maven/master/circular-progress-button/releases/"
|
|
||||||
content {
|
|
||||||
includeGroupByRegex "com\\.github\\.dmytrodanylyk\\.circular-progress-button\\.*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
maven {
|
|
||||||
url "https://raw.github.com/signalapp/maven/master/sqlcipher/release/"
|
|
||||||
content {
|
|
||||||
includeGroupByRegex "org\\.signal.*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
maven { // textdrawable
|
|
||||||
url 'https://dl.bintray.com/amulyakhare/maven'
|
|
||||||
content {
|
|
||||||
includeGroupByRegex "com\\.amulyakhare.*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
google()
|
|
||||||
jcenter()
|
|
||||||
mavenLocal()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.1.0-beta01'
|
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
|
||||||
implementation 'com.google.android.material:material:1.0.0'
|
|
||||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
|
||||||
implementation 'androidx.preference:preference:1.0.0'
|
|
||||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
|
||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.0.0'
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.0.0'
|
|
||||||
implementation "androidx.camera:camera-core:1.0.0-alpha02"
|
|
||||||
implementation "androidx.camera:camera-camera2:1.0.0-alpha02"
|
|
||||||
|
|
||||||
implementation('com.google.firebase:firebase-messaging:17.3.4') {
|
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation 'com.google.android.gms:play-services-maps:16.1.0'
|
|
||||||
implementation 'com.google.android.gms:play-services-location:16.0.0'
|
|
||||||
implementation 'com.google.android.gms:play-services-auth:16.0.1'
|
|
||||||
|
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
|
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
|
||||||
|
|
||||||
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
|
||||||
implementation 'org.signal:aesgcmprovider:0.0.3'
|
|
||||||
|
|
||||||
implementation 'org.whispersystems:signal-service-android:2.13.7'
|
|
||||||
|
|
||||||
implementation 'org.whispersystems:webrtc-android:M77'
|
|
||||||
|
|
||||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
|
||||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
|
||||||
implementation 'com.jpardogo.materialtabstrip:library:1.0.9'
|
|
||||||
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
|
|
||||||
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
|
||||||
implementation 'com.github.bumptech.glide:glide:4.9.0'
|
|
||||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
|
|
||||||
annotationProcessor 'androidx.annotation:annotation:1.1.0'
|
|
||||||
implementation 'com.makeramen:roundedimageview:2.1.0'
|
|
||||||
implementation 'com.pnikosis:materialish-progress:1.5'
|
|
||||||
implementation 'org.greenrobot:eventbus:3.0.0'
|
|
||||||
implementation 'pl.tajchert:waitingdots:0.1.0'
|
|
||||||
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
|
|
||||||
implementation 'com.melnykov:floatingactionbutton:1.3.0'
|
|
||||||
implementation 'com.google.zxing:android-integration:3.1.0'
|
|
||||||
implementation 'mobi.upod:time-duration-picker:1.1.3'
|
|
||||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
|
||||||
implementation 'com.google.zxing:core:3.2.1'
|
|
||||||
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
|
|
||||||
exclude group: 'com.android.support', module: 'support-annotations'
|
|
||||||
}
|
|
||||||
implementation ('cn.carbswang.android:NumberPickerView:1.0.9') {
|
|
||||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
|
||||||
}
|
|
||||||
implementation ('com.tomergoldst.android:tooltips:1.0.6') {
|
|
||||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
|
||||||
}
|
|
||||||
implementation ('com.klinkerapps:android-smsmms:4.0.1') {
|
|
||||||
exclude group: 'com.squareup.okhttp', module: 'okhttp'
|
|
||||||
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
|
|
||||||
}
|
|
||||||
implementation 'com.annimon:stream:1.1.8'
|
|
||||||
implementation ('com.takisoft.fix:colorpicker:0.9.1') {
|
|
||||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
|
||||||
exclude group: 'com.android.support', module: 'recyclerview-v7'
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation 'com.airbnb.android:lottie:3.0.7'
|
|
||||||
|
|
||||||
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
|
|
||||||
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
|
|
||||||
implementation 'org.signal:android-database-sqlcipher:3.5.9-S3'
|
|
||||||
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
|
|
||||||
exclude group: 'com.fasterxml.jackson.core'
|
|
||||||
exclude group: 'org.freemarker'
|
|
||||||
}
|
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.12'
|
|
||||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
|
||||||
testImplementation 'org.mockito:mockito-core:1.9.5'
|
|
||||||
testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
|
|
||||||
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
|
|
||||||
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
|
|
||||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
|
|
||||||
|
|
||||||
testImplementation 'androidx.test:core:1.2.0'
|
|
||||||
androidTestImplementation 'androidx.multidex:multidex:2.0.1'
|
|
||||||
androidTestImplementation 'androidx.multidex:multidex-instrumentation:2.0.0'
|
|
||||||
androidTestImplementation 'com.google.dexmaker:dexmaker:1.2'
|
|
||||||
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:1.2'
|
|
||||||
androidTestImplementation ('org.assertj:assertj-core:1.7.1') {
|
|
||||||
exclude group: 'org.hamcrest', module: 'hamcrest-core'
|
|
||||||
}
|
|
||||||
androidTestImplementation ('com.squareup.assertj:assertj-android:1.1.1') {
|
|
||||||
exclude group: 'org.hamcrest', module: 'hamcrest-core'
|
|
||||||
exclude group: 'com.android.support', module: 'support-annotations'
|
|
||||||
}
|
|
||||||
testImplementation 'org.robolectric:robolectric:4.2'
|
|
||||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencyVerification {
|
|
||||||
configuration = '(play|website)(Debug|Release)RuntimeClasspath'
|
|
||||||
verify = [
|
|
||||||
'com.google.android.material:material:7680e381a3c03798d999b2e441caadd8a56a0a808e108024a67af9fe26c11adc',
|
|
||||||
'androidx.legacy:legacy-preference-v14:d6d11913e56b8f2d14fd560bd1ad6d7fd5624a15dd4ec073b2d9188205f86280',
|
|
||||||
'androidx.preference:preference:ea9fde25606eb456210ffe9f7e51048abd776b55a34c0cc6608282b5699122d1',
|
|
||||||
'com.pnikosis:materialish-progress:d71d80e00717a096784482aee21001a9d299fec3833e4ebd87739ed36cf77c54',
|
|
||||||
'pl.tajchert:waitingdots:2835d49e0787dbcb606c5a60021ced66578503b1e9fddcd7a5ef0cd5f095ba2c',
|
|
||||||
'com.theartofdev.edmodo:android-image-cropper:5516ea87672e478b3d0ed5c274a7df27d22c02e66f899388f9b8bee93669e176',
|
|
||||||
'mobi.upod:time-duration-picker:db469ce0f48dd96b892eac424ed76870e54bf00fe0a28cdcddfbe5f2a226a0e1',
|
|
||||||
'cn.carbswang.android:NumberPickerView:18b3c316d62c7c277978a8d4ed57a5b8f4e943762264960f579a8a549c756729',
|
|
||||||
'com.tomergoldst.android:tooltips:4c56697dd1ad64b8066535c61f961a6d901e7ae5d97ae27084ba40ad620349b6',
|
|
||||||
'com.takisoft.fix:colorpicker:f5d0dbabe406a1800498ca9c1faf34db36e021d8488bf10360f29961fe3ab0d1',
|
|
||||||
'com.airbnb.android:lottie:6819ff968eb768096133c7873d63351705fd4ac424a0917d86c4145f5035097d',
|
|
||||||
'com.codewaves.stickyheadergrid:stickyheadergrid:5b4aa6a52a957cfd55f60f4220c11c0c371385a3cb9786cae03c260dcdef5794',
|
|
||||||
'androidx.appcompat:appcompat:49ad229add44f822fcb3c8405c3fddbd72660da6a839ce29e13158f5980514fd',
|
|
||||||
'com.melnykov:floatingactionbutton:15d58d4fac0f7a288d0e5301bbaf501a146f5b3f5921277811bf99bd3b397263',
|
|
||||||
'androidx.recyclerview:recyclerview:06956fb1ac014027ca9d2b40469a4b42aa61b4957bb11848e1ff352701ab4548',
|
|
||||||
'androidx.legacy:legacy-support-v13:65f5fcb57644d381d471a00fdf50f90b808be6b48a8ae57fb4ea39b7da8cca86',
|
|
||||||
'androidx.cardview:cardview:1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7',
|
|
||||||
'androidx.gridlayout:gridlayout:a7e5dc6f39dbc3dc6ac6d57b02a9c6fd792e80f0e45ddb3bb08e8f03d23c8755',
|
|
||||||
'androidx.camera:camera-camera2:9dc33e45da983ebd29a888401ac700323ff573821eee3fa4d993dfa3d316ee2e',
|
|
||||||
'androidx.camera:camera-core:bf32bfcb5d103d865c6af1221a1d82e994c917b53c0bc080f1e9750bdc21cbb9',
|
|
||||||
'androidx.exifinterface:exifinterface:ee48be10aab8f54efff4c14b77d11e10b9eeee4379d5ef6bf297a2923c55cc11',
|
|
||||||
'androidx.constraintlayout:constraintlayout:5ff864def9d41cd04e08348d69591143bae3ceff4284cf8608bceb98c36ac830',
|
|
||||||
'androidx.multidex:multidex:42dd32ff9f97f85771b82a20003a8d70f68ab7b4ba328964312ce0732693db09',
|
|
||||||
'androidx.lifecycle:lifecycle-extensions:8d4072201b6231d67e4192d608d46b1f5c920845106c9831632c2e3ffe706117',
|
|
||||||
'androidx.lifecycle:lifecycle-common-java8:9edc2d4f589656d470ef03b9c6ece62d335971294b033ec7d9ceb6e361e9aafa',
|
|
||||||
'com.google.firebase:firebase-messaging:e42288e7950d7d3b033d3395a5ac9365d230da3e439a2794ec13e2ef0fbaf078',
|
|
||||||
'com.google.android.gms:play-services-maps:ff50cae9e4059416202375597d99cdc8ddefd9cea3f1dc2ff53779a3a12eb480',
|
|
||||||
'com.google.android.gms:play-services-location:240a0fcb9e8e58586e38ea43b69c09ed6e89ea9a0c69770b7634d81dabf5f3a0',
|
|
||||||
'com.google.android.gms:play-services-auth:aec9e1c584d442cb9f59481a50b2c66dc191872607c04d97ecb82dd0eb5149ec',
|
|
||||||
'com.google.android.exoplayer:exoplayer-ui:7a942afcc402ff01e9bf48e8d3942850986710f06562d50a1408aaf04a683151',
|
|
||||||
'com.google.android.exoplayer:exoplayer-core:b6ab34abac36bc2bc6934b7a50008162feca2c0fde91aaf1e8c1c22f2c16e2c0',
|
|
||||||
'org.conscrypt:conscrypt-android:400ca559a49b860a82862b22cee0e3110764bdcf7ee7c79e7479895c25cdfc09',
|
|
||||||
'org.signal:aesgcmprovider:6eb4422e8a618b3b76cb2096a3619d251f9e27989dc68307a1e5414c3710f2d1',
|
|
||||||
'org.whispersystems:signal-service-android:5115aa434c52ca671c513995e6ae67d73f3abaaa605f9e6cf64c2e01da961c7e',
|
|
||||||
'org.whispersystems:webrtc-android:c1254d6d8871c8a5a0e02f682626d0fc0ba1633587334c43edc75119f6977ac2',
|
|
||||||
'me.leolin:ShortcutBadger:e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774',
|
|
||||||
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
|
|
||||||
'com.jpardogo.materialtabstrip:library:c6ef812fba4f74be7dc4a905faa4c2908cba261a94c13d4f96d5e67e4aad4aaa',
|
|
||||||
'org.apache.httpcomponents:httpclient-android:6f56466a9bd0d42934b90bfbfe9977a8b654c058bf44a12bdc2877c4e1f033f1',
|
|
||||||
'com.github.chrisbanes:PhotoView:ed06775308da260e1fd86d1d3288988fcd3d80db24ce0d7c9fcfedc39e622292',
|
|
||||||
'com.github.bumptech.glide:glide:1bf482442fce81aa9065a5e97e721039d921cc45f727a987be5f1a69f844d955',
|
|
||||||
'com.makeramen:roundedimageview:1f5a1865796b308c6cdd114acc6e78408b110f0a62fc63553278fbeacd489cd1',
|
|
||||||
'org.greenrobot:eventbus:180d4212467df06f2fbc9c8d8a2984533ac79c87769ad883bc421612f0b4e17c',
|
|
||||||
'com.google.zxing:android-integration:89e56aadf1164bd71e57949163c53abf90af368b51669c0d4a47a163335f95c4',
|
|
||||||
'com.amulyakhare:com.amulyakhare.textdrawable:54c92b5fba38cfd316a07e5a30528068f45ce8515a6890f1297df4c401af5dcb',
|
|
||||||
'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259',
|
|
||||||
'com.davemorrissey.labs:subsampling-scale-image-view:550c5baa07e0bb4ff0a18b705e96d34436d22619248bd8c08c08c730b1f55cfe',
|
|
||||||
'com.klinkerapps:android-smsmms:e7c3328a0f3a8dd44daa8129de4e99996f3057a4546e47891b036b81e0ebf1d1',
|
|
||||||
'com.annimon:stream:5da6e2e3e0551d61a3ea7014f04312276549e3dd739cf637996e4cf43c5535b9',
|
|
||||||
'com.github.dmytrodanylyk.circular-progress-button:library:8dc6a29a5a8db7b2ad5a9a7fda1dc9ae0893f4c8f0545732b2c63854ea693e8e',
|
|
||||||
'org.signal:android-database-sqlcipher:33d4063336893af00b9d68b418e7b290cace74c20ce8aacffddc0911010d3d73',
|
|
||||||
'com.googlecode.ez-vcard:ez-vcard:7e24ad50b222d2f70ac91bdccfa3c0f6200b078d797cb784837f75e77bb4210f',
|
|
||||||
'com.google.firebase:firebase-iid:bb42774e309d5eac1aa493d19711032bee4f677a409639b6a5cfa93089af93eb',
|
|
||||||
'com.google.firebase:firebase-common:3db6bfd4c6f758551e5f9acdeada2050577277e6da1aefb2412de23829759bcf',
|
|
||||||
'com.google.android.gms:play-services-auth-api-phone:19365818b9ceb048ef48db12b5ffadd5eb86dbeb2c7c7b823bfdd89c665f42e5',
|
|
||||||
'com.google.android.gms:play-services-auth-base:51dc02ad2f8d1d9dff7b5b52c4df2c6c12ef7df55d752e919d5cb4dd6002ecd0',
|
|
||||||
'com.google.firebase:firebase-iid-interop:2a86322b9346fd4836219206d249e85803311655e96036a8e4b714ce7e79693b',
|
|
||||||
'com.google.android.gms:play-services-base:aca10c780c3219bc50f3db06734f4ab88badd3113c564c0a3156ff8ff674655b',
|
|
||||||
'com.google.android.gms:play-services-tasks:b31c18d8d1cc8d9814f295ee7435471333f370ba5bd904ca14f8f2bec4f35c35',
|
|
||||||
'com.google.android.gms:play-services-places-placereport:04f8baeb1f8f8a734c7d4b1701a3974281b45591affa7e963b59dd019b8abc6e',
|
|
||||||
'com.google.android.gms:play-services-stats:5b2d8281adbfd6e74d2295c94bab9ea80fc9a84dfbb397995673f5af4d4c6368',
|
|
||||||
'com.google.android.gms:play-services-basement:e08bfd1e87c4e50ef76161d7ac76b873aeb975367eeb3afa4abe62ea1887c7c6',
|
|
||||||
'androidx.legacy:legacy-support-v4:78fec1485f0f388a4749022dd51416857127cd2544ae1c3fd0b16589055480b0',
|
|
||||||
'androidx.fragment:fragment:9656d81c472b5142bbc3471ef7259fbc93905dc38e823c63a99e48819881b6e7',
|
|
||||||
'androidx.appcompat:appcompat-resources:53c0a33d07c4bab48d4c8169bf30053aa14965af4a775b56092a9fc7079802b1',
|
|
||||||
'androidx.legacy:legacy-support-core-ui:0d1260c6e7e6a337f875df71b516931e703f716e90889817cd3a20fa5ac3d947',
|
|
||||||
'androidx.drawerlayout:drawerlayout:9402442cdc5a43cf62fb14f8cf98c63342d4d9d9b805c8033c6cf7e802749ac1',
|
|
||||||
'androidx.legacy:legacy-support-core-utils:a7edcf01d5b52b3034073027bc4775b78a4764bb6202bb91d61c829add8dd1c7',
|
|
||||||
'androidx.transition:transition:a00a0f763f401abcecda9b0eafcb738929c5801b111a9a414b81a193d0f4008d',
|
|
||||||
'androidx.media:media:b23b527b2bac870c4a7451e6982d7132e413e88d7f27dbeb1fc7640a720cd9ee',
|
|
||||||
'androidx.viewpager:viewpager:147af4e14a1984010d8f155e5e19d781f03c1d70dfed02a8e0d18428b8fc8682',
|
|
||||||
'androidx.loader:loader:11f735cb3b55c458d470bed9e25254375b518b4b1bad6926783a7026db0f5025',
|
|
||||||
'androidx.activity:activity:0d6bafb56a72da893f3990ca5d819214d047f5f6b5c5f822ed97971c05eeb85a',
|
|
||||||
'androidx.vectordrawable:vectordrawable-animated:f1613c47f1e6d2cd02ec9a42925f1a964fa63d1d028d34d884364cc3b9ffcb8f',
|
|
||||||
'androidx.vectordrawable:vectordrawable:b632152304edb506bf7eacb329ef41e43b80164bf5be4c7bb132a249a65cbc26',
|
|
||||||
'androidx.coordinatorlayout:coordinatorlayout:e508c695489493374d942bf7b4ee02abf7571d25aac4c622e57d6cd5cd29eb73',
|
|
||||||
'androidx.slidingpanelayout:slidingpanelayout:76bffb7cefbf780794d8817002dad1562f3e27c0a9f746d62401c8edb30aeede',
|
|
||||||
'androidx.customview:customview:20e5b8f6526a34595a604f56718da81167c0b40a7a94a57daa355663f2594df2',
|
|
||||||
'androidx.swiperefreshlayout:swiperefreshlayout:9761b3a809c9b093fd06a3c4bbc645756dec0e95b5c9da419bc9f2a3f3026e8d',
|
|
||||||
'androidx.asynclayoutinflater:asynclayoutinflater:f7eab60c57addd94bb06275832fe7600611beaaae1a1ec597c231956faf96c8b',
|
|
||||||
'androidx.core:core:45c7a50ad1f366e62db496d8cef7730d5ee1681215007d1a19e6b6d800a12842',
|
|
||||||
'androidx.cursoradapter:cursoradapter:a81c8fe78815fa47df5b749deb52727ad11f9397da58b16017f4eb2c11e28564',
|
|
||||||
'androidx.lifecycle:lifecycle-livedata:c82609ced8c498f0a701a30fb6771bb7480860daee84d82e0a81ee86edf7ba39',
|
|
||||||
'androidx.lifecycle:lifecycle-livedata-core:fde334ec7e22744c0f5bfe7caf1a84c9d717327044400577bdf9bd921ec4f7bc',
|
|
||||||
'androidx.arch.core:core-runtime:87e65fc767c712b437649c7cee2431ebb4bed6daef82e501d4125b3ed3f65f8e',
|
|
||||||
'androidx.concurrent:concurrent-listenablefuture-callback:14dce0acbffd705cfe9fb378960f851a9d8fc3f293d1157c310c9624a561d0a8',
|
|
||||||
'androidx.concurrent:concurrent-listenablefuture:f9ef396ca4a43b9685d28bec117b278aa9171de0e446e5138e931074e3462feb',
|
|
||||||
'com.github.bumptech.glide:gifdecoder:7ee9402ae1c48fac9232b67e81f881c217b907b3252e49ce57bdb97937ebb270',
|
|
||||||
'androidx.versionedparcelable:versionedparcelable:948c751f6352d4c0f93f15fa1bf506c59083bc7754264dd9a325a6da0e2eec05',
|
|
||||||
'androidx.collection:collection:632a0e5407461de774409352940e292a291037724207a787820c77daf7d33b72',
|
|
||||||
'androidx.interpolator:interpolator:33193135a64fe21fa2c35eec6688f1a76e512606c0fc83dc1b689e37add7732a',
|
|
||||||
'androidx.documentfile:documentfile:865a061ef2fad16522f8433536b8d47208c46ff7c7745197dfa1eeb481869487',
|
|
||||||
'androidx.localbroadcastmanager:localbroadcastmanager:e71c328ceef5c4a7d76f2d86df1b65d65fe2acf868b1a4efd84a3f34336186d8',
|
|
||||||
'androidx.print:print:1d5c7f3135a1bba661fc373fd72e11eb0a4adbb3396787826dd8e4190d5d9edd',
|
|
||||||
'androidx.lifecycle:lifecycle-process:d8ff6fd844559743050c9ae010a6df230f2a3dbdf3e14498316f30bd8df836b5',
|
|
||||||
'androidx.lifecycle:lifecycle-service:cb2b15bb0cf14134e953ed8ead96f94265018643f519367d51fd837f7311e9f8',
|
|
||||||
'androidx.lifecycle:lifecycle-runtime:7e6d414d03bb184f3015dacc6233eeaded45fa23f0cf4c1f6d3395d6495fa41c',
|
|
||||||
'androidx.lifecycle:lifecycle-viewmodel:9f2efb59328027fa9f0c413d4d5910aab68d149b139ca8ce432135105b74833a',
|
|
||||||
'androidx.savedstate:savedstate:115ac7313095b2d159565d2bc851a7722e43fc00347fc828214ff8917799b5f0',
|
|
||||||
'androidx.lifecycle:lifecycle-common:76db6be533bd730fb361c2feb12a2c26d9952824746847da82601ef81f082643',
|
|
||||||
'androidx.arch.core:core-common:fe1237bf029d063e7f29fe39aeaf73ef74c8b0a3658486fc29d3c54326653889',
|
|
||||||
'androidx.annotation:annotation:d38d63edb30f1467818d50aaf05f8a692dea8b31392a049bfa991b159ad5b692',
|
|
||||||
'androidx.constraintlayout:constraintlayout-solver:965c177e64fbd81bd1d27b402b66ef9d7bc7b5cb5f718044bf7a453abc542045',
|
|
||||||
'com.google.auto.value:auto-value-annotations:0e951fee8c31f60270bc46553a8586001b7b93dbb12aec06373aa99a150392c0',
|
|
||||||
'org.signal:signal-metadata-android:02323bc29317fa9d3b62fab0b507c94ba2e9bcc4a78d588888ffd313853757b3',
|
|
||||||
'org.whispersystems:signal-service-java:34c1efbfdc9cca44946a92f1ba330066bc533056a4db3359a1af96e519893b2e',
|
|
||||||
'com.github.bumptech.glide:disklrucache:4696a81340eb6beee21ab93f703ed6e7ae49fb4ce3bc2fbc546e5bacd21b96b9',
|
|
||||||
'com.github.bumptech.glide:annotations:702a7521cb3f6d7e55edd66e90bda1a1975baf971d25f75b75638579f86bc69b',
|
|
||||||
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
|
|
||||||
'com.klinkerapps:logger:177e325259a8b111ad6745ec10db5861723c99f402222b80629f576f49408541',
|
|
||||||
'com.google.android:flexbox:a9989fd13ae2ee42765dfc515fe362edf4f326e74925d02a10369df8092a4935',
|
|
||||||
'com.squareup.okio:okio:d78fac588458fc099e6c82e91fe5f0375c67434626451a3a77772c65d9eee85b',
|
|
||||||
'org.jsoup:jsoup:abeaf34795a4de70f72aed6de5966d2955ec7eb348eeb813324f23c999575473',
|
|
||||||
'org.whispersystems:signal-protocol-android:c80aac5f93114da2810e2e89437831f79fcbc8bece652f64aeab313a651cba85',
|
|
||||||
'org.signal:signal-metadata-java:2ce71cc4ec5dacfbaef4a265fceef61b8a09696b541994106a22a946762cbdcc',
|
|
||||||
'org.whispersystems:signal-protocol-java:7f6df67a963acbab7716424b01b12fa7279f18a9623a2a7c8ba7b1c285830168',
|
|
||||||
'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74',
|
|
||||||
'com.googlecode.libphonenumber:libphonenumber:dbf4bf566d17a60044c19e282a619684e4b4abb0f9f9f24f843c55d19826ab5e',
|
|
||||||
'com.fasterxml.jackson.core:jackson-databind:fb262d42ea2de98044b62d393950a5aa050435fec38bbcadf2325cf7dc41b848',
|
|
||||||
'com.squareup.okhttp3:okhttp:07c3d82ca7eaf4722f00b2da807dc7860f6169ae60cfedcf5d40218f90880a46',
|
|
||||||
'org.threeten:threetenbp:f4c23ffaaed717c3b99c003e0ee02d6d66377fd47d866fec7d971bd8644fc1a7',
|
|
||||||
'org.whispersystems:curve25519-android:b502bcf83efe001f09a7a9efda6f0fa772c43ed5924e97816296ed3503caa092',
|
|
||||||
'com.fasterxml.jackson.core:jackson-annotations:45d32ac61ef8a744b464c54c2b3414be571016dd46bfc2bec226761cf7ae457a',
|
|
||||||
'com.fasterxml.jackson.core:jackson-core:3083079be6088db2ed0a0c6ff92204e0aa48fa1de9db5b59c468f35acf882c2c',
|
|
||||||
'org.whispersystems:curve25519-java:0aadd43cf01d11e9b58f867b3c4f25c3194e8b0623d1953d32dfbfbee009e38d',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
def canonicalVersionCode = 540
|
|
||||||
def canonicalVersionName = "4.48.11"
|
|
||||||
|
|
||||||
def postFixSize = 10
|
|
||||||
def abiPostFix = ['universal' : 0,
|
|
||||||
'armeabi-v7a' : 1,
|
|
||||||
'arm64-v8a' : 2,
|
|
||||||
'x86' : 3,
|
|
||||||
'x86_64' : 4]
|
|
||||||
|
|
||||||
android {
|
|
||||||
flavorDimensions "none"
|
|
||||||
compileSdkVersion 28
|
|
||||||
buildToolsVersion '28.0.3'
|
|
||||||
useLibrary 'org.apache.http.legacy'
|
|
||||||
|
|
||||||
dexOptions {
|
|
||||||
javaMaxHeapSize "4g"
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
versionCode canonicalVersionCode * postFixSize
|
|
||||||
versionName canonicalVersionName
|
|
||||||
|
|
||||||
minSdkVersion 19
|
|
||||||
targetSdkVersion 28
|
|
||||||
multiDexEnabled true
|
|
||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
|
||||||
project.ext.set("archivesBaseName", "Signal");
|
|
||||||
|
|
||||||
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
|
|
||||||
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
|
|
||||||
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
|
|
||||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
|
||||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
|
||||||
buildConfigField "String", "USER_AGENT", "\"OWA\""
|
|
||||||
buildConfigField "boolean", "DEV_BUILD", "false"
|
|
||||||
buildConfigField "String", "MRENCLAVE", "\"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9\""
|
|
||||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
|
|
||||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
|
||||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
|
||||||
|
|
||||||
ndk {
|
|
||||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
|
||||||
}
|
|
||||||
|
|
||||||
resConfigs autoResConfig()
|
|
||||||
|
|
||||||
splits {
|
|
||||||
abi {
|
|
||||||
enable true
|
|
||||||
reset()
|
|
||||||
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
|
||||||
universalApk true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
|
|
||||||
packagingOptions {
|
|
||||||
exclude 'LICENSE.txt'
|
|
||||||
exclude 'LICENSE'
|
|
||||||
exclude 'NOTICE'
|
|
||||||
exclude 'asm-license.txt'
|
|
||||||
exclude 'META-INF/LICENSE'
|
|
||||||
exclude 'META-INF/NOTICE'
|
|
||||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
debug {
|
|
||||||
minifyEnabled true
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'),
|
|
||||||
'proguard-firebase-messaging.pro',
|
|
||||||
'proguard-google-play-services.pro',
|
|
||||||
'proguard-jackson.pro',
|
|
||||||
'proguard-sqlite.pro',
|
|
||||||
'proguard-appcompat-v7.pro',
|
|
||||||
'proguard-square-okhttp.pro',
|
|
||||||
'proguard-square-okio.pro',
|
|
||||||
'proguard-spongycastle.pro',
|
|
||||||
'proguard-rounded-image-view.pro',
|
|
||||||
'proguard-glide.pro',
|
|
||||||
'proguard-shortcutbadger.pro',
|
|
||||||
'proguard-retrofit.pro',
|
|
||||||
'proguard-webrtc.pro',
|
|
||||||
'proguard-klinker.pro',
|
|
||||||
'proguard-retrolambda.pro',
|
|
||||||
'proguard-okhttp.pro',
|
|
||||||
'proguard-ez-vcard.pro',
|
|
||||||
'proguard.cfg'
|
|
||||||
testProguardFiles 'proguard-automation.pro',
|
|
||||||
'proguard.cfg'
|
|
||||||
}
|
|
||||||
release {
|
|
||||||
minifyEnabled true
|
|
||||||
proguardFiles = buildTypes.debug.proguardFiles
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
productFlavors {
|
|
||||||
play {
|
|
||||||
dimension "none"
|
|
||||||
ext.websiteUpdateUrl = "null"
|
|
||||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
|
||||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
|
||||||
}
|
|
||||||
|
|
||||||
website {
|
|
||||||
dimension "none"
|
|
||||||
ext.websiteUpdateUrl = "https://updates.signal.org/android"
|
|
||||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
|
|
||||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
android.applicationVariants.all { variant ->
|
|
||||||
variant.outputs.each { output ->
|
|
||||||
output.outputFileName = output.outputFileName.replace(".apk", "-${variant.versionName}.apk")
|
|
||||||
def abiName = output.getFilter("ABI") ?: 'universal'
|
|
||||||
def postFix = abiPostFix.get(abiName, 0)
|
|
||||||
|
|
||||||
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
|
|
||||||
|
|
||||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = ['test/androidTest/java']
|
|
||||||
}
|
|
||||||
test {
|
|
||||||
java.srcDirs = ['test/unitTest/java']
|
|
||||||
resources.srcDirs = ['test/unitTest/resources']
|
|
||||||
}
|
|
||||||
|
|
||||||
website.manifest.srcFile 'website/AndroidManifest.xml'
|
|
||||||
}
|
|
||||||
|
|
||||||
lintOptions {
|
|
||||||
abortOnError true
|
|
||||||
baseline file("lint-baseline.xml")
|
|
||||||
}
|
|
||||||
|
|
||||||
testOptions {
|
|
||||||
unitTests {
|
|
||||||
includeAndroidResources = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def assembleWebsiteDescriptor = { variant, file ->
|
|
||||||
if (file.exists()) {
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
||||||
file.eachByte 4096, {bytes, size ->
|
|
||||||
md.update(bytes, 0, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
String digest = md.digest().collect {String.format "%02x", it}.join();
|
|
||||||
String url = variant.productFlavors.get(0).ext.websiteUpdateUrl
|
|
||||||
String apkName = file.getName()
|
|
||||||
|
|
||||||
String descriptor = "{" +
|
|
||||||
"\"versionCode\" : ${canonicalVersionCode * postFixSize + abiPostFix['universal']}," +
|
|
||||||
"\"versionName\" : \"$canonicalVersionName\"," +
|
|
||||||
"\"sha256sum\" : \"$digest\"," +
|
|
||||||
"\"url\" : \"$url/$apkName\"" +
|
|
||||||
"}"
|
|
||||||
|
|
||||||
File descriptorFile = new File(file.getParent(), apkName.replace(".apk", ".json"))
|
|
||||||
|
|
||||||
descriptorFile.write(descriptor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def signProductionRelease = { variant ->
|
|
||||||
variant.outputs.collect { output ->
|
|
||||||
String apkName = output.outputFile.name
|
|
||||||
File inputFile = new File(output.outputFile.path)
|
|
||||||
File outputFile = new File(output.outputFile.parent, apkName.replace('-unsigned', ''))
|
|
||||||
|
|
||||||
new ApkSignerUtil('sun.security.pkcs11.SunPKCS11',
|
|
||||||
'pkcs11.config',
|
|
||||||
'PKCS11',
|
|
||||||
'file:pkcs11.password').calculateSignature(inputFile.getAbsolutePath(),
|
|
||||||
outputFile.getAbsolutePath())
|
|
||||||
|
|
||||||
inputFile.delete()
|
|
||||||
outputFile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task signProductionPlayRelease {
|
|
||||||
doLast {
|
|
||||||
signProductionRelease(android.applicationVariants.find { (it.name == 'playRelease') })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task signProductionWebsiteRelease {
|
|
||||||
doLast {
|
|
||||||
def variant = android.applicationVariants.find { (it.name == 'websiteRelease') }
|
|
||||||
File signedRelease = signProductionRelease(variant).find { it.name.contains('universal') }
|
|
||||||
assembleWebsiteDescriptor(variant, signedRelease)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.whenTaskAdded { task ->
|
|
||||||
if (task.name.equals("assemblePlayRelease")) {
|
|
||||||
task.finalizedBy signProductionPlayRelease
|
|
||||||
}
|
|
||||||
|
|
||||||
if (task.name.equals("assembleWebsiteRelease")) {
|
|
||||||
task.finalizedBy signProductionWebsiteRelease
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def getLastCommitTimestamp() {
|
|
||||||
new ByteArrayOutputStream().withStream { os ->
|
|
||||||
def result = exec {
|
|
||||||
executable = 'git'
|
|
||||||
args = ['log', '-1', '--pretty=format:%ct']
|
|
||||||
standardOutput = os
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.toString() + "000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Discovers supported languages listed as under the res/values- directory.
|
|
||||||
*/
|
|
||||||
static def autoResConfig() {
|
|
||||||
def files = new ArrayList<String>()
|
|
||||||
def root = new File('res')
|
|
||||||
root.eachFile { f -> files.add(f.name) }
|
|
||||||
['en'] + files.collect { f -> f =~ /^values-([a-z]{2}(-r[A-Z]{2})?)$/ }
|
|
||||||
.findAll { matcher -> matcher.find() }
|
|
||||||
.collect { matcher -> matcher.group(1) }
|
|
||||||
.sort()
|
|
||||||
}
|
|
||||||
|
|
||||||
task qa {
|
|
||||||
group 'Verification'
|
|
||||||
description 'Quality Assurance. Run before pushing.'
|
|
||||||
dependsOn ':testPlayReleaseUnitTest', ':lintPlayRelease', ':assemblePlayDebug'
|
|
||||||
}
|
|
||||||
@@ -5,5 +5,5 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile 'com.android.tools.build:apksig:3.3.2'
|
implementation 'com.android.tools.build:apksig:3.5.1'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package org.whispersystems.witness
|
|||||||
import org.gradle.api.InvalidUserDataException
|
import org.gradle.api.InvalidUserDataException
|
||||||
import org.gradle.api.Plugin
|
import org.gradle.api.Plugin
|
||||||
import org.gradle.api.Project
|
import org.gradle.api.Project
|
||||||
import org.gradle.api.artifacts.Configuration
|
|
||||||
import org.gradle.api.artifacts.ResolvedArtifact
|
import org.gradle.api.artifacts.ResolvedArtifact
|
||||||
|
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
@@ -28,10 +27,10 @@ class WitnessPlugin implements Plugin<Project> {
|
|||||||
project.afterEvaluate {
|
project.afterEvaluate {
|
||||||
project.dependencyVerification.verify.each {
|
project.dependencyVerification.verify.each {
|
||||||
assertion ->
|
assertion ->
|
||||||
List parts = assertion.tokenize(":")
|
List parts = assertion[0].tokenize(':')
|
||||||
String group = parts.get(0)
|
String group = parts.get(0)
|
||||||
String name = parts.get(1)
|
String name = parts.get(1)
|
||||||
String hash = parts.get(2)
|
String hash = assertion[1]
|
||||||
|
|
||||||
def artifacts = allArtifacts(project).findAll {
|
def artifacts = allArtifacts(project).findAll {
|
||||||
return it.name.equals(name) && it.moduleVersion.id.group.equals(group)
|
return it.name.equals(name) && it.moduleVersion.id.group.equals(group)
|
||||||
@@ -56,22 +55,25 @@ class WitnessPlugin implements Plugin<Project> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
project.task('calculateChecksums').doLast {
|
project.task('calculateChecksums').doLast {
|
||||||
println "dependencyVerification {"
|
def stringBuilder = new StringBuilder()
|
||||||
|
|
||||||
def configurationName = project.dependencyVerification.configuration
|
stringBuilder.append '// Auto-generated, use ./gradlew calculateChecksums to regenerate\n\n'
|
||||||
if (configurationName != null) {
|
stringBuilder.append 'dependencyVerification {\n'
|
||||||
println " configuration = '$configurationName'"
|
|
||||||
}
|
|
||||||
|
|
||||||
println " verify = ["
|
stringBuilder.append ' verify = [\n'
|
||||||
|
|
||||||
allArtifacts(project).each {
|
allArtifacts(project)
|
||||||
dep ->
|
.findAll { dep -> !dep.id.componentIdentifier.displayName.startsWith('project :') }
|
||||||
println " '" + dep.moduleVersion.id.group+ ":" + dep.name + ":" + calculateSha256(dep.file) + "',"
|
.collect { dep -> "['$dep.moduleVersion.id.group:$dep.name:$dep.moduleVersion.id.version',\n '${calculateSha256(dep.file)}']" }
|
||||||
}
|
.sort()
|
||||||
|
.each {
|
||||||
|
dep -> stringBuilder.append "\n $dep,\n"
|
||||||
|
}
|
||||||
|
|
||||||
println " ]"
|
stringBuilder.append " ]\n"
|
||||||
println "}"
|
stringBuilder.append "}\n"
|
||||||
|
|
||||||
|
project.file("witness-verifications.gradle").write(stringBuilder.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+3
-1
@@ -1,5 +1,7 @@
|
|||||||
|
# Note: Check https://gradle.org/release-checksums/ before updating wrapper or distribution
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip
|
distributionSha256Sum=027fdd265d277bae65a0d349b6b8da02135b0b8e14ba891e26281fa877fe37a2
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
##
|
##
|
||||||
## Gradle start up script for UN*X
|
## Gradle start up script for UN*X
|
||||||
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
|
|||||||
APP_BASE_NAME=`basename "$0"`
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD="maximum"
|
MAX_FD="maximum"
|
||||||
@@ -109,8 +125,8 @@ if $darwin; then
|
|||||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# For Cygwin, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if $cygwin ; then
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|||||||
Vendored
+17
-1
@@ -1,3 +1,19 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%" == "" @echo off
|
@if "%DEBUG%" == "" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
|
|||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m"
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
@rem Find java.exe
|
@rem Find java.exe
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'java-library'
|
||||||
|
apply plugin: 'com.google.protobuf'
|
||||||
|
apply plugin: 'maven'
|
||||||
|
apply plugin: 'signing'
|
||||||
|
apply plugin: 'witness'
|
||||||
|
apply from: 'witness-verifications.gradle'
|
||||||
|
|
||||||
|
sourceCompatibility = 1.7
|
||||||
|
archivesBaseName = "signal-service-java"
|
||||||
|
version = lib_signal_service_version_number
|
||||||
|
group = lib_signal_service_group_info
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
mavenLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
|
||||||
|
api 'com.googlecode.libphonenumber:libphonenumber:8.11.0'
|
||||||
|
api 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2'
|
||||||
|
|
||||||
|
api "org.signal:signal-metadata-java:${lib_signal_metadata_version}"
|
||||||
|
api 'com.squareup.okhttp3:okhttp:3.12.1'
|
||||||
|
implementation 'org.threeten:threetenbp:1.3.6'
|
||||||
|
|
||||||
|
testImplementation 'junit:junit:4.12'
|
||||||
|
testImplementation 'org.assertj:assertj-core:1.7.1'
|
||||||
|
testImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.0.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyVerification {
|
||||||
|
configuration = '(runtime|compile)Classpath'
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.whenTaskAdded { task ->
|
||||||
|
if (task.name.equals("lint")) {
|
||||||
|
task.enabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protobuf {
|
||||||
|
protoc {
|
||||||
|
artifact = 'com.google.protobuf:protoc:3.10.0'
|
||||||
|
}
|
||||||
|
generateProtoTasks {
|
||||||
|
all().each { task ->
|
||||||
|
task.builtins {
|
||||||
|
java {
|
||||||
|
option "lite"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main.proto.srcDir 'protobuf'
|
||||||
|
}
|
||||||
|
|
||||||
|
def isReleaseBuild() {
|
||||||
|
return version.contains("SNAPSHOT") == false
|
||||||
|
}
|
||||||
|
|
||||||
|
def getReleaseRepositoryUrl() {
|
||||||
|
return hasProperty('sonatypeRepo') ? sonatypeRepo
|
||||||
|
: "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
|
||||||
|
}
|
||||||
|
|
||||||
|
def getRepositoryUsername() {
|
||||||
|
return hasProperty('whisperSonatypeUsername') ? whisperSonatypeUsername : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
def getRepositoryPassword() {
|
||||||
|
return hasProperty('whisperSonatypePassword') ? whisperSonatypePassword : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
signing {
|
||||||
|
required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
|
||||||
|
sign configurations.archives
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadArchives {
|
||||||
|
configuration = configurations.archives
|
||||||
|
repositories.mavenDeployer {
|
||||||
|
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
|
||||||
|
|
||||||
|
repository(url: getReleaseRepositoryUrl()) {
|
||||||
|
authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
|
||||||
|
}
|
||||||
|
|
||||||
|
pom.project {
|
||||||
|
name 'signal-service-java'
|
||||||
|
packaging 'jar'
|
||||||
|
description 'Signal Service communication library for Java'
|
||||||
|
url 'https://github.com/WhisperSystems/libsignal-service-java'
|
||||||
|
|
||||||
|
scm {
|
||||||
|
url 'scm:git@github.com:WhisperSystems/libsignal-service-java.git'
|
||||||
|
connection 'scm:git@github.com:WhisperSystems/libsignal-service-java.git'
|
||||||
|
developerConnection 'scm:git@github.com:WhisperSystems/libsignal-service-java.git'
|
||||||
|
}
|
||||||
|
|
||||||
|
licenses {
|
||||||
|
license {
|
||||||
|
name 'GPLv3'
|
||||||
|
url 'https://www.gnu.org/licenses/gpl-3.0.txt'
|
||||||
|
distribution 'repo'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
developers {
|
||||||
|
developer {
|
||||||
|
name 'Moxie Marlinspike'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task installArchives(type: Upload) {
|
||||||
|
description "Installs the artifacts to the local Maven repository."
|
||||||
|
configuration = configurations['archives']
|
||||||
|
repositories {
|
||||||
|
mavenDeployer {
|
||||||
|
repository url: "file://${System.properties['user.home']}/.m2/repository"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task packageJavadoc(type: Jar, dependsOn: 'javadoc') {
|
||||||
|
from javadoc.destinationDir
|
||||||
|
classifier = 'javadoc'
|
||||||
|
}
|
||||||
|
|
||||||
|
task packageSources(type: Jar) {
|
||||||
|
from sourceSets.main.allSource
|
||||||
|
classifier = 'sources'
|
||||||
|
}
|
||||||
|
|
||||||
|
artifacts {
|
||||||
|
archives packageJavadoc
|
||||||
|
archives packageSources
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2019 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
package textsecure;
|
||||||
|
|
||||||
|
option java_package = "org.whispersystems.signalservice.internal.keybackup.protos";
|
||||||
|
option java_multiple_files = true;
|
||||||
|
|
||||||
|
message Request {
|
||||||
|
optional BackupRequest backup = 1;
|
||||||
|
optional RestoreRequest restore = 2;
|
||||||
|
optional DeleteRequest delete = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Response {
|
||||||
|
optional BackupResponse backup = 1;
|
||||||
|
optional RestoreResponse restore = 2;
|
||||||
|
optional DeleteResponse delete = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message BackupRequest {
|
||||||
|
optional bytes service_id = 1;
|
||||||
|
optional bytes backup_id = 2;
|
||||||
|
optional bytes token = 3;
|
||||||
|
optional uint64 valid_from = 4;
|
||||||
|
optional bytes data = 5;
|
||||||
|
optional bytes pin = 6;
|
||||||
|
optional uint32 tries = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message BackupResponse {
|
||||||
|
enum Status {
|
||||||
|
OK = 1;
|
||||||
|
ALREADY_EXISTS = 2;
|
||||||
|
NOT_YET_VALID = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Status status = 1;
|
||||||
|
optional bytes token = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RestoreRequest {
|
||||||
|
optional bytes service_id = 1;
|
||||||
|
optional bytes backup_id = 2;
|
||||||
|
optional bytes token = 3;
|
||||||
|
optional uint64 valid_from = 4;
|
||||||
|
optional bytes pin = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RestoreResponse {
|
||||||
|
enum Status {
|
||||||
|
OK = 1;
|
||||||
|
TOKEN_MISMATCH = 2;
|
||||||
|
NOT_YET_VALID = 3;
|
||||||
|
MISSING = 4;
|
||||||
|
PIN_MISMATCH = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Status status = 1;
|
||||||
|
optional bytes token = 2;
|
||||||
|
optional bytes data = 3;
|
||||||
|
optional uint32 tries = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteRequest {
|
||||||
|
optional bytes service_id = 1;
|
||||||
|
optional bytes backup_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteResponse {
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
package signalservice;
|
||||||
|
|
||||||
|
option java_package = "org.whispersystems.signalservice.internal.push";
|
||||||
|
option java_outer_classname = "ProvisioningProtos";
|
||||||
|
|
||||||
|
message ProvisionEnvelope {
|
||||||
|
optional bytes publicKey = 1;
|
||||||
|
optional bytes body = 2; // Encrypted ProvisionMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
message ProvisionMessage {
|
||||||
|
optional bytes identityKeyPublic = 1;
|
||||||
|
optional bytes identityKeyPrivate = 2;
|
||||||
|
optional string number = 3;
|
||||||
|
optional string uuid = 8;
|
||||||
|
optional string provisioningCode = 4;
|
||||||
|
optional string userAgent = 5;
|
||||||
|
optional bytes profileKey = 6;
|
||||||
|
optional bool readReceipts = 7;
|
||||||
|
optional uint32 provisioningVersion = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ProvisioningVersion {
|
||||||
|
option allow_alias = true;
|
||||||
|
|
||||||
|
INITIAL = 0;
|
||||||
|
TABLET_SUPPORT = 1;
|
||||||
|
CURRENT = 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
package signalservice;
|
||||||
|
|
||||||
|
option java_package = "org.whispersystems.signalservice.internal.push";
|
||||||
|
option java_outer_classname = "SignalServiceProtos";
|
||||||
|
|
||||||
|
message Envelope {
|
||||||
|
enum Type {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
CIPHERTEXT = 1;
|
||||||
|
KEY_EXCHANGE = 2;
|
||||||
|
PREKEY_BUNDLE = 3;
|
||||||
|
RECEIPT = 5;
|
||||||
|
UNIDENTIFIED_SENDER = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Type type = 1;
|
||||||
|
optional string sourceE164 = 2;
|
||||||
|
optional string sourceUuid = 11;
|
||||||
|
optional uint32 sourceDevice = 7;
|
||||||
|
optional string relay = 3;
|
||||||
|
optional uint64 timestamp = 5;
|
||||||
|
optional bytes legacyMessage = 6; // Contains an encrypted DataMessage
|
||||||
|
optional bytes content = 8; // Contains an encrypted Content
|
||||||
|
optional string serverGuid = 9;
|
||||||
|
optional uint64 serverTimestamp = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Content {
|
||||||
|
optional DataMessage dataMessage = 1;
|
||||||
|
optional SyncMessage syncMessage = 2;
|
||||||
|
optional CallMessage callMessage = 3;
|
||||||
|
optional NullMessage nullMessage = 4;
|
||||||
|
optional ReceiptMessage receiptMessage = 5;
|
||||||
|
optional TypingMessage typingMessage = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CallMessage {
|
||||||
|
message Offer {
|
||||||
|
optional uint64 id = 1;
|
||||||
|
optional string description = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Answer {
|
||||||
|
optional uint64 id = 1;
|
||||||
|
optional string description = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message IceUpdate {
|
||||||
|
optional uint64 id = 1;
|
||||||
|
optional string sdpMid = 2;
|
||||||
|
optional uint32 sdpMLineIndex = 3;
|
||||||
|
optional string sdp = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Busy {
|
||||||
|
optional uint64 id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Hangup {
|
||||||
|
optional uint64 id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
optional Offer offer = 1;
|
||||||
|
optional Answer answer = 2;
|
||||||
|
repeated IceUpdate iceUpdate = 3;
|
||||||
|
optional Hangup hangup = 4;
|
||||||
|
optional Busy busy = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DataMessage {
|
||||||
|
enum Flags {
|
||||||
|
END_SESSION = 1;
|
||||||
|
EXPIRATION_TIMER_UPDATE = 2;
|
||||||
|
PROFILE_KEY_UPDATE = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Quote {
|
||||||
|
message QuotedAttachment {
|
||||||
|
optional string contentType = 1;
|
||||||
|
optional string fileName = 2;
|
||||||
|
optional AttachmentPointer thumbnail = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional uint64 id = 1;
|
||||||
|
optional string authorE164 = 2;
|
||||||
|
optional string authorUuid = 5;
|
||||||
|
optional string text = 3;
|
||||||
|
repeated QuotedAttachment attachments = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Contact {
|
||||||
|
message Name {
|
||||||
|
optional string givenName = 1;
|
||||||
|
optional string familyName = 2;
|
||||||
|
optional string prefix = 3;
|
||||||
|
optional string suffix = 4;
|
||||||
|
optional string middleName = 5;
|
||||||
|
optional string displayName = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Phone {
|
||||||
|
enum Type {
|
||||||
|
HOME = 1;
|
||||||
|
MOBILE = 2;
|
||||||
|
WORK = 3;
|
||||||
|
CUSTOM = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional string value = 1;
|
||||||
|
optional Type type = 2;
|
||||||
|
optional string label = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Email {
|
||||||
|
enum Type {
|
||||||
|
HOME = 1;
|
||||||
|
MOBILE = 2;
|
||||||
|
WORK = 3;
|
||||||
|
CUSTOM = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional string value = 1;
|
||||||
|
optional Type type = 2;
|
||||||
|
optional string label = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PostalAddress {
|
||||||
|
enum Type {
|
||||||
|
HOME = 1;
|
||||||
|
WORK = 2;
|
||||||
|
CUSTOM = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Type type = 1;
|
||||||
|
optional string label = 2;
|
||||||
|
optional string street = 3;
|
||||||
|
optional string pobox = 4;
|
||||||
|
optional string neighborhood = 5;
|
||||||
|
optional string city = 6;
|
||||||
|
optional string region = 7;
|
||||||
|
optional string postcode = 8;
|
||||||
|
optional string country = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Avatar {
|
||||||
|
optional AttachmentPointer avatar = 1;
|
||||||
|
optional bool isProfile = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Name name = 1;
|
||||||
|
repeated Phone number = 3;
|
||||||
|
repeated Email email = 4;
|
||||||
|
repeated PostalAddress address = 5;
|
||||||
|
optional Avatar avatar = 6;
|
||||||
|
optional string organization = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Preview {
|
||||||
|
optional string url = 1;
|
||||||
|
optional string title = 2;
|
||||||
|
optional AttachmentPointer image = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Sticker {
|
||||||
|
optional bytes packId = 1;
|
||||||
|
optional bytes packKey = 2;
|
||||||
|
optional uint32 stickerId = 3;
|
||||||
|
optional AttachmentPointer data = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Reaction {
|
||||||
|
optional string emoji = 1;
|
||||||
|
optional bool remove = 2;
|
||||||
|
optional string targetAuthorE164 = 3;
|
||||||
|
optional string targetAuthorUuid = 4;
|
||||||
|
optional uint64 targetSentTimestamp = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ProtocolVersion {
|
||||||
|
option allow_alias = true;
|
||||||
|
|
||||||
|
INITIAL = 0;
|
||||||
|
MESSAGE_TIMERS = 1;
|
||||||
|
VIEW_ONCE = 2;
|
||||||
|
VIEW_ONCE_VIDEO = 3;
|
||||||
|
REACTIONS = 4;
|
||||||
|
CURRENT = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional string body = 1;
|
||||||
|
repeated AttachmentPointer attachments = 2;
|
||||||
|
optional GroupContext group = 3;
|
||||||
|
optional uint32 flags = 4;
|
||||||
|
optional uint32 expireTimer = 5;
|
||||||
|
optional bytes profileKey = 6;
|
||||||
|
optional uint64 timestamp = 7;
|
||||||
|
optional Quote quote = 8;
|
||||||
|
repeated Contact contact = 9;
|
||||||
|
repeated Preview preview = 10;
|
||||||
|
optional Sticker sticker = 11;
|
||||||
|
optional uint32 requiredProtocolVersion = 12;
|
||||||
|
optional bool isViewOnce = 14;
|
||||||
|
optional Reaction reaction = 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
message NullMessage {
|
||||||
|
optional bytes padding = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ReceiptMessage {
|
||||||
|
enum Type {
|
||||||
|
DELIVERY = 0;
|
||||||
|
READ = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Type type = 1;
|
||||||
|
repeated uint64 timestamp = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TypingMessage {
|
||||||
|
enum Action {
|
||||||
|
STARTED = 0;
|
||||||
|
STOPPED = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional uint64 timestamp = 1;
|
||||||
|
optional Action action = 2;
|
||||||
|
optional bytes groupId = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Verified {
|
||||||
|
enum State {
|
||||||
|
DEFAULT = 0;
|
||||||
|
VERIFIED = 1;
|
||||||
|
UNVERIFIED = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional string destinationE164 = 1;
|
||||||
|
optional string destinationUuid = 5;
|
||||||
|
optional bytes identityKey = 2;
|
||||||
|
optional State state = 3;
|
||||||
|
optional bytes nullMessage = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SyncMessage {
|
||||||
|
message Sent {
|
||||||
|
message UnidentifiedDeliveryStatus {
|
||||||
|
optional string destinationE164 = 1;
|
||||||
|
optional string destinationUuid = 3;
|
||||||
|
optional bool unidentified = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional string destinationE164 = 1;
|
||||||
|
optional string destinationUuid = 7;
|
||||||
|
optional uint64 timestamp = 2;
|
||||||
|
optional DataMessage message = 3;
|
||||||
|
optional uint64 expirationStartTimestamp = 4;
|
||||||
|
repeated UnidentifiedDeliveryStatus unidentifiedStatus = 5;
|
||||||
|
optional bool isRecipientUpdate = 6 [default = false];
|
||||||
|
}
|
||||||
|
|
||||||
|
message Contacts {
|
||||||
|
optional AttachmentPointer blob = 1;
|
||||||
|
optional bool complete = 2 [default = false];
|
||||||
|
}
|
||||||
|
|
||||||
|
message Groups {
|
||||||
|
optional AttachmentPointer blob = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Blocked {
|
||||||
|
repeated string numbers = 1;
|
||||||
|
repeated string uuids = 3;
|
||||||
|
repeated bytes groupIds = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Request {
|
||||||
|
enum Type {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
CONTACTS = 1;
|
||||||
|
GROUPS = 2;
|
||||||
|
BLOCKED = 3;
|
||||||
|
CONFIGURATION = 4;
|
||||||
|
KEYS = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Type type = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Read {
|
||||||
|
optional string senderE164 = 1;
|
||||||
|
optional string senderUuid = 3;
|
||||||
|
optional uint64 timestamp = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Configuration {
|
||||||
|
optional bool readReceipts = 1;
|
||||||
|
optional bool unidentifiedDeliveryIndicators = 2;
|
||||||
|
optional bool typingIndicators = 3;
|
||||||
|
optional bool linkPreviews = 4;
|
||||||
|
optional uint32 provisioningVersion = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StickerPackOperation {
|
||||||
|
enum Type {
|
||||||
|
INSTALL = 0;
|
||||||
|
REMOVE = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional bytes packId = 1;
|
||||||
|
optional bytes packKey = 2;
|
||||||
|
optional Type type = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ViewOnceOpen {
|
||||||
|
optional string senderE164 = 1;
|
||||||
|
optional string senderUuid = 3;
|
||||||
|
optional uint64 timestamp = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FetchLatest {
|
||||||
|
enum Type {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
LOCAL_PROFILE = 1;
|
||||||
|
STORAGE_MANIFEST = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Type type = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Keys {
|
||||||
|
optional bytes storageService = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Sent sent = 1;
|
||||||
|
optional Contacts contacts = 2;
|
||||||
|
optional Groups groups = 3;
|
||||||
|
optional Request request = 4;
|
||||||
|
repeated Read read = 5;
|
||||||
|
optional Blocked blocked = 6;
|
||||||
|
optional Verified verified = 7;
|
||||||
|
optional Configuration configuration = 9;
|
||||||
|
optional bytes padding = 8;
|
||||||
|
repeated StickerPackOperation stickerPackOperation = 10;
|
||||||
|
optional ViewOnceOpen viewOnceOpen = 11;
|
||||||
|
optional FetchLatest fetchLatest = 12;
|
||||||
|
optional Keys keys = 13;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AttachmentPointer {
|
||||||
|
enum Flags {
|
||||||
|
VOICE_MESSAGE = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional fixed64 id = 1;
|
||||||
|
optional string contentType = 2;
|
||||||
|
optional bytes key = 3;
|
||||||
|
optional uint32 size = 4;
|
||||||
|
optional bytes thumbnail = 5;
|
||||||
|
optional bytes digest = 6;
|
||||||
|
optional string fileName = 7;
|
||||||
|
optional uint32 flags = 8;
|
||||||
|
optional uint32 width = 9;
|
||||||
|
optional uint32 height = 10;
|
||||||
|
optional string caption = 11;
|
||||||
|
optional string blurHash = 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupContext {
|
||||||
|
enum Type {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
UPDATE = 1;
|
||||||
|
DELIVER = 2;
|
||||||
|
QUIT = 3;
|
||||||
|
REQUEST_INFO = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Member {
|
||||||
|
optional string uuid = 1;
|
||||||
|
optional string e164 = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional bytes id = 1;
|
||||||
|
optional Type type = 2;
|
||||||
|
optional string name = 3;
|
||||||
|
repeated string membersE164 = 4;
|
||||||
|
repeated Member members = 6;
|
||||||
|
optional AttachmentPointer avatar = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ContactDetails {
|
||||||
|
message Avatar {
|
||||||
|
optional string contentType = 1;
|
||||||
|
optional uint32 length = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional string number = 1;
|
||||||
|
optional string uuid = 9;
|
||||||
|
optional string name = 2;
|
||||||
|
optional Avatar avatar = 3;
|
||||||
|
optional string color = 4;
|
||||||
|
optional Verified verified = 5;
|
||||||
|
optional bytes profileKey = 6;
|
||||||
|
optional bool blocked = 7;
|
||||||
|
optional uint32 expireTimer = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupDetails {
|
||||||
|
message Avatar {
|
||||||
|
optional string contentType = 1;
|
||||||
|
optional uint32 length = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Member {
|
||||||
|
optional string uuid = 1;
|
||||||
|
optional string e164 = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional bytes id = 1;
|
||||||
|
optional string name = 2;
|
||||||
|
repeated string membersE164 = 3;
|
||||||
|
repeated Member members = 9;
|
||||||
|
optional Avatar avatar = 4;
|
||||||
|
optional bool active = 5 [default = true];
|
||||||
|
optional uint32 expireTimer = 6;
|
||||||
|
optional string color = 7;
|
||||||
|
optional bool blocked = 8;
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2019 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
package signalservice;
|
||||||
|
|
||||||
|
option java_package = "org.whispersystems.signalservice.internal.storage.protos";
|
||||||
|
option java_multiple_files = true;
|
||||||
|
|
||||||
|
|
||||||
|
message StorageItem {
|
||||||
|
optional bytes key = 1;
|
||||||
|
optional bytes value = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StorageItems {
|
||||||
|
repeated StorageItem items = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StorageManifest {
|
||||||
|
optional uint64 version = 1;
|
||||||
|
optional bytes value = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ReadOperation {
|
||||||
|
repeated bytes readKey = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WriteOperation {
|
||||||
|
optional StorageManifest manifest = 1;
|
||||||
|
repeated StorageItem insertItem = 2;
|
||||||
|
repeated bytes deleteKey = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StorageRecord {
|
||||||
|
enum Type {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
CONTACT = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional uint32 type = 1;
|
||||||
|
optional ContactRecord contact = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ContactRecord {
|
||||||
|
message Identity {
|
||||||
|
enum State {
|
||||||
|
DEFAULT = 0;
|
||||||
|
VERIFIED = 1;
|
||||||
|
UNVERIFIED = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional bytes key = 1;
|
||||||
|
optional State state = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Profile {
|
||||||
|
optional string name = 1;
|
||||||
|
optional bytes key = 2;
|
||||||
|
optional string username = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional string serviceUuid = 1;
|
||||||
|
optional string serviceE164 = 2;
|
||||||
|
optional Profile profile = 3;
|
||||||
|
optional Identity identity = 4;
|
||||||
|
optional bool blocked = 5;
|
||||||
|
optional bool whitelisted = 6;
|
||||||
|
optional string nickname = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ManifestRecord {
|
||||||
|
optional uint64 version = 1;
|
||||||
|
repeated bytes keys = 2;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2019 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
package signalservice;
|
||||||
|
|
||||||
|
option java_package = "org.whispersystems.signalservice.internal.sticker";
|
||||||
|
option java_outer_classname = "StickerProtos";
|
||||||
|
|
||||||
|
message Pack {
|
||||||
|
message Sticker {
|
||||||
|
optional uint32 id = 1;
|
||||||
|
optional string emoji = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional string title = 1;
|
||||||
|
optional string author = 2;
|
||||||
|
optional Sticker cover = 3;
|
||||||
|
repeated Sticker stickers = 4;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
package signalservice;
|
||||||
|
|
||||||
|
option java_package = "org.whispersystems.signalservice.internal.websocket";
|
||||||
|
option java_outer_classname = "WebSocketProtos";
|
||||||
|
|
||||||
|
message WebSocketRequestMessage {
|
||||||
|
optional string verb = 1;
|
||||||
|
optional string path = 2;
|
||||||
|
optional bytes body = 3;
|
||||||
|
repeated string headers = 5;
|
||||||
|
optional uint64 id = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WebSocketResponseMessage {
|
||||||
|
optional uint64 id = 1;
|
||||||
|
optional uint32 status = 2;
|
||||||
|
optional string message = 3;
|
||||||
|
repeated string headers = 5;
|
||||||
|
optional bytes body = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WebSocketMessage {
|
||||||
|
enum Type {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
REQUEST = 1;
|
||||||
|
RESPONSE = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Type type = 1;
|
||||||
|
optional WebSocketRequestMessage request = 2;
|
||||||
|
optional WebSocketResponseMessage response = 3;
|
||||||
|
}
|
||||||
+281
@@ -0,0 +1,281 @@
|
|||||||
|
package org.whispersystems.signalservice.api;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.logging.Log;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||||
|
import org.whispersystems.signalservice.internal.contacts.crypto.KeyBackupCipher;
|
||||||
|
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
|
||||||
|
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
|
||||||
|
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
|
||||||
|
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
|
||||||
|
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest;
|
||||||
|
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
|
||||||
|
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||||
|
import org.whispersystems.signalservice.internal.keybackup.protos.BackupResponse;
|
||||||
|
import org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse;
|
||||||
|
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||||
|
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
|
||||||
|
import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException;
|
||||||
|
import org.whispersystems.signalservice.internal.registrationpin.PinStretcher;
|
||||||
|
import org.whispersystems.signalservice.internal.util.Hex;
|
||||||
|
import org.whispersystems.signalservice.internal.util.Util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public final class KeyBackupService {
|
||||||
|
|
||||||
|
private static final String TAG = KeyBackupService.class.getSimpleName();
|
||||||
|
|
||||||
|
private final KeyStore iasKeyStore;
|
||||||
|
private final String enclaveName;
|
||||||
|
private final String mrenclave;
|
||||||
|
private final PushServiceSocket pushServiceSocket;
|
||||||
|
private final int maxTries;
|
||||||
|
|
||||||
|
KeyBackupService(KeyStore iasKeyStore,
|
||||||
|
String enclaveName,
|
||||||
|
String mrenclave,
|
||||||
|
PushServiceSocket pushServiceSocket,
|
||||||
|
int maxTries)
|
||||||
|
{
|
||||||
|
this.iasKeyStore = iasKeyStore;
|
||||||
|
this.enclaveName = enclaveName;
|
||||||
|
this.mrenclave = mrenclave;
|
||||||
|
this.pushServiceSocket = pushServiceSocket;
|
||||||
|
this.maxTries = maxTries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this if you don't want to validate that the server has not changed since you last set the pin.
|
||||||
|
*/
|
||||||
|
public PinChangeSession newPinChangeSession()
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this if you want to validate that the server has not changed since you last set the pin.
|
||||||
|
* The supplied token will have to match for the change to be successful.
|
||||||
|
*/
|
||||||
|
public PinChangeSession newPinChangeSession(TokenResponse currentToken)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), currentToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this to validate that the pin is still set on the server with the current token.
|
||||||
|
* Additionally this validates that no one has used any tries.
|
||||||
|
*/
|
||||||
|
public RestoreSession newRestoreSession(TokenResponse currentToken)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), currentToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only call before registration, to see how many tries are left.
|
||||||
|
* <p>
|
||||||
|
* Pass the token to the newRegistrationSession.
|
||||||
|
*/
|
||||||
|
public TokenResponse getToken(String authAuthorization) throws IOException {
|
||||||
|
return pushServiceSocket.getKeyBackupServiceToken(authAuthorization, enclaveName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this during registration, good for one try, on subsequent attempts, pass the token from the previous attempt.
|
||||||
|
*
|
||||||
|
* @param tokenResponse Supplying a token response from a failed previous attempt prevents certain attacks.
|
||||||
|
*/
|
||||||
|
public RestoreSession newRegistrationSession(String authAuthorization, TokenResponse tokenResponse)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
return newSession(authAuthorization, tokenResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Session newSession(String authorization, TokenResponse currentToken)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
TokenResponse token = currentToken != null ? currentToken : pushServiceSocket.getKeyBackupServiceToken(authorization, enclaveName);
|
||||||
|
|
||||||
|
return new Session(authorization, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Session implements RestoreSession, PinChangeSession {
|
||||||
|
|
||||||
|
private final String authorization;
|
||||||
|
private final TokenResponse currentToken;
|
||||||
|
|
||||||
|
Session(String authorization, TokenResponse currentToken) {
|
||||||
|
this.authorization = authorization;
|
||||||
|
this.currentToken = currentToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RegistrationLockData restorePin(String pin)
|
||||||
|
throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, InvalidPinException
|
||||||
|
{
|
||||||
|
int attempt = 0;
|
||||||
|
SecureRandom random = new SecureRandom();
|
||||||
|
TokenResponse token = currentToken;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
|
||||||
|
attempt++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return restorePin(pin, token);
|
||||||
|
} catch (TokenException tokenException) {
|
||||||
|
|
||||||
|
token = tokenException.getToken();
|
||||||
|
|
||||||
|
if (tokenException instanceof KeyBackupServicePinException) {
|
||||||
|
throw (KeyBackupServicePinException) tokenException;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenException.isCanAutomaticallyRetry() && attempt < 5) {
|
||||||
|
// back off randomly, between 250 and 8000 ms
|
||||||
|
int backoffMs = 250 * (1 << (attempt - 1));
|
||||||
|
|
||||||
|
Util.sleep(backoffMs + random.nextInt(backoffMs));
|
||||||
|
} else {
|
||||||
|
throw new UnauthenticatedResponseException("Token mismatch, expended all automatic retries");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RegistrationLockData restorePin(String pin, TokenResponse token)
|
||||||
|
throws UnauthenticatedResponseException, IOException, TokenException, InvalidPinException
|
||||||
|
{
|
||||||
|
PinStretcher.StretchedPin stretchedPin = PinStretcher.stretchPin(pin);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final int remainingTries = token.getTries();
|
||||||
|
final RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
|
||||||
|
final KeyBackupRequest request = KeyBackupCipher.createKeyRestoreRequest(stretchedPin.getKbsAccessKey(), token, remoteAttestation, Hex.fromStringCondensed(enclaveName));
|
||||||
|
final KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
|
||||||
|
final RestoreResponse status = KeyBackupCipher.getKeyRestoreResponse(response, remoteAttestation);
|
||||||
|
|
||||||
|
TokenResponse nextToken = status.hasToken()
|
||||||
|
? new TokenResponse(token.getBackupId(), status.getToken().toByteArray(), status.getTries())
|
||||||
|
: token;
|
||||||
|
|
||||||
|
Log.i(TAG, "Restore " + status.getStatus());
|
||||||
|
switch (status.getStatus()) {
|
||||||
|
case OK:
|
||||||
|
Log.i(TAG, String.format(Locale.US,"Restore OK! data: %s tries: %d", Hex.toStringCondensed(status.getData().toByteArray()), status.getTries()));
|
||||||
|
PinStretcher.MasterKey masterKey = stretchedPin.withPinKey2(status.getData().toByteArray());
|
||||||
|
return new RegistrationLockData(masterKey, nextToken);
|
||||||
|
case PIN_MISMATCH:
|
||||||
|
Log.i(TAG, "Restore PIN_MISMATCH");
|
||||||
|
throw new KeyBackupServicePinException(nextToken);
|
||||||
|
case TOKEN_MISMATCH:
|
||||||
|
Log.i(TAG, "Restore TOKEN_MISMATCH");
|
||||||
|
// if the number of tries has not fallen, the pin is correct we're just using an out of date token
|
||||||
|
boolean canRetry = remainingTries == status.getTries();
|
||||||
|
Log.i(TAG, String.format(Locale.US, "Token MISMATCH %d %d", remainingTries, status.getTries()));
|
||||||
|
Log.i(TAG, String.format("Last token %s", Hex.toStringCondensed(token.getToken())));
|
||||||
|
Log.i(TAG, String.format("Next token %s", Hex.toStringCondensed(nextToken.getToken())));
|
||||||
|
throw new TokenException(nextToken, canRetry);
|
||||||
|
case MISSING:
|
||||||
|
Log.i(TAG, "Restore OK! No data though");
|
||||||
|
return null;
|
||||||
|
case NOT_YET_VALID:
|
||||||
|
throw new UnauthenticatedResponseException("Key is not valid yet, clock mismatch");
|
||||||
|
}
|
||||||
|
} catch (InvalidCiphertextException e) {
|
||||||
|
throw new UnauthenticatedResponseException(e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RemoteAttestation getAndVerifyRemoteAttestation() throws UnauthenticatedResponseException, IOException {
|
||||||
|
try {
|
||||||
|
return RemoteAttestationUtil.getAndVerifyRemoteAttestation(pushServiceSocket, PushServiceSocket.ClientSet.KeyBackup, iasKeyStore, enclaveName, mrenclave, authorization);
|
||||||
|
} catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | InvalidCiphertextException | SignatureException e) {
|
||||||
|
throw new UnauthenticatedResponseException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RegistrationLockData setPin(String pin) throws IOException, UnauthenticatedResponseException, InvalidPinException {
|
||||||
|
PinStretcher.MasterKey masterKey = PinStretcher.stretchPin(pin)
|
||||||
|
.withNewSecurePinKey2();
|
||||||
|
|
||||||
|
TokenResponse tokenResponse = putKbsData(masterKey.getKbsAccessKey(),
|
||||||
|
masterKey.getPinKey2(),
|
||||||
|
enclaveName,
|
||||||
|
currentToken);
|
||||||
|
|
||||||
|
pushServiceSocket.setRegistrationLock(masterKey.getRegistrationLock());
|
||||||
|
|
||||||
|
return new RegistrationLockData(masterKey, tokenResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removePin() throws IOException, UnauthenticatedResponseException {
|
||||||
|
deleteKbsData();
|
||||||
|
|
||||||
|
pushServiceSocket.removePinV2();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TokenResponse putKbsData(byte[] kbsAccessKey, byte[] kbsData, String enclaveName, TokenResponse token)
|
||||||
|
throws IOException, UnauthenticatedResponseException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
|
||||||
|
KeyBackupRequest request = KeyBackupCipher.createKeyBackupRequest(kbsAccessKey, kbsData, token, remoteAttestation, Hex.fromStringCondensed(enclaveName), maxTries);
|
||||||
|
KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
|
||||||
|
BackupResponse backupResponse = KeyBackupCipher.getKeyBackupResponse(response, remoteAttestation);
|
||||||
|
BackupResponse.Status status = backupResponse.getStatus();
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case OK:
|
||||||
|
return backupResponse.hasToken() ? new TokenResponse(token.getBackupId(), backupResponse.getToken().toByteArray(), maxTries) : token;
|
||||||
|
case ALREADY_EXISTS:
|
||||||
|
throw new UnauthenticatedResponseException("Already exists");
|
||||||
|
case NOT_YET_VALID:
|
||||||
|
throw new UnauthenticatedResponseException("Key is not valid yet, clock mismatch");
|
||||||
|
default:
|
||||||
|
throw new AssertionError("Unknown response status " + status);
|
||||||
|
}
|
||||||
|
} catch (InvalidCiphertextException e) {
|
||||||
|
throw new UnauthenticatedResponseException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteKbsData()
|
||||||
|
throws IOException, UnauthenticatedResponseException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
|
||||||
|
KeyBackupRequest request = KeyBackupCipher.createKeyDeleteRequest(currentToken, remoteAttestation, Hex.fromStringCondensed(enclaveName));
|
||||||
|
KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
|
||||||
|
|
||||||
|
KeyBackupCipher.getKeyDeleteResponseStatus(response, remoteAttestation);
|
||||||
|
} catch (InvalidCiphertextException e) {
|
||||||
|
throw new UnauthenticatedResponseException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface RestoreSession {
|
||||||
|
|
||||||
|
RegistrationLockData restorePin(String pin)
|
||||||
|
throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, InvalidPinException;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface PinChangeSession {
|
||||||
|
|
||||||
|
RegistrationLockData setPin(String pin)
|
||||||
|
throws IOException, UnauthenticatedResponseException, InvalidPinException;
|
||||||
|
|
||||||
|
void removePin()
|
||||||
|
throws IOException, UnauthenticatedResponseException;
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package org.whispersystems.signalservice.api;
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||||
|
|
||||||
|
public final class KeyBackupServicePinException extends TokenException {
|
||||||
|
|
||||||
|
private final int triesRemaining;
|
||||||
|
|
||||||
|
public KeyBackupServicePinException(TokenResponse nextToken) {
|
||||||
|
super(nextToken, false);
|
||||||
|
this.triesRemaining = nextToken.getTries();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTriesRemaining() {
|
||||||
|
return triesRemaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
package org.whispersystems.signalservice.api;
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||||
|
import org.whispersystems.signalservice.internal.registrationpin.PinStretcher;
|
||||||
|
|
||||||
|
public final class RegistrationLockData {
|
||||||
|
|
||||||
|
private final PinStretcher.MasterKey masterKey;
|
||||||
|
private final TokenResponse tokenResponse;
|
||||||
|
|
||||||
|
RegistrationLockData(PinStretcher.MasterKey masterKey, TokenResponse tokenResponse) {
|
||||||
|
this.masterKey = masterKey;
|
||||||
|
this.tokenResponse = tokenResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PinStretcher.MasterKey getMasterKey() {
|
||||||
|
return masterKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenResponse getTokenResponse() {
|
||||||
|
return tokenResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRemainingTries() {
|
||||||
|
return tokenResponse.getTries();
|
||||||
|
}
|
||||||
|
}
|
||||||
+624
@@ -0,0 +1,624 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api;
|
||||||
|
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
|
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||||
|
import org.whispersystems.libsignal.InvalidKeyException;
|
||||||
|
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
||||||
|
import org.whispersystems.libsignal.logging.Log;
|
||||||
|
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||||
|
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
||||||
|
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
||||||
|
import org.whispersystems.signalservice.api.storage.SignalStorageCipher;
|
||||||
|
import org.whispersystems.signalservice.api.storage.SignalStorageModels;
|
||||||
|
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||||
|
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||||
|
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||||
|
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||||
|
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||||
|
import org.whispersystems.signalservice.internal.contacts.crypto.ContactDiscoveryCipher;
|
||||||
|
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
|
||||||
|
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
|
||||||
|
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
|
||||||
|
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
|
||||||
|
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
|
||||||
|
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
|
||||||
|
import org.whispersystems.signalservice.internal.crypto.ProvisioningCipher;
|
||||||
|
import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
|
||||||
|
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||||
|
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
|
||||||
|
import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory;
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.ReadOperation;
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.StorageItem;
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.StorageItems;
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.StorageManifest;
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.WriteOperation;
|
||||||
|
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
|
||||||
|
import org.whispersystems.signalservice.internal.util.Util;
|
||||||
|
import org.whispersystems.util.Base64;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.whispersystems.signalservice.internal.push.ProvisioningProtos.ProvisionMessage;
|
||||||
|
import static org.whispersystems.signalservice.internal.push.ProvisioningProtos.ProvisioningVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main interface for creating, registering, and
|
||||||
|
* managing a Signal Service account.
|
||||||
|
*
|
||||||
|
* @author Moxie Marlinspike
|
||||||
|
*/
|
||||||
|
public class SignalServiceAccountManager {
|
||||||
|
|
||||||
|
private static final String TAG = SignalServiceAccountManager.class.getSimpleName();
|
||||||
|
|
||||||
|
private final PushServiceSocket pushServiceSocket;
|
||||||
|
private final CredentialsProvider credentials;
|
||||||
|
private final String userAgent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a SignalServiceAccountManager.
|
||||||
|
*
|
||||||
|
* @param configuration The URL for the Signal Service.
|
||||||
|
* @param uuid The Signal Service UUID.
|
||||||
|
* @param e164 The Signal Service phone number.
|
||||||
|
* @param password A Signal Service password.
|
||||||
|
* @param userAgent A string which identifies the client software.
|
||||||
|
*/
|
||||||
|
public SignalServiceAccountManager(SignalServiceConfiguration configuration,
|
||||||
|
UUID uuid, String e164, String password,
|
||||||
|
String userAgent)
|
||||||
|
{
|
||||||
|
this(configuration, new StaticCredentialsProvider(uuid, e164, password, null), userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceAccountManager(SignalServiceConfiguration configuration,
|
||||||
|
CredentialsProvider credentialsProvider,
|
||||||
|
String userAgent)
|
||||||
|
{
|
||||||
|
this.pushServiceSocket = new PushServiceSocket(configuration, credentialsProvider, userAgent);
|
||||||
|
this.credentials = credentialsProvider;
|
||||||
|
this.userAgent = userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getSenderCertificate() throws IOException {
|
||||||
|
return this.pushServiceSocket.getSenderCertificate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getSenderCertificateLegacy() throws IOException {
|
||||||
|
return this.pushServiceSocket.getSenderCertificateLegacy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Remove this method once KBS is live.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public void setPin(String pin) throws IOException {
|
||||||
|
this.pushServiceSocket.setPin(pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V1 Pin setting has been replaced by KeyBackupService.
|
||||||
|
* Now you can only remove the old pin but there is no need to remove the old pin if setting a KBS Pin.
|
||||||
|
*/
|
||||||
|
public void removeV1Pin() throws IOException {
|
||||||
|
this.pushServiceSocket.removePin();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getOwnUuid() throws IOException {
|
||||||
|
return this.pushServiceSocket.getOwnUuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyBackupService getKeyBackupService(KeyStore iasKeyStore,
|
||||||
|
String enclaveName,
|
||||||
|
String mrenclave,
|
||||||
|
int tries)
|
||||||
|
{
|
||||||
|
return new KeyBackupService(iasKeyStore, enclaveName, mrenclave, pushServiceSocket, tries);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register/Unregister a Google Cloud Messaging registration ID.
|
||||||
|
*
|
||||||
|
* @param gcmRegistrationId The GCM id to register. A call with an absent value will unregister.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void setGcmId(Optional<String> gcmRegistrationId) throws IOException {
|
||||||
|
if (gcmRegistrationId.isPresent()) {
|
||||||
|
this.pushServiceSocket.registerGcmId(gcmRegistrationId.get());
|
||||||
|
} else {
|
||||||
|
this.pushServiceSocket.unregisterGcmId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request a push challenge. A number will be pushed to the GCM (FCM) id. This can then be used
|
||||||
|
* during SMS/call requests to bypass the CAPTCHA.
|
||||||
|
*
|
||||||
|
* @param gcmRegistrationId The GCM (FCM) id to use.
|
||||||
|
* @param e164number The number to associate it with.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void requestPushChallenge(String gcmRegistrationId, String e164number) throws IOException {
|
||||||
|
this.pushServiceSocket.requestPushChallenge(gcmRegistrationId, e164number);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request an SMS verification code. On success, the server will send
|
||||||
|
* an SMS verification code to this Signal user.
|
||||||
|
*
|
||||||
|
* @param androidSmsRetrieverSupported
|
||||||
|
* @param captchaToken If the user has done a CAPTCHA, include this.
|
||||||
|
* @param challenge If present, it can bypass the CAPTCHA.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void requestSmsVerificationCode(boolean androidSmsRetrieverSupported, Optional<String> captchaToken, Optional<String> challenge) throws IOException {
|
||||||
|
this.pushServiceSocket.requestSmsVerificationCode(androidSmsRetrieverSupported, captchaToken, challenge);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request a Voice verification code. On success, the server will
|
||||||
|
* make a voice call to this Signal user.
|
||||||
|
*
|
||||||
|
* @param locale
|
||||||
|
* @param captchaToken If the user has done a CAPTCHA, include this.
|
||||||
|
* @param challenge If present, it can bypass the CAPTCHA.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void requestVoiceVerificationCode(Locale locale, Optional<String> captchaToken, Optional<String> challenge) throws IOException {
|
||||||
|
this.pushServiceSocket.requestVoiceVerificationCode(locale, captchaToken, challenge);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a Signal Service account with a received SMS or voice verification code.
|
||||||
|
*
|
||||||
|
* @param verificationCode The verification code received via SMS or Voice
|
||||||
|
* (see {@link #requestSmsVerificationCode} and
|
||||||
|
* {@link #requestVoiceVerificationCode}).
|
||||||
|
* @param signalingKey 52 random bytes. A 32 byte AES key and a 20 byte Hmac256 key,
|
||||||
|
* concatenated.
|
||||||
|
* @param signalProtocolRegistrationId A random 14-bit number that identifies this Signal install.
|
||||||
|
* This value should remain consistent across registrations for the
|
||||||
|
* same install, but probabilistically differ across registrations
|
||||||
|
* for separate installs.
|
||||||
|
* @param pin Deprecated, only supply the pin if you did not find a registrationLock on KBS.
|
||||||
|
* @param registrationLock Only supply if found on KBS.
|
||||||
|
* @return The UUID of the user that was registered.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public UUID verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
|
||||||
|
String pin, String registrationLock,
|
||||||
|
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
return this.pushServiceSocket.verifyAccountCode(verificationCode, signalingKey,
|
||||||
|
signalProtocolRegistrationId,
|
||||||
|
fetchesMessages,
|
||||||
|
pin, registrationLock,
|
||||||
|
unidentifiedAccessKey,
|
||||||
|
unrestrictedUnidentifiedAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh account attributes with server.
|
||||||
|
*
|
||||||
|
* @param signalingKey 52 random bytes. A 32 byte AES key and a 20 byte Hmac256 key, concatenated.
|
||||||
|
* @param signalProtocolRegistrationId A random 14-bit number that identifies this Signal install.
|
||||||
|
* This value should remain consistent across registrations for the same
|
||||||
|
* install, but probabilistically differ across registrations for
|
||||||
|
* separate installs.
|
||||||
|
* @param pin Only supply if pin has not yet been migrated to KBS.
|
||||||
|
* @param registrationLock Only supply if found on KBS.
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void setAccountAttributes(String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
|
||||||
|
String pin, String registrationLock,
|
||||||
|
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
this.pushServiceSocket.setAccountAttributes(signalingKey, signalProtocolRegistrationId, fetchesMessages,
|
||||||
|
pin, registrationLock,
|
||||||
|
unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an identity key, signed prekey, and list of one time prekeys
|
||||||
|
* with the server.
|
||||||
|
*
|
||||||
|
* @param identityKey The client's long-term identity keypair.
|
||||||
|
* @param signedPreKey The client's signed prekey.
|
||||||
|
* @param oneTimePreKeys The client's list of one-time prekeys.
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void setPreKeys(IdentityKey identityKey, SignedPreKeyRecord signedPreKey, List<PreKeyRecord> oneTimePreKeys)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
this.pushServiceSocket.registerPreKeys(identityKey, signedPreKey, oneTimePreKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The server's count of currently available (eg. unused) prekeys for this user.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public int getPreKeysCount() throws IOException {
|
||||||
|
return this.pushServiceSocket.getAvailablePreKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the client's signed prekey.
|
||||||
|
*
|
||||||
|
* @param signedPreKey The client's new signed prekey.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void setSignedPreKey(SignedPreKeyRecord signedPreKey) throws IOException {
|
||||||
|
this.pushServiceSocket.setCurrentSignedPreKey(signedPreKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The server's view of the client's current signed prekey.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public SignedPreKeyEntity getSignedPreKey() throws IOException {
|
||||||
|
return this.pushServiceSocket.getCurrentSignedPreKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a contact is currently registered with the server.
|
||||||
|
*
|
||||||
|
* @param e164number The contact to check.
|
||||||
|
* @return An optional ContactTokenDetails, present if registered, absent if not.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public Optional<ContactTokenDetails> getContact(String e164number) throws IOException {
|
||||||
|
String contactToken = createDirectoryServerToken(e164number, true);
|
||||||
|
ContactTokenDetails contactTokenDetails = this.pushServiceSocket.getContactTokenDetails(contactToken);
|
||||||
|
|
||||||
|
if (contactTokenDetails != null) {
|
||||||
|
contactTokenDetails.setNumber(e164number);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.fromNullable(contactTokenDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks which contacts in a set are registered with the server.
|
||||||
|
*
|
||||||
|
* @param e164numbers The contacts to check.
|
||||||
|
* @return A list of ContactTokenDetails for the registered users.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public List<ContactTokenDetails> getContacts(Set<String> e164numbers)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
Map<String, String> contactTokensMap = createDirectoryServerTokenMap(e164numbers);
|
||||||
|
List<ContactTokenDetails> activeTokens = this.pushServiceSocket.retrieveDirectory(contactTokensMap.keySet());
|
||||||
|
|
||||||
|
for (ContactTokenDetails activeToken : activeTokens) {
|
||||||
|
activeToken.setNumber(contactTokensMap.get(activeToken.getToken()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getRegisteredUsers(KeyStore iasKeyStore, Set<String> e164numbers, String enclaveId)
|
||||||
|
throws IOException, Quote.InvalidQuoteFormatException, UnauthenticatedQuoteException, SignatureException, UnauthenticatedResponseException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
String authorization = pushServiceSocket.getContactDiscoveryAuthorization();
|
||||||
|
RemoteAttestation remoteAttestation = RemoteAttestationUtil.getAndVerifyRemoteAttestation(pushServiceSocket, PushServiceSocket.ClientSet.ContactDiscovery, iasKeyStore, enclaveId, enclaveId, authorization);
|
||||||
|
List<String> addressBook = new LinkedList<>();
|
||||||
|
|
||||||
|
for (String e164number : e164numbers) {
|
||||||
|
addressBook.add(e164number.substring(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
DiscoveryRequest request = ContactDiscoveryCipher.createDiscoveryRequest(addressBook, remoteAttestation);
|
||||||
|
DiscoveryResponse response = pushServiceSocket.getContactDiscoveryRegisteredUsers(authorization, request, remoteAttestation.getCookies(), enclaveId);
|
||||||
|
byte[] data = ContactDiscoveryCipher.getDiscoveryResponseData(response, remoteAttestation);
|
||||||
|
|
||||||
|
Iterator<String> addressBookIterator = addressBook.iterator();
|
||||||
|
List<String> results = new LinkedList<>();
|
||||||
|
|
||||||
|
for (byte aData : data) {
|
||||||
|
String candidate = addressBookIterator.next();
|
||||||
|
|
||||||
|
if (aData != 0) results.add('+' + candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
} catch (InvalidCiphertextException e) {
|
||||||
|
throw new UnauthenticatedResponseException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reportContactDiscoveryServiceMatch() {
|
||||||
|
try {
|
||||||
|
this.pushServiceSocket.reportContactDiscoveryServiceMatch();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w(TAG, "Request to indicate a contact discovery result match failed. Ignoring.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reportContactDiscoveryServiceMismatch() {
|
||||||
|
try {
|
||||||
|
this.pushServiceSocket.reportContactDiscoveryServiceMismatch();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w(TAG, "Request to indicate a contact discovery result mismatch failed. Ignoring.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reportContactDiscoveryServiceAttestationError(String reason) {
|
||||||
|
try {
|
||||||
|
this.pushServiceSocket.reportContactDiscoveryServiceAttestationError(reason);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w(TAG, "Request to indicate a contact discovery attestation error failed. Ignoring.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reportContactDiscoveryServiceUnexpectedError(String reason) {
|
||||||
|
try {
|
||||||
|
this.pushServiceSocket.reportContactDiscoveryServiceUnexpectedError(reason);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w(TAG, "Request to indicate a contact discovery unexpected error failed. Ignoring.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getStorageManifestVersion() throws IOException {
|
||||||
|
try {
|
||||||
|
String authToken = this.pushServiceSocket.getStorageAuth();
|
||||||
|
StorageManifest storageManifest = this.pushServiceSocket.getStorageManifest(authToken);
|
||||||
|
|
||||||
|
return storageManifest.getVersion();
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<SignalStorageManifest> getStorageManifest(byte[] storageServiceKey) throws IOException, InvalidKeyException {
|
||||||
|
try {
|
||||||
|
SignalStorageCipher cipher = new SignalStorageCipher(storageServiceKey);
|
||||||
|
String authToken = this.pushServiceSocket.getStorageAuth();
|
||||||
|
StorageManifest storageManifest = this.pushServiceSocket.getStorageManifest(authToken);
|
||||||
|
byte[] rawRecord = cipher.decrypt(storageManifest.getValue().toByteArray());
|
||||||
|
ManifestRecord manifestRecord = ManifestRecord.parseFrom(rawRecord);
|
||||||
|
List<byte[]> keys = new ArrayList<>(manifestRecord.getKeysCount());
|
||||||
|
|
||||||
|
for (ByteString key : manifestRecord.getKeysList()) {
|
||||||
|
keys.add(key.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.of(new SignalStorageManifest(manifestRecord.getVersion(), keys));
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
return Optional.absent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SignalStorageRecord> readStorageRecords(byte[] storageServiceKey, List<byte[]> storageKeys) throws IOException, InvalidKeyException {
|
||||||
|
ReadOperation.Builder operation = ReadOperation.newBuilder();
|
||||||
|
|
||||||
|
for (byte[] key : storageKeys) {
|
||||||
|
operation.addReadKey(ByteString.copyFrom(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
String authToken = this.pushServiceSocket.getStorageAuth();
|
||||||
|
StorageItems items = this.pushServiceSocket.readStorageItems(authToken, operation.build());
|
||||||
|
|
||||||
|
SignalStorageCipher storageCipher = new SignalStorageCipher(storageServiceKey);
|
||||||
|
List<SignalStorageRecord> result = new ArrayList<>(items.getItemsCount());
|
||||||
|
|
||||||
|
for (StorageItem item : items.getItemsList()) {
|
||||||
|
if (item.hasKey()) {
|
||||||
|
result.add(SignalStorageModels.remoteToLocalStorageRecord(item, storageCipher));
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Encountered a StorageItem with no key! Skipping.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return If there was a conflict, the latest {@link SignalStorageManifest}. Otherwise absent.
|
||||||
|
*/
|
||||||
|
public Optional<SignalStorageManifest> writeStorageRecords(byte[] storageServiceKey,
|
||||||
|
SignalStorageManifest manifest,
|
||||||
|
List<SignalStorageRecord> inserts,
|
||||||
|
List<byte[]> deletes)
|
||||||
|
throws IOException, InvalidKeyException
|
||||||
|
{
|
||||||
|
ManifestRecord.Builder manifestRecordBuilder = ManifestRecord.newBuilder().setVersion(manifest.getVersion());
|
||||||
|
|
||||||
|
for (byte[] key : manifest.getStorageKeys()) {
|
||||||
|
manifestRecordBuilder.addKeys(ByteString.copyFrom(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
String authToken = this.pushServiceSocket.getStorageAuth();
|
||||||
|
SignalStorageCipher cipher = new SignalStorageCipher(storageServiceKey);
|
||||||
|
byte[] encryptedRecord = cipher.encrypt(manifestRecordBuilder.build().toByteArray());
|
||||||
|
StorageManifest storageManifest = StorageManifest.newBuilder()
|
||||||
|
.setVersion(manifest.getVersion())
|
||||||
|
.setValue(ByteString.copyFrom(encryptedRecord))
|
||||||
|
.build();
|
||||||
|
WriteOperation.Builder writeBuilder = WriteOperation.newBuilder().setManifest(storageManifest);
|
||||||
|
|
||||||
|
for (SignalStorageRecord insert : inserts) {
|
||||||
|
writeBuilder.addInsertItem(SignalStorageModels.localToRemoteStorageRecord(insert, cipher));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (byte[] delete : deletes) {
|
||||||
|
writeBuilder.addDeleteKey(ByteString.copyFrom(delete));
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<StorageManifest> conflict = this.pushServiceSocket.writeStorageContacts(authToken, writeBuilder.build());
|
||||||
|
|
||||||
|
if (conflict.isPresent()) {
|
||||||
|
byte[] rawManifestRecord = cipher.decrypt(conflict.get().getValue().toByteArray());
|
||||||
|
ManifestRecord record = ManifestRecord.parseFrom(rawManifestRecord);
|
||||||
|
List<byte[]> keys = new ArrayList<>(record.getKeysCount());
|
||||||
|
|
||||||
|
for (ByteString key : record.getKeysList()) {
|
||||||
|
keys.add(key.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalStorageManifest conflictManifest = new SignalStorageManifest(record.getVersion(), keys);
|
||||||
|
|
||||||
|
return Optional.of(conflictManifest);
|
||||||
|
} else {
|
||||||
|
return Optional.absent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public String getNewDeviceVerificationCode() throws IOException {
|
||||||
|
return this.pushServiceSocket.getNewDeviceVerificationCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addDevice(String deviceIdentifier,
|
||||||
|
ECPublicKey deviceKey,
|
||||||
|
IdentityKeyPair identityKeyPair,
|
||||||
|
Optional<byte[]> profileKey,
|
||||||
|
String code)
|
||||||
|
throws InvalidKeyException, IOException
|
||||||
|
{
|
||||||
|
ProvisioningCipher cipher = new ProvisioningCipher(deviceKey);
|
||||||
|
ProvisionMessage.Builder message = ProvisionMessage.newBuilder()
|
||||||
|
.setIdentityKeyPublic(ByteString.copyFrom(identityKeyPair.getPublicKey().serialize()))
|
||||||
|
.setIdentityKeyPrivate(ByteString.copyFrom(identityKeyPair.getPrivateKey().serialize()))
|
||||||
|
.setProvisioningCode(code)
|
||||||
|
.setProvisioningVersion(ProvisioningVersion.CURRENT_VALUE);
|
||||||
|
|
||||||
|
String e164 = credentials.getE164();
|
||||||
|
UUID uuid = credentials.getUuid();
|
||||||
|
|
||||||
|
if (e164 != null) {
|
||||||
|
message.setNumber(e164);
|
||||||
|
} else {
|
||||||
|
throw new AssertionError("Missing phone number!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uuid != null) {
|
||||||
|
message.setUuid(uuid.toString());
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "[addDevice] Missing UUID.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileKey.isPresent()) {
|
||||||
|
message.setProfileKey(ByteString.copyFrom(profileKey.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] ciphertext = cipher.encrypt(message.build());
|
||||||
|
this.pushServiceSocket.sendProvisioningMessage(deviceIdentifier, ciphertext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DeviceInfo> getDevices() throws IOException {
|
||||||
|
return this.pushServiceSocket.getDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeDevice(long deviceId) throws IOException {
|
||||||
|
this.pushServiceSocket.removeDevice(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TurnServerInfo getTurnServerInfo() throws IOException {
|
||||||
|
return this.pushServiceSocket.getTurnServerInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProfileName(byte[] key, String name)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
if (name == null) name = "";
|
||||||
|
|
||||||
|
String ciphertextName = Base64.encodeBytesWithoutPadding(new ProfileCipher(key).encryptName(name.getBytes("UTF-8"), ProfileCipher.NAME_PADDED_LENGTH));
|
||||||
|
|
||||||
|
this.pushServiceSocket.setProfileName(ciphertextName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProfileAvatar(byte[] key, StreamDetails avatar)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
ProfileAvatarData profileAvatarData = null;
|
||||||
|
|
||||||
|
if (avatar != null) {
|
||||||
|
profileAvatarData = new ProfileAvatarData(avatar.getStream(),
|
||||||
|
ProfileCipherOutputStream.getCiphertextLength(avatar.getLength()),
|
||||||
|
avatar.getContentType(),
|
||||||
|
new ProfileCipherOutputStreamFactory(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pushServiceSocket.setProfileAvatar(profileAvatarData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) throws IOException {
|
||||||
|
this.pushServiceSocket.setUsername(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteUsername() throws IOException {
|
||||||
|
this.pushServiceSocket.deleteUsername();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSoTimeoutMillis(long soTimeoutMillis) {
|
||||||
|
this.pushServiceSocket.setSoTimeoutMillis(soTimeoutMillis);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelInFlightRequests() {
|
||||||
|
this.pushServiceSocket.cancelInFlightRequests();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createDirectoryServerToken(String e164number, boolean urlSafe) {
|
||||||
|
try {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance("SHA1");
|
||||||
|
byte[] token = Util.trim(digest.digest(e164number.getBytes()), 10);
|
||||||
|
String encoded = Base64.encodeBytesWithoutPadding(token);
|
||||||
|
|
||||||
|
if (urlSafe) return encoded.replace('+', '-').replace('/', '_');
|
||||||
|
else return encoded;
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> createDirectoryServerTokenMap(Collection<String> e164numbers) {
|
||||||
|
Map<String,String> tokenMap = new HashMap<>(e164numbers.size());
|
||||||
|
|
||||||
|
for (String number : e164numbers) {
|
||||||
|
tokenMap.put(createDirectoryServerToken(number, false), number);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
+263
@@ -0,0 +1,263 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.InvalidVersionException;
|
||||||
|
import org.whispersystems.libsignal.util.Pair;
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||||
|
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||||
|
import org.whispersystems.signalservice.internal.push.AttachmentUploadAttributes;
|
||||||
|
import org.whispersystems.signalservice.internal.push.OutgoingPushMessageList;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SendMessageResponse;
|
||||||
|
import org.whispersystems.signalservice.internal.util.JsonUtil;
|
||||||
|
import org.whispersystems.signalservice.internal.util.Util;
|
||||||
|
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
|
||||||
|
import org.whispersystems.util.Base64;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
import static org.whispersystems.signalservice.internal.websocket.WebSocketProtos.WebSocketRequestMessage;
|
||||||
|
import static org.whispersystems.signalservice.internal.websocket.WebSocketProtos.WebSocketResponseMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A SignalServiceMessagePipe represents a dedicated connection
|
||||||
|
* to the Signal Service, which the server can push messages
|
||||||
|
* down through.
|
||||||
|
*/
|
||||||
|
public class SignalServiceMessagePipe {
|
||||||
|
|
||||||
|
private static final String TAG = SignalServiceMessagePipe.class.getName();
|
||||||
|
|
||||||
|
private final WebSocketConnection websocket;
|
||||||
|
private final Optional<CredentialsProvider> credentialsProvider;
|
||||||
|
|
||||||
|
SignalServiceMessagePipe(WebSocketConnection websocket, Optional<CredentialsProvider> credentialsProvider) {
|
||||||
|
this.websocket = websocket;
|
||||||
|
this.credentialsProvider = credentialsProvider;
|
||||||
|
|
||||||
|
this.websocket.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A blocking call that reads a message off the pipe. When this
|
||||||
|
* call returns, the message has been acknowledged and will not
|
||||||
|
* be retransmitted.
|
||||||
|
*
|
||||||
|
* @param timeout The timeout to wait for.
|
||||||
|
* @param unit The timeout time unit.
|
||||||
|
* @return A new message.
|
||||||
|
*
|
||||||
|
* @throws InvalidVersionException
|
||||||
|
* @throws IOException
|
||||||
|
* @throws TimeoutException
|
||||||
|
*/
|
||||||
|
public SignalServiceEnvelope read(long timeout, TimeUnit unit)
|
||||||
|
throws InvalidVersionException, IOException, TimeoutException
|
||||||
|
{
|
||||||
|
return read(timeout, unit, new NullMessagePipeCallback());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A blocking call that reads a message off the pipe (see {@link #read(long, java.util.concurrent.TimeUnit)}
|
||||||
|
*
|
||||||
|
* Unlike {@link #read(long, java.util.concurrent.TimeUnit)}, this method allows you
|
||||||
|
* to specify a callback that will be called before the received message is acknowledged.
|
||||||
|
* This allows you to write the received message to durable storage before acknowledging
|
||||||
|
* receipt of it to the server.
|
||||||
|
*
|
||||||
|
* @param timeout The timeout to wait for.
|
||||||
|
* @param unit The timeout time unit.
|
||||||
|
* @param callback A callback that will be called before the message receipt is
|
||||||
|
* acknowledged to the server.
|
||||||
|
* @return The message read (same as the message sent through the callback).
|
||||||
|
* @throws TimeoutException
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidVersionException
|
||||||
|
*/
|
||||||
|
public SignalServiceEnvelope read(long timeout, TimeUnit unit, MessagePipeCallback callback)
|
||||||
|
throws TimeoutException, IOException, InvalidVersionException
|
||||||
|
{
|
||||||
|
if (!credentialsProvider.isPresent()) {
|
||||||
|
throw new IllegalArgumentException("You can't read messages if you haven't specified credentials");
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
WebSocketRequestMessage request = websocket.readRequest(unit.toMillis(timeout));
|
||||||
|
WebSocketResponseMessage response = createWebSocketResponse(request);
|
||||||
|
boolean signalKeyEncrypted = isSignalKeyEncrypted(request);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isSignalServiceEnvelope(request)) {
|
||||||
|
SignalServiceEnvelope envelope = new SignalServiceEnvelope(request.getBody().toByteArray(),
|
||||||
|
credentialsProvider.get().getSignalingKey(),
|
||||||
|
signalKeyEncrypted);
|
||||||
|
|
||||||
|
callback.onMessage(envelope);
|
||||||
|
return envelope;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
websocket.sendResponse(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SendMessageResponse send(OutgoingPushMessageList list, Optional<UnidentifiedAccess> unidentifiedAccess) throws IOException {
|
||||||
|
try {
|
||||||
|
List<String> headers = new LinkedList<String>() {{
|
||||||
|
add("content-type:application/json");
|
||||||
|
}};
|
||||||
|
|
||||||
|
if (unidentifiedAccess.isPresent()) {
|
||||||
|
headers.add("Unidentified-Access-Key:" + Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder()
|
||||||
|
.setId(SecureRandom.getInstance("SHA1PRNG").nextLong())
|
||||||
|
.setVerb("PUT")
|
||||||
|
.setPath(String.format("/v1/messages/%s", list.getDestination()))
|
||||||
|
.addAllHeaders(headers)
|
||||||
|
.setBody(ByteString.copyFrom(JsonUtil.toJson(list).getBytes()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Pair<Integer, String> response = websocket.sendRequest(requestMessage).get(10, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
if (response.first() < 200 || response.first() >= 300) {
|
||||||
|
throw new IOException("Non-successful response: " + response.first());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Util.isEmpty(response.second())) return new SendMessageResponse(false);
|
||||||
|
else return JsonUtil.fromJson(response.second(), SendMessageResponse.class);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceProfile getProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess) throws IOException {
|
||||||
|
try {
|
||||||
|
List<String> headers = new LinkedList<>();
|
||||||
|
|
||||||
|
if (unidentifiedAccess.isPresent()) {
|
||||||
|
headers.add("Unidentified-Access-Key:" + Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder()
|
||||||
|
.setId(SecureRandom.getInstance("SHA1PRNG").nextLong())
|
||||||
|
.setVerb("GET")
|
||||||
|
.setPath(String.format("/v1/profile/%s", address.getIdentifier()))
|
||||||
|
.addAllHeaders(headers)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Pair<Integer, String> response = websocket.sendRequest(requestMessage).get(10, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
if (response.first() < 200 || response.first() >= 300) {
|
||||||
|
throw new IOException("Non-successful response: " + response.first());
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonUtil.fromJson(response.second(), SignalServiceProfile.class);
|
||||||
|
} catch (NoSuchAlgorithmException nsae) {
|
||||||
|
throw new AssertionError(nsae);
|
||||||
|
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AttachmentUploadAttributes getAttachmentUploadAttributes() throws IOException {
|
||||||
|
try {
|
||||||
|
WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder()
|
||||||
|
.setId(new SecureRandom().nextLong())
|
||||||
|
.setVerb("GET")
|
||||||
|
.setPath("/v2/attachments/form/upload")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Pair<Integer, String> response = websocket.sendRequest(requestMessage).get(10, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
if (response.first() < 200 || response.first() >= 300) {
|
||||||
|
throw new IOException("Non-successful response: " + response.first());
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonUtil.fromJson(response.second(), AttachmentUploadAttributes.class);
|
||||||
|
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close this connection to the server.
|
||||||
|
*/
|
||||||
|
public void shutdown() {
|
||||||
|
websocket.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSignalServiceEnvelope(WebSocketRequestMessage message) {
|
||||||
|
return "PUT".equals(message.getVerb()) && "/api/v1/message".equals(message.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSignalKeyEncrypted(WebSocketRequestMessage message) {
|
||||||
|
List<String> headers = message.getHeadersList();
|
||||||
|
|
||||||
|
if (headers == null || headers.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String header : headers) {
|
||||||
|
String[] parts = header.split(":");
|
||||||
|
|
||||||
|
if (parts.length == 2 && parts[0] != null && parts[0].trim().equalsIgnoreCase("X-Signal-Key")) {
|
||||||
|
if (parts[1] != null && parts[1].trim().equalsIgnoreCase("false")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebSocketResponseMessage createWebSocketResponse(WebSocketRequestMessage request) {
|
||||||
|
if (isSignalServiceEnvelope(request)) {
|
||||||
|
return WebSocketResponseMessage.newBuilder()
|
||||||
|
.setId(request.getId())
|
||||||
|
.setStatus(200)
|
||||||
|
.setMessage("OK")
|
||||||
|
.build();
|
||||||
|
} else {
|
||||||
|
return WebSocketResponseMessage.newBuilder()
|
||||||
|
.setId(request.getId())
|
||||||
|
.setStatus(400)
|
||||||
|
.setMessage("Unknown")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For receiving a callback when a new message has been
|
||||||
|
* received.
|
||||||
|
*/
|
||||||
|
public static interface MessagePipeCallback {
|
||||||
|
public void onMessage(SignalServiceEnvelope envelope);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NullMessagePipeCallback implements MessagePipeCallback {
|
||||||
|
@Override
|
||||||
|
public void onMessage(SignalServiceEnvelope envelope) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+264
@@ -0,0 +1,264 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.InvalidMessageException;
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest;
|
||||||
|
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||||
|
import org.whispersystems.signalservice.api.util.SleepTimer;
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
|
import org.whispersystems.signalservice.api.websocket.ConnectivityListener;
|
||||||
|
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||||
|
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceEnvelopeEntity;
|
||||||
|
import org.whispersystems.signalservice.internal.sticker.StickerProtos;
|
||||||
|
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
|
||||||
|
import org.whispersystems.signalservice.internal.util.Util;
|
||||||
|
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The primary interface for receiving Signal Service messages.
|
||||||
|
*
|
||||||
|
* @author Moxie Marlinspike
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||||
|
public class SignalServiceMessageReceiver {
|
||||||
|
|
||||||
|
private final PushServiceSocket socket;
|
||||||
|
private final SignalServiceConfiguration urls;
|
||||||
|
private final CredentialsProvider credentialsProvider;
|
||||||
|
private final String userAgent;
|
||||||
|
private final ConnectivityListener connectivityListener;
|
||||||
|
private final SleepTimer sleepTimer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a SignalServiceMessageReceiver.
|
||||||
|
*
|
||||||
|
* @param urls The URL of the Signal Service.
|
||||||
|
* @param uuid The Signal Service UUID.
|
||||||
|
* @param e164 The Signal Service phone number.
|
||||||
|
* @param password The Signal Service user password.
|
||||||
|
* @param signalingKey The 52 byte signaling key assigned to this user at registration.
|
||||||
|
*/
|
||||||
|
public SignalServiceMessageReceiver(SignalServiceConfiguration urls,
|
||||||
|
UUID uuid, String e164, String password,
|
||||||
|
String signalingKey, String userAgent,
|
||||||
|
ConnectivityListener listener,
|
||||||
|
SleepTimer timer)
|
||||||
|
{
|
||||||
|
this(urls, new StaticCredentialsProvider(uuid, e164, password, signalingKey), userAgent, listener, timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a SignalServiceMessageReceiver.
|
||||||
|
*
|
||||||
|
* @param urls The URL of the Signal Service.
|
||||||
|
* @param credentials The Signal Service user's credentials.
|
||||||
|
*/
|
||||||
|
public SignalServiceMessageReceiver(SignalServiceConfiguration urls,
|
||||||
|
CredentialsProvider credentials,
|
||||||
|
String userAgent,
|
||||||
|
ConnectivityListener listener,
|
||||||
|
SleepTimer timer)
|
||||||
|
{
|
||||||
|
this.urls = urls;
|
||||||
|
this.credentialsProvider = credentials;
|
||||||
|
this.socket = new PushServiceSocket(urls, credentials, userAgent);
|
||||||
|
this.userAgent = userAgent;
|
||||||
|
this.connectivityListener = listener;
|
||||||
|
this.sleepTimer = timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a SignalServiceAttachment.
|
||||||
|
*
|
||||||
|
* @param pointer The {@link SignalServiceAttachmentPointer}
|
||||||
|
* received in a {@link SignalServiceDataMessage}.
|
||||||
|
* @param destination The download destination for this attachment.
|
||||||
|
*
|
||||||
|
* @return An InputStream that streams the plaintext attachment contents.
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidMessageException
|
||||||
|
*/
|
||||||
|
public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, int maxSizeBytes)
|
||||||
|
throws IOException, InvalidMessageException
|
||||||
|
{
|
||||||
|
return retrieveAttachment(pointer, destination, maxSizeBytes, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceProfile retrieveProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
return socket.retrieveProfile(address, unidentifiedAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceProfile retrieveProfileByUsername(String username, Optional<UnidentifiedAccess> unidentifiedAccess)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
return socket.retrieveProfileByUsername(username, unidentifiedAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream retrieveProfileAvatar(String path, File destination, byte[] profileKey, int maxSizeBytes)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
socket.retrieveProfileAvatar(path, destination, maxSizeBytes);
|
||||||
|
return new ProfileCipherInputStream(new FileInputStream(destination), profileKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a SignalServiceAttachment.
|
||||||
|
*
|
||||||
|
* @param pointer The {@link SignalServiceAttachmentPointer}
|
||||||
|
* received in a {@link SignalServiceDataMessage}.
|
||||||
|
* @param destination The download destination for this attachment.
|
||||||
|
* @param listener An optional listener (may be null) to receive callbacks on download progress.
|
||||||
|
*
|
||||||
|
* @return An InputStream that streams the plaintext attachment contents.
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidMessageException
|
||||||
|
*/
|
||||||
|
public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, int maxSizeBytes, ProgressListener listener)
|
||||||
|
throws IOException, InvalidMessageException
|
||||||
|
{
|
||||||
|
if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!");
|
||||||
|
|
||||||
|
socket.retrieveAttachment(pointer.getId(), destination, maxSizeBytes, listener);
|
||||||
|
return AttachmentCipherInputStream.createForAttachment(destination, pointer.getSize().or(0), pointer.getKey(), pointer.getDigest().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream retrieveSticker(byte[] packId, byte[] packKey, int stickerId)
|
||||||
|
throws IOException, InvalidMessageException
|
||||||
|
{
|
||||||
|
byte[] data = socket.retrieveSticker(packId, stickerId);
|
||||||
|
return AttachmentCipherInputStream.createForStickerData(data, packKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a {@link SignalServiceStickerManifest}.
|
||||||
|
*
|
||||||
|
* @param packId The 16-byte packId that identifies the sticker pack.
|
||||||
|
* @param packKey The 32-byte packKey that decrypts the sticker pack.
|
||||||
|
* @return The {@link SignalServiceStickerManifest} representing the sticker pack.
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidMessageException
|
||||||
|
*/
|
||||||
|
public SignalServiceStickerManifest retrieveStickerManifest(byte[] packId, byte[] packKey)
|
||||||
|
throws IOException, InvalidMessageException
|
||||||
|
{
|
||||||
|
byte[] manifestBytes = socket.retrieveStickerManifest(packId);
|
||||||
|
|
||||||
|
InputStream cipherStream = AttachmentCipherInputStream.createForStickerData(manifestBytes, packKey);
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
Util.copy(cipherStream, outputStream);
|
||||||
|
|
||||||
|
StickerProtos.Pack pack = StickerProtos.Pack.parseFrom(outputStream.toByteArray());
|
||||||
|
List<SignalServiceStickerManifest.StickerInfo> stickers = new ArrayList<>(pack.getStickersCount());
|
||||||
|
SignalServiceStickerManifest.StickerInfo cover = pack.hasCover() ? new SignalServiceStickerManifest.StickerInfo(pack.getCover().getId(), pack.getCover().getEmoji())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
for (StickerProtos.Pack.Sticker sticker : pack.getStickersList()) {
|
||||||
|
stickers.add(new SignalServiceStickerManifest.StickerInfo(sticker.getId(), sticker.getEmoji()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SignalServiceStickerManifest(pack.getTitle(), pack.getAuthor(), cover, stickers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a pipe for receiving SignalService messages.
|
||||||
|
*
|
||||||
|
* Callers must call {@link SignalServiceMessagePipe#shutdown()} when finished with the pipe.
|
||||||
|
*
|
||||||
|
* @return A SignalServiceMessagePipe for receiving Signal Service messages.
|
||||||
|
*/
|
||||||
|
public SignalServiceMessagePipe createMessagePipe() {
|
||||||
|
WebSocketConnection webSocket = new WebSocketConnection(urls.getSignalServiceUrls()[0].getUrl(),
|
||||||
|
urls.getSignalServiceUrls()[0].getTrustStore(),
|
||||||
|
Optional.of(credentialsProvider), userAgent, connectivityListener,
|
||||||
|
sleepTimer);
|
||||||
|
|
||||||
|
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceMessagePipe createUnidentifiedMessagePipe() {
|
||||||
|
WebSocketConnection webSocket = new WebSocketConnection(urls.getSignalServiceUrls()[0].getUrl(),
|
||||||
|
urls.getSignalServiceUrls()[0].getTrustStore(),
|
||||||
|
Optional.<CredentialsProvider>absent(), userAgent, connectivityListener,
|
||||||
|
sleepTimer);
|
||||||
|
|
||||||
|
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SignalServiceEnvelope> retrieveMessages() throws IOException {
|
||||||
|
return retrieveMessages(new NullMessageReceivedCallback());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SignalServiceEnvelope> retrieveMessages(MessageReceivedCallback callback)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
List<SignalServiceEnvelope> results = new LinkedList<>();
|
||||||
|
List<SignalServiceEnvelopeEntity> entities = socket.getMessages();
|
||||||
|
|
||||||
|
for (SignalServiceEnvelopeEntity entity : entities) {
|
||||||
|
SignalServiceEnvelope envelope;
|
||||||
|
|
||||||
|
if (entity.hasSource() && entity.getSourceDevice() > 0) {
|
||||||
|
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(entity.getSourceUuid()), entity.getSourceE164());
|
||||||
|
envelope = new SignalServiceEnvelope(entity.getType(), Optional.of(address),
|
||||||
|
entity.getSourceDevice(), entity.getTimestamp(),
|
||||||
|
entity.getMessage(), entity.getContent(),
|
||||||
|
entity.getServerTimestamp(), entity.getServerUuid());
|
||||||
|
} else {
|
||||||
|
envelope = new SignalServiceEnvelope(entity.getType(), entity.getTimestamp(),
|
||||||
|
entity.getMessage(), entity.getContent(),
|
||||||
|
entity.getServerTimestamp(), entity.getServerUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
callback.onMessage(envelope);
|
||||||
|
results.add(envelope);
|
||||||
|
|
||||||
|
if (envelope.hasUuid()) socket.acknowledgeMessage(envelope.getUuid());
|
||||||
|
else socket.acknowledgeMessage(entity.getSourceE164(), entity.getTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSoTimeoutMillis(long soTimeoutMillis) {
|
||||||
|
socket.setSoTimeoutMillis(soTimeoutMillis);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface MessageReceivedCallback {
|
||||||
|
public void onMessage(SignalServiceEnvelope envelope);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class NullMessageReceivedCallback implements MessageReceivedCallback {
|
||||||
|
@Override
|
||||||
|
public void onMessage(SignalServiceEnvelope envelope) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+1286
File diff suppressed because it is too large
Load Diff
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package org.whispersystems.signalservice.api;
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||||
|
|
||||||
|
class TokenException extends Exception {
|
||||||
|
|
||||||
|
private final TokenResponse nextToken;
|
||||||
|
private final boolean canAutomaticallyRetry;
|
||||||
|
|
||||||
|
TokenException(TokenResponse nextToken, boolean canAutomaticallyRetry) {
|
||||||
|
this.nextToken = nextToken;
|
||||||
|
this.canAutomaticallyRetry = canAutomaticallyRetry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenResponse getToken() {
|
||||||
|
return nextToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCanAutomaticallyRetry() {
|
||||||
|
return canAutomaticallyRetry;
|
||||||
|
}
|
||||||
|
}
|
||||||
+271
@@ -0,0 +1,271 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014-2017 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.crypto;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.InvalidMacException;
|
||||||
|
import org.whispersystems.libsignal.InvalidMessageException;
|
||||||
|
import org.whispersystems.libsignal.kdf.HKDFv3;
|
||||||
|
import org.whispersystems.signalservice.internal.util.ContentLengthInputStream;
|
||||||
|
import org.whispersystems.signalservice.internal.util.Util;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FilterInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import javax.crypto.ShortBufferException;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for streaming an encrypted push attachment off disk.
|
||||||
|
*
|
||||||
|
* @author Moxie Marlinspike
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class AttachmentCipherInputStream extends FilterInputStream {
|
||||||
|
|
||||||
|
private static final int BLOCK_SIZE = 16;
|
||||||
|
private static final int CIPHER_KEY_SIZE = 32;
|
||||||
|
private static final int MAC_KEY_SIZE = 32;
|
||||||
|
|
||||||
|
private Cipher cipher;
|
||||||
|
private boolean done;
|
||||||
|
private long totalDataSize;
|
||||||
|
private long totalRead;
|
||||||
|
private byte[] overflowBuffer;
|
||||||
|
|
||||||
|
public static InputStream createForAttachment(File file, long plaintextLength, byte[] combinedKeyMaterial, byte[] digest)
|
||||||
|
throws InvalidMessageException, IOException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
byte[][] parts = Util.split(combinedKeyMaterial, CIPHER_KEY_SIZE, MAC_KEY_SIZE);
|
||||||
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
|
mac.init(new SecretKeySpec(parts[1], "HmacSHA256"));
|
||||||
|
|
||||||
|
if (file.length() <= BLOCK_SIZE + mac.getMacLength()) {
|
||||||
|
throw new InvalidMessageException("Message shorter than crypto overhead!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (digest == null) {
|
||||||
|
throw new InvalidMacException("Missing digest!");
|
||||||
|
}
|
||||||
|
|
||||||
|
try (FileInputStream fin = new FileInputStream(file)) {
|
||||||
|
verifyMac(fin, file.length(), mac, digest);
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream inputStream = new AttachmentCipherInputStream(new FileInputStream(file), parts[0], file.length() - BLOCK_SIZE - mac.getMacLength());
|
||||||
|
|
||||||
|
if (plaintextLength != 0) {
|
||||||
|
inputStream = new ContentLengthInputStream(inputStream, plaintextLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputStream;
|
||||||
|
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} catch (InvalidMacException e) {
|
||||||
|
throw new InvalidMessageException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InputStream createForStickerData(byte[] data, byte[] packKey)
|
||||||
|
throws InvalidMessageException, IOException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
byte[] combinedKeyMaterial = new HKDFv3().deriveSecrets(packKey, "Sticker Pack".getBytes(), 64);
|
||||||
|
byte[][] parts = Util.split(combinedKeyMaterial, CIPHER_KEY_SIZE, MAC_KEY_SIZE);
|
||||||
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
|
mac.init(new SecretKeySpec(parts[1], "HmacSHA256"));
|
||||||
|
|
||||||
|
if (data.length <= BLOCK_SIZE + mac.getMacLength()) {
|
||||||
|
throw new InvalidMessageException("Message shorter than crypto overhead!");
|
||||||
|
}
|
||||||
|
|
||||||
|
try (InputStream inputStream = new ByteArrayInputStream(data)) {
|
||||||
|
verifyMac(inputStream, data.length, mac, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AttachmentCipherInputStream(new ByteArrayInputStream(data), parts[0], data.length - BLOCK_SIZE - mac.getMacLength());
|
||||||
|
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} catch (InvalidMacException e) {
|
||||||
|
throw new InvalidMessageException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AttachmentCipherInputStream(InputStream inputStream, byte[] cipherKey, long totalDataSize)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
super(inputStream);
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] iv = new byte[BLOCK_SIZE];
|
||||||
|
readFully(iv);
|
||||||
|
|
||||||
|
this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||||
|
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
|
||||||
|
|
||||||
|
this.done = false;
|
||||||
|
this.totalRead = 0;
|
||||||
|
this.totalDataSize = totalDataSize;
|
||||||
|
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buffer) throws IOException {
|
||||||
|
return read(buffer, 0, buffer.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buffer, int offset, int length) throws IOException {
|
||||||
|
if (totalRead != totalDataSize) return readIncremental(buffer, offset, length);
|
||||||
|
else if (!done) return readFinal(buffer, offset, length);
|
||||||
|
else return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean markSupported() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long skip(long byteCount) throws IOException {
|
||||||
|
long skipped = 0L;
|
||||||
|
while (skipped < byteCount) {
|
||||||
|
byte[] buf = new byte[Math.min(4096, (int)(byteCount-skipped))];
|
||||||
|
int read = read(buf);
|
||||||
|
|
||||||
|
skipped += read;
|
||||||
|
}
|
||||||
|
|
||||||
|
return skipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int readFinal(byte[] buffer, int offset, int length) throws IOException {
|
||||||
|
try {
|
||||||
|
int flourish = cipher.doFinal(buffer, offset);
|
||||||
|
|
||||||
|
done = true;
|
||||||
|
return flourish;
|
||||||
|
} catch (IllegalBlockSizeException | BadPaddingException | ShortBufferException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int readIncremental(byte[] buffer, int offset, int length) throws IOException {
|
||||||
|
int readLength = 0;
|
||||||
|
if (null != overflowBuffer) {
|
||||||
|
if (overflowBuffer.length > length) {
|
||||||
|
System.arraycopy(overflowBuffer, 0, buffer, offset, length);
|
||||||
|
overflowBuffer = Arrays.copyOfRange(overflowBuffer, length, overflowBuffer.length);
|
||||||
|
return length;
|
||||||
|
} else if (overflowBuffer.length == length) {
|
||||||
|
System.arraycopy(overflowBuffer, 0, buffer, offset, length);
|
||||||
|
overflowBuffer = null;
|
||||||
|
return length;
|
||||||
|
} else {
|
||||||
|
System.arraycopy(overflowBuffer, 0, buffer, offset, overflowBuffer.length);
|
||||||
|
readLength += overflowBuffer.length;
|
||||||
|
offset += readLength;
|
||||||
|
length -= readLength;
|
||||||
|
overflowBuffer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length + totalRead > totalDataSize)
|
||||||
|
length = (int)(totalDataSize - totalRead);
|
||||||
|
|
||||||
|
byte[] internalBuffer = new byte[length];
|
||||||
|
int read = super.read(internalBuffer, 0, internalBuffer.length <= cipher.getBlockSize() ? internalBuffer.length : internalBuffer.length - cipher.getBlockSize());
|
||||||
|
totalRead += read;
|
||||||
|
|
||||||
|
try {
|
||||||
|
int outputLen = cipher.getOutputSize(read);
|
||||||
|
|
||||||
|
if (outputLen <= length) {
|
||||||
|
readLength += cipher.update(internalBuffer, 0, read, buffer, offset);
|
||||||
|
return readLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] transientBuffer = new byte[outputLen];
|
||||||
|
outputLen = cipher.update(internalBuffer, 0, read, transientBuffer, 0);
|
||||||
|
if (outputLen <= length) {
|
||||||
|
System.arraycopy(transientBuffer, 0, buffer, offset, outputLen);
|
||||||
|
readLength += outputLen;
|
||||||
|
} else {
|
||||||
|
System.arraycopy(transientBuffer, 0, buffer, offset, length);
|
||||||
|
overflowBuffer = Arrays.copyOfRange(transientBuffer, length, outputLen);
|
||||||
|
readLength += length;
|
||||||
|
}
|
||||||
|
return readLength;
|
||||||
|
} catch (ShortBufferException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyMac(InputStream inputStream, long length, Mac mac, byte[] theirDigest)
|
||||||
|
throws InvalidMacException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance("SHA256");
|
||||||
|
int remainingData = Util.toIntExact(length) - mac.getMacLength();
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
|
||||||
|
while (remainingData > 0) {
|
||||||
|
int read = inputStream.read(buffer, 0, Math.min(buffer.length, remainingData));
|
||||||
|
mac.update(buffer, 0, read);
|
||||||
|
digest.update(buffer, 0, read);
|
||||||
|
remainingData -= read;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] ourMac = mac.doFinal();
|
||||||
|
byte[] theirMac = new byte[mac.getMacLength()];
|
||||||
|
Util.readFully(inputStream, theirMac);
|
||||||
|
|
||||||
|
if (!MessageDigest.isEqual(ourMac, theirMac)) {
|
||||||
|
throw new InvalidMacException("MAC doesn't match!");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] ourDigest = digest.digest(theirMac);
|
||||||
|
|
||||||
|
if (theirDigest != null && !MessageDigest.isEqual(ourDigest, theirDigest)) {
|
||||||
|
throw new InvalidMacException("Digest doesn't match!");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException | ArithmeticException e1) {
|
||||||
|
throw new InvalidMacException(e1);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readFully(byte[] buffer) throws IOException {
|
||||||
|
int offset = 0;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
int read = super.read(buffer, offset, buffer.length - offset);
|
||||||
|
|
||||||
|
if (read + offset < buffer.length) offset += read;
|
||||||
|
else return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014-2017 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.crypto;
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.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 DigestingOutputStream {
|
||||||
|
|
||||||
|
private final Cipher cipher;
|
||||||
|
private final Mac mac;
|
||||||
|
|
||||||
|
public AttachmentCipherOutputStream(byte[] combinedKeyMaterial,
|
||||||
|
OutputStream outputStream)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
super(outputStream);
|
||||||
|
try {
|
||||||
|
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());
|
||||||
|
super.write(cipher.getIV());
|
||||||
|
} 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);
|
||||||
|
super.write(ciphertext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
|
||||||
|
super.write(ciphertext);
|
||||||
|
super.write(auth);
|
||||||
|
|
||||||
|
super.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package org.whispersystems.signalservice.api.crypto;
|
||||||
|
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
public final class CryptoUtil {
|
||||||
|
|
||||||
|
private CryptoUtil () { }
|
||||||
|
|
||||||
|
public static byte[] computeHmacSha256(byte[] key, byte[] data) {
|
||||||
|
try {
|
||||||
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
|
mac.init(new SecretKeySpec(key, "HmacSHA256"));
|
||||||
|
return mac.doFinal(data);
|
||||||
|
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
package org.whispersystems.signalservice.api.crypto;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.FilterOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
public abstract class DigestingOutputStream extends FilterOutputStream {
|
||||||
|
|
||||||
|
private final MessageDigest runningDigest;
|
||||||
|
|
||||||
|
private byte[] digest;
|
||||||
|
|
||||||
|
public DigestingOutputStream(OutputStream outputStream) {
|
||||||
|
super(outputStream);
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.runningDigest = MessageDigest.getInstance("SHA256");
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte[] buffer) throws IOException {
|
||||||
|
runningDigest.update(buffer, 0, buffer.length);
|
||||||
|
out.write(buffer, 0, buffer.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void write(byte[] buffer, int offset, int length) throws IOException {
|
||||||
|
runningDigest.update(buffer, offset, length);
|
||||||
|
out.write(buffer, offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void write(int b) throws IOException {
|
||||||
|
runningDigest.update((byte)b);
|
||||||
|
out.write(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void flush() throws IOException {
|
||||||
|
digest = runningDigest.digest();
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() throws IOException {
|
||||||
|
out.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getTransmittedDigest() {
|
||||||
|
return digest;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package org.whispersystems.signalservice.api.crypto;
|
||||||
|
|
||||||
|
public class InvalidCiphertextException extends Exception {
|
||||||
|
public InvalidCiphertextException(Exception nested) {
|
||||||
|
super(nested);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvalidCiphertextException(String s) {
|
||||||
|
super(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
package org.whispersystems.signalservice.api.crypto;
|
||||||
|
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.ByteUtil;
|
||||||
|
import org.whispersystems.signalservice.internal.util.Util;
|
||||||
|
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
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.GCMParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
public class ProfileCipher {
|
||||||
|
|
||||||
|
public static final int NAME_PADDED_LENGTH = 26;
|
||||||
|
|
||||||
|
private final byte[] key;
|
||||||
|
|
||||||
|
public ProfileCipher(byte[] key) {
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] encryptName(byte[] input, int paddedLength) {
|
||||||
|
try {
|
||||||
|
byte[] inputPadded = new byte[paddedLength];
|
||||||
|
|
||||||
|
if (input.length > inputPadded.length) {
|
||||||
|
throw new IllegalArgumentException("Input is too long: " + new String(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
System.arraycopy(input, 0, inputPadded, 0, input.length);
|
||||||
|
|
||||||
|
byte[] nonce = Util.getSecretBytes(12);
|
||||||
|
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
|
||||||
|
|
||||||
|
return ByteUtil.combine(nonce, cipher.doFinal(inputPadded));
|
||||||
|
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | BadPaddingException | NoSuchPaddingException | IllegalBlockSizeException | InvalidKeyException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] decryptName(byte[] input) throws InvalidCiphertextException {
|
||||||
|
try {
|
||||||
|
if (input.length < 12 + 16 + 1) {
|
||||||
|
throw new InvalidCiphertextException("Too short: " + input.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] nonce = new byte[12];
|
||||||
|
System.arraycopy(input, 0, nonce, 0, nonce.length);
|
||||||
|
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
|
||||||
|
|
||||||
|
byte[] paddedPlaintext = cipher.doFinal(input, nonce.length, input.length - nonce.length);
|
||||||
|
int plaintextLength = 0;
|
||||||
|
|
||||||
|
for (int i=paddedPlaintext.length-1;i>=0;i--) {
|
||||||
|
if (paddedPlaintext[i] != (byte)0x00) {
|
||||||
|
plaintextLength = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] plaintext = new byte[plaintextLength];
|
||||||
|
System.arraycopy(paddedPlaintext, 0, plaintext, 0, plaintextLength);
|
||||||
|
|
||||||
|
return plaintext;
|
||||||
|
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException | IllegalBlockSizeException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} catch (InvalidKeyException | BadPaddingException e) {
|
||||||
|
throw new InvalidCiphertextException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean verifyUnidentifiedAccess(byte[] theirUnidentifiedAccessVerifier) {
|
||||||
|
try {
|
||||||
|
if (theirUnidentifiedAccessVerifier == null || theirUnidentifiedAccessVerifier.length == 0) return false;
|
||||||
|
|
||||||
|
byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(key);
|
||||||
|
|
||||||
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
|
mac.init(new SecretKeySpec(unidentifiedAccessKey, "HmacSHA256"));
|
||||||
|
|
||||||
|
byte[] ourUnidentifiedAccessVerifier = mac.doFinal(new byte[32]);
|
||||||
|
|
||||||
|
return MessageDigest.isEqual(theirUnidentifiedAccessVerifier, ourUnidentifiedAccessVerifier);
|
||||||
|
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
package org.whispersystems.signalservice.api.crypto;
|
||||||
|
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.internal.util.Util;
|
||||||
|
|
||||||
|
import java.io.FilterInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import javax.crypto.ShortBufferException;
|
||||||
|
import javax.crypto.spec.GCMParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
public class ProfileCipherInputStream extends FilterInputStream {
|
||||||
|
|
||||||
|
private final Cipher cipher;
|
||||||
|
|
||||||
|
private boolean finished = false;
|
||||||
|
|
||||||
|
public ProfileCipherInputStream(InputStream in, byte[] key) throws IOException {
|
||||||
|
super(in);
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||||
|
|
||||||
|
byte[] nonce = new byte[12];
|
||||||
|
Util.readFully(in, nonce);
|
||||||
|
|
||||||
|
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
|
||||||
|
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() {
|
||||||
|
throw new AssertionError("Not supported!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] input) throws IOException {
|
||||||
|
return read(input, 0, input.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] output, int outputOffset, int outputLength) throws IOException {
|
||||||
|
if (finished) return -1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] ciphertext = new byte[outputLength / 2];
|
||||||
|
int read = in.read(ciphertext, 0, ciphertext.length);
|
||||||
|
|
||||||
|
if (read == -1) {
|
||||||
|
if (cipher.getOutputSize(0) > outputLength) {
|
||||||
|
throw new AssertionError("Need: " + cipher.getOutputSize(0) + " but only have: " + outputLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
finished = true;
|
||||||
|
return cipher.doFinal(output, outputOffset);
|
||||||
|
} else {
|
||||||
|
if (cipher.getOutputSize(read) > outputLength) {
|
||||||
|
throw new AssertionError("Need: " + cipher.getOutputSize(read) + " but only have: " + outputLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cipher.update(ciphertext, 0, read, output, outputOffset);
|
||||||
|
}
|
||||||
|
} catch (IllegalBlockSizeException | ShortBufferException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} catch (BadPaddingException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
package org.whispersystems.signalservice.api.crypto;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import javax.crypto.spec.GCMParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
public class ProfileCipherOutputStream extends DigestingOutputStream {
|
||||||
|
|
||||||
|
private final Cipher cipher;
|
||||||
|
|
||||||
|
public ProfileCipherOutputStream(OutputStream out, byte[] key) throws IOException {
|
||||||
|
super(out);
|
||||||
|
try {
|
||||||
|
this.cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||||
|
|
||||||
|
byte[] nonce = generateNonce();
|
||||||
|
this.cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
|
||||||
|
|
||||||
|
super.write(nonce, 0, nonce.length);
|
||||||
|
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new IOException(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[] output = cipher.update(buffer, offset, length);
|
||||||
|
super.write(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(int b) throws IOException {
|
||||||
|
byte[] input = new byte[1];
|
||||||
|
input[0] = (byte)b;
|
||||||
|
|
||||||
|
byte[] output = cipher.update(input);
|
||||||
|
super.write(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void flush() throws IOException {
|
||||||
|
try {
|
||||||
|
byte[] output = cipher.doFinal();
|
||||||
|
|
||||||
|
super.write(output);
|
||||||
|
super.flush();
|
||||||
|
} catch (BadPaddingException | IllegalBlockSizeException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] generateNonce() {
|
||||||
|
byte[] nonce = new byte[12];
|
||||||
|
new SecureRandom().nextBytes(nonce);
|
||||||
|
return nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getCiphertextLength(long plaintextLength) {
|
||||||
|
return 12 + 16 + plaintextLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
+861
@@ -0,0 +1,861 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.crypto;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
|
|
||||||
|
import org.signal.libsignal.metadata.InvalidMetadataMessageException;
|
||||||
|
import org.signal.libsignal.metadata.InvalidMetadataVersionException;
|
||||||
|
import org.signal.libsignal.metadata.ProtocolDuplicateMessageException;
|
||||||
|
import org.signal.libsignal.metadata.ProtocolInvalidKeyException;
|
||||||
|
import org.signal.libsignal.metadata.ProtocolInvalidKeyIdException;
|
||||||
|
import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
|
||||||
|
import org.signal.libsignal.metadata.ProtocolInvalidVersionException;
|
||||||
|
import org.signal.libsignal.metadata.ProtocolLegacyMessageException;
|
||||||
|
import org.signal.libsignal.metadata.ProtocolNoSessionException;
|
||||||
|
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
|
||||||
|
import org.signal.libsignal.metadata.SealedSessionCipher;
|
||||||
|
import org.signal.libsignal.metadata.SealedSessionCipher.DecryptionResult;
|
||||||
|
import org.signal.libsignal.metadata.SelfSendException;
|
||||||
|
import org.signal.libsignal.metadata.certificate.CertificateValidator;
|
||||||
|
import org.whispersystems.libsignal.DuplicateMessageException;
|
||||||
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
|
import org.whispersystems.libsignal.InvalidKeyException;
|
||||||
|
import org.whispersystems.libsignal.InvalidKeyIdException;
|
||||||
|
import org.whispersystems.libsignal.InvalidMessageException;
|
||||||
|
import org.whispersystems.libsignal.InvalidVersionException;
|
||||||
|
import org.whispersystems.libsignal.LegacyMessageException;
|
||||||
|
import org.whispersystems.libsignal.NoSessionException;
|
||||||
|
import org.whispersystems.libsignal.SessionCipher;
|
||||||
|
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||||
|
import org.whispersystems.libsignal.UntrustedIdentityException;
|
||||||
|
import org.whispersystems.libsignal.logging.Log;
|
||||||
|
import org.whispersystems.libsignal.protocol.CiphertextMessage;
|
||||||
|
import org.whispersystems.libsignal.protocol.PreKeySignalMessage;
|
||||||
|
import org.whispersystems.libsignal.protocol.SignalMessage;
|
||||||
|
import org.whispersystems.libsignal.state.SignalProtocolStore;
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Reaction;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Sticker;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage.VerifiedState;
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
|
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
|
||||||
|
import org.whispersystems.signalservice.internal.push.PushTransportDetails;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope.Type;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.ReceiptMessage;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.TypingMessage;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Verified;
|
||||||
|
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
|
||||||
|
import org.whispersystems.util.Base64;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.CallMessage;
|
||||||
|
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext.Type.DELIVER;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is used to decrypt received {@link SignalServiceEnvelope}s.
|
||||||
|
*
|
||||||
|
* @author Moxie Marlinspike
|
||||||
|
*/
|
||||||
|
public class SignalServiceCipher {
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private static final String TAG = SignalServiceCipher.class.getSimpleName();
|
||||||
|
|
||||||
|
private final SignalProtocolStore signalProtocolStore;
|
||||||
|
private final SignalServiceAddress localAddress;
|
||||||
|
private final CertificateValidator certificateValidator;
|
||||||
|
|
||||||
|
public SignalServiceCipher(SignalServiceAddress localAddress,
|
||||||
|
SignalProtocolStore signalProtocolStore,
|
||||||
|
CertificateValidator certificateValidator)
|
||||||
|
{
|
||||||
|
this.signalProtocolStore = signalProtocolStore;
|
||||||
|
this.localAddress = localAddress;
|
||||||
|
this.certificateValidator = certificateValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OutgoingPushMessage encrypt(SignalProtocolAddress destination,
|
||||||
|
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||||
|
byte[] unpaddedMessage)
|
||||||
|
throws UntrustedIdentityException, InvalidKeyException
|
||||||
|
{
|
||||||
|
if (unidentifiedAccess.isPresent()) {
|
||||||
|
SealedSessionCipher sessionCipher = new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1);
|
||||||
|
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion(destination));
|
||||||
|
byte[] ciphertext = sessionCipher.encrypt(destination, unidentifiedAccess.get().getUnidentifiedCertificate(), transportDetails.getPaddedMessageBody(unpaddedMessage));
|
||||||
|
String body = Base64.encodeBytes(ciphertext);
|
||||||
|
int remoteRegistrationId = sessionCipher.getRemoteRegistrationId(destination);
|
||||||
|
|
||||||
|
return new OutgoingPushMessage(Type.UNIDENTIFIED_SENDER_VALUE, destination.getDeviceId(), remoteRegistrationId, body);
|
||||||
|
} else {
|
||||||
|
SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, destination);
|
||||||
|
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion());
|
||||||
|
CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
|
||||||
|
int remoteRegistrationId = sessionCipher.getRemoteRegistrationId();
|
||||||
|
String body = Base64.encodeBytes(message.serialize());
|
||||||
|
|
||||||
|
int type;
|
||||||
|
|
||||||
|
switch (message.getType()) {
|
||||||
|
case CiphertextMessage.PREKEY_TYPE: type = Type.PREKEY_BUNDLE_VALUE; break;
|
||||||
|
case CiphertextMessage.WHISPER_TYPE: type = Type.CIPHERTEXT_VALUE; break;
|
||||||
|
default: throw new AssertionError("Bad type: " + message.getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OutgoingPushMessage(type, destination.getDeviceId(), remoteRegistrationId, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt a received {@link SignalServiceEnvelope}
|
||||||
|
*
|
||||||
|
* @param envelope The received SignalServiceEnvelope
|
||||||
|
*
|
||||||
|
* @return a decrypted SignalServiceContent
|
||||||
|
*/
|
||||||
|
public SignalServiceContent decrypt(SignalServiceEnvelope envelope)
|
||||||
|
throws InvalidMetadataMessageException, InvalidMetadataVersionException,
|
||||||
|
ProtocolInvalidKeyIdException, ProtocolLegacyMessageException,
|
||||||
|
ProtocolUntrustedIdentityException, ProtocolNoSessionException,
|
||||||
|
ProtocolInvalidVersionException, ProtocolInvalidMessageException,
|
||||||
|
ProtocolInvalidKeyException, ProtocolDuplicateMessageException,
|
||||||
|
SelfSendException, UnsupportedDataMessageException
|
||||||
|
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (envelope.hasLegacyMessage()) {
|
||||||
|
Plaintext plaintext = decrypt(envelope, envelope.getLegacyMessage());
|
||||||
|
DataMessage message = DataMessage.parseFrom(plaintext.getData());
|
||||||
|
return new SignalServiceContent(createSignalServiceMessage(plaintext.getMetadata(), message),
|
||||||
|
plaintext.getMetadata().getSender(),
|
||||||
|
plaintext.getMetadata().getSenderDevice(),
|
||||||
|
plaintext.getMetadata().getTimestamp(),
|
||||||
|
plaintext.getMetadata().isNeedsReceipt());
|
||||||
|
} else if (envelope.hasContent()) {
|
||||||
|
Plaintext plaintext = decrypt(envelope, envelope.getContent());
|
||||||
|
Content message = Content.parseFrom(plaintext.getData());
|
||||||
|
|
||||||
|
if (message.hasDataMessage()) {
|
||||||
|
return new SignalServiceContent(createSignalServiceMessage(plaintext.getMetadata(), message.getDataMessage()),
|
||||||
|
plaintext.getMetadata().getSender(),
|
||||||
|
plaintext.getMetadata().getSenderDevice(),
|
||||||
|
plaintext.getMetadata().getTimestamp(),
|
||||||
|
plaintext.getMetadata().isNeedsReceipt());
|
||||||
|
} else if (message.hasSyncMessage() && localAddress.matches(plaintext.getMetadata().getSender())) {
|
||||||
|
return new SignalServiceContent(createSynchronizeMessage(plaintext.getMetadata(), message.getSyncMessage()),
|
||||||
|
plaintext.getMetadata().getSender(),
|
||||||
|
plaintext.getMetadata().getSenderDevice(),
|
||||||
|
plaintext.getMetadata().getTimestamp(),
|
||||||
|
plaintext.getMetadata().isNeedsReceipt());
|
||||||
|
} else if (message.hasCallMessage()) {
|
||||||
|
return new SignalServiceContent(createCallMessage(message.getCallMessage()),
|
||||||
|
plaintext.getMetadata().getSender(),
|
||||||
|
plaintext.getMetadata().getSenderDevice(),
|
||||||
|
plaintext.getMetadata().getTimestamp(),
|
||||||
|
plaintext.getMetadata().isNeedsReceipt());
|
||||||
|
} else if (message.hasReceiptMessage()) {
|
||||||
|
return new SignalServiceContent(createReceiptMessage(plaintext.getMetadata(), message.getReceiptMessage()),
|
||||||
|
plaintext.getMetadata().getSender(),
|
||||||
|
plaintext.getMetadata().getSenderDevice(),
|
||||||
|
plaintext.getMetadata().getTimestamp(),
|
||||||
|
plaintext.getMetadata().isNeedsReceipt());
|
||||||
|
} else if (message.hasTypingMessage()) {
|
||||||
|
return new SignalServiceContent(createTypingMessage(plaintext.getMetadata(), message.getTypingMessage()),
|
||||||
|
plaintext.getMetadata().getSender(),
|
||||||
|
plaintext.getMetadata().getSenderDevice(),
|
||||||
|
plaintext.getMetadata().getTimestamp(),
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (InvalidProtocolBufferException e) {
|
||||||
|
throw new InvalidMetadataMessageException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Plaintext decrypt(SignalServiceEnvelope envelope, byte[] ciphertext)
|
||||||
|
throws InvalidMetadataMessageException, InvalidMetadataVersionException,
|
||||||
|
ProtocolDuplicateMessageException, ProtocolUntrustedIdentityException,
|
||||||
|
ProtocolLegacyMessageException, ProtocolInvalidKeyException,
|
||||||
|
ProtocolInvalidVersionException, ProtocolInvalidMessageException,
|
||||||
|
ProtocolInvalidKeyIdException, ProtocolNoSessionException,
|
||||||
|
SelfSendException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
|
||||||
|
byte[] paddedMessage;
|
||||||
|
Metadata metadata;
|
||||||
|
int sessionVersion;
|
||||||
|
|
||||||
|
if (!envelope.hasSource() && !envelope.isUnidentifiedSender()) {
|
||||||
|
throw new ProtocolInvalidMessageException(new InvalidMessageException("Non-UD envelope is missing a source!"), null, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (envelope.isPreKeySignalMessage()) {
|
||||||
|
SignalProtocolAddress sourceAddress = getPreferredProtocolAddress(signalProtocolStore, envelope.getSourceAddress(), envelope.getSourceDevice());
|
||||||
|
SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, sourceAddress);
|
||||||
|
|
||||||
|
paddedMessage = sessionCipher.decrypt(new PreKeySignalMessage(ciphertext));
|
||||||
|
metadata = new Metadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), false);
|
||||||
|
sessionVersion = sessionCipher.getSessionVersion();
|
||||||
|
} else if (envelope.isSignalMessage()) {
|
||||||
|
SignalProtocolAddress sourceAddress = getPreferredProtocolAddress(signalProtocolStore, envelope.getSourceAddress(), envelope.getSourceDevice());
|
||||||
|
SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, sourceAddress);
|
||||||
|
|
||||||
|
paddedMessage = sessionCipher.decrypt(new SignalMessage(ciphertext));
|
||||||
|
metadata = new Metadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), false);
|
||||||
|
sessionVersion = sessionCipher.getSessionVersion();
|
||||||
|
} else if (envelope.isUnidentifiedSender()) {
|
||||||
|
SealedSessionCipher sealedSessionCipher = new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1);
|
||||||
|
DecryptionResult result = sealedSessionCipher.decrypt(certificateValidator, ciphertext, envelope.getServerTimestamp());
|
||||||
|
SignalServiceAddress resultAddress = new SignalServiceAddress(UuidUtil.parse(result.getSenderUuid().orNull()), result.getSenderE164());
|
||||||
|
SignalProtocolAddress protocolAddress = getPreferredProtocolAddress(signalProtocolStore, resultAddress, result.getDeviceId());
|
||||||
|
|
||||||
|
paddedMessage = result.getPaddedMessage();
|
||||||
|
metadata = new Metadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), true);
|
||||||
|
sessionVersion = sealedSessionCipher.getSessionVersion(protocolAddress);
|
||||||
|
} else {
|
||||||
|
throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
PushTransportDetails transportDetails = new PushTransportDetails(sessionVersion);
|
||||||
|
byte[] data = transportDetails.getStrippedPaddingMessageBody(paddedMessage);
|
||||||
|
|
||||||
|
return new Plaintext(metadata, data);
|
||||||
|
} catch (DuplicateMessageException e) {
|
||||||
|
throw new ProtocolDuplicateMessageException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
|
||||||
|
} catch (LegacyMessageException e) {
|
||||||
|
throw new ProtocolLegacyMessageException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
|
||||||
|
} catch (InvalidMessageException e) {
|
||||||
|
throw new ProtocolInvalidMessageException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
|
||||||
|
} catch (InvalidKeyIdException e) {
|
||||||
|
throw new ProtocolInvalidKeyIdException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new ProtocolInvalidKeyException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
|
||||||
|
} catch (UntrustedIdentityException e) {
|
||||||
|
throw new ProtocolUntrustedIdentityException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
|
||||||
|
} catch (InvalidVersionException e) {
|
||||||
|
throw new ProtocolInvalidVersionException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
|
||||||
|
} catch (NoSessionException e) {
|
||||||
|
throw new ProtocolNoSessionException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SignalProtocolAddress getPreferredProtocolAddress(SignalProtocolStore store, SignalServiceAddress address, int sourceDevice) {
|
||||||
|
SignalProtocolAddress uuidAddress = address.getUuid().isPresent() ? new SignalProtocolAddress(address.getUuid().get().toString(), sourceDevice) : null;
|
||||||
|
SignalProtocolAddress e164Address = address.getNumber().isPresent() ? new SignalProtocolAddress(address.getNumber().get(), sourceDevice) : null;
|
||||||
|
|
||||||
|
if (uuidAddress != null && store.containsSession(uuidAddress)) {
|
||||||
|
return uuidAddress;
|
||||||
|
} else if (e164Address != null && store.containsSession(e164Address)) {
|
||||||
|
return e164Address;
|
||||||
|
} else {
|
||||||
|
return new SignalProtocolAddress(address.getLegacyIdentifier(), sourceDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SignalServiceDataMessage createSignalServiceMessage(Metadata metadata, DataMessage content)
|
||||||
|
throws ProtocolInvalidMessageException, UnsupportedDataMessageException
|
||||||
|
{
|
||||||
|
SignalServiceGroup groupInfo = createGroupInfo(content);
|
||||||
|
List<SignalServiceAttachment> attachments = new LinkedList<>();
|
||||||
|
boolean endSession = ((content.getFlags() & DataMessage.Flags.END_SESSION_VALUE ) != 0);
|
||||||
|
boolean expirationUpdate = ((content.getFlags() & DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0);
|
||||||
|
boolean profileKeyUpdate = ((content.getFlags() & DataMessage.Flags.PROFILE_KEY_UPDATE_VALUE ) != 0);
|
||||||
|
SignalServiceDataMessage.Quote quote = createQuote(content);
|
||||||
|
List<SharedContact> sharedContacts = createSharedContacts(content);
|
||||||
|
List<Preview> previews = createPreviews(content);
|
||||||
|
Sticker sticker = createSticker(content);
|
||||||
|
Reaction reaction = createReaction(content);
|
||||||
|
|
||||||
|
if (content.getRequiredProtocolVersion() > DataMessage.ProtocolVersion.CURRENT.getNumber()) {
|
||||||
|
throw new UnsupportedDataMessageException(DataMessage.ProtocolVersion.CURRENT.getNumber(),
|
||||||
|
content.getRequiredProtocolVersion(),
|
||||||
|
metadata.getSender().getIdentifier(),
|
||||||
|
metadata.getSenderDevice(),
|
||||||
|
Optional.fromNullable(groupInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (AttachmentPointer pointer : content.getAttachmentsList()) {
|
||||||
|
attachments.add(createAttachmentPointer(pointer));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.hasTimestamp() && content.getTimestamp() != metadata.getTimestamp()) {
|
||||||
|
throw new ProtocolInvalidMessageException(new InvalidMessageException("Timestamps don't match: " + content.getTimestamp() + " vs " + metadata.getTimestamp()),
|
||||||
|
metadata.getSender().getIdentifier(),
|
||||||
|
metadata.getSenderDevice());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SignalServiceDataMessage(metadata.getTimestamp(),
|
||||||
|
groupInfo,
|
||||||
|
attachments,
|
||||||
|
content.getBody(),
|
||||||
|
endSession,
|
||||||
|
content.getExpireTimer(),
|
||||||
|
expirationUpdate,
|
||||||
|
content.hasProfileKey() ? content.getProfileKey().toByteArray() : null,
|
||||||
|
profileKeyUpdate,
|
||||||
|
quote,
|
||||||
|
sharedContacts,
|
||||||
|
previews,
|
||||||
|
sticker,
|
||||||
|
content.getIsViewOnce(),
|
||||||
|
reaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SignalServiceSyncMessage createSynchronizeMessage(Metadata metadata, SyncMessage content)
|
||||||
|
throws ProtocolInvalidMessageException, ProtocolInvalidKeyException, UnsupportedDataMessageException
|
||||||
|
{
|
||||||
|
if (content.hasSent()) {
|
||||||
|
SyncMessage.Sent sentContent = content.getSent();
|
||||||
|
SignalServiceDataMessage dataMessage = createSignalServiceMessage(metadata, sentContent.getMessage());
|
||||||
|
Optional<SignalServiceAddress> address = SignalServiceAddress.isValidAddress(sentContent.getDestinationUuid(), sentContent.getDestinationE164())
|
||||||
|
? Optional.of(new SignalServiceAddress(UuidUtil.parseOrNull(sentContent.getDestinationUuid()), sentContent.getDestinationE164()))
|
||||||
|
: Optional.<SignalServiceAddress>absent();
|
||||||
|
Map<SignalServiceAddress, Boolean> unidentifiedStatuses = new HashMap<>();
|
||||||
|
|
||||||
|
if (!address.isPresent() && !dataMessage.getGroupInfo().isPresent()) {
|
||||||
|
throw new ProtocolInvalidMessageException(new InvalidMessageException("SyncMessage missing both destination and group ID!"), null, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (SyncMessage.Sent.UnidentifiedDeliveryStatus status : sentContent.getUnidentifiedStatusList()) {
|
||||||
|
if (SignalServiceAddress.isValidAddress(status.getDestinationUuid(), status.getDestinationE164())) {
|
||||||
|
SignalServiceAddress recipient = new SignalServiceAddress(UuidUtil.parseOrNull(status.getDestinationUuid()), status.getDestinationE164());
|
||||||
|
unidentifiedStatuses.put(recipient, status.getUnidentified());
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Encountered an invalid UnidentifiedDeliveryStatus in a SentTranscript! Ignoring.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SignalServiceSyncMessage.forSentTranscript(new SentTranscriptMessage(address,
|
||||||
|
sentContent.getTimestamp(),
|
||||||
|
dataMessage,
|
||||||
|
sentContent.getExpirationStartTimestamp(),
|
||||||
|
unidentifiedStatuses,
|
||||||
|
sentContent.getIsRecipientUpdate()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.hasRequest()) {
|
||||||
|
return SignalServiceSyncMessage.forRequest(new RequestMessage(content.getRequest()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.getReadList().size() > 0) {
|
||||||
|
List<ReadMessage> readMessages = new LinkedList<>();
|
||||||
|
|
||||||
|
for (SyncMessage.Read read : content.getReadList()) {
|
||||||
|
if (SignalServiceAddress.isValidAddress(read.getSenderUuid(), read.getSenderE164())) {
|
||||||
|
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(read.getSenderUuid()), read.getSenderE164());
|
||||||
|
readMessages.add(new ReadMessage(address, read.getTimestamp()));
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Encountered an invalid ReadMessage! Ignoring.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SignalServiceSyncMessage.forRead(readMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.hasViewOnceOpen()) {
|
||||||
|
if (SignalServiceAddress.isValidAddress(content.getViewOnceOpen().getSenderUuid(), content.getViewOnceOpen().getSenderE164())) {
|
||||||
|
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(content.getViewOnceOpen().getSenderUuid()), content.getViewOnceOpen().getSenderE164());
|
||||||
|
ViewOnceOpenMessage timerRead = new ViewOnceOpenMessage(address, content.getViewOnceOpen().getTimestamp());
|
||||||
|
return SignalServiceSyncMessage.forViewOnceOpen(timerRead);
|
||||||
|
} else {
|
||||||
|
throw new ProtocolInvalidMessageException(new InvalidMessageException("ViewOnceOpen message has no sender!"), null, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.hasVerified()) {
|
||||||
|
if (SignalServiceAddress.isValidAddress(content.getVerified().getDestinationUuid(), content.getVerified().getDestinationE164())) {
|
||||||
|
try {
|
||||||
|
Verified verified = content.getVerified();
|
||||||
|
SignalServiceAddress destination = new SignalServiceAddress(UuidUtil.parseOrNull(verified.getDestinationUuid()), verified.getDestinationE164());
|
||||||
|
IdentityKey identityKey = new IdentityKey(verified.getIdentityKey().toByteArray(), 0);
|
||||||
|
|
||||||
|
VerifiedState verifiedState;
|
||||||
|
|
||||||
|
if (verified.getState() == Verified.State.DEFAULT) {
|
||||||
|
verifiedState = VerifiedState.DEFAULT;
|
||||||
|
} else if (verified.getState() == Verified.State.VERIFIED) {
|
||||||
|
verifiedState = VerifiedState.VERIFIED;
|
||||||
|
} else if (verified.getState() == Verified.State.UNVERIFIED) {
|
||||||
|
verifiedState = VerifiedState.UNVERIFIED;
|
||||||
|
} else {
|
||||||
|
throw new ProtocolInvalidMessageException(new InvalidMessageException("Unknown state: " + verified.getState().getNumber()),
|
||||||
|
metadata.getSender().getIdentifier(), metadata.getSenderDevice());
|
||||||
|
}
|
||||||
|
|
||||||
|
return SignalServiceSyncMessage.forVerified(new VerifiedMessage(destination, identityKey, verifiedState, System.currentTimeMillis()));
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new ProtocolInvalidKeyException(e, metadata.getSender().getIdentifier(), metadata.getSenderDevice());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new ProtocolInvalidMessageException(new InvalidMessageException("Verified message has no sender!"), null, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.getStickerPackOperationList().size() > 0) {
|
||||||
|
List<StickerPackOperationMessage> operations = new LinkedList<>();
|
||||||
|
|
||||||
|
for (SyncMessage.StickerPackOperation operation : content.getStickerPackOperationList()) {
|
||||||
|
byte[] packId = operation.hasPackId() ? operation.getPackId().toByteArray() : null;
|
||||||
|
byte[] packKey = operation.hasPackKey() ? operation.getPackKey().toByteArray() : null;
|
||||||
|
StickerPackOperationMessage.Type type = null;
|
||||||
|
|
||||||
|
if (operation.hasType()) {
|
||||||
|
switch (operation.getType()) {
|
||||||
|
case INSTALL: type = StickerPackOperationMessage.Type.INSTALL; break;
|
||||||
|
case REMOVE: type = StickerPackOperationMessage.Type.REMOVE; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
operations.add(new StickerPackOperationMessage(packId, packKey, type));
|
||||||
|
}
|
||||||
|
|
||||||
|
return SignalServiceSyncMessage.forStickerPackOperations(operations);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.hasBlocked()) {
|
||||||
|
List<String> numbers = content.getBlocked().getNumbersList();
|
||||||
|
List<String> uuids = content.getBlocked().getUuidsList();
|
||||||
|
List<SignalServiceAddress> addresses = new ArrayList<>(numbers.size() + uuids.size());
|
||||||
|
List<byte[]> groupIds = new ArrayList<>(content.getBlocked().getGroupIdsList().size());
|
||||||
|
|
||||||
|
for (String e164 : numbers) {
|
||||||
|
Optional<SignalServiceAddress> address = SignalServiceAddress.fromRaw(null, e164);
|
||||||
|
if (address.isPresent()) {
|
||||||
|
addresses.add(address.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String uuid : uuids) {
|
||||||
|
Optional<SignalServiceAddress> address = SignalServiceAddress.fromRaw(uuid, null);
|
||||||
|
if (address.isPresent()) {
|
||||||
|
addresses.add(address.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ByteString groupId : content.getBlocked().getGroupIdsList()) {
|
||||||
|
groupIds.add(groupId.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
return SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.hasConfiguration()) {
|
||||||
|
Boolean readReceipts = content.getConfiguration().hasReadReceipts() ? content.getConfiguration().getReadReceipts() : null;
|
||||||
|
Boolean unidentifiedDeliveryIndicators = content.getConfiguration().hasUnidentifiedDeliveryIndicators() ? content.getConfiguration().getUnidentifiedDeliveryIndicators() : null;
|
||||||
|
Boolean typingIndicators = content.getConfiguration().hasTypingIndicators() ? content.getConfiguration().getTypingIndicators() : null;
|
||||||
|
Boolean linkPreviews = content.getConfiguration().hasLinkPreviews() ? content.getConfiguration().getLinkPreviews() : null;
|
||||||
|
|
||||||
|
return SignalServiceSyncMessage.forConfiguration(new ConfigurationMessage(Optional.fromNullable(readReceipts),
|
||||||
|
Optional.fromNullable(unidentifiedDeliveryIndicators),
|
||||||
|
Optional.fromNullable(typingIndicators),
|
||||||
|
Optional.fromNullable(linkPreviews)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.hasFetchLatest() && content.getFetchLatest().hasType()) {
|
||||||
|
switch (content.getFetchLatest().getType()) {
|
||||||
|
case LOCAL_PROFILE: return SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE);
|
||||||
|
case STORAGE_MANIFEST: return SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SignalServiceSyncMessage.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SignalServiceCallMessage createCallMessage(CallMessage content) {
|
||||||
|
if (content.hasOffer()) {
|
||||||
|
CallMessage.Offer offerContent = content.getOffer();
|
||||||
|
return SignalServiceCallMessage.forOffer(new OfferMessage(offerContent.getId(), offerContent.getDescription()));
|
||||||
|
} else if (content.hasAnswer()) {
|
||||||
|
CallMessage.Answer answerContent = content.getAnswer();
|
||||||
|
return SignalServiceCallMessage.forAnswer(new AnswerMessage(answerContent.getId(), answerContent.getDescription()));
|
||||||
|
} else if (content.getIceUpdateCount() > 0) {
|
||||||
|
List<IceUpdateMessage> iceUpdates = new LinkedList<>();
|
||||||
|
|
||||||
|
for (CallMessage.IceUpdate iceUpdate : content.getIceUpdateList()) {
|
||||||
|
iceUpdates.add(new IceUpdateMessage(iceUpdate.getId(), iceUpdate.getSdpMid(), iceUpdate.getSdpMLineIndex(), iceUpdate.getSdp()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return SignalServiceCallMessage.forIceUpdates(iceUpdates);
|
||||||
|
} else if (content.hasHangup()) {
|
||||||
|
CallMessage.Hangup hangup = content.getHangup();
|
||||||
|
return SignalServiceCallMessage.forHangup(new HangupMessage(hangup.getId()));
|
||||||
|
} else if (content.hasBusy()) {
|
||||||
|
CallMessage.Busy busy = content.getBusy();
|
||||||
|
return SignalServiceCallMessage.forBusy(new BusyMessage(busy.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return SignalServiceCallMessage.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SignalServiceReceiptMessage createReceiptMessage(Metadata metadata, ReceiptMessage content) {
|
||||||
|
SignalServiceReceiptMessage.Type type;
|
||||||
|
|
||||||
|
if (content.getType() == ReceiptMessage.Type.DELIVERY) type = SignalServiceReceiptMessage.Type.DELIVERY;
|
||||||
|
else if (content.getType() == ReceiptMessage.Type.READ) type = SignalServiceReceiptMessage.Type.READ;
|
||||||
|
else type = SignalServiceReceiptMessage.Type.UNKNOWN;
|
||||||
|
|
||||||
|
return new SignalServiceReceiptMessage(type, content.getTimestampList(), metadata.getTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SignalServiceTypingMessage createTypingMessage(Metadata metadata, TypingMessage content) throws ProtocolInvalidMessageException {
|
||||||
|
SignalServiceTypingMessage.Action action;
|
||||||
|
|
||||||
|
if (content.getAction() == TypingMessage.Action.STARTED) action = SignalServiceTypingMessage.Action.STARTED;
|
||||||
|
else if (content.getAction() == TypingMessage.Action.STOPPED) action = SignalServiceTypingMessage.Action.STOPPED;
|
||||||
|
else action = SignalServiceTypingMessage.Action.UNKNOWN;
|
||||||
|
|
||||||
|
if (content.hasTimestamp() && content.getTimestamp() != metadata.getTimestamp()) {
|
||||||
|
throw new ProtocolInvalidMessageException(new InvalidMessageException("Timestamps don't match: " + content.getTimestamp() + " vs " + metadata.getTimestamp()),
|
||||||
|
metadata.getSender().getIdentifier(),
|
||||||
|
metadata.getSenderDevice());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SignalServiceTypingMessage(action, content.getTimestamp(),
|
||||||
|
content.hasGroupId() ? Optional.of(content.getGroupId().toByteArray()) :
|
||||||
|
Optional.<byte[]>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SignalServiceDataMessage.Quote createQuote(DataMessage content) {
|
||||||
|
if (!content.hasQuote()) return null;
|
||||||
|
|
||||||
|
List<SignalServiceDataMessage.Quote.QuotedAttachment> attachments = new LinkedList<>();
|
||||||
|
|
||||||
|
for (DataMessage.Quote.QuotedAttachment attachment : content.getQuote().getAttachmentsList()) {
|
||||||
|
attachments.add(new SignalServiceDataMessage.Quote.QuotedAttachment(attachment.getContentType(),
|
||||||
|
attachment.getFileName(),
|
||||||
|
attachment.hasThumbnail() ? createAttachmentPointer(attachment.getThumbnail()) : null));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SignalServiceAddress.isValidAddress(content.getQuote().getAuthorUuid(), content.getQuote().getAuthorE164())) {
|
||||||
|
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(content.getQuote().getAuthorUuid()), content.getQuote().getAuthorE164());
|
||||||
|
|
||||||
|
return new SignalServiceDataMessage.Quote(content.getQuote().getId(),
|
||||||
|
address,
|
||||||
|
content.getQuote().getText(),
|
||||||
|
attachments);
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Quote was missing an author! Returning null.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Preview> createPreviews(DataMessage content) {
|
||||||
|
if (content.getPreviewCount() <= 0) return null;
|
||||||
|
|
||||||
|
List<Preview> results = new LinkedList<>();
|
||||||
|
|
||||||
|
for (DataMessage.Preview preview : content.getPreviewList()) {
|
||||||
|
SignalServiceAttachment attachment = null;
|
||||||
|
|
||||||
|
if (preview.hasImage()) {
|
||||||
|
attachment = createAttachmentPointer(preview.getImage());
|
||||||
|
}
|
||||||
|
|
||||||
|
results.add(new Preview(preview.getUrl(),
|
||||||
|
preview.getTitle(),
|
||||||
|
Optional.fromNullable(attachment)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Sticker createSticker(DataMessage content) {
|
||||||
|
if (!content.hasSticker() ||
|
||||||
|
!content.getSticker().hasPackId() ||
|
||||||
|
!content.getSticker().hasPackKey() ||
|
||||||
|
!content.getSticker().hasStickerId() ||
|
||||||
|
!content.getSticker().hasData())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
DataMessage.Sticker sticker = content.getSticker();
|
||||||
|
|
||||||
|
return new Sticker(sticker.getPackId().toByteArray(),
|
||||||
|
sticker.getPackKey().toByteArray(),
|
||||||
|
sticker.getStickerId(),
|
||||||
|
createAttachmentPointer(sticker.getData()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Reaction createReaction(DataMessage content) {
|
||||||
|
if (!content.hasReaction() ||
|
||||||
|
!content.getReaction().hasEmoji() ||
|
||||||
|
!(content.getReaction().hasTargetAuthorE164() || content.getReaction().hasTargetAuthorUuid()) ||
|
||||||
|
!content.getReaction().hasTargetSentTimestamp())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
DataMessage.Reaction reaction = content.getReaction();
|
||||||
|
|
||||||
|
return new Reaction(reaction.getEmoji(),
|
||||||
|
reaction.getRemove(),
|
||||||
|
new SignalServiceAddress(UuidUtil.parseOrNull(reaction.getTargetAuthorUuid()), reaction.getTargetAuthorE164()),
|
||||||
|
reaction.getTargetSentTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SharedContact> createSharedContacts(DataMessage content) {
|
||||||
|
if (content.getContactCount() <= 0) return null;
|
||||||
|
|
||||||
|
List<SharedContact> results = new LinkedList<>();
|
||||||
|
|
||||||
|
for (DataMessage.Contact contact : content.getContactList()) {
|
||||||
|
SharedContact.Builder builder = SharedContact.newBuilder()
|
||||||
|
.setName(SharedContact.Name.newBuilder()
|
||||||
|
.setDisplay(contact.getName().getDisplayName())
|
||||||
|
.setFamily(contact.getName().getFamilyName())
|
||||||
|
.setGiven(contact.getName().getGivenName())
|
||||||
|
.setMiddle(contact.getName().getMiddleName())
|
||||||
|
.setPrefix(contact.getName().getPrefix())
|
||||||
|
.setSuffix(contact.getName().getSuffix())
|
||||||
|
.build());
|
||||||
|
|
||||||
|
if (contact.getAddressCount() > 0) {
|
||||||
|
for (DataMessage.Contact.PostalAddress address : contact.getAddressList()) {
|
||||||
|
SharedContact.PostalAddress.Type type = SharedContact.PostalAddress.Type.HOME;
|
||||||
|
|
||||||
|
switch (address.getType()) {
|
||||||
|
case WORK: type = SharedContact.PostalAddress.Type.WORK; break;
|
||||||
|
case HOME: type = SharedContact.PostalAddress.Type.HOME; break;
|
||||||
|
case CUSTOM: type = SharedContact.PostalAddress.Type.CUSTOM; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.withAddress(SharedContact.PostalAddress.newBuilder()
|
||||||
|
.setCity(address.getCity())
|
||||||
|
.setCountry(address.getCountry())
|
||||||
|
.setLabel(address.getLabel())
|
||||||
|
.setNeighborhood(address.getNeighborhood())
|
||||||
|
.setPobox(address.getPobox())
|
||||||
|
.setPostcode(address.getPostcode())
|
||||||
|
.setRegion(address.getRegion())
|
||||||
|
.setStreet(address.getStreet())
|
||||||
|
.setType(type)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contact.getNumberCount() > 0) {
|
||||||
|
for (DataMessage.Contact.Phone phone : contact.getNumberList()) {
|
||||||
|
SharedContact.Phone.Type type = SharedContact.Phone.Type.HOME;
|
||||||
|
|
||||||
|
switch (phone.getType()) {
|
||||||
|
case HOME: type = SharedContact.Phone.Type.HOME; break;
|
||||||
|
case WORK: type = SharedContact.Phone.Type.WORK; break;
|
||||||
|
case MOBILE: type = SharedContact.Phone.Type.MOBILE; break;
|
||||||
|
case CUSTOM: type = SharedContact.Phone.Type.CUSTOM; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.withPhone(SharedContact.Phone.newBuilder()
|
||||||
|
.setLabel(phone.getLabel())
|
||||||
|
.setType(type)
|
||||||
|
.setValue(phone.getValue())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contact.getEmailCount() > 0) {
|
||||||
|
for (DataMessage.Contact.Email email : contact.getEmailList()) {
|
||||||
|
SharedContact.Email.Type type = SharedContact.Email.Type.HOME;
|
||||||
|
|
||||||
|
switch (email.getType()) {
|
||||||
|
case HOME: type = SharedContact.Email.Type.HOME; break;
|
||||||
|
case WORK: type = SharedContact.Email.Type.WORK; break;
|
||||||
|
case MOBILE: type = SharedContact.Email.Type.MOBILE; break;
|
||||||
|
case CUSTOM: type = SharedContact.Email.Type.CUSTOM; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.withEmail(SharedContact.Email.newBuilder()
|
||||||
|
.setLabel(email.getLabel())
|
||||||
|
.setType(type)
|
||||||
|
.setValue(email.getValue())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contact.hasAvatar()) {
|
||||||
|
builder.setAvatar(SharedContact.Avatar.newBuilder()
|
||||||
|
.withAttachment(createAttachmentPointer(contact.getAvatar().getAvatar()))
|
||||||
|
.withProfileFlag(contact.getAvatar().getIsProfile())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contact.hasOrganization()) {
|
||||||
|
builder.withOrganization(contact.getOrganization());
|
||||||
|
}
|
||||||
|
|
||||||
|
results.add(builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SignalServiceAttachmentPointer createAttachmentPointer(AttachmentPointer pointer) {
|
||||||
|
return new SignalServiceAttachmentPointer(pointer.getId(),
|
||||||
|
pointer.getContentType(),
|
||||||
|
pointer.getKey().toByteArray(),
|
||||||
|
pointer.hasSize() ? Optional.of(pointer.getSize()) : Optional.<Integer>absent(),
|
||||||
|
pointer.hasThumbnail() ? Optional.of(pointer.getThumbnail().toByteArray()): Optional.<byte[]>absent(),
|
||||||
|
pointer.getWidth(), pointer.getHeight(),
|
||||||
|
pointer.hasDigest() ? Optional.of(pointer.getDigest().toByteArray()) : Optional.<byte[]>absent(),
|
||||||
|
pointer.hasFileName() ? Optional.of(pointer.getFileName()) : Optional.<String>absent(),
|
||||||
|
(pointer.getFlags() & AttachmentPointer.Flags.VOICE_MESSAGE_VALUE) != 0,
|
||||||
|
pointer.hasCaption() ? Optional.of(pointer.getCaption()) : Optional.<String>absent(),
|
||||||
|
pointer.hasBlurHash() ? Optional.of(pointer.getBlurHash()) : Optional.<String>absent());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private SignalServiceGroup createGroupInfo(DataMessage content) throws ProtocolInvalidMessageException {
|
||||||
|
if (!content.hasGroup()) return null;
|
||||||
|
|
||||||
|
SignalServiceGroup.Type type;
|
||||||
|
|
||||||
|
switch (content.getGroup().getType()) {
|
||||||
|
case DELIVER: type = SignalServiceGroup.Type.DELIVER; break;
|
||||||
|
case UPDATE: type = SignalServiceGroup.Type.UPDATE; break;
|
||||||
|
case QUIT: type = SignalServiceGroup.Type.QUIT; break;
|
||||||
|
case REQUEST_INFO: type = SignalServiceGroup.Type.REQUEST_INFO; break;
|
||||||
|
default: type = SignalServiceGroup.Type.UNKNOWN; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.getGroup().getType() != DELIVER) {
|
||||||
|
String name = null;
|
||||||
|
List<SignalServiceAddress> members = null;
|
||||||
|
SignalServiceAttachmentPointer avatar = null;
|
||||||
|
|
||||||
|
if (content.getGroup().hasName()) {
|
||||||
|
name = content.getGroup().getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.getGroup().getMembersCount() > 0) {
|
||||||
|
members = new ArrayList<>(content.getGroup().getMembersCount());
|
||||||
|
|
||||||
|
for (SignalServiceProtos.GroupContext.Member member : content.getGroup().getMembersList()) {
|
||||||
|
if (SignalServiceAddress.isValidAddress(member.getUuid(), member.getE164())) {
|
||||||
|
members.add(new SignalServiceAddress(UuidUtil.parseOrNull(member.getUuid()), member.getE164()));
|
||||||
|
} else {
|
||||||
|
throw new ProtocolInvalidMessageException(new InvalidMessageException("GroupContext.Member had no address!"), null, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (content.getGroup().getMembersE164Count() > 0) {
|
||||||
|
members = new ArrayList<>(content.getGroup().getMembersE164Count());
|
||||||
|
|
||||||
|
for (String member : content.getGroup().getMembersE164List()) {
|
||||||
|
members.add(new SignalServiceAddress(null, member));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.getGroup().hasAvatar()) {
|
||||||
|
AttachmentPointer pointer = content.getGroup().getAvatar();
|
||||||
|
|
||||||
|
avatar = new SignalServiceAttachmentPointer(pointer.getId(),
|
||||||
|
pointer.getContentType(),
|
||||||
|
pointer.getKey().toByteArray(),
|
||||||
|
Optional.of(pointer.getSize()),
|
||||||
|
Optional.<byte[]>absent(), 0, 0,
|
||||||
|
Optional.fromNullable(pointer.hasDigest() ? pointer.getDigest().toByteArray() : null),
|
||||||
|
Optional.<String>absent(),
|
||||||
|
false,
|
||||||
|
Optional.<String>absent(),
|
||||||
|
Optional.<String>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SignalServiceGroup(type, content.getGroup().getId().toByteArray(), name, members, avatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SignalServiceGroup(content.getGroup().getId().toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Metadata {
|
||||||
|
private final SignalServiceAddress sender;
|
||||||
|
private final int senderDevice;
|
||||||
|
private final long timestamp;
|
||||||
|
private final boolean needsReceipt;
|
||||||
|
|
||||||
|
private Metadata(SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) {
|
||||||
|
this.sender = sender;
|
||||||
|
this.senderDevice = senderDevice;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.needsReceipt = needsReceipt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceAddress getSender() {
|
||||||
|
return sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSenderDevice() {
|
||||||
|
return senderDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNeedsReceipt() {
|
||||||
|
return needsReceipt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Plaintext {
|
||||||
|
private final Metadata metadata;
|
||||||
|
private final byte[] data;
|
||||||
|
|
||||||
|
private Plaintext(Metadata metadata, byte[] data) {
|
||||||
|
this.metadata = metadata;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Metadata getMetadata() {
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
package org.whispersystems.signalservice.api.crypto;
|
||||||
|
|
||||||
|
|
||||||
|
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
|
||||||
|
import org.signal.libsignal.metadata.certificate.SenderCertificate;
|
||||||
|
import org.whispersystems.libsignal.util.ByteUtil;
|
||||||
|
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import javax.crypto.spec.GCMParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
public class UnidentifiedAccess {
|
||||||
|
|
||||||
|
private final byte[] unidentifiedAccessKey;
|
||||||
|
private final SenderCertificate unidentifiedCertificate;
|
||||||
|
|
||||||
|
public UnidentifiedAccess(byte[] unidentifiedAccessKey, byte[] unidentifiedCertificate)
|
||||||
|
throws InvalidCertificateException
|
||||||
|
{
|
||||||
|
this.unidentifiedAccessKey = unidentifiedAccessKey;
|
||||||
|
this.unidentifiedCertificate = new SenderCertificate(unidentifiedCertificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getUnidentifiedAccessKey() {
|
||||||
|
return unidentifiedAccessKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SenderCertificate getUnidentifiedCertificate() {
|
||||||
|
return unidentifiedCertificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] deriveAccessKeyFrom(byte[] profileKey) {
|
||||||
|
try {
|
||||||
|
byte[] nonce = new byte[12];
|
||||||
|
byte[] input = new byte[16];
|
||||||
|
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(profileKey, "AES"), new GCMParameterSpec(128, nonce));
|
||||||
|
|
||||||
|
byte[] ciphertext = cipher.doFinal(input);
|
||||||
|
|
||||||
|
return ByteUtil.trim(ciphertext, 16);
|
||||||
|
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException | BadPaddingException | IllegalBlockSizeException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package org.whispersystems.signalservice.api.crypto;
|
||||||
|
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
|
public class UnidentifiedAccessPair {
|
||||||
|
|
||||||
|
private final Optional<UnidentifiedAccess> targetUnidentifiedAccess;
|
||||||
|
private final Optional<UnidentifiedAccess> selfUnidentifiedAccess;
|
||||||
|
|
||||||
|
public UnidentifiedAccessPair(UnidentifiedAccess targetUnidentifiedAccess, UnidentifiedAccess selfUnidentifiedAccess) {
|
||||||
|
this.targetUnidentifiedAccess = Optional.of(targetUnidentifiedAccess);
|
||||||
|
this.selfUnidentifiedAccess = Optional.of(selfUnidentifiedAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<UnidentifiedAccess> getTargetUnidentifiedAccess() {
|
||||||
|
return targetUnidentifiedAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<UnidentifiedAccess> getSelfUnidentifiedAccess() {
|
||||||
|
return selfUnidentifiedAccess;
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.crypto;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
|
|
||||||
|
public class UntrustedIdentityException extends Exception {
|
||||||
|
|
||||||
|
private final IdentityKey identityKey;
|
||||||
|
private final String identifier;
|
||||||
|
|
||||||
|
public UntrustedIdentityException(String s, String identifier, IdentityKey identityKey) {
|
||||||
|
super(s);
|
||||||
|
this.identifier = identifier;
|
||||||
|
this.identityKey = identityKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UntrustedIdentityException(UntrustedIdentityException e) {
|
||||||
|
this(e.getMessage(), e.getIdentifier(), e.getIdentityKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
public IdentityKey getIdentityKey() {
|
||||||
|
return identityKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIdentifier() {
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages;
|
||||||
|
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
|
public class SendMessageResult {
|
||||||
|
|
||||||
|
private final SignalServiceAddress address;
|
||||||
|
private final Success success;
|
||||||
|
private final boolean networkFailure;
|
||||||
|
private final boolean unregisteredFailure;
|
||||||
|
private final IdentityFailure identityFailure;
|
||||||
|
|
||||||
|
public static SendMessageResult success(SignalServiceAddress address, boolean unidentified, boolean needsSync) {
|
||||||
|
return new SendMessageResult(address, new Success(unidentified, needsSync), false, false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SendMessageResult networkFailure(SignalServiceAddress address) {
|
||||||
|
return new SendMessageResult(address, null, true, false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SendMessageResult unregisteredFailure(SignalServiceAddress address) {
|
||||||
|
return new SendMessageResult(address, null, false, true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SendMessageResult identityFailure(SignalServiceAddress address, IdentityKey identityKey) {
|
||||||
|
return new SendMessageResult(address, null, false, false, new IdentityFailure(identityKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceAddress getAddress() {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Success getSuccess() {
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNetworkFailure() {
|
||||||
|
return networkFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUnregisteredFailure() {
|
||||||
|
return unregisteredFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IdentityFailure getIdentityFailure() {
|
||||||
|
return identityFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SendMessageResult(SignalServiceAddress address, Success success, boolean networkFailure, boolean unregisteredFailure, IdentityFailure identityFailure) {
|
||||||
|
this.address = address;
|
||||||
|
this.success = success;
|
||||||
|
this.networkFailure = networkFailure;
|
||||||
|
this.unregisteredFailure = unregisteredFailure;
|
||||||
|
this.identityFailure = identityFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Success {
|
||||||
|
private final boolean unidentified;
|
||||||
|
private final boolean needsSync;
|
||||||
|
|
||||||
|
private Success(boolean unidentified, boolean needsSync) {
|
||||||
|
this.unidentified = unidentified;
|
||||||
|
this.needsSync = needsSync;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUnidentified() {
|
||||||
|
return unidentified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNeedsSync() {
|
||||||
|
return needsSync;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class IdentityFailure {
|
||||||
|
private final IdentityKey identityKey;
|
||||||
|
|
||||||
|
private IdentityFailure(IdentityKey identityKey) {
|
||||||
|
this.identityKey = identityKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IdentityKey getIdentityKey() {
|
||||||
|
return identityKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
+137
@@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.messages;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public abstract class SignalServiceAttachment {
|
||||||
|
|
||||||
|
private final String contentType;
|
||||||
|
|
||||||
|
protected SignalServiceAttachment(String contentType) {
|
||||||
|
this.contentType = contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContentType() {
|
||||||
|
return contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract boolean isStream();
|
||||||
|
public abstract boolean isPointer();
|
||||||
|
|
||||||
|
public SignalServiceAttachmentStream asStream() {
|
||||||
|
return (SignalServiceAttachmentStream)this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceAttachmentPointer asPointer() {
|
||||||
|
return (SignalServiceAttachmentPointer)this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder newStreamBuilder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
private InputStream inputStream;
|
||||||
|
private String contentType;
|
||||||
|
private String fileName;
|
||||||
|
private long length;
|
||||||
|
private ProgressListener listener;
|
||||||
|
private boolean voiceNote;
|
||||||
|
private int width;
|
||||||
|
private int height;
|
||||||
|
private String caption;
|
||||||
|
private String blurHash;
|
||||||
|
|
||||||
|
private Builder() {}
|
||||||
|
|
||||||
|
public Builder withStream(InputStream inputStream) {
|
||||||
|
this.inputStream = inputStream;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withContentType(String contentType) {
|
||||||
|
this.contentType = contentType;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withLength(long length) {
|
||||||
|
this.length = length;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withFileName(String fileName) {
|
||||||
|
this.fileName = fileName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withListener(ProgressListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withVoiceNote(boolean voiceNote) {
|
||||||
|
this.voiceNote = voiceNote;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withWidth(int width) {
|
||||||
|
this.width = width;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withHeight(int height) {
|
||||||
|
this.height = height;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withCaption(String caption) {
|
||||||
|
this.caption = caption;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withBlurHash(String blurHash) {
|
||||||
|
this.blurHash = blurHash;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceAttachmentStream build() {
|
||||||
|
if (inputStream == null) throw new IllegalArgumentException("Must specify stream!");
|
||||||
|
if (contentType == null) throw new IllegalArgumentException("No content type specified!");
|
||||||
|
if (length == 0) throw new IllegalArgumentException("No length specified!");
|
||||||
|
|
||||||
|
return new SignalServiceAttachmentStream(inputStream,
|
||||||
|
contentType,
|
||||||
|
length,
|
||||||
|
Optional.fromNullable(fileName),
|
||||||
|
voiceNote,
|
||||||
|
Optional.<byte[]>absent(),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
Optional.fromNullable(caption),
|
||||||
|
Optional.fromNullable(blurHash),
|
||||||
|
listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface to receive progress information on upload/download of
|
||||||
|
* an attachment.
|
||||||
|
*/
|
||||||
|
public interface ProgressListener {
|
||||||
|
/**
|
||||||
|
* Called on a progress change event.
|
||||||
|
*
|
||||||
|
* @param total The total amount to transmit/receive in bytes.
|
||||||
|
* @param progress The amount that has been transmitted/received in bytes thus far
|
||||||
|
*/
|
||||||
|
public void onAttachmentProgress(long total, long progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014-2017 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.messages;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a received SignalServiceAttachment "handle." This
|
||||||
|
* is a pointer to the actual attachment content, which needs to be
|
||||||
|
* retrieved using {@link SignalServiceMessageReceiver#retrieveAttachment(SignalServiceAttachmentPointer, java.io.File, int)}
|
||||||
|
*
|
||||||
|
* @author Moxie Marlinspike
|
||||||
|
*/
|
||||||
|
public class SignalServiceAttachmentPointer extends SignalServiceAttachment {
|
||||||
|
|
||||||
|
private final long id;
|
||||||
|
private final byte[] key;
|
||||||
|
private final Optional<Integer> size;
|
||||||
|
private final Optional<byte[]> preview;
|
||||||
|
private final Optional<byte[]> digest;
|
||||||
|
private final Optional<String> fileName;
|
||||||
|
private final boolean voiceNote;
|
||||||
|
private final int width;
|
||||||
|
private final int height;
|
||||||
|
private final Optional<String> caption;
|
||||||
|
private final Optional<String> blurHash;
|
||||||
|
|
||||||
|
public SignalServiceAttachmentPointer(long id, String contentType, byte[] key,
|
||||||
|
Optional<Integer> size, Optional<byte[]> preview,
|
||||||
|
int width, int height,
|
||||||
|
Optional<byte[]> digest, Optional<String> fileName,
|
||||||
|
boolean voiceNote, Optional<String> caption,
|
||||||
|
Optional<String> blurHash)
|
||||||
|
{
|
||||||
|
super(contentType);
|
||||||
|
this.id = id;
|
||||||
|
this.key = key;
|
||||||
|
this.size = size;
|
||||||
|
this.preview = preview;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.digest = digest;
|
||||||
|
this.fileName = fileName;
|
||||||
|
this.voiceNote = voiceNote;
|
||||||
|
this.caption = caption;
|
||||||
|
this.blurHash = blurHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isStream() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPointer() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Integer> getSize() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getFileName() {
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<byte[]> getPreview() {
|
||||||
|
return preview;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<byte[]> getDigest() {
|
||||||
|
return digest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getVoiceNote() {
|
||||||
|
return voiceNote;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getWidth() {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHeight() {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getCaption() {
|
||||||
|
return caption;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getBlurHash() {
|
||||||
|
return blurHash;
|
||||||
|
}
|
||||||
|
}
|
||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.messages;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a local SignalServiceAttachment to be sent.
|
||||||
|
*/
|
||||||
|
public class SignalServiceAttachmentStream extends SignalServiceAttachment {
|
||||||
|
|
||||||
|
private final InputStream inputStream;
|
||||||
|
private final long length;
|
||||||
|
private final Optional<String> fileName;
|
||||||
|
private final ProgressListener listener;
|
||||||
|
private final Optional<byte[]> preview;
|
||||||
|
private final boolean voiceNote;
|
||||||
|
private final int width;
|
||||||
|
private final int height;
|
||||||
|
private final Optional<String> caption;
|
||||||
|
private final Optional<String> blurHash;
|
||||||
|
|
||||||
|
public SignalServiceAttachmentStream(InputStream inputStream, String contentType, long length, Optional<String> fileName, boolean voiceNote, ProgressListener listener) {
|
||||||
|
this(inputStream, contentType, length, fileName, voiceNote, Optional.<byte[]>absent(), 0, 0, Optional.<String>absent(), Optional.<String>absent(), listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceAttachmentStream(InputStream inputStream, String contentType, long length, Optional<String> fileName, boolean voiceNote, Optional<byte[]> preview, int width, int height, Optional<String> caption, Optional<String> blurHash, ProgressListener listener) {
|
||||||
|
super(contentType);
|
||||||
|
this.inputStream = inputStream;
|
||||||
|
this.length = length;
|
||||||
|
this.fileName = fileName;
|
||||||
|
this.listener = listener;
|
||||||
|
this.voiceNote = voiceNote;
|
||||||
|
this.preview = preview;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.caption = caption;
|
||||||
|
this.blurHash = blurHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isStream() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPointer() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getInputStream() {
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLength() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getFileName() {
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProgressListener getListener() {
|
||||||
|
return listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<byte[]> getPreview() {
|
||||||
|
return preview;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getVoiceNote() {
|
||||||
|
return voiceNote;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getWidth() {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHeight() {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getCaption() {
|
||||||
|
return caption;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getBlurHash() {
|
||||||
|
return blurHash;
|
||||||
|
}
|
||||||
|
}
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.messages;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
|
public class SignalServiceContent {
|
||||||
|
|
||||||
|
private final SignalServiceAddress sender;
|
||||||
|
private final int senderDevice;
|
||||||
|
private final long timestamp;
|
||||||
|
private final boolean needsReceipt;
|
||||||
|
|
||||||
|
private final Optional<SignalServiceDataMessage> message;
|
||||||
|
private final Optional<SignalServiceSyncMessage> synchronizeMessage;
|
||||||
|
private final Optional<SignalServiceCallMessage> callMessage;
|
||||||
|
private final Optional<SignalServiceReceiptMessage> readMessage;
|
||||||
|
private final Optional<SignalServiceTypingMessage> typingMessage;
|
||||||
|
|
||||||
|
public SignalServiceContent(SignalServiceDataMessage message, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) {
|
||||||
|
this.sender = sender;
|
||||||
|
this.senderDevice = senderDevice;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.needsReceipt = needsReceipt;
|
||||||
|
|
||||||
|
this.message = Optional.fromNullable(message);
|
||||||
|
this.synchronizeMessage = Optional.absent();
|
||||||
|
this.callMessage = Optional.absent();
|
||||||
|
this.readMessage = Optional.absent();
|
||||||
|
this.typingMessage = Optional.absent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceContent(SignalServiceSyncMessage synchronizeMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) {
|
||||||
|
this.sender = sender;
|
||||||
|
this.senderDevice = senderDevice;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.needsReceipt = needsReceipt;
|
||||||
|
|
||||||
|
this.message = Optional.absent();
|
||||||
|
this.synchronizeMessage = Optional.fromNullable(synchronizeMessage);
|
||||||
|
this.callMessage = Optional.absent();
|
||||||
|
this.readMessage = Optional.absent();
|
||||||
|
this.typingMessage = Optional.absent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceContent(SignalServiceCallMessage callMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) {
|
||||||
|
this.sender = sender;
|
||||||
|
this.senderDevice = senderDevice;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.needsReceipt = needsReceipt;
|
||||||
|
|
||||||
|
this.message = Optional.absent();
|
||||||
|
this.synchronizeMessage = Optional.absent();
|
||||||
|
this.callMessage = Optional.of(callMessage);
|
||||||
|
this.readMessage = Optional.absent();
|
||||||
|
this.typingMessage = Optional.absent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceContent(SignalServiceReceiptMessage receiptMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) {
|
||||||
|
this.sender = sender;
|
||||||
|
this.senderDevice = senderDevice;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.needsReceipt = needsReceipt;
|
||||||
|
|
||||||
|
this.message = Optional.absent();
|
||||||
|
this.synchronizeMessage = Optional.absent();
|
||||||
|
this.callMessage = Optional.absent();
|
||||||
|
this.readMessage = Optional.of(receiptMessage);
|
||||||
|
this.typingMessage = Optional.absent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceContent(SignalServiceTypingMessage typingMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) {
|
||||||
|
this.sender = sender;
|
||||||
|
this.senderDevice = senderDevice;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.needsReceipt = needsReceipt;
|
||||||
|
|
||||||
|
this.message = Optional.absent();
|
||||||
|
this.synchronizeMessage = Optional.absent();
|
||||||
|
this.callMessage = Optional.absent();
|
||||||
|
this.readMessage = Optional.absent();
|
||||||
|
this.typingMessage = Optional.of(typingMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<SignalServiceDataMessage> getDataMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<SignalServiceSyncMessage> getSyncMessage() {
|
||||||
|
return synchronizeMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<SignalServiceCallMessage> getCallMessage() {
|
||||||
|
return callMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<SignalServiceReceiptMessage> getReceiptMessage() {
|
||||||
|
return readMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<SignalServiceTypingMessage> getTypingMessage() {
|
||||||
|
return typingMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceAddress getSender() {
|
||||||
|
return sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSenderDevice() {
|
||||||
|
return senderDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNeedsReceipt() {
|
||||||
|
return needsReceipt;
|
||||||
|
}
|
||||||
|
}
|
||||||
+501
@@ -0,0 +1,501 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.messages;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a decrypted Signal Service data message.
|
||||||
|
*/
|
||||||
|
public class SignalServiceDataMessage {
|
||||||
|
|
||||||
|
private final long timestamp;
|
||||||
|
private final Optional<List<SignalServiceAttachment>> attachments;
|
||||||
|
private final Optional<String> body;
|
||||||
|
private final Optional<SignalServiceGroup> group;
|
||||||
|
private final Optional<byte[]> profileKey;
|
||||||
|
private final boolean endSession;
|
||||||
|
private final boolean expirationUpdate;
|
||||||
|
private final int expiresInSeconds;
|
||||||
|
private final boolean profileKeyUpdate;
|
||||||
|
private final Optional<Quote> quote;
|
||||||
|
private final Optional<List<SharedContact>> contacts;
|
||||||
|
private final Optional<List<Preview>> previews;
|
||||||
|
private final Optional<Sticker> sticker;
|
||||||
|
private final boolean viewOnce;
|
||||||
|
private final Optional<Reaction> reaction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a SignalServiceDataMessage with a body and no attachments.
|
||||||
|
*
|
||||||
|
* @param timestamp The sent timestamp.
|
||||||
|
* @param body The message contents.
|
||||||
|
*/
|
||||||
|
public SignalServiceDataMessage(long timestamp, String body) {
|
||||||
|
this(timestamp, body, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an expiring SignalServiceDataMessage with a body and no attachments.
|
||||||
|
*
|
||||||
|
* @param timestamp The sent timestamp.
|
||||||
|
* @param body The message contents.
|
||||||
|
* @param expiresInSeconds The number of seconds in which the message should expire after having been seen.
|
||||||
|
*/
|
||||||
|
public SignalServiceDataMessage(long timestamp, String body, int expiresInSeconds) {
|
||||||
|
this(timestamp, (List<SignalServiceAttachment>)null, body, expiresInSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public SignalServiceDataMessage(final long timestamp, final SignalServiceAttachment attachment, final String body) {
|
||||||
|
this(timestamp, new LinkedList<SignalServiceAttachment>() {{add(attachment);}}, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a SignalServiceDataMessage with a body and list of attachments.
|
||||||
|
*
|
||||||
|
* @param timestamp The sent timestamp.
|
||||||
|
* @param attachments The attachments.
|
||||||
|
* @param body The message contents.
|
||||||
|
*/
|
||||||
|
public SignalServiceDataMessage(long timestamp, List<SignalServiceAttachment> attachments, String body) {
|
||||||
|
this(timestamp, attachments, body, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an expiring SignalServiceDataMessage with a body and list of attachments.
|
||||||
|
*
|
||||||
|
* @param timestamp The sent timestamp.
|
||||||
|
* @param attachments The attachments.
|
||||||
|
* @param body The message contents.
|
||||||
|
* @param expiresInSeconds The number of seconds in which the message should expire after having been seen.
|
||||||
|
*/
|
||||||
|
public SignalServiceDataMessage(long timestamp, List<SignalServiceAttachment> attachments, String body, int expiresInSeconds) {
|
||||||
|
this(timestamp, null, attachments, body, expiresInSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a SignalServiceDataMessage group message with attachments and body.
|
||||||
|
*
|
||||||
|
* @param timestamp The sent timestamp.
|
||||||
|
* @param group The group information.
|
||||||
|
* @param attachments The attachments.
|
||||||
|
* @param body The message contents.
|
||||||
|
*/
|
||||||
|
public SignalServiceDataMessage(long timestamp, SignalServiceGroup group, List<SignalServiceAttachment> attachments, String body) {
|
||||||
|
this(timestamp, group, attachments, body, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an expiring SignalServiceDataMessage group message with attachments and body.
|
||||||
|
*
|
||||||
|
* @param timestamp The sent timestamp.
|
||||||
|
* @param group The group information.
|
||||||
|
* @param attachments The attachments.
|
||||||
|
* @param body The message contents.
|
||||||
|
* @param expiresInSeconds The number of seconds in which a message should disappear after having been seen.
|
||||||
|
*/
|
||||||
|
public SignalServiceDataMessage(long timestamp, SignalServiceGroup group, List<SignalServiceAttachment> attachments, String body, int expiresInSeconds) {
|
||||||
|
this(timestamp, group, attachments, body, false, expiresInSeconds, false, null, false, null, null, null, null, false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a SignalServiceDataMessage.
|
||||||
|
*
|
||||||
|
* @param timestamp The sent timestamp.
|
||||||
|
* @param group The group information (or null if none).
|
||||||
|
* @param attachments The attachments (or null if none).
|
||||||
|
* @param body The message contents.
|
||||||
|
* @param endSession Flag indicating whether this message should close a session.
|
||||||
|
* @param expiresInSeconds Number of seconds in which the message should disappear after being seen.
|
||||||
|
*/
|
||||||
|
public SignalServiceDataMessage(long timestamp, SignalServiceGroup group,
|
||||||
|
List<SignalServiceAttachment> attachments,
|
||||||
|
String body, boolean endSession, int expiresInSeconds,
|
||||||
|
boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate,
|
||||||
|
Quote quote, List<SharedContact> sharedContacts, List<Preview> previews,
|
||||||
|
Sticker sticker, boolean viewOnce, Reaction reaction)
|
||||||
|
{
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.body = Optional.fromNullable(body);
|
||||||
|
this.group = Optional.fromNullable(group);
|
||||||
|
this.endSession = endSession;
|
||||||
|
this.expiresInSeconds = expiresInSeconds;
|
||||||
|
this.expirationUpdate = expirationUpdate;
|
||||||
|
this.profileKey = Optional.fromNullable(profileKey);
|
||||||
|
this.profileKeyUpdate = profileKeyUpdate;
|
||||||
|
this.quote = Optional.fromNullable(quote);
|
||||||
|
this.sticker = Optional.fromNullable(sticker);
|
||||||
|
this.viewOnce = viewOnce;
|
||||||
|
this.reaction = Optional.fromNullable(reaction);
|
||||||
|
|
||||||
|
if (attachments != null && !attachments.isEmpty()) {
|
||||||
|
this.attachments = Optional.of(attachments);
|
||||||
|
} else {
|
||||||
|
this.attachments = Optional.absent();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sharedContacts != null && !sharedContacts.isEmpty()) {
|
||||||
|
this.contacts = Optional.of(sharedContacts);
|
||||||
|
} else {
|
||||||
|
this.contacts = Optional.absent();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previews != null && !previews.isEmpty()) {
|
||||||
|
this.previews = Optional.of(previews);
|
||||||
|
} else {
|
||||||
|
this.previews = Optional.absent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder newBuilder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The message timestamp.
|
||||||
|
*/
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The message attachments (if any).
|
||||||
|
*/
|
||||||
|
public Optional<List<SignalServiceAttachment>> getAttachments() {
|
||||||
|
return attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The message body (if any).
|
||||||
|
*/
|
||||||
|
public Optional<String> getBody() {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The message group info (if any).
|
||||||
|
*/
|
||||||
|
public Optional<SignalServiceGroup> getGroupInfo() {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEndSession() {
|
||||||
|
return endSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExpirationUpdate() {
|
||||||
|
return expirationUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isProfileKeyUpdate() {
|
||||||
|
return profileKeyUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGroupUpdate() {
|
||||||
|
return group.isPresent() && group.get().getType() != SignalServiceGroup.Type.DELIVER;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getExpiresInSeconds() {
|
||||||
|
return expiresInSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<byte[]> getProfileKey() {
|
||||||
|
return profileKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Quote> getQuote() {
|
||||||
|
return quote;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<List<SharedContact>> getSharedContacts() {
|
||||||
|
return contacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<List<Preview>> getPreviews() {
|
||||||
|
return previews;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Sticker> getSticker() {
|
||||||
|
return sticker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isViewOnce() {
|
||||||
|
return viewOnce;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Reaction> getReaction() {
|
||||||
|
return reaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
private List<SignalServiceAttachment> attachments = new LinkedList<>();
|
||||||
|
private List<SharedContact> sharedContacts = new LinkedList<>();
|
||||||
|
private List<Preview> previews = new LinkedList<>();
|
||||||
|
|
||||||
|
private long timestamp;
|
||||||
|
private SignalServiceGroup group;
|
||||||
|
private String body;
|
||||||
|
private boolean endSession;
|
||||||
|
private int expiresInSeconds;
|
||||||
|
private boolean expirationUpdate;
|
||||||
|
private byte[] profileKey;
|
||||||
|
private boolean profileKeyUpdate;
|
||||||
|
private Quote quote;
|
||||||
|
private Sticker sticker;
|
||||||
|
private boolean viewOnce;
|
||||||
|
private Reaction reaction;
|
||||||
|
|
||||||
|
private Builder() {}
|
||||||
|
|
||||||
|
public Builder withTimestamp(long timestamp) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder asGroupMessage(SignalServiceGroup group) {
|
||||||
|
this.group = group;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withAttachment(SignalServiceAttachment attachment) {
|
||||||
|
this.attachments.add(attachment);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withAttachments(List<SignalServiceAttachment> attachments) {
|
||||||
|
this.attachments.addAll(attachments);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withBody(String body) {
|
||||||
|
this.body = body;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder asEndSessionMessage() {
|
||||||
|
return asEndSessionMessage(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder asEndSessionMessage(boolean endSession) {
|
||||||
|
this.endSession = endSession;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder asExpirationUpdate() {
|
||||||
|
return asExpirationUpdate(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder asExpirationUpdate(boolean expirationUpdate) {
|
||||||
|
this.expirationUpdate = expirationUpdate;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withExpiration(int expiresInSeconds) {
|
||||||
|
this.expiresInSeconds = expiresInSeconds;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withProfileKey(byte[] profileKey) {
|
||||||
|
this.profileKey = profileKey;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder asProfileKeyUpdate(boolean profileKeyUpdate) {
|
||||||
|
this.profileKeyUpdate = profileKeyUpdate;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withQuote(Quote quote) {
|
||||||
|
this.quote = quote;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withSharedContact(SharedContact contact) {
|
||||||
|
this.sharedContacts.add(contact);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withSharedContacts(List<SharedContact> contacts) {
|
||||||
|
this.sharedContacts.addAll(contacts);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withPreviews(List<Preview> previews) {
|
||||||
|
this.previews.addAll(previews);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withSticker(Sticker sticker) {
|
||||||
|
this.sticker = sticker;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withViewOnce(boolean viewOnce) {
|
||||||
|
this.viewOnce = viewOnce;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withReaction(Reaction reaction) {
|
||||||
|
this.reaction = reaction;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceDataMessage build() {
|
||||||
|
if (timestamp == 0) timestamp = System.currentTimeMillis();
|
||||||
|
return new SignalServiceDataMessage(timestamp, group, attachments, body, endSession,
|
||||||
|
expiresInSeconds, expirationUpdate, profileKey,
|
||||||
|
profileKeyUpdate, quote, sharedContacts, previews,
|
||||||
|
sticker, viewOnce, reaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Quote {
|
||||||
|
private final long id;
|
||||||
|
private final SignalServiceAddress author;
|
||||||
|
private final String text;
|
||||||
|
private final List<QuotedAttachment> attachments;
|
||||||
|
|
||||||
|
public Quote(long id, SignalServiceAddress author, String text, List<QuotedAttachment> attachments) {
|
||||||
|
this.id = id;
|
||||||
|
this.author = author;
|
||||||
|
this.text = text;
|
||||||
|
this.attachments = attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceAddress getAuthor() {
|
||||||
|
return author;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getText() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<QuotedAttachment> getAttachments() {
|
||||||
|
return attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class QuotedAttachment {
|
||||||
|
private final String contentType;
|
||||||
|
private final String fileName;
|
||||||
|
private final SignalServiceAttachment thumbnail;
|
||||||
|
|
||||||
|
public QuotedAttachment(String contentType, String fileName, SignalServiceAttachment thumbnail) {
|
||||||
|
this.contentType = contentType;
|
||||||
|
this.fileName = fileName;
|
||||||
|
this.thumbnail = thumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContentType() {
|
||||||
|
return contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFileName() {
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceAttachment getThumbnail() {
|
||||||
|
return thumbnail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Preview {
|
||||||
|
private final String url;
|
||||||
|
private final String title;
|
||||||
|
private final Optional<SignalServiceAttachment> image;
|
||||||
|
|
||||||
|
public Preview(String url, String title, Optional<SignalServiceAttachment> image) {
|
||||||
|
this.url = url;
|
||||||
|
this.title = title;
|
||||||
|
this.image = image;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<SignalServiceAttachment> getImage() {
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Sticker {
|
||||||
|
private final byte[] packId;
|
||||||
|
private final byte[] packKey;
|
||||||
|
private final int stickerId;
|
||||||
|
private final SignalServiceAttachment attachment;
|
||||||
|
|
||||||
|
public Sticker(byte[] packId, byte[] packKey, int stickerId, SignalServiceAttachment attachment) {
|
||||||
|
this.packId = packId;
|
||||||
|
this.packKey = packKey;
|
||||||
|
this.stickerId = stickerId;
|
||||||
|
this.attachment = attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPackId() {
|
||||||
|
return packId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPackKey() {
|
||||||
|
return packKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getStickerId() {
|
||||||
|
return stickerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceAttachment getAttachment() {
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Reaction {
|
||||||
|
private final String emoji;
|
||||||
|
private final boolean remove;
|
||||||
|
private final SignalServiceAddress targetAuthor;
|
||||||
|
private final long targetSentTimestamp;
|
||||||
|
|
||||||
|
public Reaction(String emoji, boolean remove, SignalServiceAddress targetAuthor, long targetSentTimestamp) {
|
||||||
|
this.emoji = emoji;
|
||||||
|
this.remove = remove;
|
||||||
|
this.targetAuthor = targetAuthor;
|
||||||
|
this.targetSentTimestamp = targetSentTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmoji() {
|
||||||
|
return emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRemove() {
|
||||||
|
return remove;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceAddress getTargetAuthor() {
|
||||||
|
return targetAuthor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTargetSentTimestamp() {
|
||||||
|
return targetSentTimestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+331
@@ -0,0 +1,331 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.messages;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.InvalidVersionException;
|
||||||
|
import org.whispersystems.libsignal.logging.Log;
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope;
|
||||||
|
import org.whispersystems.signalservice.internal.util.Hex;
|
||||||
|
import org.whispersystems.util.Base64;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents an encrypted Signal Service envelope.
|
||||||
|
*
|
||||||
|
* The envelope contains the wrapping information, such as the sender, the
|
||||||
|
* message timestamp, the encrypted message type, etc.
|
||||||
|
*
|
||||||
|
* @author Moxie Marlinspike
|
||||||
|
*/
|
||||||
|
public class SignalServiceEnvelope {
|
||||||
|
|
||||||
|
private static final String TAG = SignalServiceEnvelope.class.getSimpleName();
|
||||||
|
|
||||||
|
private static final int SUPPORTED_VERSION = 1;
|
||||||
|
private static final int CIPHER_KEY_SIZE = 32;
|
||||||
|
private static final int MAC_KEY_SIZE = 20;
|
||||||
|
private static final int MAC_SIZE = 10;
|
||||||
|
|
||||||
|
private static final int VERSION_OFFSET = 0;
|
||||||
|
private static final int VERSION_LENGTH = 1;
|
||||||
|
private static final int IV_OFFSET = VERSION_OFFSET + VERSION_LENGTH;
|
||||||
|
private static final int IV_LENGTH = 16;
|
||||||
|
private static final int CIPHERTEXT_OFFSET = IV_OFFSET + IV_LENGTH;
|
||||||
|
|
||||||
|
private final Envelope envelope;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an envelope from a serialized, Base64 encoded SignalServiceEnvelope, encrypted
|
||||||
|
* with a signaling key.
|
||||||
|
*
|
||||||
|
* @param message The serialized SignalServiceEnvelope, base64 encoded and encrypted.
|
||||||
|
* @param signalingKey The signaling key.
|
||||||
|
* @throws IOException
|
||||||
|
* @throws InvalidVersionException
|
||||||
|
*/
|
||||||
|
public SignalServiceEnvelope(String message, String signalingKey, boolean isSignalingKeyEncrypted)
|
||||||
|
throws IOException, InvalidVersionException
|
||||||
|
{
|
||||||
|
this(Base64.decode(message), signalingKey, isSignalingKeyEncrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an envelope from a serialized SignalServiceEnvelope, encrypted with a signaling key.
|
||||||
|
*
|
||||||
|
* @param input The serialized and (optionally) encrypted SignalServiceEnvelope.
|
||||||
|
* @param signalingKey The signaling key.
|
||||||
|
* @throws InvalidVersionException
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public SignalServiceEnvelope(byte[] input, String signalingKey, boolean isSignalingKeyEncrypted)
|
||||||
|
throws InvalidVersionException, IOException
|
||||||
|
{
|
||||||
|
if (!isSignalingKeyEncrypted) {
|
||||||
|
this.envelope = Envelope.parseFrom(input);
|
||||||
|
} else {
|
||||||
|
if (input.length < VERSION_LENGTH || input[VERSION_OFFSET] != SUPPORTED_VERSION) {
|
||||||
|
throw new InvalidVersionException("Unsupported version!");
|
||||||
|
}
|
||||||
|
|
||||||
|
SecretKeySpec cipherKey = getCipherKey(signalingKey);
|
||||||
|
SecretKeySpec macKey = getMacKey(signalingKey);
|
||||||
|
|
||||||
|
verifyMac(input, macKey);
|
||||||
|
|
||||||
|
this.envelope = Envelope.parseFrom(getPlaintext(input, cipherKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceEnvelope(int type, Optional<SignalServiceAddress> sender, int senderDevice, long timestamp, byte[] legacyMessage, byte[] content, long serverTimestamp, String uuid) {
|
||||||
|
Envelope.Builder builder = Envelope.newBuilder()
|
||||||
|
.setType(Envelope.Type.valueOf(type))
|
||||||
|
.setSourceDevice(senderDevice)
|
||||||
|
.setTimestamp(timestamp)
|
||||||
|
.setServerTimestamp(serverTimestamp);
|
||||||
|
|
||||||
|
if (sender.isPresent()) {
|
||||||
|
if (sender.get().getUuid().isPresent()) {
|
||||||
|
builder.setSourceUuid(sender.get().getUuid().get().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sender.get().getNumber().isPresent()) {
|
||||||
|
builder.setSourceE164(sender.get().getNumber().get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uuid != null) {
|
||||||
|
builder.setServerGuid(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (legacyMessage != null) builder.setLegacyMessage(ByteString.copyFrom(legacyMessage));
|
||||||
|
if (content != null) builder.setContent(ByteString.copyFrom(content));
|
||||||
|
|
||||||
|
this.envelope = builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceEnvelope(int type, long timestamp, byte[] legacyMessage, byte[] content, long serverTimestamp, String uuid) {
|
||||||
|
Envelope.Builder builder = Envelope.newBuilder()
|
||||||
|
.setType(Envelope.Type.valueOf(type))
|
||||||
|
.setTimestamp(timestamp)
|
||||||
|
.setServerTimestamp(serverTimestamp);
|
||||||
|
|
||||||
|
if (uuid != null) {
|
||||||
|
builder.setServerGuid(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (legacyMessage != null) builder.setLegacyMessage(ByteString.copyFrom(legacyMessage));
|
||||||
|
if (content != null) builder.setContent(ByteString.copyFrom(content));
|
||||||
|
|
||||||
|
this.envelope = builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUuid() {
|
||||||
|
return envelope.getServerGuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasUuid() {
|
||||||
|
return envelope.hasServerGuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return True if either a source E164 or UUID is present.
|
||||||
|
*/
|
||||||
|
public boolean hasSource() {
|
||||||
|
return envelope.hasSourceE164() || envelope.hasSourceUuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The envelope's sender as an E164 number.
|
||||||
|
*/
|
||||||
|
public Optional<String> getSourceE164() {
|
||||||
|
return Optional.fromNullable(envelope.getSourceE164());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The envelope's sender as a UUID.
|
||||||
|
*/
|
||||||
|
public Optional<String> getSourceUuid() {
|
||||||
|
return Optional.fromNullable(envelope.getSourceUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSourceIdentifier() {
|
||||||
|
return getSourceUuid().or(getSourceE164()).orNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasSourceDevice() {
|
||||||
|
return envelope.hasSourceDevice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The envelope's sender device ID.
|
||||||
|
*/
|
||||||
|
public int getSourceDevice() {
|
||||||
|
return envelope.getSourceDevice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The envelope's sender as a SignalServiceAddress.
|
||||||
|
*/
|
||||||
|
public SignalServiceAddress getSourceAddress() {
|
||||||
|
return new SignalServiceAddress(UuidUtil.parseOrNull(envelope.getSourceUuid()), envelope.getSourceE164());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The envelope content type.
|
||||||
|
*/
|
||||||
|
public int getType() {
|
||||||
|
return envelope.getType().getNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The timestamp this envelope was sent.
|
||||||
|
*/
|
||||||
|
public long getTimestamp() {
|
||||||
|
return envelope.getTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getServerTimestamp() {
|
||||||
|
return envelope.getServerTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Whether the envelope contains a SignalServiceDataMessage
|
||||||
|
*/
|
||||||
|
public boolean hasLegacyMessage() {
|
||||||
|
return envelope.hasLegacyMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The envelope's containing SignalService message.
|
||||||
|
*/
|
||||||
|
public byte[] getLegacyMessage() {
|
||||||
|
return envelope.getLegacyMessage().toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Whether the envelope contains an encrypted SignalServiceContent
|
||||||
|
*/
|
||||||
|
public boolean hasContent() {
|
||||||
|
return envelope.hasContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The envelope's encrypted SignalServiceContent.
|
||||||
|
*/
|
||||||
|
public byte[] getContent() {
|
||||||
|
return envelope.getContent().toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the containing message is a {@link org.whispersystems.libsignal.protocol.SignalMessage}
|
||||||
|
*/
|
||||||
|
public boolean isSignalMessage() {
|
||||||
|
return envelope.getType().getNumber() == Envelope.Type.CIPHERTEXT_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the containing message is a {@link org.whispersystems.libsignal.protocol.PreKeySignalMessage}
|
||||||
|
*/
|
||||||
|
public boolean isPreKeySignalMessage() {
|
||||||
|
return envelope.getType().getNumber() == Envelope.Type.PREKEY_BUNDLE_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the containing message is a delivery receipt.
|
||||||
|
*/
|
||||||
|
public boolean isReceipt() {
|
||||||
|
return envelope.getType().getNumber() == Envelope.Type.RECEIPT_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUnidentifiedSender() {
|
||||||
|
return envelope.getType().getNumber() == Envelope.Type.UNIDENTIFIED_SENDER_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getPlaintext(byte[] ciphertext, SecretKeySpec cipherKey) throws IOException {
|
||||||
|
try {
|
||||||
|
byte[] ivBytes = new byte[IV_LENGTH];
|
||||||
|
System.arraycopy(ciphertext, IV_OFFSET, ivBytes, 0, ivBytes.length);
|
||||||
|
IvParameterSpec iv = new IvParameterSpec(ivBytes);
|
||||||
|
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, cipherKey, iv);
|
||||||
|
|
||||||
|
return cipher.doFinal(ciphertext, CIPHERTEXT_OFFSET,
|
||||||
|
ciphertext.length - VERSION_LENGTH - IV_LENGTH - MAC_SIZE);
|
||||||
|
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} catch (BadPaddingException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
throw new IOException("Bad padding?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyMac(byte[] ciphertext, SecretKeySpec macKey) throws IOException {
|
||||||
|
try {
|
||||||
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
|
mac.init(macKey);
|
||||||
|
|
||||||
|
if (ciphertext.length < MAC_SIZE + 1)
|
||||||
|
throw new IOException("Invalid MAC!");
|
||||||
|
|
||||||
|
mac.update(ciphertext, 0, ciphertext.length - MAC_SIZE);
|
||||||
|
|
||||||
|
byte[] ourMacFull = mac.doFinal();
|
||||||
|
byte[] ourMacBytes = new byte[MAC_SIZE];
|
||||||
|
System.arraycopy(ourMacFull, 0, ourMacBytes, 0, ourMacBytes.length);
|
||||||
|
|
||||||
|
byte[] theirMacBytes = new byte[MAC_SIZE];
|
||||||
|
System.arraycopy(ciphertext, ciphertext.length-MAC_SIZE, theirMacBytes, 0, theirMacBytes.length);
|
||||||
|
|
||||||
|
Log.w(TAG, "Our MAC: " + Hex.toString(ourMacBytes));
|
||||||
|
Log.w(TAG, "Thr MAC: " + Hex.toString(theirMacBytes));
|
||||||
|
|
||||||
|
if (!Arrays.equals(ourMacBytes, theirMacBytes)) {
|
||||||
|
throw new IOException("Invalid MAC compare!");
|
||||||
|
}
|
||||||
|
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private SecretKeySpec getCipherKey(String signalingKey) throws IOException {
|
||||||
|
byte[] signalingKeyBytes = Base64.decode(signalingKey);
|
||||||
|
byte[] cipherKey = new byte[CIPHER_KEY_SIZE];
|
||||||
|
System.arraycopy(signalingKeyBytes, 0, cipherKey, 0, cipherKey.length);
|
||||||
|
|
||||||
|
return new SecretKeySpec(cipherKey, "AES");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private SecretKeySpec getMacKey(String signalingKey) throws IOException {
|
||||||
|
byte[] signalingKeyBytes = Base64.decode(signalingKey);
|
||||||
|
byte[] macKey = new byte[MAC_KEY_SIZE];
|
||||||
|
System.arraycopy(signalingKeyBytes, CIPHER_KEY_SIZE, macKey, 0, macKey.length);
|
||||||
|
|
||||||
|
return new SecretKeySpec(macKey, "HmacSHA256");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.messages;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group information to include in SignalServiceMessages destined to groups.
|
||||||
|
*
|
||||||
|
* This class represents a "context" that is included with Signal Service messages
|
||||||
|
* to make them group messages. There are three types of context:
|
||||||
|
*
|
||||||
|
* 1) Update -- Sent when either creating a group, or updating the properties
|
||||||
|
* of a group (such as the avatar icon, membership list, or title).
|
||||||
|
* 2) Deliver -- Sent when a message is to be delivered to an existing group.
|
||||||
|
* 3) Quit -- Sent when the sender wishes to leave an existing group.
|
||||||
|
*
|
||||||
|
* @author Moxie Marlinspike
|
||||||
|
*/
|
||||||
|
public class SignalServiceGroup {
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
UNKNOWN,
|
||||||
|
UPDATE,
|
||||||
|
DELIVER,
|
||||||
|
QUIT,
|
||||||
|
REQUEST_INFO
|
||||||
|
}
|
||||||
|
|
||||||
|
private final byte[] groupId;
|
||||||
|
private final Type type;
|
||||||
|
private final Optional<String> name;
|
||||||
|
private final Optional<List<SignalServiceAddress>> members;
|
||||||
|
private final Optional<SignalServiceAttachment> avatar;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a DELIVER group context.
|
||||||
|
* @param groupId
|
||||||
|
*/
|
||||||
|
public SignalServiceGroup(byte[] groupId) {
|
||||||
|
this(Type.DELIVER, groupId, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a group context.
|
||||||
|
* @param type The group message type (update, deliver, quit).
|
||||||
|
* @param groupId The group ID.
|
||||||
|
* @param name The group title.
|
||||||
|
* @param members The group membership list.
|
||||||
|
* @param avatar The group avatar icon.
|
||||||
|
*/
|
||||||
|
public SignalServiceGroup(Type type, byte[] groupId, String name,
|
||||||
|
List<SignalServiceAddress> members,
|
||||||
|
SignalServiceAttachment 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<SignalServiceAddress>> getMembers() {
|
||||||
|
return members;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<SignalServiceAttachment> getAvatar() {
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder newUpdateBuilder() {
|
||||||
|
return new Builder(Type.UPDATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder newBuilder(Type type) {
|
||||||
|
return new Builder(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
private Type type;
|
||||||
|
private byte[] id;
|
||||||
|
private String name;
|
||||||
|
private List<SignalServiceAddress> members;
|
||||||
|
private SignalServiceAttachment avatar;
|
||||||
|
|
||||||
|
private Builder(Type type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withId(byte[] id) {
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withMembers(List<SignalServiceAddress> members) {
|
||||||
|
this.members = members;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withAvatar(SignalServiceAttachment avatar) {
|
||||||
|
this.avatar = avatar;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceGroup build() {
|
||||||
|
if (id == null) throw new IllegalArgumentException("No group ID specified!");
|
||||||
|
|
||||||
|
if (type == Type.UPDATE && name == null && members == null && avatar == null) {
|
||||||
|
throw new IllegalArgumentException("Group update with no updates!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SignalServiceGroup(type, id, name, members, avatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SignalServiceReceiptMessage {
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
UNKNOWN, DELIVERY, READ
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Type type;
|
||||||
|
private final List<Long> timestamps;
|
||||||
|
private final long when;
|
||||||
|
|
||||||
|
public SignalServiceReceiptMessage(Type type, List<Long> timestamps, long when) {
|
||||||
|
this.type = type;
|
||||||
|
this.timestamps = timestamps;
|
||||||
|
this.when = when;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Long> getTimestamps() {
|
||||||
|
return timestamps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getWhen() {
|
||||||
|
return when;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDeliveryReceipt() {
|
||||||
|
return type == Type.DELIVERY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReadReceipt() {
|
||||||
|
return type == Type.READ;
|
||||||
|
}
|
||||||
|
}
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SignalServiceStickerManifest {
|
||||||
|
|
||||||
|
private final Optional<String> title;
|
||||||
|
private final Optional<String> author;
|
||||||
|
private final Optional<StickerInfo> cover;
|
||||||
|
private final List<StickerInfo> stickers;
|
||||||
|
|
||||||
|
public SignalServiceStickerManifest(String title, String author, StickerInfo cover, List<StickerInfo> stickers) {
|
||||||
|
this.title = Optional.of(title);
|
||||||
|
this.author = Optional.of(author);
|
||||||
|
this.cover = Optional.of(cover);
|
||||||
|
this.stickers = (stickers == null) ? Collections.<StickerInfo>emptyList() : new ArrayList<>(stickers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getAuthor() {
|
||||||
|
return author;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<StickerInfo> getCover() {
|
||||||
|
return cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<StickerInfo> getStickers() {
|
||||||
|
return stickers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class StickerInfo {
|
||||||
|
private final int id;
|
||||||
|
private final String emoji;
|
||||||
|
|
||||||
|
public StickerInfo(int id, String emoji) {
|
||||||
|
this.id = id;
|
||||||
|
this.emoji = emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmoji() {
|
||||||
|
return emoji;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
|
public class SignalServiceTypingMessage {
|
||||||
|
|
||||||
|
public enum Action {
|
||||||
|
UNKNOWN, STARTED, STOPPED
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Action action;
|
||||||
|
private final long timestamp;
|
||||||
|
private final Optional<byte[]> groupId;
|
||||||
|
|
||||||
|
public SignalServiceTypingMessage(Action action, long timestamp, Optional<byte[]> groupId) {
|
||||||
|
this.action = action;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.groupId = groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Action getAction() {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<byte[]> getGroupId() {
|
||||||
|
return groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTypingStarted() {
|
||||||
|
return action == Action.STARTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTypingStopped() {
|
||||||
|
return action == Action.STOPPED;
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages.calls;
|
||||||
|
|
||||||
|
|
||||||
|
public class AnswerMessage {
|
||||||
|
|
||||||
|
private final long id;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
public AnswerMessage(long id, String description) {
|
||||||
|
this.id = id;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages.calls;
|
||||||
|
|
||||||
|
|
||||||
|
public class BusyMessage {
|
||||||
|
|
||||||
|
private final long id;
|
||||||
|
|
||||||
|
public BusyMessage(long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages.calls;
|
||||||
|
|
||||||
|
|
||||||
|
public class HangupMessage {
|
||||||
|
|
||||||
|
private final long id;
|
||||||
|
|
||||||
|
public HangupMessage(long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages.calls;
|
||||||
|
|
||||||
|
|
||||||
|
public class IceUpdateMessage {
|
||||||
|
|
||||||
|
private final long id;
|
||||||
|
private final String sdpMid;
|
||||||
|
private final int sdpMLineIndex;
|
||||||
|
private final String sdp;
|
||||||
|
|
||||||
|
public IceUpdateMessage(long id, String sdpMid, int sdpMLineIndex, String sdp) {
|
||||||
|
this.id = id;
|
||||||
|
this.sdpMid = sdpMid;
|
||||||
|
this.sdpMLineIndex = sdpMLineIndex;
|
||||||
|
this.sdp = sdp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSdpMid() {
|
||||||
|
return sdpMid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSdpMLineIndex() {
|
||||||
|
return sdpMLineIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSdp() {
|
||||||
|
return sdp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages.calls;
|
||||||
|
|
||||||
|
|
||||||
|
public class OfferMessage {
|
||||||
|
|
||||||
|
private final long id;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
public OfferMessage(long id, String description) {
|
||||||
|
this.id = id;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages.calls;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SignalServiceCallMessage {
|
||||||
|
|
||||||
|
private final Optional<OfferMessage> offerMessage;
|
||||||
|
private final Optional<AnswerMessage> answerMessage;
|
||||||
|
private final Optional<HangupMessage> hangupMessage;
|
||||||
|
private final Optional<BusyMessage> busyMessage;
|
||||||
|
private final Optional<List<IceUpdateMessage>> iceUpdateMessages;
|
||||||
|
|
||||||
|
private SignalServiceCallMessage(Optional<OfferMessage> offerMessage,
|
||||||
|
Optional<AnswerMessage> answerMessage,
|
||||||
|
Optional<List<IceUpdateMessage>> iceUpdateMessages,
|
||||||
|
Optional<HangupMessage> hangupMessage,
|
||||||
|
Optional<BusyMessage> busyMessage)
|
||||||
|
{
|
||||||
|
this.offerMessage = offerMessage;
|
||||||
|
this.answerMessage = answerMessage;
|
||||||
|
this.iceUpdateMessages = iceUpdateMessages;
|
||||||
|
this.hangupMessage = hangupMessage;
|
||||||
|
this.busyMessage = busyMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceCallMessage forOffer(OfferMessage offerMessage) {
|
||||||
|
return new SignalServiceCallMessage(Optional.of(offerMessage),
|
||||||
|
Optional.<AnswerMessage>absent(),
|
||||||
|
Optional.<List<IceUpdateMessage>>absent(),
|
||||||
|
Optional.<HangupMessage>absent(),
|
||||||
|
Optional.<BusyMessage>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceCallMessage forAnswer(AnswerMessage answerMessage) {
|
||||||
|
return new SignalServiceCallMessage(Optional.<OfferMessage>absent(),
|
||||||
|
Optional.of(answerMessage),
|
||||||
|
Optional.<List<IceUpdateMessage>>absent(),
|
||||||
|
Optional.<HangupMessage>absent(),
|
||||||
|
Optional.<BusyMessage>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceCallMessage forIceUpdates(List<IceUpdateMessage> iceUpdateMessages) {
|
||||||
|
return new SignalServiceCallMessage(Optional.<OfferMessage>absent(),
|
||||||
|
Optional.<AnswerMessage>absent(),
|
||||||
|
Optional.of(iceUpdateMessages),
|
||||||
|
Optional.<HangupMessage>absent(),
|
||||||
|
Optional.<BusyMessage>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceCallMessage forIceUpdate(final IceUpdateMessage iceUpdateMessage) {
|
||||||
|
List<IceUpdateMessage> iceUpdateMessages = new LinkedList<>();
|
||||||
|
iceUpdateMessages.add(iceUpdateMessage);
|
||||||
|
|
||||||
|
return new SignalServiceCallMessage(Optional.<OfferMessage>absent(),
|
||||||
|
Optional.<AnswerMessage>absent(),
|
||||||
|
Optional.of(iceUpdateMessages),
|
||||||
|
Optional.<HangupMessage>absent(),
|
||||||
|
Optional.<BusyMessage>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceCallMessage forHangup(HangupMessage hangupMessage) {
|
||||||
|
return new SignalServiceCallMessage(Optional.<OfferMessage>absent(),
|
||||||
|
Optional.<AnswerMessage>absent(),
|
||||||
|
Optional.<List<IceUpdateMessage>>absent(),
|
||||||
|
Optional.of(hangupMessage),
|
||||||
|
Optional.<BusyMessage>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceCallMessage forBusy(BusyMessage busyMessage) {
|
||||||
|
return new SignalServiceCallMessage(Optional.<OfferMessage>absent(),
|
||||||
|
Optional.<AnswerMessage>absent(),
|
||||||
|
Optional.<List<IceUpdateMessage>>absent(),
|
||||||
|
Optional.<HangupMessage>absent(),
|
||||||
|
Optional.of(busyMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static SignalServiceCallMessage empty() {
|
||||||
|
return new SignalServiceCallMessage(Optional.<OfferMessage>absent(),
|
||||||
|
Optional.<AnswerMessage>absent(),
|
||||||
|
Optional.<List<IceUpdateMessage>>absent(),
|
||||||
|
Optional.<HangupMessage>absent(),
|
||||||
|
Optional.<BusyMessage>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<List<IceUpdateMessage>> getIceUpdateMessages() {
|
||||||
|
return iceUpdateMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<AnswerMessage> getAnswerMessage() {
|
||||||
|
return answerMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<OfferMessage> getOfferMessage() {
|
||||||
|
return offerMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<HangupMessage> getHangupMessage() {
|
||||||
|
return hangupMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<BusyMessage> getBusyMessage() {
|
||||||
|
return busyMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages.calls;
|
||||||
|
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TurnServerInfo {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<String> urls;
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getUrls() {
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class BlockedListMessage {
|
||||||
|
|
||||||
|
private final List<SignalServiceAddress> addresses;
|
||||||
|
private final List<byte[]> groupIds;
|
||||||
|
|
||||||
|
public BlockedListMessage(List<SignalServiceAddress> addresses, List<byte[]> groupIds) {
|
||||||
|
this.addresses = addresses;
|
||||||
|
this.groupIds = groupIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SignalServiceAddress> getAddresses() {
|
||||||
|
return addresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<byte[]> getGroupIds() {
|
||||||
|
return groupIds;
|
||||||
|
}
|
||||||
|
}
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
import java.io.FilterInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public class ChunkedInputStream {
|
||||||
|
|
||||||
|
protected final InputStream in;
|
||||||
|
|
||||||
|
public ChunkedInputStream(InputStream in) {
|
||||||
|
this.in = in;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int readRawVarint32() throws IOException {
|
||||||
|
byte tmp = (byte)in.read();
|
||||||
|
if (tmp >= 0) {
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
int result = tmp & 0x7f;
|
||||||
|
if ((tmp = (byte)in.read()) >= 0) {
|
||||||
|
result |= tmp << 7;
|
||||||
|
} else {
|
||||||
|
result |= (tmp & 0x7f) << 7;
|
||||||
|
if ((tmp = (byte)in.read()) >= 0) {
|
||||||
|
result |= tmp << 14;
|
||||||
|
} else {
|
||||||
|
result |= (tmp & 0x7f) << 14;
|
||||||
|
if ((tmp = (byte)in.read()) >= 0) {
|
||||||
|
result |= tmp << 21;
|
||||||
|
} else {
|
||||||
|
result |= (tmp & 0x7f) << 21;
|
||||||
|
result |= (tmp = (byte)in.read()) << 28;
|
||||||
|
if (tmp < 0) {
|
||||||
|
// Discard upper 32 bits.
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
if ((byte)in.read() >= 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IOException("Malformed varint!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static final class LimitedInputStream extends FilterInputStream {
|
||||||
|
|
||||||
|
private long left;
|
||||||
|
private long mark = -1;
|
||||||
|
|
||||||
|
LimitedInputStream(InputStream in, long limit) {
|
||||||
|
super(in);
|
||||||
|
left = limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public int available() throws IOException {
|
||||||
|
return (int) Math.min(in.available(), left);
|
||||||
|
}
|
||||||
|
|
||||||
|
// it's okay to mark even if mark isn't supported, as reset won't work
|
||||||
|
@Override public synchronized void mark(int readLimit) {
|
||||||
|
in.mark(readLimit);
|
||||||
|
mark = left;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public int read() throws IOException {
|
||||||
|
if (left == 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int result = in.read();
|
||||||
|
if (result != -1) {
|
||||||
|
--left;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public int read(byte[] b, int off, int len) throws IOException {
|
||||||
|
if (left == 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
len = (int) Math.min(len, left);
|
||||||
|
int result = in.read(b, off, len);
|
||||||
|
if (result != -1) {
|
||||||
|
left -= result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public synchronized void reset() throws IOException {
|
||||||
|
if (!in.markSupported()) {
|
||||||
|
throw new IOException("Mark not supported");
|
||||||
|
}
|
||||||
|
if (mark == -1) {
|
||||||
|
throw new IOException("Mark not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
in.reset();
|
||||||
|
left = mark;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public long skip(long n) throws IOException {
|
||||||
|
n = Math.min(n, left);
|
||||||
|
long skipped = in.skip(n);
|
||||||
|
left -= skipped;
|
||||||
|
return skipped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public class ChunkedOutputStream {
|
||||||
|
|
||||||
|
protected final OutputStream out;
|
||||||
|
|
||||||
|
public ChunkedOutputStream(OutputStream out) {
|
||||||
|
this.out = out;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void writeVarint32(int value) throws IOException {
|
||||||
|
while (true) {
|
||||||
|
if ((value & ~0x7F) == 0) {
|
||||||
|
out.write(value);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
out.write((value & 0x7F) | 0x80);
|
||||||
|
value >>>= 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void writeStream(InputStream in) throws IOException {
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int read;
|
||||||
|
|
||||||
|
while ((read = in.read(buffer)) != -1) {
|
||||||
|
out.write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
|
||||||
|
in.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
|
public class ConfigurationMessage {
|
||||||
|
|
||||||
|
private final Optional<Boolean> readReceipts;
|
||||||
|
private final Optional<Boolean> unidentifiedDeliveryIndicators;
|
||||||
|
private final Optional<Boolean> typingIndicators;
|
||||||
|
private final Optional<Boolean> linkPreviews;
|
||||||
|
|
||||||
|
public ConfigurationMessage(Optional<Boolean> readReceipts,
|
||||||
|
Optional<Boolean> unidentifiedDeliveryIndicators,
|
||||||
|
Optional<Boolean> typingIndicators,
|
||||||
|
Optional<Boolean> linkPreviews)
|
||||||
|
{
|
||||||
|
this.readReceipts = readReceipts;
|
||||||
|
this.unidentifiedDeliveryIndicators = unidentifiedDeliveryIndicators;
|
||||||
|
this.typingIndicators = typingIndicators;
|
||||||
|
this.linkPreviews = linkPreviews;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Boolean> getReadReceipts() {
|
||||||
|
return readReceipts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Boolean> getUnidentifiedDeliveryIndicators() {
|
||||||
|
return unidentifiedDeliveryIndicators;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Boolean> getTypingIndicators() {
|
||||||
|
return typingIndicators;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Boolean> getLinkPreviews() {
|
||||||
|
return linkPreviews;
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||||
|
|
||||||
|
public class ContactsMessage {
|
||||||
|
|
||||||
|
private final SignalServiceAttachment contacts;
|
||||||
|
private final boolean complete;
|
||||||
|
|
||||||
|
public ContactsMessage(SignalServiceAttachment contacts, boolean complete) {
|
||||||
|
this.contacts = contacts;
|
||||||
|
this.complete = complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceAttachment getContactsStream() {
|
||||||
|
return contacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isComplete() {
|
||||||
|
return complete;
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
|
public class DeviceContact {
|
||||||
|
|
||||||
|
private final SignalServiceAddress address;
|
||||||
|
private final Optional<String> name;
|
||||||
|
private final Optional<SignalServiceAttachmentStream> avatar;
|
||||||
|
private final Optional<String> color;
|
||||||
|
private final Optional<VerifiedMessage> verified;
|
||||||
|
private final Optional<byte[]> profileKey;
|
||||||
|
private final boolean blocked;
|
||||||
|
private final Optional<Integer> expirationTimer;
|
||||||
|
|
||||||
|
public DeviceContact(SignalServiceAddress address, Optional<String> name,
|
||||||
|
Optional<SignalServiceAttachmentStream> avatar,
|
||||||
|
Optional<String> color,
|
||||||
|
Optional<VerifiedMessage> verified,
|
||||||
|
Optional<byte[]> profileKey,
|
||||||
|
boolean blocked,
|
||||||
|
Optional<Integer> expirationTimer)
|
||||||
|
{
|
||||||
|
this.address = address;
|
||||||
|
this.name = name;
|
||||||
|
this.avatar = avatar;
|
||||||
|
this.color = color;
|
||||||
|
this.verified = verified;
|
||||||
|
this.profileKey = profileKey;
|
||||||
|
this.blocked = blocked;
|
||||||
|
this.expirationTimer = expirationTimer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<SignalServiceAttachmentStream> getAvatar() {
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceAddress getAddress() {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getColor() {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<VerifiedMessage> getVerified() {
|
||||||
|
return verified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<byte[]> getProfileKey() {
|
||||||
|
return profileKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBlocked() {
|
||||||
|
return blocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Integer> getExpirationTimer() {
|
||||||
|
return expirationTimer;
|
||||||
|
}
|
||||||
|
}
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014-2018 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
|
import org.whispersystems.libsignal.InvalidKeyException;
|
||||||
|
import org.whispersystems.libsignal.InvalidMessageException;
|
||||||
|
import org.whispersystems.libsignal.logging.Log;
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||||
|
import org.whispersystems.signalservice.internal.util.Util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public class DeviceContactsInputStream extends ChunkedInputStream {
|
||||||
|
|
||||||
|
private static final String TAG = DeviceContactsInputStream.class.getSimpleName();
|
||||||
|
|
||||||
|
public DeviceContactsInputStream(InputStream in) {
|
||||||
|
super(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeviceContact read() throws IOException {
|
||||||
|
long detailsLength = readRawVarint32();
|
||||||
|
byte[] detailsSerialized = new byte[(int)detailsLength];
|
||||||
|
Util.readFully(in, detailsSerialized);
|
||||||
|
|
||||||
|
SignalServiceProtos.ContactDetails details = SignalServiceProtos.ContactDetails.parseFrom(detailsSerialized);
|
||||||
|
|
||||||
|
if (!SignalServiceAddress.isValidAddress(details.getUuid(), details.getNumber())) {
|
||||||
|
throw new IOException("Missing contact address!");
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(details.getUuid()), details.getNumber());
|
||||||
|
Optional<String> name = Optional.fromNullable(details.getName());
|
||||||
|
Optional<SignalServiceAttachmentStream> avatar = Optional.absent();
|
||||||
|
Optional<String> color = details.hasColor() ? Optional.of(details.getColor()) : Optional.<String>absent();
|
||||||
|
Optional<VerifiedMessage> verified = Optional.absent();
|
||||||
|
Optional<byte[]> profileKey = Optional.absent();
|
||||||
|
boolean blocked = false;
|
||||||
|
Optional<Integer> expireTimer = Optional.absent();
|
||||||
|
|
||||||
|
if (details.hasAvatar()) {
|
||||||
|
long avatarLength = details.getAvatar().getLength();
|
||||||
|
InputStream avatarStream = new LimitedInputStream(in, avatarLength);
|
||||||
|
String avatarContentType = details.getAvatar().getContentType();
|
||||||
|
|
||||||
|
avatar = Optional.of(new SignalServiceAttachmentStream(avatarStream, avatarContentType, avatarLength, Optional.<String>absent(), false, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.hasVerified()) {
|
||||||
|
try {
|
||||||
|
if (!SignalServiceAddress.isValidAddress(details.getVerified().getDestinationUuid(), details.getVerified().getDestinationE164())) {
|
||||||
|
throw new InvalidMessageException("Missing Verified address!");
|
||||||
|
}
|
||||||
|
IdentityKey identityKey = new IdentityKey(details.getVerified().getIdentityKey().toByteArray(), 0);
|
||||||
|
SignalServiceAddress destination = new SignalServiceAddress(UuidUtil.parseOrNull(details.getVerified().getDestinationUuid()),
|
||||||
|
details.getVerified().getDestinationE164());
|
||||||
|
|
||||||
|
VerifiedMessage.VerifiedState state;
|
||||||
|
|
||||||
|
switch (details.getVerified().getState()) {
|
||||||
|
case VERIFIED: state = VerifiedMessage.VerifiedState.VERIFIED; break;
|
||||||
|
case UNVERIFIED:state = VerifiedMessage.VerifiedState.UNVERIFIED; break;
|
||||||
|
case DEFAULT: state = VerifiedMessage.VerifiedState.DEFAULT; break;
|
||||||
|
default: throw new InvalidMessageException("Unknown state: " + details.getVerified().getState());
|
||||||
|
}
|
||||||
|
|
||||||
|
verified = Optional.of(new VerifiedMessage(destination, identityKey, state, System.currentTimeMillis()));
|
||||||
|
} catch (InvalidKeyException | InvalidMessageException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
verified = Optional.absent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.hasProfileKey()) {
|
||||||
|
profileKey = Optional.fromNullable(details.getProfileKey().toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.hasExpireTimer() && details.getExpireTimer() > 0) {
|
||||||
|
expireTimer = Optional.of(details.getExpireTimer());
|
||||||
|
}
|
||||||
|
|
||||||
|
blocked = details.getBlocked();
|
||||||
|
|
||||||
|
return new DeviceContact(address, name, avatar, color, verified, profileKey, blocked, expireTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014-2018 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public class DeviceContactsOutputStream extends ChunkedOutputStream {
|
||||||
|
|
||||||
|
public DeviceContactsOutputStream(OutputStream out) {
|
||||||
|
super(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void write(DeviceContact contact) throws IOException {
|
||||||
|
writeContactDetails(contact);
|
||||||
|
writeAvatarImage(contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() throws IOException {
|
||||||
|
out.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeAvatarImage(DeviceContact contact) throws IOException {
|
||||||
|
if (contact.getAvatar().isPresent()) {
|
||||||
|
writeStream(contact.getAvatar().get().getInputStream());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeContactDetails(DeviceContact contact) throws IOException {
|
||||||
|
SignalServiceProtos.ContactDetails.Builder contactDetails = SignalServiceProtos.ContactDetails.newBuilder();
|
||||||
|
|
||||||
|
if (contact.getAddress().getUuid().isPresent()) {
|
||||||
|
contactDetails.setUuid(contact.getAddress().getUuid().get().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contact.getAddress().getNumber().isPresent()) {
|
||||||
|
contactDetails.setNumber(contact.getAddress().getNumber().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contact.getName().isPresent()) {
|
||||||
|
contactDetails.setName(contact.getName().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contact.getAvatar().isPresent()) {
|
||||||
|
SignalServiceProtos.ContactDetails.Avatar.Builder avatarBuilder = SignalServiceProtos.ContactDetails.Avatar.newBuilder();
|
||||||
|
avatarBuilder.setContentType(contact.getAvatar().get().getContentType());
|
||||||
|
avatarBuilder.setLength((int)contact.getAvatar().get().getLength());
|
||||||
|
contactDetails.setAvatar(avatarBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contact.getColor().isPresent()) {
|
||||||
|
contactDetails.setColor(contact.getColor().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contact.getVerified().isPresent()) {
|
||||||
|
SignalServiceProtos.Verified.State state;
|
||||||
|
|
||||||
|
switch (contact.getVerified().get().getVerified()) {
|
||||||
|
case VERIFIED: state = SignalServiceProtos.Verified.State.VERIFIED; break;
|
||||||
|
case UNVERIFIED: state = SignalServiceProtos.Verified.State.UNVERIFIED; break;
|
||||||
|
default: state = SignalServiceProtos.Verified.State.DEFAULT; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalServiceProtos.Verified.Builder verifiedBuilder = SignalServiceProtos.Verified.newBuilder()
|
||||||
|
.setIdentityKey(ByteString.copyFrom(contact.getVerified().get().getIdentityKey().serialize()))
|
||||||
|
.setState(state);
|
||||||
|
|
||||||
|
if (contact.getVerified().get().getDestination().getUuid().isPresent()) {
|
||||||
|
verifiedBuilder.setDestinationUuid(contact.getVerified().get().getDestination().getUuid().get().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contact.getVerified().get().getDestination().getNumber().isPresent()) {
|
||||||
|
verifiedBuilder.setDestinationE164(contact.getVerified().get().getDestination().getNumber().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
contactDetails.setVerified(verifiedBuilder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contact.getProfileKey().isPresent()) {
|
||||||
|
contactDetails.setProfileKey(ByteString.copyFrom(contact.getProfileKey().get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contact.getExpirationTimer().isPresent()) {
|
||||||
|
contactDetails.setExpireTimer(contact.getExpirationTimer().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
contactDetails.setBlocked(contact.isBlocked());
|
||||||
|
|
||||||
|
byte[] serializedContactDetails = contactDetails.build().toByteArray();
|
||||||
|
|
||||||
|
writeVarint32(serializedContactDetails.length);
|
||||||
|
out.write(serializedContactDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class DeviceGroup {
|
||||||
|
|
||||||
|
private final byte[] id;
|
||||||
|
private final Optional<String> name;
|
||||||
|
private final List<SignalServiceAddress> members;
|
||||||
|
private final Optional<SignalServiceAttachmentStream> avatar;
|
||||||
|
private final boolean active;
|
||||||
|
private final Optional<Integer> expirationTimer;
|
||||||
|
private final Optional<String> color;
|
||||||
|
private final boolean blocked;
|
||||||
|
|
||||||
|
public DeviceGroup(byte[] id, Optional<String> name, List<SignalServiceAddress> members,
|
||||||
|
Optional<SignalServiceAttachmentStream> avatar,
|
||||||
|
boolean active, Optional<Integer> expirationTimer,
|
||||||
|
Optional<String> color, boolean blocked)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.members = members;
|
||||||
|
this.avatar = avatar;
|
||||||
|
this.active = active;
|
||||||
|
this.expirationTimer = expirationTimer;
|
||||||
|
this.color = color;
|
||||||
|
this.blocked = blocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<SignalServiceAttachmentStream> getAvatar() {
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SignalServiceAddress> getMembers() {
|
||||||
|
return members;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActive() {
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Integer> getExpirationTimer() {
|
||||||
|
return expirationTimer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getColor() {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBlocked() {
|
||||||
|
return blocked;
|
||||||
|
}
|
||||||
|
}
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014-2018 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupDetails;
|
||||||
|
import org.whispersystems.signalservice.internal.util.Util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class DeviceGroupsInputStream extends ChunkedInputStream{
|
||||||
|
|
||||||
|
public DeviceGroupsInputStream(InputStream in) {
|
||||||
|
super(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeviceGroup read() throws IOException {
|
||||||
|
long detailsLength = readRawVarint32();
|
||||||
|
byte[] detailsSerialized = new byte[(int)detailsLength];
|
||||||
|
Util.readFully(in, detailsSerialized);
|
||||||
|
|
||||||
|
GroupDetails details = GroupDetails.parseFrom(detailsSerialized);
|
||||||
|
|
||||||
|
if (!details.hasId()) {
|
||||||
|
throw new IOException("ID missing on group record!");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] id = details.getId().toByteArray();
|
||||||
|
Optional<String> name = Optional.fromNullable(details.getName());
|
||||||
|
List<GroupDetails.Member> members = details.getMembersList();
|
||||||
|
Optional<SignalServiceAttachmentStream> avatar = Optional.absent();
|
||||||
|
boolean active = details.getActive();
|
||||||
|
Optional<Integer> expirationTimer = Optional.absent();
|
||||||
|
Optional<String> color = Optional.fromNullable(details.getColor());
|
||||||
|
boolean blocked = details.getBlocked();
|
||||||
|
|
||||||
|
if (details.hasAvatar()) {
|
||||||
|
long avatarLength = details.getAvatar().getLength();
|
||||||
|
InputStream avatarStream = new ChunkedInputStream.LimitedInputStream(in, avatarLength);
|
||||||
|
String avatarContentType = details.getAvatar().getContentType();
|
||||||
|
|
||||||
|
avatar = Optional.of(new SignalServiceAttachmentStream(avatarStream, avatarContentType, avatarLength, Optional.<String>absent(), false, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.hasExpireTimer() && details.getExpireTimer() > 0) {
|
||||||
|
expirationTimer = Optional.of(details.getExpireTimer());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SignalServiceAddress> addressMembers = new ArrayList<>(members.size());
|
||||||
|
for (GroupDetails.Member member : members) {
|
||||||
|
if (SignalServiceAddress.isValidAddress(member.getUuid(), member.getE164())) {
|
||||||
|
addressMembers.add(new SignalServiceAddress(UuidUtil.parseOrNull(member.getUuid()), member.getE164()));
|
||||||
|
} else {
|
||||||
|
throw new IOException("Missing group member address!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DeviceGroup(id, name, addressMembers, avatar, active, expirationTimer, color, blocked);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupDetails;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class DeviceGroupsOutputStream extends ChunkedOutputStream {
|
||||||
|
|
||||||
|
public DeviceGroupsOutputStream(OutputStream out) {
|
||||||
|
super(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void write(DeviceGroup group) throws IOException {
|
||||||
|
writeGroupDetails(group);
|
||||||
|
writeAvatarImage(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() throws IOException {
|
||||||
|
out.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeAvatarImage(DeviceGroup contact) throws IOException {
|
||||||
|
if (contact.getAvatar().isPresent()) {
|
||||||
|
writeStream(contact.getAvatar().get().getInputStream());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeGroupDetails(DeviceGroup group) throws IOException {
|
||||||
|
GroupDetails.Builder groupDetails = GroupDetails.newBuilder();
|
||||||
|
groupDetails.setId(ByteString.copyFrom(group.getId()));
|
||||||
|
|
||||||
|
if (group.getName().isPresent()) {
|
||||||
|
groupDetails.setName(group.getName().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.getAvatar().isPresent()) {
|
||||||
|
GroupDetails.Avatar.Builder avatarBuilder = GroupDetails.Avatar.newBuilder();
|
||||||
|
avatarBuilder.setContentType(group.getAvatar().get().getContentType());
|
||||||
|
avatarBuilder.setLength((int)group.getAvatar().get().getLength());
|
||||||
|
groupDetails.setAvatar(avatarBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.getExpirationTimer().isPresent()) {
|
||||||
|
groupDetails.setExpireTimer(group.getExpirationTimer().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.getColor().isPresent()) {
|
||||||
|
groupDetails.setColor(group.getColor().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GroupDetails.Member> members = new ArrayList<>(group.getMembers().size());
|
||||||
|
List<String> membersE164 = new ArrayList<>(group.getMembers().size());
|
||||||
|
|
||||||
|
for (SignalServiceAddress address : group.getMembers()) {
|
||||||
|
GroupDetails.Member.Builder builder = GroupDetails.Member.newBuilder();
|
||||||
|
|
||||||
|
if (address.getUuid().isPresent()) {
|
||||||
|
builder.setUuid(address.getUuid().get().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address.getNumber().isPresent()) {
|
||||||
|
builder.setE164(address.getNumber().get());
|
||||||
|
membersE164.add(address.getNumber().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
members.add(builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
groupDetails.addAllMembers(members);
|
||||||
|
groupDetails.addAllMembersE164(membersE164);
|
||||||
|
groupDetails.setActive(group.isActive());
|
||||||
|
groupDetails.setBlocked(group.isBlocked());
|
||||||
|
|
||||||
|
byte[] serializedContactDetails = groupDetails.build().toByteArray();
|
||||||
|
|
||||||
|
writeVarint32(serializedContactDetails.length);
|
||||||
|
out.write(serializedContactDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class DeviceInfo {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long created;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long lastSeen;
|
||||||
|
|
||||||
|
public DeviceInfo() {}
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCreated() {
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastSeen() {
|
||||||
|
return lastSeen;
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
|
public class KeysMessage {
|
||||||
|
|
||||||
|
private final Optional<byte[]> storageService;
|
||||||
|
|
||||||
|
public KeysMessage(Optional<byte[]> storageService) {
|
||||||
|
this.storageService = storageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<byte[]> getStorageService() {
|
||||||
|
return storageService;
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
|
public class ReadMessage {
|
||||||
|
|
||||||
|
private final SignalServiceAddress sender;
|
||||||
|
private final long timestamp;
|
||||||
|
|
||||||
|
public ReadMessage(SignalServiceAddress sender, long timestamp) {
|
||||||
|
this.sender = sender;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceAddress getSender() {
|
||||||
|
return sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.Request;
|
||||||
|
|
||||||
|
public class RequestMessage {
|
||||||
|
|
||||||
|
private final Request request;
|
||||||
|
|
||||||
|
public RequestMessage(Request request) {
|
||||||
|
this.request = request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isContactsRequest() {
|
||||||
|
return request.getType() == Request.Type.CONTACTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGroupsRequest() {
|
||||||
|
return request.getType() == Request.Type.GROUPS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBlockedListRequest() {
|
||||||
|
return request.getType() == Request.Type.BLOCKED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isConfigurationRequest() {
|
||||||
|
return request.getType() == Request.Type.CONFIGURATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isKeysRequest() {
|
||||||
|
return request.getType() == Request.Type.KEYS;
|
||||||
|
}
|
||||||
|
}
|
||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class SentTranscriptMessage {
|
||||||
|
|
||||||
|
private final Optional<SignalServiceAddress> destination;
|
||||||
|
private final long timestamp;
|
||||||
|
private final long expirationStartTimestamp;
|
||||||
|
private final SignalServiceDataMessage message;
|
||||||
|
private final Map<String, Boolean> unidentifiedStatusByUuid;
|
||||||
|
private final Map<String, Boolean> unidentifiedStatusByE164;
|
||||||
|
private final Set<SignalServiceAddress> recipients;
|
||||||
|
private final boolean isRecipientUpdate;
|
||||||
|
|
||||||
|
public SentTranscriptMessage(Optional<SignalServiceAddress> destination, long timestamp, SignalServiceDataMessage message,
|
||||||
|
long expirationStartTimestamp, Map<SignalServiceAddress, Boolean> unidentifiedStatus,
|
||||||
|
boolean isRecipientUpdate)
|
||||||
|
{
|
||||||
|
this.destination = destination;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.message = message;
|
||||||
|
this.expirationStartTimestamp = expirationStartTimestamp;
|
||||||
|
this.unidentifiedStatusByUuid = new HashMap<>();
|
||||||
|
this.unidentifiedStatusByE164 = new HashMap<>();
|
||||||
|
this.recipients = unidentifiedStatus.keySet();
|
||||||
|
this.isRecipientUpdate = isRecipientUpdate;
|
||||||
|
|
||||||
|
for (Map.Entry<SignalServiceAddress, Boolean> entry : unidentifiedStatus.entrySet()) {
|
||||||
|
if (entry.getKey().getUuid().isPresent()) {
|
||||||
|
unidentifiedStatusByUuid.put(entry.getKey().getUuid().get().toString(), entry.getValue());
|
||||||
|
}
|
||||||
|
if (entry.getKey().getNumber().isPresent()) {
|
||||||
|
unidentifiedStatusByE164.put(entry.getKey().getNumber().get(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<SignalServiceAddress> getDestination() {
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getExpirationStartTimestamp() {
|
||||||
|
return expirationStartTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceDataMessage getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUnidentified(UUID uuid) {
|
||||||
|
return isUnidentified(uuid.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUnidentified(String destination) {
|
||||||
|
if (unidentifiedStatusByUuid.containsKey(destination)) {
|
||||||
|
return unidentifiedStatusByUuid.get(destination);
|
||||||
|
} else if (unidentifiedStatusByE164.containsKey(destination)) {
|
||||||
|
return unidentifiedStatusByE164.get(destination);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<SignalServiceAddress> getRecipients() {
|
||||||
|
return recipients;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRecipientUpdate() {
|
||||||
|
return isRecipientUpdate;
|
||||||
|
}
|
||||||
|
}
|
||||||
+322
@@ -0,0 +1,322 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* Licensed according to the LICENSE file in this repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SignalServiceSyncMessage {
|
||||||
|
|
||||||
|
private final Optional<SentTranscriptMessage> sent;
|
||||||
|
private final Optional<ContactsMessage> contacts;
|
||||||
|
private final Optional<SignalServiceAttachment> groups;
|
||||||
|
private final Optional<BlockedListMessage> blockedList;
|
||||||
|
private final Optional<RequestMessage> request;
|
||||||
|
private final Optional<List<ReadMessage>> reads;
|
||||||
|
private final Optional<ViewOnceOpenMessage> viewOnceOpen;
|
||||||
|
private final Optional<VerifiedMessage> verified;
|
||||||
|
private final Optional<ConfigurationMessage> configuration;
|
||||||
|
private final Optional<List<StickerPackOperationMessage>> stickerPackOperations;
|
||||||
|
private final Optional<FetchType> fetchType;
|
||||||
|
private final Optional<KeysMessage> keys;
|
||||||
|
|
||||||
|
private SignalServiceSyncMessage(Optional<SentTranscriptMessage> sent,
|
||||||
|
Optional<ContactsMessage> contacts,
|
||||||
|
Optional<SignalServiceAttachment> groups,
|
||||||
|
Optional<BlockedListMessage> blockedList,
|
||||||
|
Optional<RequestMessage> request,
|
||||||
|
Optional<List<ReadMessage>> reads,
|
||||||
|
Optional<ViewOnceOpenMessage> viewOnceOpen,
|
||||||
|
Optional<VerifiedMessage> verified,
|
||||||
|
Optional<ConfigurationMessage> configuration,
|
||||||
|
Optional<List<StickerPackOperationMessage>> stickerPackOperations,
|
||||||
|
Optional<FetchType> fetchType,
|
||||||
|
Optional<KeysMessage> keys)
|
||||||
|
{
|
||||||
|
this.sent = sent;
|
||||||
|
this.contacts = contacts;
|
||||||
|
this.groups = groups;
|
||||||
|
this.blockedList = blockedList;
|
||||||
|
this.request = request;
|
||||||
|
this.reads = reads;
|
||||||
|
this.viewOnceOpen = viewOnceOpen;
|
||||||
|
this.verified = verified;
|
||||||
|
this.configuration = configuration;
|
||||||
|
this.stickerPackOperations = stickerPackOperations;
|
||||||
|
this.fetchType = fetchType;
|
||||||
|
this.keys = keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceSyncMessage forSentTranscript(SentTranscriptMessage sent) {
|
||||||
|
return new SignalServiceSyncMessage(Optional.of(sent),
|
||||||
|
Optional.<ContactsMessage>absent(),
|
||||||
|
Optional.<SignalServiceAttachment>absent(),
|
||||||
|
Optional.<BlockedListMessage>absent(),
|
||||||
|
Optional.<RequestMessage>absent(),
|
||||||
|
Optional.<List<ReadMessage>>absent(),
|
||||||
|
Optional.<ViewOnceOpenMessage>absent(),
|
||||||
|
Optional.<VerifiedMessage>absent(),
|
||||||
|
Optional.<ConfigurationMessage>absent(),
|
||||||
|
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||||
|
Optional.<FetchType>absent(),
|
||||||
|
Optional.<KeysMessage>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceSyncMessage forContacts(ContactsMessage contacts) {
|
||||||
|
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||||
|
Optional.of(contacts),
|
||||||
|
Optional.<SignalServiceAttachment>absent(),
|
||||||
|
Optional.<BlockedListMessage>absent(),
|
||||||
|
Optional.<RequestMessage>absent(),
|
||||||
|
Optional.<List<ReadMessage>>absent(),
|
||||||
|
Optional.<ViewOnceOpenMessage>absent(),
|
||||||
|
Optional.<VerifiedMessage>absent(),
|
||||||
|
Optional.<ConfigurationMessage>absent(),
|
||||||
|
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||||
|
Optional.<FetchType>absent(),
|
||||||
|
Optional.<KeysMessage>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceSyncMessage forGroups(SignalServiceAttachment groups) {
|
||||||
|
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||||
|
Optional.<ContactsMessage>absent(),
|
||||||
|
Optional.of(groups),
|
||||||
|
Optional.<BlockedListMessage>absent(),
|
||||||
|
Optional.<RequestMessage>absent(),
|
||||||
|
Optional.<List<ReadMessage>>absent(),
|
||||||
|
Optional.<ViewOnceOpenMessage>absent(),
|
||||||
|
Optional.<VerifiedMessage>absent(),
|
||||||
|
Optional.<ConfigurationMessage>absent(),
|
||||||
|
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||||
|
Optional.<FetchType>absent(),
|
||||||
|
Optional.<KeysMessage>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceSyncMessage forRequest(RequestMessage request) {
|
||||||
|
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||||
|
Optional.<ContactsMessage>absent(),
|
||||||
|
Optional.<SignalServiceAttachment>absent(),
|
||||||
|
Optional.<BlockedListMessage>absent(),
|
||||||
|
Optional.of(request),
|
||||||
|
Optional.<List<ReadMessage>>absent(),
|
||||||
|
Optional.<ViewOnceOpenMessage>absent(),
|
||||||
|
Optional.<VerifiedMessage>absent(),
|
||||||
|
Optional.<ConfigurationMessage>absent(),
|
||||||
|
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||||
|
Optional.<FetchType>absent(),
|
||||||
|
Optional.<KeysMessage>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceSyncMessage forRead(List<ReadMessage> reads) {
|
||||||
|
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||||
|
Optional.<ContactsMessage>absent(),
|
||||||
|
Optional.<SignalServiceAttachment>absent(),
|
||||||
|
Optional.<BlockedListMessage>absent(),
|
||||||
|
Optional.<RequestMessage>absent(),
|
||||||
|
Optional.of(reads),
|
||||||
|
Optional.<ViewOnceOpenMessage>absent(),
|
||||||
|
Optional.<VerifiedMessage>absent(),
|
||||||
|
Optional.<ConfigurationMessage>absent(),
|
||||||
|
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||||
|
Optional.<FetchType>absent(),
|
||||||
|
Optional.<KeysMessage>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceSyncMessage forViewOnceOpen(ViewOnceOpenMessage timerRead) {
|
||||||
|
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||||
|
Optional.<ContactsMessage>absent(),
|
||||||
|
Optional.<SignalServiceAttachment>absent(),
|
||||||
|
Optional.<BlockedListMessage>absent(),
|
||||||
|
Optional.<RequestMessage>absent(),
|
||||||
|
Optional.<List<ReadMessage>>absent(),
|
||||||
|
Optional.of(timerRead),
|
||||||
|
Optional.<VerifiedMessage>absent(),
|
||||||
|
Optional.<ConfigurationMessage>absent(),
|
||||||
|
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||||
|
Optional.<FetchType>absent(),
|
||||||
|
Optional.<KeysMessage>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceSyncMessage forRead(ReadMessage read) {
|
||||||
|
List<ReadMessage> reads = new LinkedList<>();
|
||||||
|
reads.add(read);
|
||||||
|
|
||||||
|
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||||
|
Optional.<ContactsMessage>absent(),
|
||||||
|
Optional.<SignalServiceAttachment>absent(),
|
||||||
|
Optional.<BlockedListMessage>absent(),
|
||||||
|
Optional.<RequestMessage>absent(),
|
||||||
|
Optional.of(reads),
|
||||||
|
Optional.<ViewOnceOpenMessage>absent(),
|
||||||
|
Optional.<VerifiedMessage>absent(),
|
||||||
|
Optional.<ConfigurationMessage>absent(),
|
||||||
|
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||||
|
Optional.<FetchType>absent(),
|
||||||
|
Optional.<KeysMessage>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceSyncMessage forVerified(VerifiedMessage verifiedMessage) {
|
||||||
|
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||||
|
Optional.<ContactsMessage>absent(),
|
||||||
|
Optional.<SignalServiceAttachment>absent(),
|
||||||
|
Optional.<BlockedListMessage>absent(),
|
||||||
|
Optional.<RequestMessage>absent(),
|
||||||
|
Optional.<List<ReadMessage>>absent(),
|
||||||
|
Optional.<ViewOnceOpenMessage>absent(),
|
||||||
|
Optional.of(verifiedMessage),
|
||||||
|
Optional.<ConfigurationMessage>absent(),
|
||||||
|
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||||
|
Optional.<FetchType>absent(),
|
||||||
|
Optional.<KeysMessage>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceSyncMessage forBlocked(BlockedListMessage blocked) {
|
||||||
|
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||||
|
Optional.<ContactsMessage>absent(),
|
||||||
|
Optional.<SignalServiceAttachment>absent(),
|
||||||
|
Optional.of(blocked),
|
||||||
|
Optional.<RequestMessage>absent(),
|
||||||
|
Optional.<List<ReadMessage>>absent(),
|
||||||
|
Optional.<ViewOnceOpenMessage>absent(),
|
||||||
|
Optional.<VerifiedMessage>absent(),
|
||||||
|
Optional.<ConfigurationMessage>absent(),
|
||||||
|
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||||
|
Optional.<FetchType>absent(),
|
||||||
|
Optional.<KeysMessage>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceSyncMessage forConfiguration(ConfigurationMessage configuration) {
|
||||||
|
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||||
|
Optional.<ContactsMessage>absent(),
|
||||||
|
Optional.<SignalServiceAttachment>absent(),
|
||||||
|
Optional.<BlockedListMessage>absent(),
|
||||||
|
Optional.<RequestMessage>absent(),
|
||||||
|
Optional.<List<ReadMessage>>absent(),
|
||||||
|
Optional.<ViewOnceOpenMessage>absent(),
|
||||||
|
Optional.<VerifiedMessage>absent(),
|
||||||
|
Optional.of(configuration),
|
||||||
|
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||||
|
Optional.<FetchType>absent(),
|
||||||
|
Optional.<KeysMessage>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceSyncMessage forStickerPackOperations(List<StickerPackOperationMessage> stickerPackOperations) {
|
||||||
|
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||||
|
Optional.<ContactsMessage>absent(),
|
||||||
|
Optional.<SignalServiceAttachment>absent(),
|
||||||
|
Optional.<BlockedListMessage>absent(),
|
||||||
|
Optional.<RequestMessage>absent(),
|
||||||
|
Optional.<List<ReadMessage>>absent(),
|
||||||
|
Optional.<ViewOnceOpenMessage>absent(),
|
||||||
|
Optional.<VerifiedMessage>absent(),
|
||||||
|
Optional.<ConfigurationMessage>absent(),
|
||||||
|
Optional.of(stickerPackOperations),
|
||||||
|
Optional.<FetchType>absent(),
|
||||||
|
Optional.<KeysMessage>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceSyncMessage forFetchLatest(FetchType fetchType) {
|
||||||
|
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||||
|
Optional.<ContactsMessage>absent(),
|
||||||
|
Optional.<SignalServiceAttachment>absent(),
|
||||||
|
Optional.<BlockedListMessage>absent(),
|
||||||
|
Optional.<RequestMessage>absent(),
|
||||||
|
Optional.<List<ReadMessage>>absent(),
|
||||||
|
Optional.<ViewOnceOpenMessage>absent(),
|
||||||
|
Optional.<VerifiedMessage>absent(),
|
||||||
|
Optional.<ConfigurationMessage>absent(),
|
||||||
|
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||||
|
Optional.of(fetchType),
|
||||||
|
Optional.<KeysMessage>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceSyncMessage forKeys(KeysMessage keys) {
|
||||||
|
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||||
|
Optional.<ContactsMessage>absent(),
|
||||||
|
Optional.<SignalServiceAttachment>absent(),
|
||||||
|
Optional.<BlockedListMessage>absent(),
|
||||||
|
Optional.<RequestMessage>absent(),
|
||||||
|
Optional.<List<ReadMessage>>absent(),
|
||||||
|
Optional.<ViewOnceOpenMessage>absent(),
|
||||||
|
Optional.<VerifiedMessage>absent(),
|
||||||
|
Optional.<ConfigurationMessage>absent(),
|
||||||
|
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||||
|
Optional.<FetchType>absent(),
|
||||||
|
Optional.of(keys));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignalServiceSyncMessage empty() {
|
||||||
|
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||||
|
Optional.<ContactsMessage>absent(),
|
||||||
|
Optional.<SignalServiceAttachment>absent(),
|
||||||
|
Optional.<BlockedListMessage>absent(),
|
||||||
|
Optional.<RequestMessage>absent(),
|
||||||
|
Optional.<List<ReadMessage>>absent(),
|
||||||
|
Optional.<ViewOnceOpenMessage>absent(),
|
||||||
|
Optional.<VerifiedMessage>absent(),
|
||||||
|
Optional.<ConfigurationMessage>absent(),
|
||||||
|
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||||
|
Optional.<FetchType>absent(),
|
||||||
|
Optional.<KeysMessage>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<SentTranscriptMessage> getSent() {
|
||||||
|
return sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<SignalServiceAttachment> getGroups() {
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<ContactsMessage> getContacts() {
|
||||||
|
return contacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<RequestMessage> getRequest() {
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<List<ReadMessage>> getRead() {
|
||||||
|
return reads;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<ViewOnceOpenMessage> getViewOnceOpen() {
|
||||||
|
return viewOnceOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<BlockedListMessage> getBlockedList() {
|
||||||
|
return blockedList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<VerifiedMessage> getVerified() {
|
||||||
|
return verified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<ConfigurationMessage> getConfiguration() {
|
||||||
|
return configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<List<StickerPackOperationMessage>> getStickerPackOperations() {
|
||||||
|
return stickerPackOperations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<FetchType> getFetchType() {
|
||||||
|
return fetchType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<KeysMessage> getKeys() {
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FetchType {
|
||||||
|
LOCAL_PROFILE,
|
||||||
|
STORAGE_MANIFEST
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
|
public class StickerPackOperationMessage {
|
||||||
|
|
||||||
|
private final Optional<byte[]> packId;
|
||||||
|
private final Optional<byte[]> packKey;
|
||||||
|
private final Optional<Type> type;
|
||||||
|
|
||||||
|
public StickerPackOperationMessage(byte[] packId, byte[] packKey, Type type) {
|
||||||
|
this.packId = Optional.fromNullable(packId);
|
||||||
|
this.packKey = Optional.fromNullable(packKey);
|
||||||
|
this.type = Optional.fromNullable(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<byte[]> getPackId() {
|
||||||
|
return packId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<byte[]> getPackKey() {
|
||||||
|
return packKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Type> getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
INSTALL, REMOVE
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
|
public class VerifiedMessage {
|
||||||
|
|
||||||
|
public enum VerifiedState {
|
||||||
|
DEFAULT, VERIFIED, UNVERIFIED
|
||||||
|
}
|
||||||
|
|
||||||
|
private final SignalServiceAddress destination;
|
||||||
|
private final IdentityKey identityKey;
|
||||||
|
private final VerifiedState verified;
|
||||||
|
private final long timestamp;
|
||||||
|
|
||||||
|
public VerifiedMessage(SignalServiceAddress destination, IdentityKey identityKey, VerifiedState verified, long timestamp) {
|
||||||
|
this.destination = destination;
|
||||||
|
this.identityKey = identityKey;
|
||||||
|
this.verified = verified;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceAddress getDestination() {
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IdentityKey getIdentityKey() {
|
||||||
|
return identityKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VerifiedState getVerified() {
|
||||||
|
return verified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
|
public class ViewOnceOpenMessage {
|
||||||
|
|
||||||
|
private final SignalServiceAddress sender;
|
||||||
|
private final long timestamp;
|
||||||
|
|
||||||
|
public ViewOnceOpenMessage(SignalServiceAddress sender, long timestamp) {
|
||||||
|
this.sender = sender;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceAddress getSender() {
|
||||||
|
return sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+513
@@ -0,0 +1,513 @@
|
|||||||
|
package org.whispersystems.signalservice.api.messages.shared;
|
||||||
|
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SharedContact {
|
||||||
|
|
||||||
|
private final Name name;
|
||||||
|
private final Optional<Avatar> avatar;
|
||||||
|
private final Optional<List<Phone>> phone;
|
||||||
|
private final Optional<List<Email>> email;
|
||||||
|
private final Optional<List<PostalAddress>> address;
|
||||||
|
private final Optional<String> organization;
|
||||||
|
|
||||||
|
public SharedContact(Name name,
|
||||||
|
Optional<Avatar> avatar,
|
||||||
|
Optional<List<Phone>> phone,
|
||||||
|
Optional<List<Email>> email,
|
||||||
|
Optional<List<PostalAddress>> address,
|
||||||
|
Optional<String> organization)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
this.avatar = avatar;
|
||||||
|
this.phone = phone;
|
||||||
|
this.email = email;
|
||||||
|
this.address = address;
|
||||||
|
this.organization = organization;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder newBuilder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Name getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Avatar> getAvatar() {
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<List<Phone>> getPhone() {
|
||||||
|
return phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<List<Email>> getEmail() {
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<List<PostalAddress>> getAddress() {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getOrganization() {
|
||||||
|
return organization;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Avatar {
|
||||||
|
private final SignalServiceAttachment attachment;
|
||||||
|
private final boolean isProfile;
|
||||||
|
|
||||||
|
public Avatar(SignalServiceAttachment attachment, boolean isProfile) {
|
||||||
|
this.attachment = attachment;
|
||||||
|
this.isProfile = isProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceAttachment getAttachment() {
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isProfile() {
|
||||||
|
return isProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder newBuilder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private SignalServiceAttachment attachment;
|
||||||
|
private boolean isProfile;
|
||||||
|
|
||||||
|
public Builder withAttachment(SignalServiceAttachment attachment) {
|
||||||
|
this.attachment = attachment;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withProfileFlag(boolean isProfile) {
|
||||||
|
this.isProfile = isProfile;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Avatar build() {
|
||||||
|
return new Avatar(attachment, isProfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Name {
|
||||||
|
|
||||||
|
private final Optional<String> display;
|
||||||
|
private final Optional<String> given;
|
||||||
|
private final Optional<String> family;
|
||||||
|
private final Optional<String> prefix;
|
||||||
|
private final Optional<String> suffix;
|
||||||
|
private final Optional<String> middle;
|
||||||
|
|
||||||
|
public Name(Optional<String> display, Optional<String> given, Optional<String> family, Optional<String> prefix, Optional<String> suffix, Optional<String> middle) {
|
||||||
|
this.display = display;
|
||||||
|
this.given = given;
|
||||||
|
this.family = family;
|
||||||
|
this.prefix = prefix;
|
||||||
|
this.suffix = suffix;
|
||||||
|
this.middle = middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder newBuilder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getDisplay() {
|
||||||
|
return display;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getGiven() {
|
||||||
|
return given;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getFamily() {
|
||||||
|
return family;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getPrefix() {
|
||||||
|
return prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getSuffix() {
|
||||||
|
return suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getMiddle() {
|
||||||
|
return middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private String display;
|
||||||
|
private String given;
|
||||||
|
private String family;
|
||||||
|
private String prefix;
|
||||||
|
private String suffix;
|
||||||
|
private String middle;
|
||||||
|
|
||||||
|
public Builder setDisplay(String display) {
|
||||||
|
this.display = display;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setGiven(String given) {
|
||||||
|
this.given = given;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setFamily(String family) {
|
||||||
|
this.family = family;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setPrefix(String prefix) {
|
||||||
|
this.prefix = prefix;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setSuffix(String suffix) {
|
||||||
|
this.suffix = suffix;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setMiddle(String middle) {
|
||||||
|
this.middle = middle;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Name build() {
|
||||||
|
return new Name(Optional.fromNullable(display),
|
||||||
|
Optional.fromNullable(given),
|
||||||
|
Optional.fromNullable(family),
|
||||||
|
Optional.fromNullable(prefix),
|
||||||
|
Optional.fromNullable(suffix),
|
||||||
|
Optional.fromNullable(middle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Phone {
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
HOME, WORK, MOBILE, CUSTOM
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
private final Type type;
|
||||||
|
private final Optional<String> label;
|
||||||
|
|
||||||
|
public Phone(String value, Type type, Optional<String> label) {
|
||||||
|
this.value = value;
|
||||||
|
this.type = type;
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder newBuilder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private String value;
|
||||||
|
private Type type;
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
public Builder setValue(String value) {
|
||||||
|
this.value = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setType(Type type) {
|
||||||
|
this.type = type;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setLabel(String label) {
|
||||||
|
this.label = label;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Phone build() {
|
||||||
|
return new Phone(value, type, Optional.fromNullable(label));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Email {
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
HOME, WORK, MOBILE, CUSTOM
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
private final Type type;
|
||||||
|
private final Optional<String> label;
|
||||||
|
|
||||||
|
public Email(String value, Type type, Optional<String> label) {
|
||||||
|
this.value = value;
|
||||||
|
this.type = type;
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder newBuilder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private String value;
|
||||||
|
private Type type;
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
public Builder setValue(String value) {
|
||||||
|
this.value = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setType(Type type) {
|
||||||
|
this.type = type;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setLabel(String label) {
|
||||||
|
this.label = label;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Email build() {
|
||||||
|
return new Email(value, type, Optional.fromNullable(label));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class PostalAddress {
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
HOME, WORK, CUSTOM
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Type type;
|
||||||
|
private final Optional<String> label;
|
||||||
|
private final Optional<String> street;
|
||||||
|
private final Optional<String> pobox;
|
||||||
|
private final Optional<String> neighborhood;
|
||||||
|
private final Optional<String> city;
|
||||||
|
private final Optional<String> region;
|
||||||
|
private final Optional<String> postcode;
|
||||||
|
private final Optional<String> country;
|
||||||
|
|
||||||
|
public PostalAddress(Type type, Optional<String> label, Optional<String> street,
|
||||||
|
Optional<String> pobox, Optional<String> neighborhood,
|
||||||
|
Optional<String> city, Optional<String> region,
|
||||||
|
Optional<String> postcode, Optional<String> country)
|
||||||
|
{
|
||||||
|
this.type = type;
|
||||||
|
this.label = label;
|
||||||
|
this.street = street;
|
||||||
|
this.pobox = pobox;
|
||||||
|
this.neighborhood = neighborhood;
|
||||||
|
this.city = city;
|
||||||
|
this.region = region;
|
||||||
|
this.postcode = postcode;
|
||||||
|
this.country = country;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder newBuilder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getStreet() {
|
||||||
|
return street;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getPobox() {
|
||||||
|
return pobox;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getNeighborhood() {
|
||||||
|
return neighborhood;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getCity() {
|
||||||
|
return city;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getRegion() {
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getPostcode() {
|
||||||
|
return postcode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getCountry() {
|
||||||
|
return country;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private Type type;
|
||||||
|
private String label;
|
||||||
|
private String street;
|
||||||
|
private String pobox;
|
||||||
|
private String neighborhood;
|
||||||
|
private String city;
|
||||||
|
private String region;
|
||||||
|
private String postcode;
|
||||||
|
private String country;
|
||||||
|
|
||||||
|
public Builder setType(Type type) {
|
||||||
|
this.type = type;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setLabel(String label) {
|
||||||
|
this.label = label;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setStreet(String street) {
|
||||||
|
this.street = street;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setPobox(String pobox) {
|
||||||
|
this.pobox = pobox;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setNeighborhood(String neighborhood) {
|
||||||
|
this.neighborhood = neighborhood;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setCity(String city) {
|
||||||
|
this.city = city;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setRegion(String region) {
|
||||||
|
this.region = region;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setPostcode(String postcode) {
|
||||||
|
this.postcode = postcode;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setCountry(String country) {
|
||||||
|
this.country = country;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PostalAddress build() {
|
||||||
|
return new PostalAddress(type, Optional.fromNullable(label), Optional.fromNullable(street),
|
||||||
|
Optional.fromNullable(pobox), Optional.fromNullable(neighborhood),
|
||||||
|
Optional.fromNullable(city), Optional.fromNullable(region),
|
||||||
|
Optional.fromNullable(postcode), Optional.fromNullable(country));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private Name name;
|
||||||
|
private Avatar avatar;
|
||||||
|
private String organization;
|
||||||
|
|
||||||
|
private List<Phone> phone = new LinkedList<>();
|
||||||
|
private List<Email> email = new LinkedList<>();
|
||||||
|
private List<PostalAddress> address = new LinkedList<>();
|
||||||
|
|
||||||
|
public Builder setName(Name name) {
|
||||||
|
this.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withOrganization(String organization) {
|
||||||
|
this.organization = organization;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setAvatar(Avatar avatar) {
|
||||||
|
this.avatar = avatar;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withPhone(Phone phone) {
|
||||||
|
this.phone.add(phone);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withPhones(List<Phone> phones) {
|
||||||
|
this.phone.addAll(phones);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withEmail(Email email) {
|
||||||
|
this.email.add(email);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withEmails(List<Email> emails) {
|
||||||
|
this.email.addAll(emails);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withAddress(PostalAddress address) {
|
||||||
|
this.address.add(address);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withAddresses(List<PostalAddress> addresses) {
|
||||||
|
this.address.addAll(addresses);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SharedContact build() {
|
||||||
|
return new SharedContact(name, Optional.fromNullable(avatar),
|
||||||
|
phone.isEmpty() ? Optional.<List<Phone>>absent() : Optional.of(phone),
|
||||||
|
email.isEmpty() ? Optional.<List<Email>>absent() : Optional.of(email),
|
||||||
|
address.isEmpty() ? Optional.<List<PostalAddress>>absent() : Optional.of(address),
|
||||||
|
Optional.fromNullable(organization));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
package org.whispersystems.signalservice.api.profiles;
|
||||||
|
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.internal.util.JsonUtil;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class SignalServiceProfile {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String identityKey;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String unidentifiedAccess;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private boolean unrestrictedUnidentifiedAccess;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private Capabilities capabilities;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@JsonSerialize(using = JsonUtil.UuidSerializer.class)
|
||||||
|
@JsonDeserialize(using = JsonUtil.UuidDeserializer.class)
|
||||||
|
private UUID uuid;
|
||||||
|
|
||||||
|
public SignalServiceProfile() {}
|
||||||
|
|
||||||
|
public String getIdentityKey() {
|
||||||
|
return identityKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAvatar() {
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUnidentifiedAccess() {
|
||||||
|
return unidentifiedAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUnrestrictedUnidentifiedAccess() {
|
||||||
|
return unrestrictedUnidentifiedAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Capabilities getCapabilities() {
|
||||||
|
return capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getUuid() {
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Capabilities {
|
||||||
|
@JsonProperty
|
||||||
|
private boolean uuid;
|
||||||
|
|
||||||
|
public Capabilities() {}
|
||||||
|
|
||||||
|
public boolean isUuid() {
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user