Compare commits
269 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e53bbe8453 | ||
|
|
ecb67cd84f | ||
|
|
861d27279d | ||
|
|
bef5b8f3e9 | ||
|
|
9e74b5c892 | ||
|
|
3597915d17 | ||
|
|
40ce0cebe0 | ||
|
|
26d58047b5 | ||
|
|
7d688846f9 | ||
|
|
acc7c4c1c6 | ||
|
|
530ad7bc86 | ||
|
|
bea3c33223 | ||
|
|
9ef14a0f64 | ||
|
|
c632b32ff8 | ||
|
|
40698212bb | ||
|
|
19ae5043cc | ||
|
|
d1dd50e31c | ||
|
|
23a1c1c8fa | ||
|
|
3a62a8b428 | ||
|
|
cfac27645b | ||
|
|
f6e04d0f89 | ||
|
|
61d18f49ad | ||
|
|
f26f89d63d | ||
|
|
66aad852f8 | ||
|
|
da0eb5a779 | ||
|
|
a82d2dfc5c | ||
|
|
d429f9113b | ||
|
|
8f85eb1822 | ||
|
|
358c923891 | ||
|
|
2d9cd8eb52 | ||
|
|
db1d846833 | ||
|
|
5121ab0eed | ||
|
|
f63f95404e | ||
|
|
622d8975fc | ||
|
|
81365eff36 | ||
|
|
453610c39f | ||
|
|
5ce6dc954a | ||
|
|
c85a8bbb38 | ||
|
|
0f9a6e6296 | ||
|
|
d8cb893681 | ||
|
|
1ad54e7b88 | ||
|
|
0d35e2bfa9 | ||
|
|
983bf672cf | ||
|
|
1c2e1a07f5 | ||
|
|
2d739a324e | ||
|
|
ba1055df8e | ||
|
|
a54d20f3ef | ||
|
|
ea0fa58265 | ||
|
|
359fe280e8 | ||
|
|
34e147838a | ||
|
|
d8e6a93584 | ||
|
|
5ae8a7a8c4 | ||
|
|
0e6773b4b7 | ||
|
|
fb13d33e2e | ||
|
|
92fd8ededd | ||
|
|
8713a85beb | ||
|
|
9b82411c3d | ||
|
|
16764f74fe | ||
|
|
bd889d8fa9 | ||
|
|
d51adab76b | ||
|
|
b990202468 | ||
|
|
7208018097 | ||
|
|
c719a48a2c | ||
|
|
68747142d6 | ||
|
|
7c9282f306 | ||
|
|
fa3cb871d0 | ||
|
|
a19899a11f | ||
|
|
667da3b2cf | ||
|
|
1a86483b7f | ||
|
|
de90222c95 | ||
|
|
7cf84e904a | ||
|
|
12e92b9cdf | ||
|
|
4153c8dae9 | ||
|
|
ef72702f0d | ||
|
|
f5e2010455 | ||
|
|
bc769debe2 | ||
|
|
df1c96a662 | ||
|
|
db356a0ec9 | ||
|
|
468eb3382c | ||
|
|
12d217991c | ||
|
|
5785860631 | ||
|
|
addea8d340 | ||
|
|
59899b1caf | ||
|
|
829097d891 | ||
|
|
d95bb21065 | ||
|
|
0fbe765447 | ||
|
|
f190321e40 | ||
|
|
1cb4d479f1 | ||
|
|
023d776e96 | ||
|
|
ce7b8ab75a | ||
|
|
82bb0c07e8 | ||
|
|
d8d5848dae | ||
|
|
542e1984c1 | ||
|
|
dff6997a65 | ||
|
|
5bfe64752e | ||
|
|
d6d76fa953 | ||
|
|
03ecd79fe0 | ||
|
|
7a3d509ef4 | ||
|
|
7a54f33f68 | ||
|
|
d4b4667d5a | ||
|
|
08d899e2e1 | ||
|
|
716519f4b8 | ||
|
|
02d3760b31 | ||
|
|
521fbc77c6 | ||
|
|
0574ec170a | ||
|
|
cebad39422 | ||
|
|
1d07ca3e6f | ||
|
|
ca8c950553 | ||
|
|
7349378d8d | ||
|
|
dc9a9b14b2 | ||
|
|
df9afc4e7f | ||
|
|
e9a50ce6c3 | ||
|
|
4d52d2ee36 | ||
|
|
c21e5b74f1 | ||
|
|
c5b3b27c49 | ||
|
|
ccb0cc6baf | ||
|
|
ef77dc9d6d | ||
|
|
454673cd0c | ||
|
|
0dc6ec294b | ||
|
|
12dac6ccc3 | ||
|
|
e2f7c1529a | ||
|
|
b715debefc | ||
|
|
4438b4ae69 | ||
|
|
7b3f2c169a | ||
|
|
7ceaf59bcc | ||
|
|
4caff2e511 | ||
|
|
5e8e13ed5a | ||
|
|
4ea481f9dc | ||
|
|
e94fa0d752 | ||
|
|
817070e76f | ||
|
|
92b90cd798 | ||
|
|
33ecc4d690 | ||
|
|
7d5e66eb6e | ||
|
|
d4ac0c077d | ||
|
|
cbe87aa05c | ||
|
|
9e2f82954f | ||
|
|
d4d684b670 | ||
|
|
c43ef8bce0 | ||
|
|
54a882a11d | ||
|
|
73e2f6ce59 | ||
|
|
54d2184c72 | ||
|
|
832763f695 | ||
|
|
40629a3bcf | ||
|
|
5a3daf4846 | ||
|
|
fd1a18d2d0 | ||
|
|
ad5d6d5bb7 | ||
|
|
926d3c929f | ||
|
|
edc20883eb | ||
|
|
0d7363e36e | ||
|
|
ca6d8a8a0d | ||
|
|
c414334059 | ||
|
|
24a38985cf | ||
|
|
60f2d4d6b6 | ||
|
|
06659fd98f | ||
|
|
3c9c5213a7 | ||
|
|
a183f8d387 | ||
|
|
216446c55b | ||
|
|
bc143059f6 | ||
|
|
33000582ed | ||
|
|
e651f352bb | ||
|
|
cab4a06974 | ||
|
|
ccc1f5e9d6 | ||
|
|
b860aeff85 | ||
|
|
34c885f08d | ||
|
|
71ab6f5c7d | ||
|
|
61fbf382eb | ||
|
|
8b21f3f7d6 | ||
|
|
941d0089f4 | ||
|
|
8b8c6dd45f | ||
|
|
938545444e | ||
|
|
d827ab1b36 | ||
|
|
4701e59197 | ||
|
|
2b2da84918 | ||
|
|
d229a4274c | ||
|
|
64711771f0 | ||
|
|
ad54d2a05f | ||
|
|
068c40336c | ||
|
|
11cfc4f1a1 | ||
|
|
f51989b23e | ||
|
|
0b4fe84a41 | ||
|
|
b855a1805f | ||
|
|
339193af12 | ||
|
|
574f33c92d | ||
|
|
b6a9eb5bf2 | ||
|
|
54b43b7536 | ||
|
|
37c9fb7cd1 | ||
|
|
667d22bace | ||
|
|
3d782449ed | ||
|
|
003095b08c | ||
|
|
d121d9531e | ||
|
|
42aeca26f1 | ||
|
|
ea8a1bae46 | ||
|
|
054fcdca8d | ||
|
|
278220cf18 | ||
|
|
35eeaa9bd5 | ||
|
|
fbf02603ce | ||
|
|
fa423e4432 | ||
|
|
5caec4a146 | ||
|
|
dc77c43435 | ||
|
|
827298d1a2 | ||
|
|
48f5b932f7 | ||
|
|
90169e9468 | ||
|
|
b5fe378bc9 | ||
|
|
0f53c9d170 | ||
|
|
3474950830 | ||
|
|
39ee363150 | ||
|
|
411e3ceff6 | ||
|
|
b7f8c3b3d3 | ||
|
|
3efa8e6899 | ||
|
|
948f481530 | ||
|
|
716fdefa4c | ||
|
|
2dc893730a | ||
|
|
1af99ce155 | ||
|
|
0850f1b0f1 | ||
|
|
1ddc45fd9c | ||
|
|
977765c80f | ||
|
|
4b2d07ab35 | ||
|
|
003ebe6364 | ||
|
|
2f7c005c23 | ||
|
|
367b481d07 | ||
|
|
5cd8c922d2 | ||
|
|
a2fe8a9d5c | ||
|
|
ee921a8f49 | ||
|
|
e0394b4481 | ||
|
|
de100f5be7 | ||
|
|
a020a57be6 | ||
|
|
28f1a0a636 | ||
|
|
5a807ffc28 | ||
|
|
757cb1c846 | ||
|
|
4f066757e7 | ||
|
|
633aa9b057 | ||
|
|
a5c26b2e16 | ||
|
|
ca561d76ff | ||
|
|
107d999ee7 | ||
|
|
0f6c7660cb | ||
|
|
8ea4db03db | ||
|
|
81ee9e31c5 | ||
|
|
a3e900ecbe | ||
|
|
384fb3b2b5 | ||
|
|
d795aa30b3 | ||
|
|
bac4d63312 | ||
|
|
bf60f90019 | ||
|
|
852ca2ac05 | ||
|
|
038bebfdbb | ||
|
|
3b25b87aa8 | ||
|
|
5a62856e46 | ||
|
|
37a52df4e6 | ||
|
|
d3148b6766 | ||
|
|
6fb85aff6d | ||
|
|
ed45067227 | ||
|
|
0015711759 | ||
|
|
15390e477e | ||
|
|
a8c23413ba | ||
|
|
ce68429a9b | ||
|
|
1a9a88a5a1 | ||
|
|
7987362c25 | ||
|
|
5d42110d6c | ||
|
|
dec7fd4c8a | ||
|
|
82df23dd41 | ||
|
|
ce710b378f | ||
|
|
20fd881613 | ||
|
|
5fa429b0d5 | ||
|
|
630dce04fc | ||
|
|
0da1d8818e | ||
|
|
c84285c639 | ||
|
|
5a525a2e58 | ||
|
|
dda8a214a4 | ||
|
|
bd167cbb17 | ||
|
|
a0aaa7d724 |
7
.travis.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
language: android
|
||||
android:
|
||||
components:
|
||||
- platform-tools
|
||||
- build-tools-19.1.0
|
||||
- android-19
|
||||
- extra-android-m2repository
|
||||
@@ -1,6 +1,6 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = fr_CA:fr-rCA,pt_BR:pt-rBR,pt_PT:pt,zh_CN:zh-rCN,zh_HK:zh-rHK,zh_TW:zh-rTW,da_DK:da-rDK,de_DE:de,fr_FR:fr,es_ES:es,hu_HU:hu,sv_SE:sv-rSE,bg_BG:bg,el_GR:el,kn_IN:kn-rIN,cs_CZ:cs
|
||||
lang_map = fr_CA:fr-rCA,pt_BR:pt-rBR,pt_PT:pt,zh_CN:zh-rCN,zh_HK:zh-rHK,zh_TW:zh-rTW,da_DK:da-rDK,de_DE:de,tr_TR:tr,fr_FR:fr,es_ES:es,hu_HU:hu,sv_SE:sv-rSE,bg_BG:bg,el_GR:el,kn_IN:kn-rIN,cs_CZ:cs
|
||||
|
||||
|
||||
[textsecure-official.master]
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.thoughtcrime.securesms"
|
||||
android:versionCode="64"
|
||||
android:versionName="2.0.1">
|
||||
|
||||
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="19"/>
|
||||
android:versionCode="74"
|
||||
android:versionName="2.1.2">
|
||||
|
||||
<permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"
|
||||
android:label="Access to TextSecure Secrets"
|
||||
@@ -33,7 +31,6 @@
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_CALL_LOG" />
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
||||
|
||||
<permission android:name="org.thoughtcrime.securesms.permission.C2D_MESSAGE"
|
||||
@@ -45,6 +42,9 @@
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/TextSecure.LightTheme">
|
||||
|
||||
<meta-data android:name="com.google.android.gms.version"
|
||||
android:value="@integer/google_play_services_version" />
|
||||
|
||||
<activity android:name=".RoutingActivity"
|
||||
android:theme="@style/NoAnimation.Theme.BlackScreen"
|
||||
android:launchMode="singleTask"
|
||||
@@ -69,8 +69,9 @@
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="text/*" />
|
||||
</intent-filter>
|
||||
<data android:mimeType="text/plain" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
@@ -89,14 +90,21 @@
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".MmsPreferencesActivity"
|
||||
<activity android:name=".MmsPreferencesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ConversationListActivity"
|
||||
android:label="@string/app_name"
|
||||
<activity android:name=".ShareActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:noHistory="true"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ConversationListActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ConversationActivity"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
@@ -118,15 +126,15 @@
|
||||
<activity android:name=".PassphraseCreateActivity"
|
||||
android:label="@string/AndroidManifest__create_passphrase"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:theme="@style/NoAnimation.Theme.Sherlock.Light.DarkActionBar"
|
||||
android:theme="@style/TextSecure.IntroTheme"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".PassphrasePromptActivity"
|
||||
android:label="@string/AndroidManifest__enter_passphrase"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/NoAnimation.Theme.Sherlock.Light.DarkActionBar"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:theme="@style/TextSecure.IntroTheme"
|
||||
android:windowSoftInputMode="stateAlwaysVisible"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ContactSelectionActivity"
|
||||
@@ -134,15 +142,15 @@
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".SingleContactSelectionActivity"
|
||||
android:label="@string/AndroidManifest__select_contact"
|
||||
<activity android:name=".NewConversationActivity"
|
||||
android:label="@string/AndroidManifest__select_contacts"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".PushContactSelectionActivity"
|
||||
android:label="@string/AndroidManifest__select_contacts"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:label="@string/AndroidManifest__select_contacts"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".AutoInitiateActivity"
|
||||
android:theme="@style/TextSecure.Light.Dialog"
|
||||
@@ -164,11 +172,7 @@
|
||||
android:label="@string/AndroidManifest__verify_identity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ReviewIdentitiesActivity"
|
||||
android:label="@string/AndroidManifest__manage_identity_keys"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ReceiveKeyActivity"
|
||||
<activity android:name=".ReceiveKeyActivity"
|
||||
android:label="@string/AndroidManifest__complete_key_exchange"
|
||||
android:theme="@style/TextSecure.Light.Dialog"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
@@ -183,11 +187,29 @@
|
||||
<activity android:name=".RegistrationProgressActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".LogSubmitActivity"
|
||||
android:label="@string/AndroidManifest__log_submit"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".DummyActivity"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:enabled="true"
|
||||
android:allowTaskReparenting="true"
|
||||
android:noHistory="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:alwaysRetainTaskState="false"
|
||||
android:stateNotNeeded="true"
|
||||
android:clearTaskOnLaunch="true"
|
||||
android:finishOnTaskLaunch="true" />
|
||||
|
||||
<service android:enabled="true" android:name=".service.GcmRegistrationService"/>
|
||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||
<service android:enabled="true" android:name=".service.KeyCachingService"/>
|
||||
<service android:enabled="true" android:name=".service.SendReceiveService"/>
|
||||
<service android:enabled="true" android:name=".service.RegistrationService"/>
|
||||
<service android:enabled="true" android:name=".gcm.GcmIntentService"/>
|
||||
<service android:enabled="true" android:name=".service.DirectoryRefreshService"/>
|
||||
<service android:enabled="true" android:name=".service.PreKeyService"/>
|
||||
|
||||
<service android:name=".service.QuickResponseService"
|
||||
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
|
||||
@@ -214,7 +236,6 @@
|
||||
<receiver android:name=".gcm.GcmBroadcastReceiver" android:permission="com.google.android.c2dm.permission.SEND" >
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
|
||||
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
|
||||
<category android:name="org.thoughtcrime.securesms" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
@@ -280,8 +301,4 @@
|
||||
|
||||
<uses-library android:name="android.test.runner" />
|
||||
</application>
|
||||
|
||||
<instrumentation android:name="android.test.InstrumentationTestRunner"
|
||||
android:targetPackage="org.thoughtcrime.securesms.tests" android:label="Tests for My App" />
|
||||
|
||||
</manifest>
|
||||
|
||||
67
BUILDING.md
@@ -1,8 +1,67 @@
|
||||
Building TextSecure
|
||||
===================
|
||||
=====================
|
||||
|
||||
1. Ensure the 'Android Support Repository' is installed from the Android SDK manager.
|
||||
Basics
|
||||
------
|
||||
|
||||
Execute Gradle:
|
||||
TextSecure uses [Gradle](http://gradle.org) to build the project and to maintain
|
||||
dependencies.
|
||||
|
||||
./gradlew build
|
||||
Building TextSecure
|
||||
-------------------
|
||||
|
||||
The following steps should help you (re)build TextSecure from the command line.
|
||||
|
||||
1. Checkout the source somewhere on your filesystem with
|
||||
|
||||
git clone https://github.com/WhisperSystems/TextSecure.git
|
||||
|
||||
2. Make sure you have the [Android SDK](https://developer.android.com/sdk/index.html) installed somewhere on your system.
|
||||
3. Ensure the "Android Support Repository" and "Android SDK Build-tools" are installed from the Android SDK manager.
|
||||
4. Create a local.properties file at the root of your source checkout and add an sdk.dir entry to it.
|
||||
|
||||
sdk.dir=\<path to your sdk installation\>
|
||||
|
||||
5. Execute Gradle:
|
||||
|
||||
./gradlew build
|
||||
|
||||
Re-building native components
|
||||
-----------------------------
|
||||
|
||||
Note: This step is optional; native components are contained as binaries (see [library/libs](library/libs)).
|
||||
|
||||
1. Ensure that the Android NDK is installed.
|
||||
|
||||
Execute ndk-build:
|
||||
|
||||
cd library
|
||||
ndk-build
|
||||
|
||||
Afterwards, execute Gradle as above to re-create the APK.
|
||||
|
||||
Setting up a development environment
|
||||
------------------------------------
|
||||
|
||||
[Android Studio](https://developer.android.com/sdk/installing/studio.html) is the recommended development environment.
|
||||
|
||||
1. Install Android Studio.
|
||||
2. Make sure the "Android Support Repository" is installed in the Android Studio SDK.
|
||||
3. Make sure the latest "Android SDK build-tools" is installed in the Android Studio SDK.
|
||||
4. Create a new Android Studio project. from the Quickstart pannel (use File > Close Project to see it), choose "Checkout from Version Control" then "git".
|
||||
5. Paste the URL for the TextSecure project when prompted (https://github.com/WhisperSystems/TextSecure.git).
|
||||
6. Android studio should detect the presence of a project file and ask you whether to open it. Click "yes".
|
||||
7. Default config options should be good enough.
|
||||
8. Project initialisation and build should proceed.
|
||||
|
||||
Contributing code
|
||||
-----------------
|
||||
|
||||
Code contributions should be sent via github as pull requests, from feature branches [as explained here](https://help.github.com/articles/using-pull-requests).
|
||||
|
||||
Mailing list
|
||||
------------
|
||||
|
||||
Development discussion happens on the whispersystems mailing list.
|
||||
[To join](https://lists.riseup.net/www/info/whispersystems)
|
||||
Send emails to whispersystems@lists.riseup.net
|
||||
|
||||
77
README.md
@@ -1,70 +1,73 @@
|
||||
TextSecure
|
||||
=================
|
||||
# TextSecure [](https://travis-ci.org/WhisperSystems/TextSecure)
|
||||
|
||||
A secure text messaging application for Android.
|
||||
TextSecure is a messaging app for easy private communicate with friends.
|
||||
|
||||
TextSecure is a replacement for the standard text messaging application, allowing you to send and receive text messages as normal. Additionally, TextSecure provides:
|
||||
TextSecure can use either data (WiFi/3G/4G) or SMS to communicate securely, and all TextSecure
|
||||
messages can also be encrypted locally on your device.
|
||||
|
||||
1. *Local Encryption* -- All text messages, regardless of destination, that are sent or received with TextSecure are stored in an encrypted database on your phone.
|
||||
2. *Wire Encryption* -- When communicating with a recipient who is also using TextSecure, text messages are encrypted during transmission.
|
||||
Currently available on the Play store.
|
||||
|
||||
Current BitHub Payment For Commit:
|
||||
=================
|
||||
[](https://whispersystems.org/blog/bithub/)
|
||||
*[](https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms)*
|
||||
|
||||
|
||||
Bug tracker
|
||||
-----------
|
||||
|
||||
Have a bug? Please create an issue here on GitHub!
|
||||
## Contributing Bug reports
|
||||
We use GitHub for bug tracking. Please search the existing issues for your bug and create a new one if the issue is not yet tracked!
|
||||
|
||||
https://github.com/WhisperSystems/TextSecure/issues
|
||||
|
||||
## Contributing Translations
|
||||
Interested in helping to translate TextSecure? Contribute here:
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
https://www.transifex.com/projects/p/textsecure-official/
|
||||
|
||||
Looking for documentation? Check out the wiki!
|
||||
## Contributing Code
|
||||
Instructions on how to setup your development environment and build TextSecure can be found in [BUILDING.md](https://github.com/WhisperSystems/TextSecure/blob/master/BUILDING.md).
|
||||
|
||||
https://github.com/WhisperSystems/TextSecure/wiki
|
||||
If you're new to the TextSecure codebase, we recommend going through our issues and picking out a simple bug to fix (check the "easy" label in our issues) in order to get yourself familiar.
|
||||
|
||||
Mailing list
|
||||
------------
|
||||
For larger changes and feature ideas, we ask that you propose it on the mailing list for a high-level discussion before implementation.
|
||||
|
||||
Have a question? Ask on our mailing list!
|
||||
This repository is set up with [BitHub](https://whispersystems.org/blog/bithub/), so you can make money for committing to TextSecure. The current BitHub price for an accepted pull request is:
|
||||
|
||||
[](https://whispersystems.org/blog/bithub/)
|
||||
|
||||
## Contributing Ideas
|
||||
Have something you want to say about Open Whisper Systems projects or want to be part of the conversation? Get involved in the mailing list!
|
||||
|
||||
whispersystems@lists.riseup.net
|
||||
|
||||
https://lists.riseup.net/www/info/whispersystems
|
||||
|
||||
Translation
|
||||
------------
|
||||
## Contributing Funds
|
||||
[](https://whispersystems.org/blog/bithub/)
|
||||
|
||||
Interested in helping to translate TextSecure? Contribute here:
|
||||
You can add funds to BitHub to directly help further development efforts.
|
||||
|
||||
https://www.transifex.com/projects/p/textsecure-official/
|
||||
Help
|
||||
====
|
||||
## Support
|
||||
For troubleshooting and questions, please visit our support center!
|
||||
|
||||
Downloads
|
||||
------------
|
||||
http://support.whispersystems.org/
|
||||
|
||||
TextSecure can be downloaded from the Play Store here:
|
||||
## Documentation
|
||||
Looking for documentation? Check out the wiki!
|
||||
|
||||
https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms
|
||||
https://github.com/WhisperSystems/TextSecure/wiki
|
||||
|
||||
Cryptography Notice
|
||||
------------
|
||||
# Legal things
|
||||
## Cryptography Notice
|
||||
|
||||
This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software.
|
||||
BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted.
|
||||
This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software.
|
||||
BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted.
|
||||
See <http://www.wassenaar.org/> for more information.
|
||||
|
||||
The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms.
|
||||
The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms.
|
||||
The form and manner of this distribution makes it eligible for export under the License Exception ENC Technology Software Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Section 740.13) for both object code and source code.
|
||||
|
||||
License
|
||||
---------------------
|
||||
## License
|
||||
|
||||
Copyright 2011 Whisper Systems
|
||||
Copyright 2013 Open WhisperSystems
|
||||
|
||||
Copyright 2013-2014 Open Whisper Systems
|
||||
|
||||
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
|
||||
public class CanonicalAddressDatabaseTest extends InstrumentationTestCase {
|
||||
private static final String AMBIGUOUS_NUMBER = "222-3333";
|
||||
private static final String SPECIFIC_NUMBER = "+49 444 222 3333";
|
||||
private static final String EMAIL = "a@b.fom";
|
||||
private static final String SIMILAR_EMAIL = "a@b.com";
|
||||
private static final String GROUP = "__textsecure_group__!000111222333";
|
||||
private static final String SIMILAR_GROUP = "__textsecure_group__!100111222333";
|
||||
private static final String ALPHA = "T-Mobile";
|
||||
private static final String SIMILAR_ALPHA = "T-Mobila";
|
||||
|
||||
private CanonicalAddressDatabase db;
|
||||
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
this.db = CanonicalAddressDatabase.getInstance(getInstrumentation().getTargetContext());
|
||||
}
|
||||
|
||||
public void tearDown() throws Exception {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw two equivalent numbers (one without locale info, one with full info) at the canonical
|
||||
* address db and see that the caching and DB operations work properly in revealing the right
|
||||
* addresses. This is run twice to ensure cache logic is hit.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public void testNumberAddressUpdates() throws Exception {
|
||||
final long id = db.getCanonicalAddressId(AMBIGUOUS_NUMBER);
|
||||
|
||||
assertThat(db.getAddressFromId(id)).isEqualTo(AMBIGUOUS_NUMBER);
|
||||
assertThat(db.getCanonicalAddressId(SPECIFIC_NUMBER)).isEqualTo(id);
|
||||
assertThat(db.getAddressFromId(id)).isEqualTo(SPECIFIC_NUMBER);
|
||||
assertThat(db.getCanonicalAddressId(AMBIGUOUS_NUMBER)).isEqualTo(id);
|
||||
|
||||
assertThat(db.getCanonicalAddressId(AMBIGUOUS_NUMBER)).isEqualTo(id);
|
||||
assertThat(db.getAddressFromId(id)).isEqualTo(AMBIGUOUS_NUMBER);
|
||||
assertThat(db.getCanonicalAddressId(SPECIFIC_NUMBER)).isEqualTo(id);
|
||||
assertThat(db.getAddressFromId(id)).isEqualTo(SPECIFIC_NUMBER);
|
||||
assertThat(db.getCanonicalAddressId(AMBIGUOUS_NUMBER)).isEqualTo(id);
|
||||
}
|
||||
|
||||
public void testSimilarNumbers() throws Exception {
|
||||
assertThat(db.getCanonicalAddressId("This is a phone number 222-333-444"))
|
||||
.isNotEqualTo(db.getCanonicalAddressId("222-333-4444"));
|
||||
assertThat(db.getCanonicalAddressId("222-333-444"))
|
||||
.isNotEqualTo(db.getCanonicalAddressId("222-333-4444"));
|
||||
assertThat(db.getCanonicalAddressId("222-333-44"))
|
||||
.isNotEqualTo(db.getCanonicalAddressId("222-333-4444"));
|
||||
assertThat(db.getCanonicalAddressId("222-333-4"))
|
||||
.isNotEqualTo(db.getCanonicalAddressId("222-333-4444"));
|
||||
assertThat(db.getCanonicalAddressId("+49 222-333-4444"))
|
||||
.isNotEqualTo(db.getCanonicalAddressId("+1 222-333-4444"));
|
||||
|
||||
assertThat(db.getCanonicalAddressId("1 222-333-4444"))
|
||||
.isEqualTo(db.getCanonicalAddressId("222-333-4444"));
|
||||
assertThat(db.getCanonicalAddressId("1 (222) 333-4444"))
|
||||
.isEqualTo(db.getCanonicalAddressId("222-333-4444"));
|
||||
assertThat(db.getCanonicalAddressId("+12223334444"))
|
||||
.isEqualTo(db.getCanonicalAddressId("222-333-4444"));
|
||||
assertThat(db.getCanonicalAddressId("+1 (222) 333.4444"))
|
||||
.isEqualTo(db.getCanonicalAddressId("222-333-4444"));
|
||||
assertThat(db.getCanonicalAddressId("+49 (222) 333.4444"))
|
||||
.isEqualTo(db.getCanonicalAddressId("222-333-4444"));
|
||||
|
||||
}
|
||||
|
||||
public void testEmailAddresses() throws Exception {
|
||||
final long emailId = db.getCanonicalAddressId(EMAIL);
|
||||
final long similarEmailId = db.getCanonicalAddressId(SIMILAR_EMAIL);
|
||||
|
||||
assertThat(emailId).isNotEqualTo(similarEmailId);
|
||||
|
||||
assertThat(db.getAddressFromId(emailId)).isEqualTo(EMAIL);
|
||||
assertThat(db.getAddressFromId(similarEmailId)).isEqualTo(SIMILAR_EMAIL);
|
||||
}
|
||||
|
||||
public void testGroups() throws Exception {
|
||||
final long groupId = db.getCanonicalAddressId(GROUP);
|
||||
final long similarGroupId = db.getCanonicalAddressId(SIMILAR_GROUP);
|
||||
|
||||
assertThat(groupId).isNotEqualTo(similarGroupId);
|
||||
|
||||
assertThat(db.getAddressFromId(groupId)).isEqualTo(GROUP);
|
||||
assertThat(db.getAddressFromId(similarGroupId)).isEqualTo(SIMILAR_GROUP);
|
||||
}
|
||||
|
||||
public void testAlpha() throws Exception {
|
||||
final long id = db.getCanonicalAddressId(ALPHA);
|
||||
final long similarId = db.getCanonicalAddressId(SIMILAR_ALPHA);
|
||||
|
||||
assertThat(id).isNotEqualTo(similarId);
|
||||
|
||||
assertThat(db.getAddressFromId(id)).isEqualTo(ALPHA);
|
||||
assertThat(db.getAddressFromId(similarId)).isEqualTo(SIMILAR_ALPHA);
|
||||
}
|
||||
|
||||
public void testIsNumber() throws Exception {
|
||||
assertThat(CanonicalAddressDatabase.isNumberAddress("+495556666777")).isTrue();
|
||||
assertThat(CanonicalAddressDatabase.isNumberAddress("(222) 333-4444")).isTrue();
|
||||
assertThat(CanonicalAddressDatabase.isNumberAddress("1 (222) 333-4444")).isTrue();
|
||||
assertThat(CanonicalAddressDatabase.isNumberAddress("T-Mobile123")).isTrue();
|
||||
assertThat(CanonicalAddressDatabase.isNumberAddress("333-4444")).isTrue();
|
||||
assertThat(CanonicalAddressDatabase.isNumberAddress("12345")).isTrue();
|
||||
assertThat(CanonicalAddressDatabase.isNumberAddress("T-Mobile")).isFalse();
|
||||
assertThat(CanonicalAddressDatabase.isNumberAddress("T-Mobile1")).isFalse();
|
||||
assertThat(CanonicalAddressDatabase.isNumberAddress("Wherever bank")).isFalse();
|
||||
assertThat(CanonicalAddressDatabase.isNumberAddress("__textsecure_group__!afafafafafaf")).isFalse();
|
||||
assertThat(CanonicalAddressDatabase.isNumberAddress("email@domain.com")).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import junit.framework.AssertionFailedError;
|
||||
|
||||
import org.whispersystems.textsecure.util.InvalidNumberException;
|
||||
import org.whispersystems.textsecure.util.PhoneNumberFormatter;
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
|
||||
public class PhoneNumberFormatterTest extends AndroidTestCase {
|
||||
private static final String LOCAL_NUMBER = "+15555555555";
|
||||
|
||||
public void testFormatNumberE164() throws Exception, InvalidNumberException {
|
||||
assertThat(PhoneNumberFormatter.formatNumber("(555) 555-5555", LOCAL_NUMBER)).isEqualTo(LOCAL_NUMBER);
|
||||
assertThat(PhoneNumberFormatter.formatNumber("555-5555", LOCAL_NUMBER)).isEqualTo(LOCAL_NUMBER);
|
||||
assertThat(PhoneNumberFormatter.formatNumber("(123) 555-5555", LOCAL_NUMBER)).isNotEqualTo(LOCAL_NUMBER);
|
||||
}
|
||||
|
||||
public void testFormatNumberEmail() throws Exception {
|
||||
try {
|
||||
PhoneNumberFormatter.formatNumber("person@domain.com", LOCAL_NUMBER);
|
||||
throw new AssertionFailedError("should have thrown on email");
|
||||
} catch (InvalidNumberException ine) {
|
||||
// success
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
|
||||
public class UtilTest extends AndroidTestCase {
|
||||
|
||||
}
|
||||
BIN
artwork/ic_send.psd
Normal file
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
BIN
assets/emoji_0_wrapped.png
Normal file
|
After Width: | Height: | Size: 522 KiB |
BIN
assets/emoji_1_wrapped.png
Normal file
|
After Width: | Height: | Size: 629 KiB |
BIN
assets/emoji_2_wrapped.png
Normal file
|
After Width: | Height: | Size: 764 KiB |
BIN
assets/emoji_3_wrapped.png
Normal file
|
After Width: | Height: | Size: 420 KiB |
BIN
assets/emoji_4_wrapped.png
Normal file
|
After Width: | Height: | Size: 429 KiB |
37
build.gradle
@@ -3,11 +3,13 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:0.8.+'
|
||||
classpath 'com.android.tools.build:gradle:0.12.+'
|
||||
classpath files('libs/gradle-witness.jar')
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'android'
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'witness'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@@ -21,15 +23,32 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
compile 'com.actionbarsherlock:actionbarsherlock:4.4.0@aar'
|
||||
compile 'com.android.support:support-v4:19.0.1'
|
||||
compile 'com.google.android.gcm:gcm-client:1.0.2'
|
||||
compile 'com.android.support:support-v4:19.1.0'
|
||||
compile 'se.emilsjolander:stickylistheaders:2.2.0'
|
||||
compile 'com.google.android.gms:play-services:5.0.77'
|
||||
compile 'com.astuetz:pagerslidingtabstrip:1.0.1'
|
||||
|
||||
androidTestCompile 'com.squareup:fest-android:1.0.8'
|
||||
|
||||
compile project(':library')
|
||||
}
|
||||
|
||||
dependencyVerification {
|
||||
verify = [
|
||||
'com.actionbarsherlock:actionbarsherlock:5ab04d74101f70024b222e3ff9c87bee151ec43331b4a2134b6cc08cf8565819',
|
||||
'com.android.support:support-v4:3f40fa7b3a4ead01ce15dce9453b061646e7fe2e7c51cb75ca01ee1e77037f3f',
|
||||
'se.emilsjolander:stickylistheaders:89146b46c96fea0e40200474a2625cda10fe94891e4128f53cdb42375091b9b6',
|
||||
'com.astuetz:pagerslidingtabstrip:f1641396732c7132a7abb837e482e5ee2b0ebb8d10813fc52bbaec2c15c184c2',
|
||||
'com.google.protobuf:protobuf-java:ad9769a22989e688a46af4d3accc348cc501ced22118033230542bc916e33f0b',
|
||||
'com.madgag:sc-light-jdk15on:931f39d351429fb96c2f749e7ecb1a256a8ebbf5edca7995c9cc085b94d1841d',
|
||||
'com.googlecode.libphonenumber:libphonenumber:eba17eae81dd622ea89a00a3a8c025b2f25d342e0d9644c5b62e16f15687c3ab',
|
||||
'org.whispersystems:gson:08f4f7498455d1539c9233e5aac18e9b1805815ef29221572996508eb512fe51',
|
||||
]
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 19
|
||||
buildToolsVersion '19.0.0'
|
||||
buildToolsVersion '19.1.0'
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
@@ -42,11 +61,17 @@ android {
|
||||
manifest.srcFile 'AndroidManifest.xml'
|
||||
java.srcDirs = ['src']
|
||||
resources.srcDirs = ['src']
|
||||
aild.srcDirs = ['src']
|
||||
aidl.srcDirs = ['src']
|
||||
renderscript.srcDirs = ['src']
|
||||
res.srcDirs = ['res']
|
||||
assets.srcDirs = ['assets']
|
||||
}
|
||||
androidTest {
|
||||
java.srcDirs = ['androidTest']
|
||||
resources.srcDirs = ['androidTest']
|
||||
aidl.srcDirs = ['androidTest']
|
||||
renderscript.srcDirs = ['androidTest']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
30
contributing.md
Normal file
@@ -0,0 +1,30 @@
|
||||
##Translations
|
||||
|
||||
Please do not submit issues or pull requests for translation fixes. Anyone can update the translations in [Transifex](https://www.transifex.com/projects/p/textsecure-official/). Please submit your corrections there.
|
||||
|
||||
## Submitting useful bug reports
|
||||
1. Search our issues first to make sure this is not a duplicate.
|
||||
1. Read the [Submitting useful bug reports guide](https://github.com/WhisperSystems/TextSecure/wiki/Submitting-useful-bug-reports) before posting a bug.
|
||||
|
||||
## Development Ideology
|
||||
|
||||
Truths which we believe to be self-evident:
|
||||
|
||||
1. **The answer is not more options.** If you feel compelled to add a
|
||||
preference that's exposed to the user, it's very possible you've made
|
||||
a wrong turn somewhere.
|
||||
1. **The user doesn't know what a key is.** We need to minimize the points
|
||||
at which a user is exposed to this sort of terminology as extremely as
|
||||
possible.
|
||||
1. **There are no power users.** The idea that some users "understand"
|
||||
concepts better than others has proven to be, for the most part, false.
|
||||
If anything, "power users" are more dangerous than the rest, and we
|
||||
should avoid exposing dangerous functionality to them.
|
||||
1. **If it's "like PGP," it's wrong.** PGP is our spirit guide for what
|
||||
not to do.
|
||||
1. **It's an asynchronous world.** Be wary of anything that is
|
||||
anti-asynchronous: ACKs, protocol confirmations, or any protocol-level
|
||||
"advisory" message.
|
||||
1. **There is no such thing as time.** Protocol ideas that require synchronized
|
||||
clocks are doomed to failure.
|
||||
|
||||
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#Sat Dec 21 23:48:05 PST 2013
|
||||
#Mon Jun 09 23:26:49 PDT 2014
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=http\://services.gradle.org/distributions/gradle-1.10-bin.zip
|
||||
distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-all.zip
|
||||
|
||||
@@ -4,7 +4,7 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:0.8.+'
|
||||
classpath 'com.android.tools.build:gradle:0.12.+'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,14 +21,13 @@ repositories {
|
||||
dependencies {
|
||||
compile 'com.google.protobuf:protobuf-java:2.4.1'
|
||||
compile 'com.madgag:sc-light-jdk15on:1.47.0.2'
|
||||
compile 'com.googlecode.libphonenumber:libphonenumber:5.3'
|
||||
compile 'com.googlecode.libphonenumber:libphonenumber:6.1'
|
||||
compile 'org.whispersystems:gson:2.2.4'
|
||||
compile fileTree(dir: 'libs', include: 'armeabi.jar')
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 19
|
||||
buildToolsVersion '19.0.0'
|
||||
buildToolsVersion '19.1.0'
|
||||
|
||||
android {
|
||||
sourceSets {
|
||||
@@ -36,10 +35,11 @@ android {
|
||||
manifest.srcFile 'AndroidManifest.xml'
|
||||
java.srcDirs = ['src']
|
||||
resources.srcDirs = ['src']
|
||||
aild.srcDirs = ['src']
|
||||
aidl.srcDirs = ['src']
|
||||
renderscript.srcDirs = ['src']
|
||||
res.srcDirs = ['res']
|
||||
assets.srcDirs = ['assets']
|
||||
jniLibs.srcDirs = ['libs']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
library/jni/Application.mk
Normal file
@@ -0,0 +1 @@
|
||||
APP_ABI := armeabi armeabi-v7a x86
|
||||
@@ -22,11 +22,16 @@
|
||||
#include "curve25519-donna.h"
|
||||
|
||||
JNIEXPORT jbyteArray JNICALL Java_org_whispersystems_textsecure_crypto_ecc_Curve25519_generatePrivateKey
|
||||
(JNIEnv *env, jclass clazz, jbyteArray random)
|
||||
(JNIEnv *env, jclass clazz, jbyteArray random, jboolean ephemeral)
|
||||
{
|
||||
uint8_t* privateKey = (uint8_t*)(*env)->GetByteArrayElements(env, random, 0);
|
||||
|
||||
privateKey[0] &= 248;
|
||||
|
||||
if (ephemeral) {
|
||||
privateKey[0] |= 1;
|
||||
}
|
||||
|
||||
privateKey[31] &= 127;
|
||||
privateKey[31] |= 64;
|
||||
|
||||
|
||||
@@ -720,9 +720,6 @@ curve25519_donna(u8 *mypublic, const u8 *secret, const u8 *basepoint) {
|
||||
int i;
|
||||
|
||||
for (i = 0; i < 32; ++i) e[i] = secret[i];
|
||||
e[0] &= 248;
|
||||
e[31] &= 127;
|
||||
e[31] |= 64;
|
||||
|
||||
fexpand(bp, basepoint);
|
||||
cmult(x, z, e, bp);
|
||||
|
||||
BIN
library/libs/armeabi-v7a/libcurve25519.so
Executable file
BIN
library/libs/armeabi/libcurve25519.so
Executable file
BIN
library/libs/x86/libcurve25519.so
Executable file
@@ -54,6 +54,13 @@ message SessionStructure {
|
||||
|
||||
optional uint32 remoteRegistrationId = 10;
|
||||
optional uint32 localRegistrationId = 11;
|
||||
|
||||
optional bool needsRefresh = 12;
|
||||
}
|
||||
|
||||
message RecordStructure {
|
||||
optional SessionStructure currentSession = 1;
|
||||
repeated SessionStructure previousSessions = 2;
|
||||
}
|
||||
|
||||
message PreKeyRecordStructure {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
public class DuplicateMessageException extends Exception {
|
||||
public DuplicateMessageException(String s) {
|
||||
super(s);
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import android.os.Parcelable;
|
||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
/**
|
||||
* A class for representing an identity key.
|
||||
@@ -80,14 +79,7 @@ public class IdentityKey implements Parcelable, SerializableKey {
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
if (publicKey.getType() == Curve.NIST_TYPE) {
|
||||
byte[] versionBytes = {0x01};
|
||||
byte[] encodedKey = publicKey.serialize();
|
||||
|
||||
return Util.combine(versionBytes, encodedKey);
|
||||
} else {
|
||||
return publicKey.serialize();
|
||||
}
|
||||
return publicKey.serialize();
|
||||
}
|
||||
|
||||
public String getFingerprint() {
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPrivateKey;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
/**
|
||||
* Represents a session's active KeyPair.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class KeyPair {
|
||||
|
||||
private PublicKey publicKey;
|
||||
private ECPrivateKey privateKey;
|
||||
|
||||
private final MasterCipher masterCipher;
|
||||
|
||||
public KeyPair(int keyPairId, ECKeyPair keyPair, MasterSecret masterSecret) {
|
||||
this.masterCipher = new MasterCipher(masterSecret);
|
||||
this.publicKey = new PublicKey(keyPairId, keyPair.getPublicKey());
|
||||
this.privateKey = keyPair.getPrivateKey();
|
||||
}
|
||||
|
||||
public KeyPair(byte[] bytes, MasterCipher masterCipher) throws InvalidKeyException {
|
||||
this.masterCipher = masterCipher;
|
||||
deserialize(bytes);
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return publicKey.getId();
|
||||
}
|
||||
|
||||
public PublicKey getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public ECPrivateKey getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
public byte[] toBytes() {
|
||||
return serialize();
|
||||
}
|
||||
|
||||
private void deserialize(byte[] bytes) throws InvalidKeyException {
|
||||
this.publicKey = new PublicKey(bytes);
|
||||
byte[] privateKeyBytes = new byte[bytes.length - PublicKey.KEY_SIZE];
|
||||
System.arraycopy(bytes, PublicKey.KEY_SIZE, privateKeyBytes, 0, privateKeyBytes.length);
|
||||
this.privateKey = masterCipher.decryptKey(this.publicKey.getType(), privateKeyBytes);
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
byte[] publicKeyBytes = publicKey.serialize();
|
||||
Log.w("KeyPair", "Serialized public key bytes: " + Hex.toString(publicKeyBytes));
|
||||
byte[] privateKeyBytes = masterCipher.encryptKey(privateKey);
|
||||
return Util.combine(publicKeyBytes, privateKeyBytes);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
public class LegacyMessageException extends Exception {
|
||||
public LegacyMessageException(String s) {
|
||||
super(s);
|
||||
}
|
||||
}
|
||||
@@ -83,11 +83,11 @@ public class MasterCipher {
|
||||
return new String(decodeAndDecryptBytes(body));
|
||||
}
|
||||
|
||||
public ECPrivateKey decryptKey(int type, byte[] key)
|
||||
public ECPrivateKey decryptKey(byte[] key)
|
||||
throws org.whispersystems.textsecure.crypto.InvalidKeyException
|
||||
{
|
||||
try {
|
||||
return Curve.decodePrivatePoint(type, decryptBytes(key));
|
||||
return Curve.decodePrivatePoint(decryptBytes(key));
|
||||
} catch (InvalidMessageException ime) {
|
||||
throw new org.whispersystems.textsecure.crypto.InvalidKeyException(ime);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -49,7 +48,7 @@ public class PreKeyUtil {
|
||||
|
||||
for (int i=0;i<BATCH_SIZE;i++) {
|
||||
int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
|
||||
ECKeyPair keyPair = Curve25519.generateKeyPair();
|
||||
ECKeyPair keyPair = Curve25519.generateKeyPair(true);
|
||||
PreKeyRecord record = new PreKeyRecord(context, masterSecret, preKeyId, keyPair);
|
||||
|
||||
record.save();
|
||||
@@ -70,7 +69,7 @@ public class PreKeyUtil {
|
||||
}
|
||||
}
|
||||
|
||||
ECKeyPair keyPair = Curve25519.generateKeyPair();
|
||||
ECKeyPair keyPair = Curve25519.generateKeyPair(true);
|
||||
PreKeyRecord record = new PreKeyRecord(context, masterSecret, Medium.MAX_VALUE, keyPair);
|
||||
|
||||
record.save();
|
||||
@@ -78,38 +77,38 @@ public class PreKeyUtil {
|
||||
return record;
|
||||
}
|
||||
|
||||
public static List<PreKeyRecord> getPreKeys(Context context, MasterSecret masterSecret) {
|
||||
List<PreKeyRecord> records = new LinkedList<PreKeyRecord>();
|
||||
File directory = getPreKeysDirectory(context);
|
||||
String[] keyRecordIds = directory.list();
|
||||
|
||||
Arrays.sort(keyRecordIds, new PreKeyRecordIdComparator());
|
||||
|
||||
for (String keyRecordId : keyRecordIds) {
|
||||
try {
|
||||
if (!keyRecordId.equals(PreKeyIndex.FILE_NAME) && Integer.parseInt(keyRecordId) != Medium.MAX_VALUE) {
|
||||
records.add(new PreKeyRecord(context, masterSecret, Integer.parseInt(keyRecordId)));
|
||||
}
|
||||
} catch (InvalidKeyIdException e) {
|
||||
Log.w("PreKeyUtil", e);
|
||||
new File(getPreKeysDirectory(context), keyRecordId).delete();
|
||||
} catch (NumberFormatException nfe) {
|
||||
Log.w("PreKeyUtil", nfe);
|
||||
new File(getPreKeysDirectory(context), keyRecordId).delete();
|
||||
}
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
public static void clearPreKeys(Context context) {
|
||||
File directory = getPreKeysDirectory(context);
|
||||
String[] keyRecords = directory.list();
|
||||
|
||||
for (String keyRecord : keyRecords) {
|
||||
new File(directory, keyRecord).delete();
|
||||
}
|
||||
}
|
||||
// public static List<PreKeyRecord> getPreKeys(Context context, MasterSecret masterSecret) {
|
||||
// List<PreKeyRecord> records = new LinkedList<PreKeyRecord>();
|
||||
// File directory = getPreKeysDirectory(context);
|
||||
// String[] keyRecordIds = directory.list();
|
||||
//
|
||||
// Arrays.sort(keyRecordIds, new PreKeyRecordIdComparator());
|
||||
//
|
||||
// for (String keyRecordId : keyRecordIds) {
|
||||
// try {
|
||||
// if (!keyRecordId.equals(PreKeyIndex.FILE_NAME) && Integer.parseInt(keyRecordId) != Medium.MAX_VALUE) {
|
||||
// records.add(new PreKeyRecord(context, masterSecret, Integer.parseInt(keyRecordId)));
|
||||
// }
|
||||
// } catch (InvalidKeyIdException e) {
|
||||
// Log.w("PreKeyUtil", e);
|
||||
// new File(getPreKeysDirectory(context), keyRecordId).delete();
|
||||
// } catch (NumberFormatException nfe) {
|
||||
// Log.w("PreKeyUtil", nfe);
|
||||
// new File(getPreKeysDirectory(context), keyRecordId).delete();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return records;
|
||||
// }
|
||||
//
|
||||
// public static void clearPreKeys(Context context) {
|
||||
// File directory = getPreKeysDirectory(context);
|
||||
// String[] keyRecords = directory.list();
|
||||
//
|
||||
// for (String keyRecord : keyRecords) {
|
||||
// new File(directory, keyRecord).delete();
|
||||
// }
|
||||
// }
|
||||
|
||||
private static void setNextPreKeyId(Context context, int id) {
|
||||
try {
|
||||
@@ -126,7 +125,7 @@ public class PreKeyUtil {
|
||||
try {
|
||||
File nextFile = new File(getPreKeysDirectory(context), PreKeyIndex.FILE_NAME);
|
||||
|
||||
if (nextFile.exists()) {
|
||||
if (!nextFile.exists()) {
|
||||
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
|
||||
} else {
|
||||
InputStreamReader reader = new InputStreamReader(new FileInputStream(nextFile));
|
||||
|
||||
@@ -18,31 +18,258 @@ package org.whispersystems.textsecure.crypto;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
|
||||
import org.whispersystems.textsecure.crypto.protocol.PreKeyWhisperMessage;
|
||||
import org.whispersystems.textsecure.crypto.protocol.WhisperMessage;
|
||||
import org.whispersystems.textsecure.crypto.ratchet.ChainKey;
|
||||
import org.whispersystems.textsecure.crypto.ratchet.MessageKeys;
|
||||
import org.whispersystems.textsecure.crypto.ratchet.RootKey;
|
||||
import org.whispersystems.textsecure.storage.RecipientDevice;
|
||||
import org.whispersystems.textsecure.storage.SessionRecordV1;
|
||||
import org.whispersystems.textsecure.storage.SessionRecordV2;
|
||||
import org.whispersystems.textsecure.storage.SessionState;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
public abstract class SessionCipher {
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
|
||||
protected static final Object SESSION_LOCK = new Object();
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public abstract CiphertextMessage encrypt(byte[] paddedMessage);
|
||||
public abstract byte[] decrypt(byte[] decodedMessage) throws InvalidMessageException;
|
||||
public abstract int getRemoteRegistrationId();
|
||||
public class SessionCipher {
|
||||
|
||||
private static final Object SESSION_LOCK = new Object();
|
||||
|
||||
private final Context context;
|
||||
private final MasterSecret masterSecret;
|
||||
private final RecipientDevice recipient;
|
||||
|
||||
public static SessionCipher createFor(Context context,
|
||||
MasterSecret masterSecret,
|
||||
RecipientDevice recipient)
|
||||
{
|
||||
if (SessionRecordV2.hasSession(context, masterSecret, recipient)) {
|
||||
return new SessionCipherV2(context, masterSecret, recipient);
|
||||
} else if (SessionRecordV1.hasSession(context, recipient.getRecipientId())) {
|
||||
return new SessionCipherV1(context, masterSecret, recipient.getRecipient());
|
||||
return new SessionCipher(context, masterSecret, recipient);
|
||||
} else {
|
||||
throw new AssertionError("Attempt to initialize cipher for non-existing session.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private SessionCipher(Context context, MasterSecret masterSecret, RecipientDevice recipient) {
|
||||
this.recipient = recipient;
|
||||
this.masterSecret = masterSecret;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public CiphertextMessage encrypt(byte[] paddedMessage) {
|
||||
synchronized (SESSION_LOCK) {
|
||||
SessionRecordV2 sessionRecord = getSessionRecord();
|
||||
SessionState sessionState = sessionRecord.getSessionState();
|
||||
ChainKey chainKey = sessionState.getSenderChainKey();
|
||||
MessageKeys messageKeys = chainKey.getMessageKeys();
|
||||
ECPublicKey senderEphemeral = sessionState.getSenderEphemeral();
|
||||
int previousCounter = sessionState.getPreviousCounter();
|
||||
|
||||
byte[] ciphertextBody = getCiphertext(messageKeys, paddedMessage);
|
||||
CiphertextMessage ciphertextMessage = new WhisperMessage(messageKeys.getMacKey(),
|
||||
senderEphemeral, chainKey.getIndex(),
|
||||
previousCounter, ciphertextBody);
|
||||
|
||||
if (sessionState.hasPendingPreKey()) {
|
||||
Pair<Integer, ECPublicKey> pendingPreKey = sessionState.getPendingPreKey();
|
||||
int localRegistrationId = sessionState.getLocalRegistrationId();
|
||||
|
||||
ciphertextMessage = new PreKeyWhisperMessage(localRegistrationId, pendingPreKey.first,
|
||||
pendingPreKey.second,
|
||||
sessionState.getLocalIdentityKey(),
|
||||
(WhisperMessage) ciphertextMessage);
|
||||
}
|
||||
|
||||
sessionState.setSenderChainKey(chainKey.getNextChainKey());
|
||||
sessionRecord.save();
|
||||
|
||||
return ciphertextMessage;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decrypt(byte[] decodedMessage)
|
||||
throws InvalidMessageException, DuplicateMessageException, LegacyMessageException
|
||||
{
|
||||
synchronized (SESSION_LOCK) {
|
||||
SessionRecordV2 sessionRecord = getSessionRecord();
|
||||
SessionState sessionState = sessionRecord.getSessionState();
|
||||
List<SessionState> previousStates = sessionRecord.getPreviousSessions();
|
||||
|
||||
try {
|
||||
byte[] plaintext = decrypt(sessionState, decodedMessage);
|
||||
sessionRecord.save();
|
||||
|
||||
return plaintext;
|
||||
} catch (InvalidMessageException e) {
|
||||
Log.w("SessionCipherV2", e);
|
||||
}
|
||||
|
||||
for (SessionState previousState : previousStates) {
|
||||
try {
|
||||
Log.w("SessionCipherV2", "Attempting decrypt on previous state...");
|
||||
byte[] plaintext = decrypt(previousState, decodedMessage);
|
||||
sessionRecord.save();
|
||||
|
||||
return plaintext;
|
||||
} catch (InvalidMessageException e) {
|
||||
Log.w("SessionCipherV2", e);
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidMessageException("No valid sessions.");
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decrypt(SessionState sessionState, byte[] decodedMessage)
|
||||
throws InvalidMessageException, DuplicateMessageException, LegacyMessageException
|
||||
{
|
||||
if (!sessionState.hasSenderChain()) {
|
||||
throw new InvalidMessageException("Uninitialized session!");
|
||||
}
|
||||
|
||||
WhisperMessage ciphertextMessage = new WhisperMessage(decodedMessage);
|
||||
ECPublicKey theirEphemeral = ciphertextMessage.getSenderEphemeral();
|
||||
int counter = ciphertextMessage.getCounter();
|
||||
ChainKey chainKey = getOrCreateChainKey(sessionState, theirEphemeral);
|
||||
MessageKeys messageKeys = getOrCreateMessageKeys(sessionState, theirEphemeral,
|
||||
chainKey, counter);
|
||||
|
||||
ciphertextMessage.verifyMac(messageKeys.getMacKey());
|
||||
|
||||
byte[] plaintext = getPlaintext(messageKeys, ciphertextMessage.getBody());
|
||||
|
||||
sessionState.clearPendingPreKey();
|
||||
|
||||
return plaintext;
|
||||
|
||||
}
|
||||
|
||||
public int getRemoteRegistrationId() {
|
||||
synchronized (SESSION_LOCK) {
|
||||
SessionRecordV2 sessionRecord = getSessionRecord();
|
||||
return sessionRecord.getSessionState().getRemoteRegistrationId();
|
||||
}
|
||||
}
|
||||
|
||||
private ChainKey getOrCreateChainKey(SessionState sessionState, ECPublicKey theirEphemeral)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
try {
|
||||
if (sessionState.hasReceiverChain(theirEphemeral)) {
|
||||
return sessionState.getReceiverChainKey(theirEphemeral);
|
||||
} else {
|
||||
RootKey rootKey = sessionState.getRootKey();
|
||||
ECKeyPair ourEphemeral = sessionState.getSenderEphemeralPair();
|
||||
Pair<RootKey, ChainKey> receiverChain = rootKey.createChain(theirEphemeral, ourEphemeral);
|
||||
ECKeyPair ourNewEphemeral = Curve.generateKeyPair(true);
|
||||
Pair<RootKey, ChainKey> senderChain = receiverChain.first.createChain(theirEphemeral, ourNewEphemeral);
|
||||
|
||||
sessionState.setRootKey(senderChain.first);
|
||||
sessionState.addReceiverChain(theirEphemeral, receiverChain.second);
|
||||
sessionState.setPreviousCounter(sessionState.getSenderChainKey().getIndex()-1);
|
||||
sessionState.setSenderChain(ourNewEphemeral, senderChain.second);
|
||||
|
||||
return receiverChain.second;
|
||||
}
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private MessageKeys getOrCreateMessageKeys(SessionState sessionState,
|
||||
ECPublicKey theirEphemeral,
|
||||
ChainKey chainKey, int counter)
|
||||
throws InvalidMessageException, DuplicateMessageException
|
||||
{
|
||||
if (chainKey.getIndex() > counter) {
|
||||
if (sessionState.hasMessageKeys(theirEphemeral, counter)) {
|
||||
return sessionState.removeMessageKeys(theirEphemeral, counter);
|
||||
} else {
|
||||
throw new DuplicateMessageException("Received message with old counter: " +
|
||||
chainKey.getIndex() + " , " + counter);
|
||||
}
|
||||
}
|
||||
|
||||
if (chainKey.getIndex() - counter > 2000) {
|
||||
throw new InvalidMessageException("Over 2000 messages into the future!");
|
||||
}
|
||||
|
||||
while (chainKey.getIndex() < counter) {
|
||||
MessageKeys messageKeys = chainKey.getMessageKeys();
|
||||
sessionState.setMessageKeys(theirEphemeral, messageKeys);
|
||||
chainKey = chainKey.getNextChainKey();
|
||||
}
|
||||
|
||||
sessionState.setReceiverChainKey(theirEphemeral, chainKey.getNextChainKey());
|
||||
return chainKey.getMessageKeys();
|
||||
}
|
||||
|
||||
private byte[] getCiphertext(MessageKeys messageKeys, byte[] plaintext) {
|
||||
try {
|
||||
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE,
|
||||
messageKeys.getCipherKey(),
|
||||
messageKeys.getCounter());
|
||||
|
||||
return cipher.doFinal(plaintext);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getPlaintext(MessageKeys messageKeys, byte[] cipherText) {
|
||||
try {
|
||||
Cipher cipher = getCipher(Cipher.DECRYPT_MODE,
|
||||
messageKeys.getCipherKey(),
|
||||
messageKeys.getCounter());
|
||||
return cipher.doFinal(cipherText);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Cipher getCipher(int mode, SecretKeySpec key, int counter) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
|
||||
byte[] ivBytes = new byte[16];
|
||||
Conversions.intToByteArray(ivBytes, 0, counter);
|
||||
|
||||
IvParameterSpec iv = new IvParameterSpec(ivBytes);
|
||||
cipher.init(mode, key, iv);
|
||||
|
||||
return cipher;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (java.security.InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private SessionRecordV2 getSessionRecord() {
|
||||
return new SessionRecordV2(context, masterSecret, recipient);
|
||||
}
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets;
|
||||
import org.whispersystems.textsecure.crypto.kdf.NKDF;
|
||||
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
|
||||
import org.whispersystems.textsecure.crypto.protocol.WhisperMessageV1;
|
||||
import org.whispersystems.textsecure.storage.CanonicalRecipient;
|
||||
import org.whispersystems.textsecure.storage.RecipientDevice;
|
||||
import org.whispersystems.textsecure.storage.InvalidKeyIdException;
|
||||
import org.whispersystems.textsecure.storage.LocalKeyRecord;
|
||||
import org.whispersystems.textsecure.storage.RemoteKeyRecord;
|
||||
import org.whispersystems.textsecure.storage.SessionKey;
|
||||
import org.whispersystems.textsecure.storage.SessionRecordV1;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class SessionCipherV1 extends SessionCipher {
|
||||
|
||||
private final Context context;
|
||||
private final MasterSecret masterSecret;
|
||||
private final CanonicalRecipient recipient;
|
||||
|
||||
public SessionCipherV1(Context context,
|
||||
MasterSecret masterSecret,
|
||||
CanonicalRecipient recipient)
|
||||
{
|
||||
this.context = context;
|
||||
this.masterSecret = masterSecret;
|
||||
this.recipient = recipient;
|
||||
}
|
||||
|
||||
public CiphertextMessage encrypt(byte[] paddedMessageBody) {
|
||||
synchronized (SESSION_LOCK) {
|
||||
SessionCipherContext encryptionContext = getEncryptionContext();
|
||||
byte[] cipherText = getCiphertext(paddedMessageBody,
|
||||
encryptionContext.getSessionKey().getCipherKey(),
|
||||
encryptionContext.getSessionRecord().getCounter());
|
||||
|
||||
encryptionContext.getSessionRecord().setSessionKey(encryptionContext.getSessionKey());
|
||||
encryptionContext.getSessionRecord().incrementCounter();
|
||||
encryptionContext.getSessionRecord().save();
|
||||
|
||||
return new WhisperMessageV1(encryptionContext, cipherText);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decrypt(byte[] decodedCiphertext) throws InvalidMessageException {
|
||||
synchronized (SESSION_LOCK) {
|
||||
WhisperMessageV1 message = new WhisperMessageV1(decodedCiphertext);
|
||||
SessionCipherContext decryptionContext = getDecryptionContext(message);
|
||||
|
||||
message.verifyMac(decryptionContext);
|
||||
|
||||
byte[] plaintextWithPadding = getPlaintext(message.getBody(),
|
||||
decryptionContext.getSessionKey().getCipherKey(),
|
||||
decryptionContext.getCounter());
|
||||
|
||||
decryptionContext.getRemoteKeyRecord().updateCurrentRemoteKey(decryptionContext.getNextKey());
|
||||
decryptionContext.getRemoteKeyRecord().save();
|
||||
|
||||
decryptionContext.getLocalKeyRecord().advanceKeyIfNecessary(decryptionContext.getRecipientKeyId());
|
||||
decryptionContext.getLocalKeyRecord().save();
|
||||
|
||||
decryptionContext.getSessionRecord().setSessionKey(decryptionContext.getSessionKey());
|
||||
decryptionContext.getSessionRecord().save();
|
||||
|
||||
return plaintextWithPadding;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRemoteRegistrationId() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private SessionCipherContext getEncryptionContext() {
|
||||
try {
|
||||
KeyRecords records = getKeyRecords(context, masterSecret, recipient);
|
||||
int localKeyId = records.getLocalKeyRecord().getCurrentKeyPair().getId();
|
||||
int remoteKeyId = records.getRemoteKeyRecord().getCurrentRemoteKey().getId();
|
||||
int sessionVersion = records.getSessionRecord().getSessionVersion();
|
||||
SessionKey sessionKey = getSessionKey(masterSecret, Cipher.ENCRYPT_MODE,
|
||||
records, localKeyId, remoteKeyId);
|
||||
PublicKey nextKey = records.getLocalKeyRecord().getNextKeyPair().getPublicKey();
|
||||
int counter = records.getSessionRecord().getCounter();
|
||||
|
||||
|
||||
return new SessionCipherContext(records, sessionKey, localKeyId, remoteKeyId,
|
||||
nextKey, counter, sessionVersion);
|
||||
} catch (InvalidKeyIdException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public SessionCipherContext getDecryptionContext(WhisperMessageV1 message)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
try {
|
||||
KeyRecords records = getKeyRecords(context, masterSecret, recipient);
|
||||
int messageVersion = message.getCurrentVersion();
|
||||
int recipientKeyId = message.getReceiverKeyId();
|
||||
int senderKeyId = message.getSenderKeyId();
|
||||
PublicKey nextKey = new PublicKey(message.getNextKeyBytes());
|
||||
int counter = message.getCounter();
|
||||
|
||||
if (messageVersion < records.getSessionRecord().getSessionVersion()) {
|
||||
throw new InvalidMessageException("Message version: " + messageVersion +
|
||||
" but negotiated session version: " +
|
||||
records.getSessionRecord().getSessionVersion());
|
||||
}
|
||||
|
||||
SessionKey sessionKey = getSessionKey(masterSecret, Cipher.DECRYPT_MODE,
|
||||
records, recipientKeyId, senderKeyId);
|
||||
|
||||
return new SessionCipherContext(records, sessionKey, senderKeyId,
|
||||
recipientKeyId, nextKey, counter,
|
||||
messageVersion);
|
||||
} catch (InvalidKeyIdException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getCiphertext(byte[] message, SecretKeySpec key, int counter) {
|
||||
try {
|
||||
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, key, counter);
|
||||
return cipher.doFinal(message);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getPlaintext(byte[] cipherText, SecretKeySpec key, int counter) {
|
||||
try {
|
||||
Cipher cipher = getCipher(Cipher.DECRYPT_MODE, key, counter);
|
||||
return cipher.doFinal(cipherText);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Cipher getCipher(int mode, SecretKeySpec key, int counter) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
|
||||
byte[] ivBytes = new byte[16];
|
||||
Conversions.mediumToByteArray(ivBytes, 0, counter);
|
||||
|
||||
IvParameterSpec iv = new IvParameterSpec(ivBytes);
|
||||
cipher.init(mode, key, iv);
|
||||
|
||||
return cipher;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalArgumentException("AES Not Supported!");
|
||||
} catch (NoSuchPaddingException e) {
|
||||
throw new IllegalArgumentException("NoPadding Not Supported!");
|
||||
} catch (java.security.InvalidKeyException e) {
|
||||
Log.w("SessionCipher", e);
|
||||
throw new IllegalArgumentException("Invaid Key?");
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
Log.w("SessionCipher", e);
|
||||
throw new IllegalArgumentException("Bad IV?");
|
||||
}
|
||||
}
|
||||
|
||||
private SessionKey getSessionKey(MasterSecret masterSecret, int mode,
|
||||
KeyRecords records,
|
||||
int localKeyId, int remoteKeyId)
|
||||
throws InvalidKeyIdException, InvalidKeyException
|
||||
{
|
||||
Log.w("SessionCipher", "Getting session key for local: " + localKeyId + " remote: " + remoteKeyId);
|
||||
SessionKey sessionKey = records.getSessionRecord().getSessionKey(mode, localKeyId, remoteKeyId);
|
||||
|
||||
if (sessionKey != null)
|
||||
return sessionKey;
|
||||
|
||||
DerivedSecrets derivedSecrets = calculateSharedSecret(mode, records, localKeyId, remoteKeyId);
|
||||
|
||||
return new SessionKey(mode, localKeyId, remoteKeyId, derivedSecrets.getCipherKey(),
|
||||
derivedSecrets.getMacKey(), masterSecret);
|
||||
}
|
||||
|
||||
private DerivedSecrets calculateSharedSecret(int mode, KeyRecords records,
|
||||
int localKeyId, int remoteKeyId)
|
||||
throws InvalidKeyIdException, InvalidKeyException
|
||||
{
|
||||
NKDF kdf = new NKDF();
|
||||
KeyPair localKeyPair = records.getLocalKeyRecord().getKeyPairForId(localKeyId);
|
||||
ECPublicKey remoteKey = records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getKey();
|
||||
byte[] sharedSecret = Curve.calculateAgreement(remoteKey, localKeyPair.getPrivateKey());
|
||||
boolean isLowEnd = isLowEnd(records, localKeyId, remoteKeyId);
|
||||
|
||||
isLowEnd = (mode == Cipher.ENCRYPT_MODE ? isLowEnd : !isLowEnd);
|
||||
|
||||
return kdf.deriveSecrets(sharedSecret, isLowEnd);
|
||||
}
|
||||
|
||||
private boolean isLowEnd(KeyRecords records, int localKeyId, int remoteKeyId)
|
||||
throws InvalidKeyIdException
|
||||
{
|
||||
ECPublicKey localPublic = records.getLocalKeyRecord().getKeyPairForId(localKeyId).getPublicKey().getKey();
|
||||
ECPublicKey remotePublic = records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getKey();
|
||||
|
||||
return localPublic.compareTo(remotePublic) < 0;
|
||||
}
|
||||
|
||||
private KeyRecords getKeyRecords(Context context, MasterSecret masterSecret,
|
||||
CanonicalRecipient recipient)
|
||||
{
|
||||
LocalKeyRecord localKeyRecord = new LocalKeyRecord(context, masterSecret, recipient);
|
||||
RemoteKeyRecord remoteKeyRecord = new RemoteKeyRecord(context, recipient);
|
||||
SessionRecordV1 sessionRecord = new SessionRecordV1(context, masterSecret, recipient);
|
||||
return new KeyRecords(localKeyRecord, remoteKeyRecord, sessionRecord);
|
||||
}
|
||||
|
||||
private static class KeyRecords {
|
||||
|
||||
private final LocalKeyRecord localKeyRecord;
|
||||
private final RemoteKeyRecord remoteKeyRecord;
|
||||
private final SessionRecordV1 sessionRecord;
|
||||
|
||||
public KeyRecords(LocalKeyRecord localKeyRecord,
|
||||
RemoteKeyRecord remoteKeyRecord,
|
||||
SessionRecordV1 sessionRecord)
|
||||
{
|
||||
this.localKeyRecord = localKeyRecord;
|
||||
this.remoteKeyRecord = remoteKeyRecord;
|
||||
this.sessionRecord = sessionRecord;
|
||||
}
|
||||
|
||||
private LocalKeyRecord getLocalKeyRecord() {
|
||||
return localKeyRecord;
|
||||
}
|
||||
|
||||
private RemoteKeyRecord getRemoteKeyRecord() {
|
||||
return remoteKeyRecord;
|
||||
}
|
||||
|
||||
private SessionRecordV1 getSessionRecord() {
|
||||
return sessionRecord;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SessionCipherContext {
|
||||
|
||||
private final LocalKeyRecord localKeyRecord;
|
||||
private final RemoteKeyRecord remoteKeyRecord;
|
||||
private final SessionRecordV1 sessionRecord;
|
||||
private final SessionKey sessionKey;
|
||||
private final int senderKeyId;
|
||||
private final int recipientKeyId;
|
||||
private final PublicKey nextKey;
|
||||
private final int counter;
|
||||
private final int messageVersion;
|
||||
|
||||
public SessionCipherContext(KeyRecords records,
|
||||
SessionKey sessionKey,
|
||||
int senderKeyId,
|
||||
int receiverKeyId,
|
||||
PublicKey nextKey,
|
||||
int counter,
|
||||
int messageVersion)
|
||||
{
|
||||
this.localKeyRecord = records.getLocalKeyRecord();
|
||||
this.remoteKeyRecord = records.getRemoteKeyRecord();
|
||||
this.sessionRecord = records.getSessionRecord();
|
||||
this.sessionKey = sessionKey;
|
||||
this.senderKeyId = senderKeyId;
|
||||
this.recipientKeyId = receiverKeyId;
|
||||
this.nextKey = nextKey;
|
||||
this.counter = counter;
|
||||
this.messageVersion = messageVersion;
|
||||
}
|
||||
|
||||
public LocalKeyRecord getLocalKeyRecord() {
|
||||
return localKeyRecord;
|
||||
}
|
||||
|
||||
public RemoteKeyRecord getRemoteKeyRecord() {
|
||||
return remoteKeyRecord;
|
||||
}
|
||||
|
||||
public SessionRecordV1 getSessionRecord() {
|
||||
return sessionRecord;
|
||||
}
|
||||
|
||||
public SessionKey getSessionKey() {
|
||||
return sessionKey;
|
||||
}
|
||||
|
||||
public PublicKey getNextKey() {
|
||||
return nextKey;
|
||||
}
|
||||
|
||||
public int getCounter() {
|
||||
return counter;
|
||||
}
|
||||
|
||||
public int getSenderKeyId() {
|
||||
return senderKeyId;
|
||||
}
|
||||
|
||||
public int getRecipientKeyId() {
|
||||
return recipientKeyId;
|
||||
}
|
||||
|
||||
public int getMessageVersion() {
|
||||
return messageVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
|
||||
import org.whispersystems.textsecure.crypto.protocol.PreKeyWhisperMessage;
|
||||
import org.whispersystems.textsecure.crypto.protocol.WhisperMessageV2;
|
||||
import org.whispersystems.textsecure.crypto.ratchet.ChainKey;
|
||||
import org.whispersystems.textsecure.crypto.ratchet.MessageKeys;
|
||||
import org.whispersystems.textsecure.crypto.ratchet.RootKey;
|
||||
import org.whispersystems.textsecure.storage.RecipientDevice;
|
||||
import org.whispersystems.textsecure.storage.SessionRecordV2;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class SessionCipherV2 extends SessionCipher {
|
||||
|
||||
private final Context context;
|
||||
private final MasterSecret masterSecret;
|
||||
private final RecipientDevice recipient;
|
||||
|
||||
public SessionCipherV2(Context context,
|
||||
MasterSecret masterSecret,
|
||||
RecipientDevice recipient)
|
||||
{
|
||||
this.context = context;
|
||||
this.masterSecret = masterSecret;
|
||||
this.recipient = recipient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CiphertextMessage encrypt(byte[] paddedMessage) {
|
||||
synchronized (SESSION_LOCK) {
|
||||
SessionRecordV2 sessionRecord = getSessionRecord();
|
||||
ChainKey chainKey = sessionRecord.getSenderChainKey();
|
||||
MessageKeys messageKeys = chainKey.getMessageKeys();
|
||||
ECPublicKey senderEphemeral = sessionRecord.getSenderEphemeral();
|
||||
int previousCounter = sessionRecord.getPreviousCounter();
|
||||
|
||||
byte[] ciphertextBody = getCiphertext(messageKeys, paddedMessage);
|
||||
CiphertextMessage ciphertextMessage = new WhisperMessageV2(messageKeys.getMacKey(),
|
||||
senderEphemeral, chainKey.getIndex(),
|
||||
previousCounter, ciphertextBody);
|
||||
|
||||
if (sessionRecord.hasPendingPreKey()) {
|
||||
Pair<Integer, ECPublicKey> pendingPreKey = sessionRecord.getPendingPreKey();
|
||||
int localRegistrationId = sessionRecord.getLocalRegistrationId();
|
||||
|
||||
ciphertextMessage = new PreKeyWhisperMessage(localRegistrationId, pendingPreKey.first,
|
||||
pendingPreKey.second,
|
||||
sessionRecord.getLocalIdentityKey(),
|
||||
(WhisperMessageV2) ciphertextMessage);
|
||||
}
|
||||
|
||||
sessionRecord.setSenderChainKey(chainKey.getNextChainKey());
|
||||
sessionRecord.save();
|
||||
|
||||
return ciphertextMessage;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decrypt(byte[] decodedMessage) throws InvalidMessageException {
|
||||
synchronized (SESSION_LOCK) {
|
||||
SessionRecordV2 sessionRecord = getSessionRecord();
|
||||
WhisperMessageV2 ciphertextMessage = new WhisperMessageV2(decodedMessage);
|
||||
ECPublicKey theirEphemeral = ciphertextMessage.getSenderEphemeral();
|
||||
int counter = ciphertextMessage.getCounter();
|
||||
ChainKey chainKey = getOrCreateChainKey(sessionRecord, theirEphemeral);
|
||||
MessageKeys messageKeys = getOrCreateMessageKeys(sessionRecord, theirEphemeral,
|
||||
chainKey, counter);
|
||||
|
||||
ciphertextMessage.verifyMac(messageKeys.getMacKey());
|
||||
|
||||
byte[] plaintext = getPlaintext(messageKeys, ciphertextMessage.getBody());
|
||||
|
||||
sessionRecord.clearPendingPreKey();
|
||||
sessionRecord.save();
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRemoteRegistrationId() {
|
||||
synchronized (SESSION_LOCK) {
|
||||
SessionRecordV2 sessionRecord = getSessionRecord();
|
||||
return sessionRecord.getRemoteRegistrationId();
|
||||
}
|
||||
}
|
||||
|
||||
private ChainKey getOrCreateChainKey(SessionRecordV2 sessionRecord, ECPublicKey theirEphemeral)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
try {
|
||||
if (sessionRecord.hasReceiverChain(theirEphemeral)) {
|
||||
return sessionRecord.getReceiverChainKey(theirEphemeral);
|
||||
} else {
|
||||
RootKey rootKey = sessionRecord.getRootKey();
|
||||
ECKeyPair ourEphemeral = sessionRecord.getSenderEphemeralPair();
|
||||
Pair<RootKey, ChainKey> receiverChain = rootKey.createChain(theirEphemeral, ourEphemeral);
|
||||
ECKeyPair ourNewEphemeral = Curve.generateKeyPairForType(Curve.DJB_TYPE);
|
||||
Pair<RootKey, ChainKey> senderChain = receiverChain.first.createChain(theirEphemeral, ourNewEphemeral);
|
||||
|
||||
sessionRecord.setRootKey(senderChain.first);
|
||||
sessionRecord.addReceiverChain(theirEphemeral, receiverChain.second);
|
||||
sessionRecord.setPreviousCounter(sessionRecord.getSenderChainKey().getIndex()-1);
|
||||
sessionRecord.setSenderChain(ourNewEphemeral, senderChain.second);
|
||||
|
||||
return receiverChain.second;
|
||||
}
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private MessageKeys getOrCreateMessageKeys(SessionRecordV2 sessionRecord,
|
||||
ECPublicKey theirEphemeral,
|
||||
ChainKey chainKey, int counter)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
if (chainKey.getIndex() > counter) {
|
||||
if (sessionRecord.hasMessageKeys(theirEphemeral, counter)) {
|
||||
return sessionRecord.removeMessageKeys(theirEphemeral, counter);
|
||||
} else {
|
||||
throw new InvalidMessageException("Received message with old counter!");
|
||||
}
|
||||
}
|
||||
|
||||
if (chainKey.getIndex() - counter > 500) {
|
||||
throw new InvalidMessageException("Over 500 messages into the future!");
|
||||
}
|
||||
|
||||
while (chainKey.getIndex() < counter) {
|
||||
MessageKeys messageKeys = chainKey.getMessageKeys();
|
||||
sessionRecord.setMessageKeys(theirEphemeral, messageKeys);
|
||||
chainKey = chainKey.getNextChainKey();
|
||||
}
|
||||
|
||||
sessionRecord.setReceiverChainKey(theirEphemeral, chainKey.getNextChainKey());
|
||||
return chainKey.getMessageKeys();
|
||||
}
|
||||
|
||||
private byte[] getCiphertext(MessageKeys messageKeys, byte[] plaintext) {
|
||||
try {
|
||||
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE,
|
||||
messageKeys.getCipherKey(),
|
||||
messageKeys.getCounter());
|
||||
|
||||
return cipher.doFinal(plaintext);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getPlaintext(MessageKeys messageKeys, byte[] cipherText) {
|
||||
try {
|
||||
Cipher cipher = getCipher(Cipher.DECRYPT_MODE,
|
||||
messageKeys.getCipherKey(),
|
||||
messageKeys.getCounter());
|
||||
return cipher.doFinal(cipherText);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Cipher getCipher(int mode, SecretKeySpec key, int counter) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
|
||||
byte[] ivBytes = new byte[16];
|
||||
Conversions.intToByteArray(ivBytes, 0, counter);
|
||||
|
||||
IvParameterSpec iv = new IvParameterSpec(ivBytes);
|
||||
cipher.init(mode, key, iv);
|
||||
|
||||
return cipher;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (java.security.InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private SessionRecordV2 getSessionRecord() {
|
||||
return new SessionRecordV2(context, masterSecret, recipient);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,26 +21,10 @@ import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
|
||||
|
||||
public class Curve {
|
||||
|
||||
public static final int NIST_TYPE = 0x02;
|
||||
private static final int NIST_TYPE2 = 0x03;
|
||||
public static final int DJB_TYPE = 0x05;
|
||||
|
||||
public static ECKeyPair generateKeyPairForType(int keyType) {
|
||||
if (keyType == DJB_TYPE) {
|
||||
return Curve25519.generateKeyPair();
|
||||
} else if (keyType == NIST_TYPE || keyType == NIST_TYPE2) {
|
||||
return CurveP256.generateKeyPair();
|
||||
} else {
|
||||
throw new AssertionError("Bad key type: " + keyType);
|
||||
}
|
||||
}
|
||||
|
||||
public static ECKeyPair generateKeyPairForSession(int messageVersion) {
|
||||
if (messageVersion <= CiphertextMessage.LEGACY_VERSION) {
|
||||
return generateKeyPairForType(NIST_TYPE);
|
||||
} else {
|
||||
return generateKeyPairForType(DJB_TYPE);
|
||||
}
|
||||
public static ECKeyPair generateKeyPair(boolean ephemeral) {
|
||||
return Curve25519.generateKeyPair(ephemeral);
|
||||
}
|
||||
|
||||
public static ECPublicKey decodePoint(byte[] bytes, int offset)
|
||||
@@ -50,21 +34,13 @@ public class Curve {
|
||||
|
||||
if (type == DJB_TYPE) {
|
||||
return Curve25519.decodePoint(bytes, offset);
|
||||
} else if (type == NIST_TYPE || type == NIST_TYPE2) {
|
||||
return CurveP256.decodePoint(bytes, offset);
|
||||
} else {
|
||||
throw new InvalidKeyException("Unknown key type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
public static ECPrivateKey decodePrivatePoint(int type, byte[] bytes) {
|
||||
if (type == DJB_TYPE) {
|
||||
return new DjbECPrivateKey(bytes);
|
||||
} else if (type == NIST_TYPE || type == NIST_TYPE2) {
|
||||
return CurveP256.decodePrivatePoint(bytes);
|
||||
} else {
|
||||
throw new AssertionError("Bad key type: " + type);
|
||||
}
|
||||
public static ECPrivateKey decodePrivatePoint(byte[] bytes) {
|
||||
return new DjbECPrivateKey(bytes);
|
||||
}
|
||||
|
||||
public static byte[] calculateAgreement(ECPublicKey publicKey, ECPrivateKey privateKey)
|
||||
@@ -76,8 +52,6 @@ public class Curve {
|
||||
|
||||
if (publicKey.getType() == DJB_TYPE) {
|
||||
return Curve25519.calculateAgreement(publicKey, privateKey);
|
||||
} else if (publicKey.getType() == NIST_TYPE || publicKey.getType() == NIST_TYPE2) {
|
||||
return CurveP256.calculateAgreement(publicKey, privateKey);
|
||||
} else {
|
||||
throw new InvalidKeyException("Unknown type: " + publicKey.getType());
|
||||
}
|
||||
|
||||
@@ -37,10 +37,10 @@ public class Curve25519 {
|
||||
|
||||
private static native byte[] calculateAgreement(byte[] ourPrivate, byte[] theirPublic);
|
||||
private static native byte[] generatePublicKey(byte[] privateKey);
|
||||
private static native byte[] generatePrivateKey(byte[] random);
|
||||
private static native byte[] generatePrivateKey(byte[] random, boolean ephemeral);
|
||||
|
||||
public static ECKeyPair generateKeyPair() {
|
||||
byte[] privateKey = generatePrivateKey();
|
||||
public static ECKeyPair generateKeyPair(boolean ephemeral) {
|
||||
byte[] privateKey = generatePrivateKey(ephemeral);
|
||||
byte[] publicKey = generatePublicKey(privateKey);
|
||||
|
||||
return new ECKeyPair(new DjbECPublicKey(publicKey), new DjbECPrivateKey(privateKey));
|
||||
@@ -65,11 +65,11 @@ public class Curve25519 {
|
||||
return new DjbECPublicKey(keyBytes);
|
||||
}
|
||||
|
||||
private static byte[] generatePrivateKey() {
|
||||
private static byte[] generatePrivateKey(boolean ephemeral) {
|
||||
byte[] privateKey = new byte[32];
|
||||
random.nextBytes(privateKey);
|
||||
|
||||
return generatePrivateKey(privateKey);
|
||||
return generatePrivateKey(privateKey, ephemeral);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecure.crypto.ecc;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.spongycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.spongycastle.crypto.agreement.ECDHBasicAgreement;
|
||||
import org.spongycastle.crypto.generators.ECKeyPairGenerator;
|
||||
import org.spongycastle.crypto.params.ECDomainParameters;
|
||||
import org.spongycastle.crypto.params.ECKeyGenerationParameters;
|
||||
import org.spongycastle.crypto.params.ECPrivateKeyParameters;
|
||||
import org.spongycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.spongycastle.math.ec.ECCurve;
|
||||
import org.spongycastle.math.ec.ECFieldElement;
|
||||
import org.spongycastle.math.ec.ECPoint;
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class CurveP256 {
|
||||
|
||||
private static final BigInteger q = new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", 16);
|
||||
private static final BigInteger a = new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", 16);
|
||||
private static final BigInteger b = new BigInteger("5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", 16);
|
||||
private static final BigInteger n = new BigInteger("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", 16);
|
||||
|
||||
private static final ECFieldElement x = new ECFieldElement.Fp(q, new BigInteger("6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296", 16));
|
||||
private static final ECFieldElement y = new ECFieldElement.Fp(q, new BigInteger("4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5", 16));
|
||||
|
||||
private static final ECCurve curve = new ECCurve.Fp(q, a, b);
|
||||
private static final ECPoint g = new ECPoint.Fp(curve, x, y, true);
|
||||
|
||||
private static final ECDomainParameters domainParameters = new ECDomainParameters(curve, g, n);
|
||||
|
||||
public static final int P256_POINT_SIZE = 33;
|
||||
|
||||
static byte[] encodePoint(ECPoint point) {
|
||||
synchronized (curve) {
|
||||
return point.getEncoded();
|
||||
}
|
||||
}
|
||||
|
||||
static ECPublicKey decodePoint(byte[] encoded, int offset)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
byte[] pointBytes = new byte[P256_POINT_SIZE];
|
||||
System.arraycopy(encoded, offset, pointBytes, 0, pointBytes.length);
|
||||
|
||||
synchronized (curve) {
|
||||
ECPoint Q;
|
||||
|
||||
try {
|
||||
Q = curve.decodePoint(pointBytes);
|
||||
} catch (RuntimeException re) {
|
||||
throw new InvalidKeyException(re);
|
||||
}
|
||||
|
||||
return new NistECPublicKey(new ECPublicKeyParameters(Q, domainParameters));
|
||||
}
|
||||
}
|
||||
|
||||
static ECPrivateKey decodePrivatePoint(byte[] encoded) {
|
||||
BigInteger d = new BigInteger(encoded);
|
||||
return new NistECPrivateKey(new ECPrivateKeyParameters(d, domainParameters));
|
||||
}
|
||||
|
||||
static byte[] calculateAgreement(ECPublicKey publicKey, ECPrivateKey privateKey) {
|
||||
ECDHBasicAgreement agreement = new ECDHBasicAgreement();
|
||||
agreement.init(((NistECPrivateKey)privateKey).getParameters());
|
||||
|
||||
synchronized (curve) {
|
||||
return agreement.calculateAgreement(((NistECPublicKey)publicKey).getParameters()).toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static ECKeyPair generateKeyPair() {
|
||||
try {
|
||||
synchronized (curve) {
|
||||
ECKeyGenerationParameters keyParamters = new ECKeyGenerationParameters(domainParameters, SecureRandom.getInstance("SHA1PRNG"));
|
||||
ECKeyPairGenerator generator = new ECKeyPairGenerator();
|
||||
generator.init(keyParamters);
|
||||
|
||||
AsymmetricCipherKeyPair keyPair = generator.generateKeyPair();
|
||||
keyPair = cloneKeyPairWithPointCompression(keyPair);
|
||||
|
||||
return new ECKeyPair(new NistECPublicKey((ECPublicKeyParameters)keyPair.getPublic()),
|
||||
new NistECPrivateKey((ECPrivateKeyParameters)keyPair.getPrivate()));
|
||||
}
|
||||
} catch (NoSuchAlgorithmException nsae) {
|
||||
Log.w("CurveP256", nsae);
|
||||
throw new AssertionError(nsae);
|
||||
}
|
||||
}
|
||||
|
||||
// This is dumb, but the ECPublicKeys that the generator makes by default don't have point compression
|
||||
// turned on, and there's no setter. Great.
|
||||
private static AsymmetricCipherKeyPair cloneKeyPairWithPointCompression(AsymmetricCipherKeyPair keyPair) {
|
||||
ECPublicKeyParameters publicKey = (ECPublicKeyParameters)keyPair.getPublic();
|
||||
ECPoint q = publicKey.getQ();
|
||||
|
||||
return new AsymmetricCipherKeyPair(new ECPublicKeyParameters(new ECPoint.Fp(q.getCurve(), q.getX(), q.getY(), true),
|
||||
publicKey.getParameters()), keyPair.getPrivate());
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecure.crypto.ecc;
|
||||
|
||||
import org.spongycastle.crypto.params.ECPublicKeyParameters;
|
||||
|
||||
public class NistECPublicKey implements ECPublicKey {
|
||||
|
||||
private final ECPublicKeyParameters publicKey;
|
||||
|
||||
NistECPublicKey(ECPublicKeyParameters publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return CurveP256.encodePoint(publicKey.getQ());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return Curve.NIST_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null) return false;
|
||||
if (!(other instanceof NistECPublicKey)) return false;
|
||||
|
||||
NistECPublicKey that = (NistECPublicKey)other;
|
||||
return publicKey.getQ().equals(that.publicKey.getQ());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return publicKey.getQ().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ECPublicKey another) {
|
||||
return publicKey.getQ().getX().toBigInteger()
|
||||
.compareTo(((NistECPublicKey) another).publicKey.getQ().getX().toBigInteger());
|
||||
}
|
||||
|
||||
public ECPublicKeyParameters getParameters() {
|
||||
return publicKey;
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,14 @@ package org.whispersystems.textsecure.crypto.protocol;
|
||||
|
||||
public interface CiphertextMessage {
|
||||
|
||||
public static final int LEGACY_VERSION = 1;
|
||||
public static final int CURRENT_VERSION = 2;
|
||||
public static final int UNSUPPORTED_VERSION = 1;
|
||||
public static final int CURRENT_VERSION = 2;
|
||||
|
||||
public static final int LEGACY_WHISPER_TYPE = 1;
|
||||
public static final int CURRENT_WHISPER_TYPE = 2;
|
||||
public static final int PREKEY_WHISPER_TYPE = 3;
|
||||
public static final int WHISPER_TYPE = 2;
|
||||
public static final int PREKEY_TYPE = 3;
|
||||
|
||||
// This should be the worst case (worse than V2). So not always accurate, but good enough for padding.
|
||||
public static final int ENCRYPTED_MESSAGE_OVERHEAD = WhisperMessageV1.ENCRYPTED_MESSAGE_OVERHEAD;
|
||||
public static final int ENCRYPTED_MESSAGE_OVERHEAD = 53;
|
||||
|
||||
public byte[] serialize();
|
||||
public int getType();
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.whispersystems.textsecure.crypto.IdentityKey;
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
||||
import org.whispersystems.textsecure.crypto.InvalidVersionException;
|
||||
import org.whispersystems.textsecure.crypto.LegacyMessageException;
|
||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
@@ -14,19 +15,19 @@ import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
public class PreKeyWhisperMessage implements CiphertextMessage {
|
||||
|
||||
private final int version;
|
||||
private final int registrationId;
|
||||
private final int preKeyId;
|
||||
private final ECPublicKey baseKey;
|
||||
private final IdentityKey identityKey;
|
||||
private final WhisperMessageV2 message;
|
||||
private final byte[] serialized;
|
||||
private final int version;
|
||||
private final int registrationId;
|
||||
private final int preKeyId;
|
||||
private final ECPublicKey baseKey;
|
||||
private final IdentityKey identityKey;
|
||||
private final WhisperMessage message;
|
||||
private final byte[] serialized;
|
||||
|
||||
public PreKeyWhisperMessage(byte[] serialized)
|
||||
throws InvalidMessageException, InvalidVersionException
|
||||
{
|
||||
try {
|
||||
this.version = Conversions.lowBitsToInt(serialized[0]);
|
||||
this.version = Conversions.highBitsToInt(serialized[0]);
|
||||
|
||||
if (this.version > CiphertextMessage.CURRENT_VERSION) {
|
||||
throw new InvalidVersionException("Unknown version: " + this.version);
|
||||
@@ -49,16 +50,18 @@ public class PreKeyWhisperMessage implements CiphertextMessage {
|
||||
this.preKeyId = preKeyWhisperMessage.getPreKeyId();
|
||||
this.baseKey = Curve.decodePoint(preKeyWhisperMessage.getBaseKey().toByteArray(), 0);
|
||||
this.identityKey = new IdentityKey(Curve.decodePoint(preKeyWhisperMessage.getIdentityKey().toByteArray(), 0));
|
||||
this.message = new WhisperMessageV2(preKeyWhisperMessage.getMessage().toByteArray());
|
||||
this.message = new WhisperMessage(preKeyWhisperMessage.getMessage().toByteArray());
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
} catch (LegacyMessageException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public PreKeyWhisperMessage(int registrationId, int preKeyId, ECPublicKey baseKey,
|
||||
IdentityKey identityKey, WhisperMessageV2 message)
|
||||
IdentityKey identityKey, WhisperMessage message)
|
||||
{
|
||||
this.version = CiphertextMessage.CURRENT_VERSION;
|
||||
this.registrationId = registrationId;
|
||||
@@ -95,7 +98,7 @@ public class PreKeyWhisperMessage implements CiphertextMessage {
|
||||
return baseKey;
|
||||
}
|
||||
|
||||
public WhisperMessageV2 getWhisperMessage() {
|
||||
public WhisperMessage getWhisperMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
@@ -106,7 +109,7 @@ public class PreKeyWhisperMessage implements CiphertextMessage {
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return CiphertextMessage.PREKEY_WHISPER_TYPE;
|
||||
return CiphertextMessage.PREKEY_TYPE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
package org.whispersystems.textsecure.crypto.protocol;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
||||
import org.whispersystems.textsecure.crypto.LegacyMessageException;
|
||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecure.crypto.protocol.WhisperProtos.WhisperMessage;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.ParseException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class WhisperMessageV2 implements CiphertextMessage {
|
||||
public class WhisperMessage implements CiphertextMessage {
|
||||
|
||||
private static final int MAC_LENGTH = 8;
|
||||
|
||||
@@ -31,18 +28,22 @@ public class WhisperMessageV2 implements CiphertextMessage {
|
||||
private final byte[] ciphertext;
|
||||
private final byte[] serialized;
|
||||
|
||||
public WhisperMessageV2(byte[] serialized) throws InvalidMessageException {
|
||||
public WhisperMessage(byte[] serialized) throws InvalidMessageException, LegacyMessageException {
|
||||
try {
|
||||
byte[][] messageParts = Util.split(serialized, 1, serialized.length - 1 - MAC_LENGTH, MAC_LENGTH);
|
||||
byte version = messageParts[0][0];
|
||||
byte[] message = messageParts[1];
|
||||
byte[] mac = messageParts[2];
|
||||
|
||||
if (Conversions.highBitsToInt(version) <= CiphertextMessage.UNSUPPORTED_VERSION) {
|
||||
throw new LegacyMessageException("Legacy message: " + Conversions.highBitsToInt(version));
|
||||
}
|
||||
|
||||
if (Conversions.highBitsToInt(version) != CURRENT_VERSION) {
|
||||
throw new InvalidMessageException("Unknown version: " + Conversions.highBitsToInt(version));
|
||||
}
|
||||
|
||||
WhisperMessage whisperMessage = WhisperMessage.parseFrom(message);
|
||||
WhisperProtos.WhisperMessage whisperMessage = WhisperProtos.WhisperMessage.parseFrom(message);
|
||||
|
||||
if (!whisperMessage.hasCiphertext() ||
|
||||
!whisperMessage.hasCounter() ||
|
||||
@@ -65,11 +66,11 @@ public class WhisperMessageV2 implements CiphertextMessage {
|
||||
}
|
||||
}
|
||||
|
||||
public WhisperMessageV2(SecretKeySpec macKey, ECPublicKey senderEphemeral,
|
||||
int counter, int previousCounter, byte[] ciphertext)
|
||||
public WhisperMessage(SecretKeySpec macKey, ECPublicKey senderEphemeral,
|
||||
int counter, int previousCounter, byte[] ciphertext)
|
||||
{
|
||||
byte[] version = {Conversions.intsToByteHighAndLow(CURRENT_VERSION, CURRENT_VERSION)};
|
||||
byte[] message = WhisperMessage.newBuilder()
|
||||
byte[] message = WhisperProtos.WhisperMessage.newBuilder()
|
||||
.setEphemeralKey(ByteString.copyFrom(senderEphemeral.serialize()))
|
||||
.setCounter(counter)
|
||||
.setPreviousCounter(previousCounter)
|
||||
@@ -103,7 +104,7 @@ public class WhisperMessageV2 implements CiphertextMessage {
|
||||
byte[] ourMac = getMac(macKey, parts[0]);
|
||||
byte[] theirMac = parts[1];
|
||||
|
||||
if (!Arrays.equals(ourMac, theirMac)) {
|
||||
if (!MessageDigest.isEqual(ourMac, theirMac)) {
|
||||
throw new InvalidMessageException("Bad Mac!");
|
||||
}
|
||||
}
|
||||
@@ -129,7 +130,12 @@ public class WhisperMessageV2 implements CiphertextMessage {
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return CiphertextMessage.CURRENT_WHISPER_TYPE;
|
||||
return CiphertextMessage.WHISPER_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isLegacy(byte[] message) {
|
||||
return message != null && message.length >= 1 &&
|
||||
Conversions.highBitsToInt(message[0]) <= CiphertextMessage.UNSUPPORTED_VERSION;
|
||||
}
|
||||
|
||||
}
|
||||