mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-16 07:57:38 +00:00
Compare commits
323 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25e03b3579 | ||
|
|
52ff4ecfd2 | ||
|
|
43c1576aab | ||
|
|
23c607430d | ||
|
|
829a92d371 | ||
|
|
559228af5b | ||
|
|
e8a0fac05b | ||
|
|
7bb45d8a91 | ||
|
|
fa0c783c64 | ||
|
|
377a332789 | ||
|
|
2ab8c77748 | ||
|
|
d5f0415907 | ||
|
|
5000957b99 | ||
|
|
fc12f2cf8e | ||
|
|
e3aed66ded | ||
|
|
f30304423d | ||
|
|
86b3de2a93 | ||
|
|
0ae1004142 | ||
|
|
da344a0218 | ||
|
|
aab34e491e | ||
|
|
e80de7de2b | ||
|
|
3dd27ed59a | ||
|
|
ca2eecaedf | ||
|
|
e188f62d70 | ||
|
|
8093b14922 | ||
|
|
636c50b3b8 | ||
|
|
9298dded15 | ||
|
|
c36b5869fc | ||
|
|
46de25d658 | ||
|
|
8794661433 | ||
|
|
056dbd1a4e | ||
|
|
125a60290f | ||
|
|
315cf2d8e4 | ||
|
|
da57a689c1 | ||
|
|
9c9866e7ee | ||
|
|
a6e1d56cde | ||
|
|
b855f8a163 | ||
|
|
7beab36c6a | ||
|
|
7667264789 | ||
|
|
ccd1691b22 | ||
|
|
2026330f8a | ||
|
|
3ca25de034 | ||
|
|
ff238a1ce9 | ||
|
|
918f223149 | ||
|
|
c5821f770a | ||
|
|
5b22a7bdcb | ||
|
|
2a74dc8d82 | ||
|
|
145e4335fc | ||
|
|
1d4d9663db | ||
|
|
a0cbbd2d21 | ||
|
|
28b96204a9 | ||
|
|
c647820fb7 | ||
|
|
9614dc9055 | ||
|
|
0cdc6fd87d | ||
|
|
19dddd7adf | ||
|
|
0688dd0c2c | ||
|
|
649f037ed8 | ||
|
|
9d9a0ec218 | ||
|
|
d19ab04bdd | ||
|
|
e05bacd8c3 | ||
|
|
760c96171d | ||
|
|
3999171377 | ||
|
|
abce678cb4 | ||
|
|
fb75d90edc | ||
|
|
25324a45b3 | ||
|
|
877ed8f59c | ||
|
|
54b087c458 | ||
|
|
ca51ddac7f | ||
|
|
e15ff6193f | ||
|
|
e8251800ab | ||
|
|
93329df530 | ||
|
|
948f888670 | ||
|
|
c19ac8ec1e | ||
|
|
04327e9ed7 | ||
|
|
e81526e581 | ||
|
|
4f87c1e52e | ||
|
|
725e943842 | ||
|
|
9bb327db42 | ||
|
|
94b54a6d63 | ||
|
|
fe03a22926 | ||
|
|
d8e1df9233 | ||
|
|
6b5753337c | ||
|
|
18f3314cd2 | ||
|
|
b9f4fba98a | ||
|
|
5810062b25 | ||
|
|
e7e5bc0884 | ||
|
|
e2989373cd | ||
|
|
4e703d5a00 | ||
|
|
b79bc4c234 | ||
|
|
a305bb80e6 | ||
|
|
8e3aa94a05 | ||
|
|
b2fdd74a1c | ||
|
|
7d3a2acb29 | ||
|
|
556b0dec1d | ||
|
|
9b41675f8f | ||
|
|
9438973eac | ||
|
|
2f98622948 | ||
|
|
067799be06 | ||
|
|
7c46f3cbf8 | ||
|
|
41aa53dd66 | ||
|
|
b47076cf1b | ||
|
|
bb4a4d33c9 | ||
|
|
c5d010c86f | ||
|
|
fe55ac57d9 | ||
|
|
01a438de09 | ||
|
|
b4a4e629f1 | ||
|
|
9cd5a67ec5 | ||
|
|
fc3411da83 | ||
|
|
dc85b23a06 | ||
|
|
0af473d880 | ||
|
|
0ace469d74 | ||
|
|
ed3151bdb5 | ||
|
|
6157a0df7a | ||
|
|
5a75ff9299 | ||
|
|
9fd2c4753e | ||
|
|
c8ef21774d | ||
|
|
fc458770ea | ||
|
|
0d2ce2528c | ||
|
|
6bfae9ebc9 | ||
|
|
2f01569e45 | ||
|
|
d51ccb8ce2 | ||
|
|
49daa45dca | ||
|
|
63b90bb57f | ||
|
|
8f7c9d45aa | ||
|
|
4851a555e7 | ||
|
|
a0dd1689da | ||
|
|
63d217bd80 | ||
|
|
6cdd2aff0d | ||
|
|
1e77c9904a | ||
|
|
c134709009 | ||
|
|
4a93f7d012 | ||
|
|
29cdfd4b96 | ||
|
|
a14897f8b5 | ||
|
|
9b6d4e3696 | ||
|
|
28c1c5006b | ||
|
|
8ca60a8746 | ||
|
|
e9a8c7474c | ||
|
|
75ce00cb90 | ||
|
|
149671095b | ||
|
|
c488947248 | ||
|
|
04b2c3772a | ||
|
|
a4a3b4a453 | ||
|
|
5c97b1e864 | ||
|
|
dde2d2564a | ||
|
|
71664926e9 | ||
|
|
3c3028c8e3 | ||
|
|
842df1ac39 | ||
|
|
ff0dff745c | ||
|
|
d4188c4a1c | ||
|
|
f002072f38 | ||
|
|
72be77b47e | ||
|
|
327ee4ff62 | ||
|
|
1ab4e7e9de | ||
|
|
ff5ad4b85d | ||
|
|
ce5f3c5157 | ||
|
|
f7b71e5e28 | ||
|
|
44092a3eff | ||
|
|
dc73bc2a5c | ||
|
|
f8dda5afd6 | ||
|
|
4380b46a35 | ||
|
|
7e926d08ac | ||
|
|
fde8d32848 | ||
|
|
f10d6a7b0b | ||
|
|
320cc84392 | ||
|
|
dadabdfaa8 | ||
|
|
07b7696937 | ||
|
|
c38a8aa699 | ||
|
|
a03fff8b24 | ||
|
|
dbc070cd65 | ||
|
|
6e640db39c | ||
|
|
44d59d0fd1 | ||
|
|
51b9affe90 | ||
|
|
246cd10454 | ||
|
|
ca3c82f581 | ||
|
|
5b2caa0074 | ||
|
|
fa073e0b46 | ||
|
|
7fd9946275 | ||
|
|
fa5ccc3f8a | ||
|
|
25a2ad7289 | ||
|
|
e7a21752d8 | ||
|
|
b010c5194a | ||
|
|
eb4cc7f4e5 | ||
|
|
1ac32346c1 | ||
|
|
2d083208cc | ||
|
|
75cca3add1 | ||
|
|
073b1f69e3 | ||
|
|
5e6d39beea | ||
|
|
8f6590b738 | ||
|
|
0dd36c64a4 | ||
|
|
cddba2738f | ||
|
|
0cc5837d7f | ||
|
|
68ec0a3727 | ||
|
|
45e380a5bb | ||
|
|
1bbcedabd4 | ||
|
|
c3b8b62d32 | ||
|
|
43492b66c4 | ||
|
|
90814be167 | ||
|
|
7f642666dd | ||
|
|
1cc2762656 | ||
|
|
d1969412fb | ||
|
|
edb89ee3e9 | ||
|
|
499de2d2bf | ||
|
|
b8f663b69c | ||
|
|
2042ca6cb7 | ||
|
|
cfb7b8fcba | ||
|
|
03ff55db97 | ||
|
|
0e899b93d4 | ||
|
|
fb378a6e00 | ||
|
|
9287d413ac | ||
|
|
4bb337a3a0 | ||
|
|
fd045f2354 | ||
|
|
53803630d4 | ||
|
|
d7070e7ecf | ||
|
|
52e5274a2f | ||
|
|
4c615546e8 | ||
|
|
1d1492b15c | ||
|
|
0ed9da9a2c | ||
|
|
26dadfcb7a | ||
|
|
1ac06312a0 | ||
|
|
a200d29514 | ||
|
|
21eee19380 | ||
|
|
2539723410 | ||
|
|
0958c30400 | ||
|
|
7eef93c829 | ||
|
|
3634ba0b55 | ||
|
|
6ca029f64a | ||
|
|
7bf96ec7d7 | ||
|
|
389fdaed32 | ||
|
|
8186694555 | ||
|
|
b98d5ca244 | ||
|
|
eb1b762a76 | ||
|
|
ca92b4d904 | ||
|
|
a0c821f6ac | ||
|
|
d260cdcc9f | ||
|
|
496d5e21d2 | ||
|
|
bba49fcb76 | ||
|
|
6eb094c87e | ||
|
|
23e8035edd | ||
|
|
0fed5593a8 | ||
|
|
60f5363da0 | ||
|
|
bee964dcf8 | ||
|
|
f18324e2d6 | ||
|
|
e7f4a6039c | ||
|
|
fccb4cc0cf | ||
|
|
91b52bed18 | ||
|
|
f91cfe1c44 | ||
|
|
c8422a8942 | ||
|
|
f74119a58c | ||
|
|
ce4f935fa6 | ||
|
|
ee0a84ec16 | ||
|
|
7d931f5e6b | ||
|
|
1740f9a7cd | ||
|
|
94913f34da | ||
|
|
15284cd4d9 | ||
|
|
7c2fb74af4 | ||
|
|
7489f3463a | ||
|
|
ed73bd57a1 | ||
|
|
99d295abc5 | ||
|
|
b56a4f9740 | ||
|
|
cc5981594f | ||
|
|
37fb8fe445 | ||
|
|
7f44b029fe | ||
|
|
b2bd19d787 | ||
|
|
c2b809c7c6 | ||
|
|
e0aa7fd359 | ||
|
|
3a5f9748ff | ||
|
|
33fd6414b8 | ||
|
|
ce4b8701fb | ||
|
|
10580a7f20 | ||
|
|
d89ef70060 | ||
|
|
aad82e314d | ||
|
|
8f7f1ea49c | ||
|
|
0c0db28358 | ||
|
|
89ae5ed4ed | ||
|
|
afa20058b8 | ||
|
|
9dc3730142 | ||
|
|
504ec976c0 | ||
|
|
fda01796ab | ||
|
|
b0216e1494 | ||
|
|
e81756f443 | ||
|
|
56fd6e7035 | ||
|
|
9ddfa6499c | ||
|
|
4931d7327b | ||
|
|
adfa3c1b10 | ||
|
|
3cc6344c8b | ||
|
|
546dd5485c | ||
|
|
f9c7687ce2 | ||
|
|
b0e0011060 | ||
|
|
9c0db4c259 | ||
|
|
9cf1751c49 | ||
|
|
981c9a1543 | ||
|
|
73bf21602a | ||
|
|
8d8a6ab998 | ||
|
|
798e3c0656 | ||
|
|
891e3aeb97 | ||
|
|
2c2a03e5e2 | ||
|
|
c7b39eca11 | ||
|
|
3d690faff0 | ||
|
|
f3fdde6040 | ||
|
|
f49c62793b | ||
|
|
f27c7c4178 | ||
|
|
ee6a74cb66 | ||
|
|
cfc255a7b7 | ||
|
|
615ca931d4 | ||
|
|
04eafd5fab | ||
|
|
73b1e3f3d4 | ||
|
|
8d115562f0 | ||
|
|
f26b9070f2 | ||
|
|
2cbc4d3b0a | ||
|
|
ee8c8530bf | ||
|
|
763e4b1b27 | ||
|
|
4f49216103 | ||
|
|
ed4c8cd799 | ||
|
|
4826dbcc34 | ||
|
|
2cf7f82f07 | ||
|
|
1c8f8666ba | ||
|
|
91dd558f7a | ||
|
|
99e3e596bb | ||
|
|
f23fc9b3dd | ||
|
|
1149d8b625 | ||
|
|
282e40db40 | ||
|
|
27fadf70f0 | ||
|
|
45f4faaf4c |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -5,10 +5,17 @@ project.properties
|
||||
bin/
|
||||
gen/
|
||||
.idea/
|
||||
TextSecure.iml
|
||||
*.iml
|
||||
out
|
||||
tests
|
||||
lint.xml
|
||||
local.properties
|
||||
ant.properties
|
||||
.DS_Store
|
||||
build.log
|
||||
build-log.xml
|
||||
.gradle
|
||||
build
|
||||
signing.properties
|
||||
library/lib/
|
||||
library/obj/
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
|
||||
[textsecure-official.master]
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.thoughtcrime.securesms"
|
||||
android:versionCode="56"
|
||||
android:versionName="1.0">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.thoughtcrime.securesms"
|
||||
android:versionCode="64"
|
||||
android:versionName="2.0.1">
|
||||
|
||||
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="16"/>
|
||||
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="19"/>
|
||||
|
||||
<permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"
|
||||
android:label="Access to TextSecure Secrets"
|
||||
android:protectionLevel="signature" />
|
||||
|
||||
android:protectionLevel="signature" />
|
||||
|
||||
<uses-permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"/>
|
||||
<uses-permission android:name="android.permission.READ_PROFILE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_PROFILE"/>
|
||||
<uses-permission android:name="android.permission.BROADCAST_WAP_PUSH"/>
|
||||
<uses-permission android:name="android.permission.BROADCAST_WAP_PUSH"
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
@@ -37,14 +39,14 @@
|
||||
<permission android:name="org.thoughtcrime.securesms.permission.C2D_MESSAGE"
|
||||
android:protectionLevel="signature" />
|
||||
<uses-permission android:name="org.thoughtcrime.securesms.permission.C2D_MESSAGE" />
|
||||
|
||||
|
||||
<application android:name="org.thoughtcrime.securesms.ApplicationListener"
|
||||
android:icon="@drawable/icon"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/TextSecure.LightTheme">
|
||||
|
||||
<activity android:name=".RoutingActivity"
|
||||
android:theme="@style/NoAnimation.Theme.Sherlock.Light.DarkActionBar"
|
||||
android:theme="@style/NoAnimation.Theme.BlackScreen"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
|
||||
@@ -52,14 +54,16 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SENDTO"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="sms" />
|
||||
<data android:scheme="smsto" />
|
||||
<data android:scheme="mms" />
|
||||
<data android:scheme="mmsto" />
|
||||
</intent-filter>
|
||||
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
@@ -67,27 +71,40 @@
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="text/*" />
|
||||
</intent-filter>
|
||||
|
||||
|
||||
</activity>
|
||||
|
||||
<activity android:name=".RegistrationProblemsActivity"
|
||||
android:theme="@style/TextSecure.Light.Dialog"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".CountrySelectionActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ImportExportActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".PromptApnActivity"
|
||||
<activity android:name=".PromptMmsActivity"
|
||||
android:label="Configure MMS Settings"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".MmsPreferencesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ConversationListActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:uiOptions="splitActionBarWhenNarrow"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ConversationActivity"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".GroupCreateActivity"
|
||||
android:windowSoftInputMode="stateVisible"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".DatabaseMigrationActivity"
|
||||
android:theme="@style/NoAnimation.Theme.Sherlock.Light.DarkActionBar"
|
||||
android:launchMode="singleTask"
|
||||
@@ -114,10 +131,21 @@
|
||||
|
||||
<activity android:name=".ContactSelectionActivity"
|
||||
android:label="@string/AndroidManifest__select_contacts"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".SingleContactSelectionActivity"
|
||||
android:label="@string/AndroidManifest__select_contact"
|
||||
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"/>
|
||||
|
||||
<activity android:name=".AutoInitiateActivity"
|
||||
android:theme="@style/Theme.Sherlock.Light.Dialog"
|
||||
android:theme="@style/TextSecure.Light.Dialog"
|
||||
android:label="@string/AndroidManifest__textsecure_detected"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
@@ -132,10 +160,6 @@
|
||||
android:label="@string/AndroidManifest__change_passphrase"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".VerifyKeysActivity"
|
||||
android:label="@string/AndroidManifest__verify_session"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".VerifyIdentityActivity"
|
||||
android:label="@string/AndroidManifest__verify_identity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
@@ -145,8 +169,8 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ReceiveKeyActivity"
|
||||
android:theme="@style/Theme.Sherlock.Light.Dialog"
|
||||
android:label="@string/AndroidManifest__complete_key_exchange"
|
||||
android:label="@string/AndroidManifest__complete_key_exchange"
|
||||
android:theme="@style/TextSecure.Light.Dialog"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ApplicationPreferencesActivity"
|
||||
@@ -165,6 +189,20 @@
|
||||
<service android:enabled="true" android:name=".service.RegistrationService"/>
|
||||
<service android:enabled="true" android:name=".gcm.GcmIntentService"/>
|
||||
|
||||
<service android:name=".service.QuickResponseService"
|
||||
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
|
||||
android:exported="true" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="sms" />
|
||||
<data android:scheme="smsto" />
|
||||
<data android:scheme="mms" />
|
||||
<data android:scheme="mmsto" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
|
||||
<!-- <receiver android:name=".service.BootListener" -->
|
||||
<!-- android:enabled="true" -->
|
||||
<!-- android:exported="false">-->
|
||||
@@ -182,45 +220,64 @@
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.SmsListener"
|
||||
android:permission="android.permission.BROADCAST_SMS"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter android:priority="1001">
|
||||
<action android:name="android.provider.Telephony.SMS_RECEIVED"></action>
|
||||
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.services.MESSAGE_SENT"></action>
|
||||
<action android:name="android.provider.Telephony.SMS_DELIVER"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.SmsDeliveryListener"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.services.MESSAGE_SENT"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.MmsListener"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BROADCAST_WAP_PUSH">
|
||||
<intent-filter android:priority="1001">
|
||||
<action android:name="android.provider.Telephony.WAP_PUSH_RECEIVED"></action>
|
||||
<action android:name="android.provider.Telephony.WAP_PUSH_RECEIVED"/>
|
||||
<data android:mimeType="application/vnd.wap.mms-message" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
|
||||
<data android:mimeType="application/vnd.wap.mms-message" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.SystemStateListener"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"></action>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".notifications.MarkReadReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.notifications.CLEAR"></action>
|
||||
<action android:name="org.thoughtcrime.securesms.notifications.CLEAR"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<provider android:name=".providers.PartProvider"
|
||||
<provider android:name=".providers.PartProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:authorities="org.thoughtcrime.provider.securesms" />
|
||||
|
||||
<receiver android:name=".service.RegistrationNotifier"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.REGISTRATION_EVENT" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.DirectoryRefreshListener">
|
||||
<intent-filter>
|
||||
<action android:name="org.whispersystems.whisperpush.DIRECTORY_REFRESH"/>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<uses-library android:name="android.test.runner" />
|
||||
</application>
|
||||
|
||||
|
||||
20
BUILDING.md
20
BUILDING.md
@@ -1,22 +1,8 @@
|
||||
Building TextSecure
|
||||
===================
|
||||
|
||||
Fetch ActionBarSherlock:
|
||||
1. Ensure the 'Android Support Repository' is installed from the Android SDK manager.
|
||||
|
||||
git clone --branch 4.2.0 git://github.com/JakeWharton/ActionBarSherlock.git ../ActionBarSherlock
|
||||
Execute Gradle:
|
||||
|
||||
Configure ActionBarSherlock for your android target:
|
||||
|
||||
android update project --path ../ActionBarSherlock/library --target 1
|
||||
|
||||
Configure TextSecure for your android target, linking to ASB:
|
||||
|
||||
android update project --path . --target 1 --library ../ActionBarSherlock/library
|
||||
|
||||
Finally, both codebases must share the android-support jar. As TextSecure's is newer, use it:
|
||||
|
||||
cp libs/android-support-v4.jar ../ActionBarSherlock/library/libs/android-support-v4.jar
|
||||
|
||||
Assuming your android toolchain is correctly configured, it should now be possible to build the TextSecure apk.
|
||||
|
||||
ant debug
|
||||
./gradlew build
|
||||
|
||||
@@ -8,6 +8,11 @@ TextSecure is a replacement for the standard text messaging application, allowin
|
||||
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.
|
||||
|
||||
Current BitHub Payment For Commit:
|
||||
=================
|
||||
[](https://whispersystems.org/blog/bithub/)
|
||||
|
||||
|
||||
Bug tracker
|
||||
-----------
|
||||
|
||||
@@ -60,5 +65,6 @@ License
|
||||
---------------------
|
||||
|
||||
Copyright 2011 Whisper Systems
|
||||
Copyright 2013 Open WhisperSystems
|
||||
|
||||
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
BIN
artwork/ic_contact_photo.psd
Normal file
BIN
artwork/ic_contact_photo.psd
Normal file
Binary file not shown.
BIN
artwork/ic_group_photo.psd
Normal file
BIN
artwork/ic_group_photo.psd
Normal file
Binary file not shown.
31036
artwork/icon.ai
Normal file
31036
artwork/icon.ai
Normal file
File diff suppressed because one or more lines are too long
BIN
artwork/icon_gigantic.png
Normal file
BIN
artwork/icon_gigantic.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 582 KiB |
BIN
artwork/icon_playstore_512x512.png
Normal file
BIN
artwork/icon_playstore_512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
BIN
artwork/lower_right_divet_dark.psd
Normal file
BIN
artwork/lower_right_divet_dark.psd
Normal file
Binary file not shown.
BIN
artwork/lower_right_divet_light.psd
Normal file
BIN
artwork/lower_right_divet_light.psd
Normal file
Binary file not shown.
90
build.gradle
Normal file
90
build.gradle
Normal file
@@ -0,0 +1,90 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:0.8.+'
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'android'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url "https://raw.github.com/whispersystems/maven/master/gcm-client/releases/"
|
||||
}
|
||||
maven {
|
||||
url "https://raw.github.com/whispersystems/maven/master/gson/releases/"
|
||||
}
|
||||
}
|
||||
|
||||
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 project(':library')
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 19
|
||||
buildToolsVersion '19.0.0'
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
targetSdkVersion 19
|
||||
}
|
||||
|
||||
android {
|
||||
sourceSets {
|
||||
main {
|
||||
manifest.srcFile 'AndroidManifest.xml'
|
||||
java.srcDirs = ['src']
|
||||
resources.srcDirs = ['src']
|
||||
aild.srcDirs = ['src']
|
||||
renderscript.srcDirs = ['src']
|
||||
res.srcDirs = ['res']
|
||||
assets.srcDirs = ['assets']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
def Properties props = new Properties()
|
||||
def propFile = new File('signing.properties')
|
||||
|
||||
if (propFile.canRead()){
|
||||
props.load(new FileInputStream(propFile))
|
||||
|
||||
if (props !=null &&
|
||||
props.containsKey('STORE_FILE') &&
|
||||
props.containsKey('STORE_PASSWORD') &&
|
||||
props.containsKey('KEY_ALIAS') &&
|
||||
props.containsKey('KEY_PASSWORD'))
|
||||
{
|
||||
android.signingConfigs.release.storeFile = file(props['STORE_FILE'])
|
||||
android.signingConfigs.release.storePassword = props['STORE_PASSWORD']
|
||||
android.signingConfigs.release.keyAlias = props['KEY_ALIAS']
|
||||
android.signingConfigs.release.keyPassword = props['KEY_PASSWORD']
|
||||
} else {
|
||||
println 'signing.properties found but some entries are missing'
|
||||
android.buildTypes.release.signingConfig = null
|
||||
}
|
||||
}else {
|
||||
println 'signing.properties not found'
|
||||
android.buildTypes.release.signingConfig = null
|
||||
}
|
||||
25
build.xml
25
build.xml
@@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project name="TextSecure" default="help">
|
||||
|
||||
<property file="local.properties" />
|
||||
|
||||
<property file="ant.properties" />
|
||||
|
||||
<property environment="env" />
|
||||
<condition property="sdk.dir" value="${env.ANDROID_HOME}">
|
||||
<isset property="env.ANDROID_HOME" />
|
||||
</condition>
|
||||
|
||||
<loadproperties srcFile="project.properties" />
|
||||
|
||||
<fail
|
||||
message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
|
||||
unless="sdk.dir"
|
||||
/>
|
||||
|
||||
<import file="custom_rules.xml" optional="true" />
|
||||
|
||||
<!-- version-tag: 1 -->
|
||||
<import file="${sdk.dir}/tools/ant/build.xml" />
|
||||
|
||||
</project>
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
#Sat Dec 21 23:48:05 PST 2013
|
||||
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
|
||||
164
gradlew
vendored
Executable file
164
gradlew
vendored
Executable file
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn ( ) {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die ( ) {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
esac
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched.
|
||||
if $cygwin ; then
|
||||
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||
fi
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >&-
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >&-
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||
function splitJvmOpts() {
|
||||
JVM_OPTS=("$@")
|
||||
}
|
||||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||
|
||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
||||
90
gradlew.bat
vendored
Normal file
90
gradlew.bat
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windowz variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
8
library/AndroidManifest.xml
Normal file
8
library/AndroidManifest.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.whispersystems.textsecure"
|
||||
android:versionCode="1"
|
||||
android:versionName="0.1">
|
||||
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="16"/>
|
||||
<application />
|
||||
</manifest>
|
||||
58
library/build.gradle
Normal file
58
library/build.gradle
Normal file
@@ -0,0 +1,58 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:0.8.+'
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'android-library'
|
||||
apply plugin: 'maven'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url "https://raw.github.com/whispersystems/maven/master/gson/releases/"
|
||||
}
|
||||
}
|
||||
|
||||
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 'org.whispersystems:gson:2.2.4'
|
||||
compile fileTree(dir: 'libs', include: 'armeabi.jar')
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 19
|
||||
buildToolsVersion '19.0.0'
|
||||
|
||||
android {
|
||||
sourceSets {
|
||||
main {
|
||||
manifest.srcFile 'AndroidManifest.xml'
|
||||
java.srcDirs = ['src']
|
||||
resources.srcDirs = ['src']
|
||||
aild.srcDirs = ['src']
|
||||
renderscript.srcDirs = ['src']
|
||||
res.srcDirs = ['res']
|
||||
assets.srcDirs = ['assets']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
version '0.1'
|
||||
group 'org.whispersystems.textsecure'
|
||||
archivesBaseName = 'textsecure-library'
|
||||
|
||||
uploadArchives {
|
||||
repositories {
|
||||
mavenDeployer {
|
||||
repository(url: mavenLocal().getUrl())
|
||||
}
|
||||
}
|
||||
}
|
||||
92
library/build.xml
Normal file
92
library/build.xml
Normal file
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project name="library" default="help">
|
||||
|
||||
<!-- The local.properties file is created and updated by the 'android' tool.
|
||||
It contains the path to the SDK. It should *NOT* be checked into
|
||||
Version Control Systems. -->
|
||||
<property file="local.properties"/>
|
||||
|
||||
<!-- The ant.properties file can be created by you. It is only edited by the
|
||||
'android' tool to add properties to it.
|
||||
This is the place to change some Ant specific build properties.
|
||||
Here are some properties you may want to change/update:
|
||||
|
||||
source.dir
|
||||
The name of the source directory. Default is 'src'.
|
||||
out.dir
|
||||
The name of the output directory. Default is 'bin'.
|
||||
|
||||
For other overridable properties, look at the beginning of the rules
|
||||
files in the SDK, at tools/ant/build.xml
|
||||
|
||||
Properties related to the SDK location or the project target should
|
||||
be updated using the 'android' tool with the 'update' action.
|
||||
|
||||
This file is an integral part of the build system for your
|
||||
application and should be checked into Version Control Systems.
|
||||
|
||||
-->
|
||||
<property file="ant.properties"/>
|
||||
|
||||
<!-- if sdk.dir was not set from one of the property file, then
|
||||
get it from the ANDROID_HOME env var.
|
||||
This must be done before we load project.properties since
|
||||
the proguard config can use sdk.dir -->
|
||||
<property environment="env"/>
|
||||
<condition property="sdk.dir" value="${env.ANDROID_HOME}">
|
||||
<isset property="env.ANDROID_HOME"/>
|
||||
</condition>
|
||||
|
||||
<!-- The project.properties file is created and updated by the 'android'
|
||||
tool, as well as ADT.
|
||||
|
||||
This contains project specific properties such as project target, and library
|
||||
dependencies. Lower level build properties are stored in ant.properties
|
||||
(or in .classpath for Eclipse projects).
|
||||
|
||||
This file is an integral part of the build system for your
|
||||
application and should be checked into Version Control Systems. -->
|
||||
<loadproperties srcFile="project.properties"/>
|
||||
|
||||
<!-- quick check on sdk.dir -->
|
||||
<fail
|
||||
message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
|
||||
unless="sdk.dir"
|
||||
/>
|
||||
|
||||
<!--
|
||||
Import per project custom build rules if present at the root of the project.
|
||||
This is the place to put custom intermediary targets such as:
|
||||
-pre-build
|
||||
-pre-compile
|
||||
-post-compile (This is typically used for code obfuscation.
|
||||
Compiled code location: ${out.classes.absolute.dir}
|
||||
If this is not done in place, override ${out.dex.input.absolute.dir})
|
||||
-post-package
|
||||
-post-build
|
||||
-pre-clean
|
||||
-->
|
||||
<import file="custom_rules.xml" optional="true"/>
|
||||
|
||||
<!-- Import the actual build file.
|
||||
|
||||
To customize existing targets, there are two options:
|
||||
- Customize only one target:
|
||||
- copy/paste the target into this file, *before* the
|
||||
<import> task.
|
||||
- customize it to your needs.
|
||||
- Customize the whole content of build.xml
|
||||
- copy/paste the content of the rules files (minus the top node)
|
||||
into this file, replacing the <import> task.
|
||||
- customize to your needs.
|
||||
|
||||
***********************
|
||||
****** IMPORTANT ******
|
||||
***********************
|
||||
In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
|
||||
in order to avoid having your file be overridden by tools such as "android update project"
|
||||
-->
|
||||
<!-- version-tag: 1 -->
|
||||
<import file="${sdk.dir}/tools/ant/build.xml"/>
|
||||
|
||||
</project>
|
||||
17
library/jni/Android.mk
Normal file
17
library/jni/Android.mk
Normal file
@@ -0,0 +1,17 @@
|
||||
LOCAL_PATH:= $(call my-dir)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_MODULE := libcurve25519-donna
|
||||
LOCAL_SRC_FILES := curve25519-donna.c
|
||||
|
||||
include $(BUILD_STATIC_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_MODULE := libcurve25519
|
||||
LOCAL_SRC_FILES := curve25519-donna-jni.c
|
||||
|
||||
LOCAL_STATIC_LIBRARIES := libcurve25519-donna
|
||||
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
70
library/jni/curve25519-donna-jni.c
Normal file
70
library/jni/curve25519-donna-jni.c
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <jni.h>
|
||||
#include "curve25519-donna.h"
|
||||
|
||||
JNIEXPORT jbyteArray JNICALL Java_org_whispersystems_textsecure_crypto_ecc_Curve25519_generatePrivateKey
|
||||
(JNIEnv *env, jclass clazz, jbyteArray random)
|
||||
{
|
||||
uint8_t* privateKey = (uint8_t*)(*env)->GetByteArrayElements(env, random, 0);
|
||||
|
||||
privateKey[0] &= 248;
|
||||
privateKey[31] &= 127;
|
||||
privateKey[31] |= 64;
|
||||
|
||||
(*env)->ReleaseByteArrayElements(env, random, privateKey, 0);
|
||||
|
||||
return random;
|
||||
}
|
||||
|
||||
JNIEXPORT jbyteArray JNICALL Java_org_whispersystems_textsecure_crypto_ecc_Curve25519_generatePublicKey
|
||||
(JNIEnv *env, jclass clazz, jbyteArray privateKey)
|
||||
{
|
||||
static const uint8_t basepoint[32] = {9};
|
||||
|
||||
jbyteArray publicKey = (*env)->NewByteArray(env, 32);
|
||||
uint8_t* publicKeyBytes = (uint8_t*)(*env)->GetByteArrayElements(env, publicKey, 0);
|
||||
uint8_t* privateKeyBytes = (uint8_t*)(*env)->GetByteArrayElements(env, privateKey, 0);
|
||||
|
||||
curve25519_donna(publicKeyBytes, privateKeyBytes, basepoint);
|
||||
|
||||
(*env)->ReleaseByteArrayElements(env, publicKey, publicKeyBytes, 0);
|
||||
(*env)->ReleaseByteArrayElements(env, privateKey, privateKeyBytes, 0);
|
||||
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
JNIEXPORT jbyteArray JNICALL Java_org_whispersystems_textsecure_crypto_ecc_Curve25519_calculateAgreement
|
||||
(JNIEnv *env, jclass clazz, jbyteArray privateKey, jbyteArray publicKey)
|
||||
{
|
||||
jbyteArray sharedKey = (*env)->NewByteArray(env, 32);
|
||||
uint8_t* sharedKeyBytes = (uint8_t*)(*env)->GetByteArrayElements(env, sharedKey, 0);
|
||||
uint8_t* privateKeyBytes = (uint8_t*)(*env)->GetByteArrayElements(env, privateKey, 0);
|
||||
uint8_t* publicKeyBytes = (uint8_t*)(*env)->GetByteArrayElements(env, publicKey, 0);
|
||||
|
||||
curve25519_donna(sharedKeyBytes, privateKeyBytes, publicKeyBytes);
|
||||
|
||||
(*env)->ReleaseByteArrayElements(env, sharedKey, sharedKeyBytes, 0);
|
||||
(*env)->ReleaseByteArrayElements(env, publicKey, publicKeyBytes, 0);
|
||||
(*env)->ReleaseByteArrayElements(env, privateKey, privateKeyBytes, 0);
|
||||
|
||||
return sharedKey;
|
||||
}
|
||||
734
library/jni/curve25519-donna.c
Normal file
734
library/jni/curve25519-donna.c
Normal file
@@ -0,0 +1,734 @@
|
||||
/* Copyright 2008, Google Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are
|
||||
* met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above
|
||||
* copyright notice, this list of conditions and the following disclaimer
|
||||
* in the documentation and/or other materials provided with the
|
||||
* distribution.
|
||||
* * Neither the name of Google Inc. nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* curve25519-donna: Curve25519 elliptic curve, public key function
|
||||
*
|
||||
* http://code.google.com/p/curve25519-donna/
|
||||
*
|
||||
* Adam Langley <agl@imperialviolet.org>
|
||||
*
|
||||
* Derived from public domain C code by Daniel J. Bernstein <djb@cr.yp.to>
|
||||
*
|
||||
* More information about curve25519 can be found here
|
||||
* http://cr.yp.to/ecdh.html
|
||||
*
|
||||
* djb's sample implementation of curve25519 is written in a special assembly
|
||||
* language called qhasm and uses the floating point registers.
|
||||
*
|
||||
* This is, almost, a clean room reimplementation from the curve25519 paper. It
|
||||
* uses many of the tricks described therein. Only the crecip function is taken
|
||||
* from the sample implementation.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#define inline __inline
|
||||
#endif
|
||||
|
||||
typedef uint8_t u8;
|
||||
typedef int32_t s32;
|
||||
typedef int64_t limb;
|
||||
|
||||
/* Field element representation:
|
||||
*
|
||||
* Field elements are written as an array of signed, 64-bit limbs, least
|
||||
* significant first. The value of the field element is:
|
||||
* x[0] + 2^26·x[1] + x^51·x[2] + 2^102·x[3] + ...
|
||||
*
|
||||
* i.e. the limbs are 26, 25, 26, 25, ... bits wide.
|
||||
*/
|
||||
|
||||
/* Sum two numbers: output += in */
|
||||
static void fsum(limb *output, const limb *in) {
|
||||
unsigned i;
|
||||
for (i = 0; i < 10; i += 2) {
|
||||
output[0+i] = (output[0+i] + in[0+i]);
|
||||
output[1+i] = (output[1+i] + in[1+i]);
|
||||
}
|
||||
}
|
||||
|
||||
/* Find the difference of two numbers: output = in - output
|
||||
* (note the order of the arguments!)
|
||||
*/
|
||||
static void fdifference(limb *output, const limb *in) {
|
||||
unsigned i;
|
||||
for (i = 0; i < 10; ++i) {
|
||||
output[i] = (in[i] - output[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/* Multiply a number by a scalar: output = in * scalar */
|
||||
static void fscalar_product(limb *output, const limb *in, const limb scalar) {
|
||||
unsigned i;
|
||||
for (i = 0; i < 10; ++i) {
|
||||
output[i] = in[i] * scalar;
|
||||
}
|
||||
}
|
||||
|
||||
/* Multiply two numbers: output = in2 * in
|
||||
*
|
||||
* output must be distinct to both inputs. The inputs are reduced coefficient
|
||||
* form, the output is not.
|
||||
*/
|
||||
static void fproduct(limb *output, const limb *in2, const limb *in) {
|
||||
output[0] = ((limb) ((s32) in2[0])) * ((s32) in[0]);
|
||||
output[1] = ((limb) ((s32) in2[0])) * ((s32) in[1]) +
|
||||
((limb) ((s32) in2[1])) * ((s32) in[0]);
|
||||
output[2] = 2 * ((limb) ((s32) in2[1])) * ((s32) in[1]) +
|
||||
((limb) ((s32) in2[0])) * ((s32) in[2]) +
|
||||
((limb) ((s32) in2[2])) * ((s32) in[0]);
|
||||
output[3] = ((limb) ((s32) in2[1])) * ((s32) in[2]) +
|
||||
((limb) ((s32) in2[2])) * ((s32) in[1]) +
|
||||
((limb) ((s32) in2[0])) * ((s32) in[3]) +
|
||||
((limb) ((s32) in2[3])) * ((s32) in[0]);
|
||||
output[4] = ((limb) ((s32) in2[2])) * ((s32) in[2]) +
|
||||
2 * (((limb) ((s32) in2[1])) * ((s32) in[3]) +
|
||||
((limb) ((s32) in2[3])) * ((s32) in[1])) +
|
||||
((limb) ((s32) in2[0])) * ((s32) in[4]) +
|
||||
((limb) ((s32) in2[4])) * ((s32) in[0]);
|
||||
output[5] = ((limb) ((s32) in2[2])) * ((s32) in[3]) +
|
||||
((limb) ((s32) in2[3])) * ((s32) in[2]) +
|
||||
((limb) ((s32) in2[1])) * ((s32) in[4]) +
|
||||
((limb) ((s32) in2[4])) * ((s32) in[1]) +
|
||||
((limb) ((s32) in2[0])) * ((s32) in[5]) +
|
||||
((limb) ((s32) in2[5])) * ((s32) in[0]);
|
||||
output[6] = 2 * (((limb) ((s32) in2[3])) * ((s32) in[3]) +
|
||||
((limb) ((s32) in2[1])) * ((s32) in[5]) +
|
||||
((limb) ((s32) in2[5])) * ((s32) in[1])) +
|
||||
((limb) ((s32) in2[2])) * ((s32) in[4]) +
|
||||
((limb) ((s32) in2[4])) * ((s32) in[2]) +
|
||||
((limb) ((s32) in2[0])) * ((s32) in[6]) +
|
||||
((limb) ((s32) in2[6])) * ((s32) in[0]);
|
||||
output[7] = ((limb) ((s32) in2[3])) * ((s32) in[4]) +
|
||||
((limb) ((s32) in2[4])) * ((s32) in[3]) +
|
||||
((limb) ((s32) in2[2])) * ((s32) in[5]) +
|
||||
((limb) ((s32) in2[5])) * ((s32) in[2]) +
|
||||
((limb) ((s32) in2[1])) * ((s32) in[6]) +
|
||||
((limb) ((s32) in2[6])) * ((s32) in[1]) +
|
||||
((limb) ((s32) in2[0])) * ((s32) in[7]) +
|
||||
((limb) ((s32) in2[7])) * ((s32) in[0]);
|
||||
output[8] = ((limb) ((s32) in2[4])) * ((s32) in[4]) +
|
||||
2 * (((limb) ((s32) in2[3])) * ((s32) in[5]) +
|
||||
((limb) ((s32) in2[5])) * ((s32) in[3]) +
|
||||
((limb) ((s32) in2[1])) * ((s32) in[7]) +
|
||||
((limb) ((s32) in2[7])) * ((s32) in[1])) +
|
||||
((limb) ((s32) in2[2])) * ((s32) in[6]) +
|
||||
((limb) ((s32) in2[6])) * ((s32) in[2]) +
|
||||
((limb) ((s32) in2[0])) * ((s32) in[8]) +
|
||||
((limb) ((s32) in2[8])) * ((s32) in[0]);
|
||||
output[9] = ((limb) ((s32) in2[4])) * ((s32) in[5]) +
|
||||
((limb) ((s32) in2[5])) * ((s32) in[4]) +
|
||||
((limb) ((s32) in2[3])) * ((s32) in[6]) +
|
||||
((limb) ((s32) in2[6])) * ((s32) in[3]) +
|
||||
((limb) ((s32) in2[2])) * ((s32) in[7]) +
|
||||
((limb) ((s32) in2[7])) * ((s32) in[2]) +
|
||||
((limb) ((s32) in2[1])) * ((s32) in[8]) +
|
||||
((limb) ((s32) in2[8])) * ((s32) in[1]) +
|
||||
((limb) ((s32) in2[0])) * ((s32) in[9]) +
|
||||
((limb) ((s32) in2[9])) * ((s32) in[0]);
|
||||
output[10] = 2 * (((limb) ((s32) in2[5])) * ((s32) in[5]) +
|
||||
((limb) ((s32) in2[3])) * ((s32) in[7]) +
|
||||
((limb) ((s32) in2[7])) * ((s32) in[3]) +
|
||||
((limb) ((s32) in2[1])) * ((s32) in[9]) +
|
||||
((limb) ((s32) in2[9])) * ((s32) in[1])) +
|
||||
((limb) ((s32) in2[4])) * ((s32) in[6]) +
|
||||
((limb) ((s32) in2[6])) * ((s32) in[4]) +
|
||||
((limb) ((s32) in2[2])) * ((s32) in[8]) +
|
||||
((limb) ((s32) in2[8])) * ((s32) in[2]);
|
||||
output[11] = ((limb) ((s32) in2[5])) * ((s32) in[6]) +
|
||||
((limb) ((s32) in2[6])) * ((s32) in[5]) +
|
||||
((limb) ((s32) in2[4])) * ((s32) in[7]) +
|
||||
((limb) ((s32) in2[7])) * ((s32) in[4]) +
|
||||
((limb) ((s32) in2[3])) * ((s32) in[8]) +
|
||||
((limb) ((s32) in2[8])) * ((s32) in[3]) +
|
||||
((limb) ((s32) in2[2])) * ((s32) in[9]) +
|
||||
((limb) ((s32) in2[9])) * ((s32) in[2]);
|
||||
output[12] = ((limb) ((s32) in2[6])) * ((s32) in[6]) +
|
||||
2 * (((limb) ((s32) in2[5])) * ((s32) in[7]) +
|
||||
((limb) ((s32) in2[7])) * ((s32) in[5]) +
|
||||
((limb) ((s32) in2[3])) * ((s32) in[9]) +
|
||||
((limb) ((s32) in2[9])) * ((s32) in[3])) +
|
||||
((limb) ((s32) in2[4])) * ((s32) in[8]) +
|
||||
((limb) ((s32) in2[8])) * ((s32) in[4]);
|
||||
output[13] = ((limb) ((s32) in2[6])) * ((s32) in[7]) +
|
||||
((limb) ((s32) in2[7])) * ((s32) in[6]) +
|
||||
((limb) ((s32) in2[5])) * ((s32) in[8]) +
|
||||
((limb) ((s32) in2[8])) * ((s32) in[5]) +
|
||||
((limb) ((s32) in2[4])) * ((s32) in[9]) +
|
||||
((limb) ((s32) in2[9])) * ((s32) in[4]);
|
||||
output[14] = 2 * (((limb) ((s32) in2[7])) * ((s32) in[7]) +
|
||||
((limb) ((s32) in2[5])) * ((s32) in[9]) +
|
||||
((limb) ((s32) in2[9])) * ((s32) in[5])) +
|
||||
((limb) ((s32) in2[6])) * ((s32) in[8]) +
|
||||
((limb) ((s32) in2[8])) * ((s32) in[6]);
|
||||
output[15] = ((limb) ((s32) in2[7])) * ((s32) in[8]) +
|
||||
((limb) ((s32) in2[8])) * ((s32) in[7]) +
|
||||
((limb) ((s32) in2[6])) * ((s32) in[9]) +
|
||||
((limb) ((s32) in2[9])) * ((s32) in[6]);
|
||||
output[16] = ((limb) ((s32) in2[8])) * ((s32) in[8]) +
|
||||
2 * (((limb) ((s32) in2[7])) * ((s32) in[9]) +
|
||||
((limb) ((s32) in2[9])) * ((s32) in[7]));
|
||||
output[17] = ((limb) ((s32) in2[8])) * ((s32) in[9]) +
|
||||
((limb) ((s32) in2[9])) * ((s32) in[8]);
|
||||
output[18] = 2 * ((limb) ((s32) in2[9])) * ((s32) in[9]);
|
||||
}
|
||||
|
||||
/* Reduce a long form to a short form by taking the input mod 2^255 - 19. */
|
||||
static void freduce_degree(limb *output) {
|
||||
/* Each of these shifts and adds ends up multiplying the value by 19. */
|
||||
output[8] += output[18] << 4;
|
||||
output[8] += output[18] << 1;
|
||||
output[8] += output[18];
|
||||
output[7] += output[17] << 4;
|
||||
output[7] += output[17] << 1;
|
||||
output[7] += output[17];
|
||||
output[6] += output[16] << 4;
|
||||
output[6] += output[16] << 1;
|
||||
output[6] += output[16];
|
||||
output[5] += output[15] << 4;
|
||||
output[5] += output[15] << 1;
|
||||
output[5] += output[15];
|
||||
output[4] += output[14] << 4;
|
||||
output[4] += output[14] << 1;
|
||||
output[4] += output[14];
|
||||
output[3] += output[13] << 4;
|
||||
output[3] += output[13] << 1;
|
||||
output[3] += output[13];
|
||||
output[2] += output[12] << 4;
|
||||
output[2] += output[12] << 1;
|
||||
output[2] += output[12];
|
||||
output[1] += output[11] << 4;
|
||||
output[1] += output[11] << 1;
|
||||
output[1] += output[11];
|
||||
output[0] += output[10] << 4;
|
||||
output[0] += output[10] << 1;
|
||||
output[0] += output[10];
|
||||
}
|
||||
|
||||
#if (-1 & 3) != 3
|
||||
#error "This code only works on a two's complement system"
|
||||
#endif
|
||||
|
||||
/* return v / 2^26, using only shifts and adds. */
|
||||
static inline limb
|
||||
div_by_2_26(const limb v)
|
||||
{
|
||||
/* High word of v; no shift needed*/
|
||||
const uint32_t highword = (uint32_t) (((uint64_t) v) >> 32);
|
||||
/* Set to all 1s if v was negative; else set to 0s. */
|
||||
const int32_t sign = ((int32_t) highword) >> 31;
|
||||
/* Set to 0x3ffffff if v was negative; else set to 0. */
|
||||
const int32_t roundoff = ((uint32_t) sign) >> 6;
|
||||
/* Should return v / (1<<26) */
|
||||
return (v + roundoff) >> 26;
|
||||
}
|
||||
|
||||
/* return v / (2^25), using only shifts and adds. */
|
||||
static inline limb
|
||||
div_by_2_25(const limb v)
|
||||
{
|
||||
/* High word of v; no shift needed*/
|
||||
const uint32_t highword = (uint32_t) (((uint64_t) v) >> 32);
|
||||
/* Set to all 1s if v was negative; else set to 0s. */
|
||||
const int32_t sign = ((int32_t) highword) >> 31;
|
||||
/* Set to 0x1ffffff if v was negative; else set to 0. */
|
||||
const int32_t roundoff = ((uint32_t) sign) >> 7;
|
||||
/* Should return v / (1<<25) */
|
||||
return (v + roundoff) >> 25;
|
||||
}
|
||||
|
||||
static inline s32
|
||||
div_s32_by_2_25(const s32 v)
|
||||
{
|
||||
const s32 roundoff = ((uint32_t)(v >> 31)) >> 7;
|
||||
return (v + roundoff) >> 25;
|
||||
}
|
||||
|
||||
/* Reduce all coefficients of the short form input so that |x| < 2^26.
|
||||
*
|
||||
* On entry: |output[i]| < 2^62
|
||||
*/
|
||||
static void freduce_coefficients(limb *output) {
|
||||
unsigned i;
|
||||
|
||||
output[10] = 0;
|
||||
|
||||
for (i = 0; i < 10; i += 2) {
|
||||
limb over = div_by_2_26(output[i]);
|
||||
output[i] -= over << 26;
|
||||
output[i+1] += over;
|
||||
|
||||
over = div_by_2_25(output[i+1]);
|
||||
output[i+1] -= over << 25;
|
||||
output[i+2] += over;
|
||||
}
|
||||
/* Now |output[10]| < 2 ^ 38 and all other coefficients are reduced. */
|
||||
output[0] += output[10] << 4;
|
||||
output[0] += output[10] << 1;
|
||||
output[0] += output[10];
|
||||
|
||||
output[10] = 0;
|
||||
|
||||
/* Now output[1..9] are reduced, and |output[0]| < 2^26 + 19 * 2^38
|
||||
* So |over| will be no more than 77825 */
|
||||
{
|
||||
limb over = div_by_2_26(output[0]);
|
||||
output[0] -= over << 26;
|
||||
output[1] += over;
|
||||
}
|
||||
|
||||
/* Now output[0,2..9] are reduced, and |output[1]| < 2^25 + 77825
|
||||
* So |over| will be no more than 1. */
|
||||
{
|
||||
/* output[1] fits in 32 bits, so we can use div_s32_by_2_25 here. */
|
||||
s32 over32 = div_s32_by_2_25((s32) output[1]);
|
||||
output[1] -= over32 << 25;
|
||||
output[2] += over32;
|
||||
}
|
||||
|
||||
/* Finally, output[0,1,3..9] are reduced, and output[2] is "nearly reduced":
|
||||
* we have |output[2]| <= 2^26. This is good enough for all of our math,
|
||||
* but it will require an extra freduce_coefficients before fcontract. */
|
||||
}
|
||||
|
||||
/* A helpful wrapper around fproduct: output = in * in2.
|
||||
*
|
||||
* output must be distinct to both inputs. The output is reduced degree and
|
||||
* reduced coefficient.
|
||||
*/
|
||||
static void
|
||||
fmul(limb *output, const limb *in, const limb *in2) {
|
||||
limb t[19];
|
||||
fproduct(t, in, in2);
|
||||
freduce_degree(t);
|
||||
freduce_coefficients(t);
|
||||
memcpy(output, t, sizeof(limb) * 10);
|
||||
}
|
||||
|
||||
static void fsquare_inner(limb *output, const limb *in) {
|
||||
output[0] = ((limb) ((s32) in[0])) * ((s32) in[0]);
|
||||
output[1] = 2 * ((limb) ((s32) in[0])) * ((s32) in[1]);
|
||||
output[2] = 2 * (((limb) ((s32) in[1])) * ((s32) in[1]) +
|
||||
((limb) ((s32) in[0])) * ((s32) in[2]));
|
||||
output[3] = 2 * (((limb) ((s32) in[1])) * ((s32) in[2]) +
|
||||
((limb) ((s32) in[0])) * ((s32) in[3]));
|
||||
output[4] = ((limb) ((s32) in[2])) * ((s32) in[2]) +
|
||||
4 * ((limb) ((s32) in[1])) * ((s32) in[3]) +
|
||||
2 * ((limb) ((s32) in[0])) * ((s32) in[4]);
|
||||
output[5] = 2 * (((limb) ((s32) in[2])) * ((s32) in[3]) +
|
||||
((limb) ((s32) in[1])) * ((s32) in[4]) +
|
||||
((limb) ((s32) in[0])) * ((s32) in[5]));
|
||||
output[6] = 2 * (((limb) ((s32) in[3])) * ((s32) in[3]) +
|
||||
((limb) ((s32) in[2])) * ((s32) in[4]) +
|
||||
((limb) ((s32) in[0])) * ((s32) in[6]) +
|
||||
2 * ((limb) ((s32) in[1])) * ((s32) in[5]));
|
||||
output[7] = 2 * (((limb) ((s32) in[3])) * ((s32) in[4]) +
|
||||
((limb) ((s32) in[2])) * ((s32) in[5]) +
|
||||
((limb) ((s32) in[1])) * ((s32) in[6]) +
|
||||
((limb) ((s32) in[0])) * ((s32) in[7]));
|
||||
output[8] = ((limb) ((s32) in[4])) * ((s32) in[4]) +
|
||||
2 * (((limb) ((s32) in[2])) * ((s32) in[6]) +
|
||||
((limb) ((s32) in[0])) * ((s32) in[8]) +
|
||||
2 * (((limb) ((s32) in[1])) * ((s32) in[7]) +
|
||||
((limb) ((s32) in[3])) * ((s32) in[5])));
|
||||
output[9] = 2 * (((limb) ((s32) in[4])) * ((s32) in[5]) +
|
||||
((limb) ((s32) in[3])) * ((s32) in[6]) +
|
||||
((limb) ((s32) in[2])) * ((s32) in[7]) +
|
||||
((limb) ((s32) in[1])) * ((s32) in[8]) +
|
||||
((limb) ((s32) in[0])) * ((s32) in[9]));
|
||||
output[10] = 2 * (((limb) ((s32) in[5])) * ((s32) in[5]) +
|
||||
((limb) ((s32) in[4])) * ((s32) in[6]) +
|
||||
((limb) ((s32) in[2])) * ((s32) in[8]) +
|
||||
2 * (((limb) ((s32) in[3])) * ((s32) in[7]) +
|
||||
((limb) ((s32) in[1])) * ((s32) in[9])));
|
||||
output[11] = 2 * (((limb) ((s32) in[5])) * ((s32) in[6]) +
|
||||
((limb) ((s32) in[4])) * ((s32) in[7]) +
|
||||
((limb) ((s32) in[3])) * ((s32) in[8]) +
|
||||
((limb) ((s32) in[2])) * ((s32) in[9]));
|
||||
output[12] = ((limb) ((s32) in[6])) * ((s32) in[6]) +
|
||||
2 * (((limb) ((s32) in[4])) * ((s32) in[8]) +
|
||||
2 * (((limb) ((s32) in[5])) * ((s32) in[7]) +
|
||||
((limb) ((s32) in[3])) * ((s32) in[9])));
|
||||
output[13] = 2 * (((limb) ((s32) in[6])) * ((s32) in[7]) +
|
||||
((limb) ((s32) in[5])) * ((s32) in[8]) +
|
||||
((limb) ((s32) in[4])) * ((s32) in[9]));
|
||||
output[14] = 2 * (((limb) ((s32) in[7])) * ((s32) in[7]) +
|
||||
((limb) ((s32) in[6])) * ((s32) in[8]) +
|
||||
2 * ((limb) ((s32) in[5])) * ((s32) in[9]));
|
||||
output[15] = 2 * (((limb) ((s32) in[7])) * ((s32) in[8]) +
|
||||
((limb) ((s32) in[6])) * ((s32) in[9]));
|
||||
output[16] = ((limb) ((s32) in[8])) * ((s32) in[8]) +
|
||||
4 * ((limb) ((s32) in[7])) * ((s32) in[9]);
|
||||
output[17] = 2 * ((limb) ((s32) in[8])) * ((s32) in[9]);
|
||||
output[18] = 2 * ((limb) ((s32) in[9])) * ((s32) in[9]);
|
||||
}
|
||||
|
||||
static void
|
||||
fsquare(limb *output, const limb *in) {
|
||||
limb t[19];
|
||||
fsquare_inner(t, in);
|
||||
freduce_degree(t);
|
||||
freduce_coefficients(t);
|
||||
memcpy(output, t, sizeof(limb) * 10);
|
||||
}
|
||||
|
||||
/* Take a little-endian, 32-byte number and expand it into polynomial form */
|
||||
static void
|
||||
fexpand(limb *output, const u8 *input) {
|
||||
#define F(n,start,shift,mask) \
|
||||
output[n] = ((((limb) input[start + 0]) | \
|
||||
((limb) input[start + 1]) << 8 | \
|
||||
((limb) input[start + 2]) << 16 | \
|
||||
((limb) input[start + 3]) << 24) >> shift) & mask;
|
||||
F(0, 0, 0, 0x3ffffff);
|
||||
F(1, 3, 2, 0x1ffffff);
|
||||
F(2, 6, 3, 0x3ffffff);
|
||||
F(3, 9, 5, 0x1ffffff);
|
||||
F(4, 12, 6, 0x3ffffff);
|
||||
F(5, 16, 0, 0x1ffffff);
|
||||
F(6, 19, 1, 0x3ffffff);
|
||||
F(7, 22, 3, 0x1ffffff);
|
||||
F(8, 25, 4, 0x3ffffff);
|
||||
F(9, 28, 6, 0x3ffffff);
|
||||
#undef F
|
||||
}
|
||||
|
||||
#if (-32 >> 1) != -16
|
||||
#error "This code only works when >> does sign-extension on negative numbers"
|
||||
#endif
|
||||
|
||||
/* Take a fully reduced polynomial form number and contract it into a
|
||||
* little-endian, 32-byte array
|
||||
*/
|
||||
static void
|
||||
fcontract(u8 *output, limb *input) {
|
||||
int i;
|
||||
int j;
|
||||
|
||||
for (j = 0; j < 2; ++j) {
|
||||
for (i = 0; i < 9; ++i) {
|
||||
if ((i & 1) == 1) {
|
||||
/* This calculation is a time-invariant way to make input[i] positive
|
||||
by borrowing from the next-larger limb.
|
||||
*/
|
||||
const s32 mask = (s32)(input[i]) >> 31;
|
||||
const s32 carry = -(((s32)(input[i]) & mask) >> 25);
|
||||
input[i] = (s32)(input[i]) + (carry << 25);
|
||||
input[i+1] = (s32)(input[i+1]) - carry;
|
||||
} else {
|
||||
const s32 mask = (s32)(input[i]) >> 31;
|
||||
const s32 carry = -(((s32)(input[i]) & mask) >> 26);
|
||||
input[i] = (s32)(input[i]) + (carry << 26);
|
||||
input[i+1] = (s32)(input[i+1]) - carry;
|
||||
}
|
||||
}
|
||||
{
|
||||
const s32 mask = (s32)(input[9]) >> 31;
|
||||
const s32 carry = -(((s32)(input[9]) & mask) >> 25);
|
||||
input[9] = (s32)(input[9]) + (carry << 25);
|
||||
input[0] = (s32)(input[0]) - (carry * 19);
|
||||
}
|
||||
}
|
||||
|
||||
/* The first borrow-propagation pass above ended with every limb
|
||||
except (possibly) input[0] non-negative.
|
||||
|
||||
Since each input limb except input[0] is decreased by at most 1
|
||||
by a borrow-propagation pass, the second borrow-propagation pass
|
||||
could only have wrapped around to decrease input[0] again if the
|
||||
first pass left input[0] negative *and* input[1] through input[9]
|
||||
were all zero. In that case, input[1] is now 2^25 - 1, and this
|
||||
last borrow-propagation step will leave input[1] non-negative.
|
||||
*/
|
||||
{
|
||||
const s32 mask = (s32)(input[0]) >> 31;
|
||||
const s32 carry = -(((s32)(input[0]) & mask) >> 26);
|
||||
input[0] = (s32)(input[0]) + (carry << 26);
|
||||
input[1] = (s32)(input[1]) - carry;
|
||||
}
|
||||
|
||||
/* Both passes through the above loop, plus the last 0-to-1 step, are
|
||||
necessary: if input[9] is -1 and input[0] through input[8] are 0,
|
||||
negative values will remain in the array until the end.
|
||||
*/
|
||||
|
||||
input[1] <<= 2;
|
||||
input[2] <<= 3;
|
||||
input[3] <<= 5;
|
||||
input[4] <<= 6;
|
||||
input[6] <<= 1;
|
||||
input[7] <<= 3;
|
||||
input[8] <<= 4;
|
||||
input[9] <<= 6;
|
||||
#define F(i, s) \
|
||||
output[s+0] |= input[i] & 0xff; \
|
||||
output[s+1] = (input[i] >> 8) & 0xff; \
|
||||
output[s+2] = (input[i] >> 16) & 0xff; \
|
||||
output[s+3] = (input[i] >> 24) & 0xff;
|
||||
output[0] = 0;
|
||||
output[16] = 0;
|
||||
F(0,0);
|
||||
F(1,3);
|
||||
F(2,6);
|
||||
F(3,9);
|
||||
F(4,12);
|
||||
F(5,16);
|
||||
F(6,19);
|
||||
F(7,22);
|
||||
F(8,25);
|
||||
F(9,28);
|
||||
#undef F
|
||||
}
|
||||
|
||||
/* Input: Q, Q', Q-Q'
|
||||
* Output: 2Q, Q+Q'
|
||||
*
|
||||
* x2 z3: long form
|
||||
* x3 z3: long form
|
||||
* x z: short form, destroyed
|
||||
* xprime zprime: short form, destroyed
|
||||
* qmqp: short form, preserved
|
||||
*/
|
||||
static void fmonty(limb *x2, limb *z2, /* output 2Q */
|
||||
limb *x3, limb *z3, /* output Q + Q' */
|
||||
limb *x, limb *z, /* input Q */
|
||||
limb *xprime, limb *zprime, /* input Q' */
|
||||
const limb *qmqp /* input Q - Q' */) {
|
||||
limb origx[10], origxprime[10], zzz[19], xx[19], zz[19], xxprime[19],
|
||||
zzprime[19], zzzprime[19], xxxprime[19];
|
||||
|
||||
memcpy(origx, x, 10 * sizeof(limb));
|
||||
fsum(x, z);
|
||||
fdifference(z, origx); // does x - z
|
||||
|
||||
memcpy(origxprime, xprime, sizeof(limb) * 10);
|
||||
fsum(xprime, zprime);
|
||||
fdifference(zprime, origxprime);
|
||||
fproduct(xxprime, xprime, z);
|
||||
fproduct(zzprime, x, zprime);
|
||||
freduce_degree(xxprime);
|
||||
freduce_coefficients(xxprime);
|
||||
freduce_degree(zzprime);
|
||||
freduce_coefficients(zzprime);
|
||||
memcpy(origxprime, xxprime, sizeof(limb) * 10);
|
||||
fsum(xxprime, zzprime);
|
||||
fdifference(zzprime, origxprime);
|
||||
fsquare(xxxprime, xxprime);
|
||||
fsquare(zzzprime, zzprime);
|
||||
fproduct(zzprime, zzzprime, qmqp);
|
||||
freduce_degree(zzprime);
|
||||
freduce_coefficients(zzprime);
|
||||
memcpy(x3, xxxprime, sizeof(limb) * 10);
|
||||
memcpy(z3, zzprime, sizeof(limb) * 10);
|
||||
|
||||
fsquare(xx, x);
|
||||
fsquare(zz, z);
|
||||
fproduct(x2, xx, zz);
|
||||
freduce_degree(x2);
|
||||
freduce_coefficients(x2);
|
||||
fdifference(zz, xx); // does zz = xx - zz
|
||||
memset(zzz + 10, 0, sizeof(limb) * 9);
|
||||
fscalar_product(zzz, zz, 121665);
|
||||
/* No need to call freduce_degree here:
|
||||
fscalar_product doesn't increase the degree of its input. */
|
||||
freduce_coefficients(zzz);
|
||||
fsum(zzz, xx);
|
||||
fproduct(z2, zz, zzz);
|
||||
freduce_degree(z2);
|
||||
freduce_coefficients(z2);
|
||||
}
|
||||
|
||||
/* Conditionally swap two reduced-form limb arrays if 'iswap' is 1, but leave
|
||||
* them unchanged if 'iswap' is 0. Runs in data-invariant time to avoid
|
||||
* side-channel attacks.
|
||||
*
|
||||
* NOTE that this function requires that 'iswap' be 1 or 0; other values give
|
||||
* wrong results. Also, the two limb arrays must be in reduced-coefficient,
|
||||
* reduced-degree form: the values in a[10..19] or b[10..19] aren't swapped,
|
||||
* and all all values in a[0..9],b[0..9] must have magnitude less than
|
||||
* INT32_MAX.
|
||||
*/
|
||||
static void
|
||||
swap_conditional(limb a[19], limb b[19], limb iswap) {
|
||||
unsigned i;
|
||||
const s32 swap = (s32) -iswap;
|
||||
|
||||
for (i = 0; i < 10; ++i) {
|
||||
const s32 x = swap & ( ((s32)a[i]) ^ ((s32)b[i]) );
|
||||
a[i] = ((s32)a[i]) ^ x;
|
||||
b[i] = ((s32)b[i]) ^ x;
|
||||
}
|
||||
}
|
||||
|
||||
/* Calculates nQ where Q is the x-coordinate of a point on the curve
|
||||
*
|
||||
* resultx/resultz: the x coordinate of the resulting curve point (short form)
|
||||
* n: a little endian, 32-byte number
|
||||
* q: a point of the curve (short form)
|
||||
*/
|
||||
static void
|
||||
cmult(limb *resultx, limb *resultz, const u8 *n, const limb *q) {
|
||||
limb a[19] = {0}, b[19] = {1}, c[19] = {1}, d[19] = {0};
|
||||
limb *nqpqx = a, *nqpqz = b, *nqx = c, *nqz = d, *t;
|
||||
limb e[19] = {0}, f[19] = {1}, g[19] = {0}, h[19] = {1};
|
||||
limb *nqpqx2 = e, *nqpqz2 = f, *nqx2 = g, *nqz2 = h;
|
||||
|
||||
unsigned i, j;
|
||||
|
||||
memcpy(nqpqx, q, sizeof(limb) * 10);
|
||||
|
||||
for (i = 0; i < 32; ++i) {
|
||||
u8 byte = n[31 - i];
|
||||
for (j = 0; j < 8; ++j) {
|
||||
const limb bit = byte >> 7;
|
||||
|
||||
swap_conditional(nqx, nqpqx, bit);
|
||||
swap_conditional(nqz, nqpqz, bit);
|
||||
fmonty(nqx2, nqz2,
|
||||
nqpqx2, nqpqz2,
|
||||
nqx, nqz,
|
||||
nqpqx, nqpqz,
|
||||
q);
|
||||
swap_conditional(nqx2, nqpqx2, bit);
|
||||
swap_conditional(nqz2, nqpqz2, bit);
|
||||
|
||||
t = nqx;
|
||||
nqx = nqx2;
|
||||
nqx2 = t;
|
||||
t = nqz;
|
||||
nqz = nqz2;
|
||||
nqz2 = t;
|
||||
t = nqpqx;
|
||||
nqpqx = nqpqx2;
|
||||
nqpqx2 = t;
|
||||
t = nqpqz;
|
||||
nqpqz = nqpqz2;
|
||||
nqpqz2 = t;
|
||||
|
||||
byte <<= 1;
|
||||
}
|
||||
}
|
||||
|
||||
memcpy(resultx, nqx, sizeof(limb) * 10);
|
||||
memcpy(resultz, nqz, sizeof(limb) * 10);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Shamelessly copied from djb's code
|
||||
// -----------------------------------------------------------------------------
|
||||
static void
|
||||
crecip(limb *out, const limb *z) {
|
||||
limb z2[10];
|
||||
limb z9[10];
|
||||
limb z11[10];
|
||||
limb z2_5_0[10];
|
||||
limb z2_10_0[10];
|
||||
limb z2_20_0[10];
|
||||
limb z2_50_0[10];
|
||||
limb z2_100_0[10];
|
||||
limb t0[10];
|
||||
limb t1[10];
|
||||
int i;
|
||||
|
||||
/* 2 */ fsquare(z2,z);
|
||||
/* 4 */ fsquare(t1,z2);
|
||||
/* 8 */ fsquare(t0,t1);
|
||||
/* 9 */ fmul(z9,t0,z);
|
||||
/* 11 */ fmul(z11,z9,z2);
|
||||
/* 22 */ fsquare(t0,z11);
|
||||
/* 2^5 - 2^0 = 31 */ fmul(z2_5_0,t0,z9);
|
||||
|
||||
/* 2^6 - 2^1 */ fsquare(t0,z2_5_0);
|
||||
/* 2^7 - 2^2 */ fsquare(t1,t0);
|
||||
/* 2^8 - 2^3 */ fsquare(t0,t1);
|
||||
/* 2^9 - 2^4 */ fsquare(t1,t0);
|
||||
/* 2^10 - 2^5 */ fsquare(t0,t1);
|
||||
/* 2^10 - 2^0 */ fmul(z2_10_0,t0,z2_5_0);
|
||||
|
||||
/* 2^11 - 2^1 */ fsquare(t0,z2_10_0);
|
||||
/* 2^12 - 2^2 */ fsquare(t1,t0);
|
||||
/* 2^20 - 2^10 */ for (i = 2;i < 10;i += 2) { fsquare(t0,t1); fsquare(t1,t0); }
|
||||
/* 2^20 - 2^0 */ fmul(z2_20_0,t1,z2_10_0);
|
||||
|
||||
/* 2^21 - 2^1 */ fsquare(t0,z2_20_0);
|
||||
/* 2^22 - 2^2 */ fsquare(t1,t0);
|
||||
/* 2^40 - 2^20 */ for (i = 2;i < 20;i += 2) { fsquare(t0,t1); fsquare(t1,t0); }
|
||||
/* 2^40 - 2^0 */ fmul(t0,t1,z2_20_0);
|
||||
|
||||
/* 2^41 - 2^1 */ fsquare(t1,t0);
|
||||
/* 2^42 - 2^2 */ fsquare(t0,t1);
|
||||
/* 2^50 - 2^10 */ for (i = 2;i < 10;i += 2) { fsquare(t1,t0); fsquare(t0,t1); }
|
||||
/* 2^50 - 2^0 */ fmul(z2_50_0,t0,z2_10_0);
|
||||
|
||||
/* 2^51 - 2^1 */ fsquare(t0,z2_50_0);
|
||||
/* 2^52 - 2^2 */ fsquare(t1,t0);
|
||||
/* 2^100 - 2^50 */ for (i = 2;i < 50;i += 2) { fsquare(t0,t1); fsquare(t1,t0); }
|
||||
/* 2^100 - 2^0 */ fmul(z2_100_0,t1,z2_50_0);
|
||||
|
||||
/* 2^101 - 2^1 */ fsquare(t1,z2_100_0);
|
||||
/* 2^102 - 2^2 */ fsquare(t0,t1);
|
||||
/* 2^200 - 2^100 */ for (i = 2;i < 100;i += 2) { fsquare(t1,t0); fsquare(t0,t1); }
|
||||
/* 2^200 - 2^0 */ fmul(t1,t0,z2_100_0);
|
||||
|
||||
/* 2^201 - 2^1 */ fsquare(t0,t1);
|
||||
/* 2^202 - 2^2 */ fsquare(t1,t0);
|
||||
/* 2^250 - 2^50 */ for (i = 2;i < 50;i += 2) { fsquare(t0,t1); fsquare(t1,t0); }
|
||||
/* 2^250 - 2^0 */ fmul(t0,t1,z2_50_0);
|
||||
|
||||
/* 2^251 - 2^1 */ fsquare(t1,t0);
|
||||
/* 2^252 - 2^2 */ fsquare(t0,t1);
|
||||
/* 2^253 - 2^3 */ fsquare(t1,t0);
|
||||
/* 2^254 - 2^4 */ fsquare(t0,t1);
|
||||
/* 2^255 - 2^5 */ fsquare(t1,t0);
|
||||
/* 2^255 - 21 */ fmul(out,t1,z11);
|
||||
}
|
||||
|
||||
int curve25519_donna(u8 *, const u8 *, const u8 *);
|
||||
|
||||
int
|
||||
curve25519_donna(u8 *mypublic, const u8 *secret, const u8 *basepoint) {
|
||||
limb bp[10], x[10], z[11], zmone[10];
|
||||
uint8_t e[32];
|
||||
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);
|
||||
crecip(zmone, z);
|
||||
fmul(z, x, zmone);
|
||||
freduce_coefficients(z);
|
||||
fcontract(mypublic, z);
|
||||
return 0;
|
||||
}
|
||||
6
library/jni/curve25519-donna.h
Normal file
6
library/jni/curve25519-donna.h
Normal file
@@ -0,0 +1,6 @@
|
||||
#ifndef CURVE25519_DONNA_H
|
||||
#define CURVE25519_DONNA_H
|
||||
|
||||
extern int curve25519_donna(uint8_t *, const uint8_t *, const uint8_t *);
|
||||
|
||||
#endif
|
||||
BIN
library/libs/armeabi.jar
Normal file
BIN
library/libs/armeabi.jar
Normal file
Binary file not shown.
20
library/proguard-project.txt
Normal file
20
library/proguard-project.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
# To enable ProGuard in your project, edit project.properties
|
||||
# to define the proguard.config property as described in that file.
|
||||
#
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in ${sdk.dir}/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the ProGuard
|
||||
# include property in project.properties.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
52
library/protobuf/IncomingPushMessageSignal.proto
Normal file
52
library/protobuf/IncomingPushMessageSignal.proto
Normal file
@@ -0,0 +1,52 @@
|
||||
package textsecure;
|
||||
|
||||
option java_package = "org.whispersystems.textsecure.push";
|
||||
option java_outer_classname = "PushMessageProtos";
|
||||
|
||||
message IncomingPushMessageSignal {
|
||||
enum Type {
|
||||
UNKNOWN = 0;
|
||||
CIPHERTEXT = 1;
|
||||
KEY_EXCHANGE = 2;
|
||||
PREKEY_BUNDLE = 3;
|
||||
PLAINTEXT = 4;
|
||||
}
|
||||
optional Type type = 1;
|
||||
optional string source = 2;
|
||||
optional uint32 sourceDevice = 7;
|
||||
optional string relay = 3;
|
||||
optional uint64 timestamp = 5;
|
||||
optional bytes message = 6; // Contains an encrypted PushMessageContent
|
||||
// repeated string destinations = 4; // No longer supported
|
||||
}
|
||||
|
||||
message PushMessageContent {
|
||||
message AttachmentPointer {
|
||||
optional fixed64 id = 1;
|
||||
optional string contentType = 2;
|
||||
optional bytes key = 3;
|
||||
}
|
||||
|
||||
message GroupContext {
|
||||
enum Type {
|
||||
UNKNOWN = 0;
|
||||
UPDATE = 1;
|
||||
DELIVER = 2;
|
||||
QUIT = 3;
|
||||
}
|
||||
optional bytes id = 1;
|
||||
optional Type type = 2;
|
||||
optional string name = 3;
|
||||
repeated string members = 4;
|
||||
optional AttachmentPointer avatar = 5;
|
||||
}
|
||||
|
||||
enum Flags {
|
||||
END_SESSION = 1;
|
||||
}
|
||||
|
||||
optional string body = 1;
|
||||
repeated AttachmentPointer attachments = 2;
|
||||
optional GroupContext group = 3;
|
||||
optional uint32 flags = 4;
|
||||
}
|
||||
63
library/protobuf/LocalStorageProtocol.proto
Normal file
63
library/protobuf/LocalStorageProtocol.proto
Normal file
@@ -0,0 +1,63 @@
|
||||
package textsecure;
|
||||
|
||||
option java_package = "org.whispersystems.textsecure.storage";
|
||||
option java_outer_classname = "StorageProtos";
|
||||
|
||||
message SessionStructure {
|
||||
message Chain {
|
||||
optional bytes senderEphemeral = 1;
|
||||
optional bytes senderEphemeralPrivate = 2;
|
||||
|
||||
message ChainKey {
|
||||
optional uint32 index = 1;
|
||||
optional bytes key = 2;
|
||||
}
|
||||
|
||||
optional ChainKey chainKey = 3;
|
||||
|
||||
message MessageKey {
|
||||
optional uint32 index = 1;
|
||||
optional bytes cipherKey = 2;
|
||||
optional bytes macKey = 3;
|
||||
}
|
||||
|
||||
repeated MessageKey messageKeys = 4;
|
||||
}
|
||||
|
||||
message PendingKeyExchange {
|
||||
optional uint32 sequence = 1;
|
||||
optional bytes localBaseKey = 2;
|
||||
optional bytes localBaseKeyPrivate = 3;
|
||||
optional bytes localEphemeralKey = 4;
|
||||
optional bytes localEphemeralKeyPrivate = 5;
|
||||
optional bytes localIdentityKey = 7;
|
||||
optional bytes localIdentityKeyPrivate = 8;
|
||||
}
|
||||
|
||||
message PendingPreKey {
|
||||
optional uint32 preKeyId = 1;
|
||||
optional bytes baseKey = 2;
|
||||
}
|
||||
|
||||
optional uint32 sessionVersion = 1;
|
||||
optional bytes localIdentityPublic = 2;
|
||||
optional bytes remoteIdentityPublic = 3;
|
||||
|
||||
optional bytes rootKey = 4;
|
||||
optional uint32 previousCounter = 5;
|
||||
|
||||
optional Chain senderChain = 6;
|
||||
repeated Chain receiverChains = 7;
|
||||
|
||||
optional PendingKeyExchange pendingKeyExchange = 8;
|
||||
optional PendingPreKey pendingPreKey = 9;
|
||||
|
||||
optional uint32 remoteRegistrationId = 10;
|
||||
optional uint32 localRegistrationId = 11;
|
||||
}
|
||||
|
||||
message PreKeyRecordStructure {
|
||||
optional uint32 id = 1;
|
||||
optional bytes publicKey = 2;
|
||||
optional bytes privateKey = 3;
|
||||
}
|
||||
3
library/protobuf/Makefile
Normal file
3
library/protobuf/Makefile
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
all:
|
||||
protoc --java_out=../src/ IncomingPushMessageSignal.proto WhisperTextProtocol.proto LocalStorageProtocol.proto
|
||||
26
library/protobuf/WhisperTextProtocol.proto
Normal file
26
library/protobuf/WhisperTextProtocol.proto
Normal file
@@ -0,0 +1,26 @@
|
||||
package textsecure;
|
||||
|
||||
option java_package = "org.whispersystems.textsecure.crypto.protocol";
|
||||
option java_outer_classname = "WhisperProtos";
|
||||
|
||||
message WhisperMessage {
|
||||
optional bytes ephemeralKey = 1;
|
||||
optional uint32 counter = 2;
|
||||
optional uint32 previousCounter = 3;
|
||||
optional bytes ciphertext = 4;
|
||||
}
|
||||
|
||||
message PreKeyWhisperMessage {
|
||||
optional uint32 registrationId = 5;
|
||||
optional uint32 preKeyId = 1;
|
||||
optional bytes baseKey = 2;
|
||||
optional bytes identityKey = 3;
|
||||
optional bytes message = 4; // WhisperMessage
|
||||
}
|
||||
|
||||
message KeyExchangeMessage {
|
||||
optional uint32 id = 1;
|
||||
optional bytes baseKey = 2;
|
||||
optional bytes ephemeralKey = 3;
|
||||
optional bytes identityKey = 4;
|
||||
}
|
||||
3
library/res/values/strings.xml
Normal file
3
library/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
</resources>
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* 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.util.Hex;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.ParseException;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Encrypts push attachments.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public class AttachmentCipher {
|
||||
|
||||
static final int CIPHER_KEY_SIZE = 32;
|
||||
static final int MAC_KEY_SIZE = 32;
|
||||
|
||||
private final SecretKeySpec cipherKey;
|
||||
private final SecretKeySpec macKey;
|
||||
private final Cipher cipher;
|
||||
private final Mac mac;
|
||||
|
||||
public AttachmentCipher() {
|
||||
this.cipherKey = initializeRandomCipherKey();
|
||||
this.macKey = initializeRandomMacKey();
|
||||
this.cipher = initializeCipher();
|
||||
this.mac = initializeMac();
|
||||
}
|
||||
|
||||
public AttachmentCipher(byte[] combinedKeyMaterial) {
|
||||
byte[][] parts = Util.split(combinedKeyMaterial, CIPHER_KEY_SIZE, MAC_KEY_SIZE);
|
||||
this.cipherKey = new SecretKeySpec(parts[0], "AES");
|
||||
this.macKey = new SecretKeySpec(parts[1], "HmacSHA256");
|
||||
this.cipher = initializeCipher();
|
||||
this.mac = initializeMac();
|
||||
}
|
||||
|
||||
public byte[] getCombinedKeyMaterial() {
|
||||
return Util.combine(this.cipherKey.getEncoded(), this.macKey.getEncoded());
|
||||
}
|
||||
|
||||
public byte[] encrypt(byte[] plaintext) {
|
||||
try {
|
||||
this.cipher.init(Cipher.ENCRYPT_MODE, this.cipherKey);
|
||||
this.mac.init(this.macKey);
|
||||
|
||||
byte[] ciphertext = this.cipher.doFinal(plaintext);
|
||||
byte[] iv = this.cipher.getIV();
|
||||
byte[] mac = this.mac.doFinal(Util.combine(iv, ciphertext));
|
||||
|
||||
return Util.combine(iv, ciphertext, mac);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decrypt(byte[] ciphertext)
|
||||
throws InvalidMacException, InvalidMessageException
|
||||
{
|
||||
try {
|
||||
if (ciphertext.length <= cipher.getBlockSize() + mac.getMacLength()) {
|
||||
throw new InvalidMessageException("Message too short!");
|
||||
}
|
||||
|
||||
byte[][] ciphertextParts = Util.split(ciphertext,
|
||||
this.cipher.getBlockSize(),
|
||||
ciphertext.length - this.cipher.getBlockSize() - this.mac.getMacLength(),
|
||||
this.mac.getMacLength());
|
||||
|
||||
this.mac.update(ciphertext, 0, ciphertext.length - mac.getMacLength());
|
||||
byte[] ourMac = this.mac.doFinal();
|
||||
|
||||
if (!Arrays.equals(ourMac, ciphertextParts[2])) {
|
||||
throw new InvalidMacException("Mac doesn't match!");
|
||||
}
|
||||
|
||||
this.cipher.init(Cipher.DECRYPT_MODE, this.cipherKey,
|
||||
new IvParameterSpec(ciphertextParts[0]));
|
||||
|
||||
return cipher.doFinal(ciphertextParts[1]);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
} catch (ParseException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Mac initializeMac() {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
return mac;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Cipher initializeCipher() {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
return cipher;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private SecretKeySpec initializeRandomCipherKey() {
|
||||
byte[] key = new byte[CIPHER_KEY_SIZE];
|
||||
Util.getSecureRandom().nextBytes(key);
|
||||
return new SecretKeySpec(key, "AES");
|
||||
}
|
||||
|
||||
private SecretKeySpec initializeRandomMacKey() {
|
||||
byte[] key = new byte[MAC_KEY_SIZE];
|
||||
Util.getSecureRandom().nextBytes(key);
|
||||
return new SecretKeySpec(key, "HmacSHA256");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* 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.util.Util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.ShortBufferException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Class for streaming an encrypted push attachment off disk.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class AttachmentCipherInputStream extends FileInputStream {
|
||||
|
||||
private static final int BLOCK_SIZE = 16;
|
||||
|
||||
private Cipher cipher;
|
||||
private boolean done;
|
||||
private long totalDataSize;
|
||||
private long totalRead;
|
||||
private byte[] overflowBuffer;
|
||||
|
||||
public AttachmentCipherInputStream(File file, byte[] combinedKeyMaterial)
|
||||
throws IOException, InvalidMessageException
|
||||
{
|
||||
super(file);
|
||||
|
||||
try {
|
||||
byte[][] parts = Util.split(combinedKeyMaterial,
|
||||
AttachmentCipher.CIPHER_KEY_SIZE,
|
||||
AttachmentCipher.MAC_KEY_SIZE);
|
||||
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(parts[1], "HmacSHA256"));
|
||||
|
||||
if (file.length() <= BLOCK_SIZE + mac.getMacLength()) {
|
||||
throw new InvalidMessageException("Message shorter than crypto overhead!");
|
||||
}
|
||||
|
||||
verifyMac(file, mac);
|
||||
|
||||
byte[] iv = new byte[BLOCK_SIZE];
|
||||
readFully(iv);
|
||||
|
||||
this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(parts[0], "AES"), new IvParameterSpec(iv));
|
||||
|
||||
this.done = false;
|
||||
this.totalRead = 0;
|
||||
this.totalDataSize = file.length() - cipher.getBlockSize() - mac.getMacLength();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidMacException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
} catch (NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer) throws IOException {
|
||||
return read(buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int offset, int length) throws IOException {
|
||||
if (totalRead != totalDataSize) return readIncremental(buffer, offset, length);
|
||||
else if (!done) return readFinal(buffer, offset, length);
|
||||
else return -1;
|
||||
}
|
||||
|
||||
private int readFinal(byte[] buffer, int offset, int length) throws IOException {
|
||||
try {
|
||||
int flourish = cipher.doFinal(buffer, offset);
|
||||
|
||||
done = true;
|
||||
return flourish;
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
Log.w("EncryptingPartInputStream", e);
|
||||
throw new IOException("Illegal block size exception!");
|
||||
} catch (ShortBufferException e) {
|
||||
Log.w("EncryptingPartInputStream", e);
|
||||
throw new IOException("Short buffer exception!");
|
||||
} catch (BadPaddingException e) {
|
||||
Log.w("EncryptingPartInputStream", e);
|
||||
throw new IOException("Bad padding exception!");
|
||||
}
|
||||
}
|
||||
|
||||
private int readIncremental(byte[] buffer, int offset, int length) throws IOException {
|
||||
int readLength = 0;
|
||||
if (null != overflowBuffer) {
|
||||
if (overflowBuffer.length > length) {
|
||||
System.arraycopy(overflowBuffer, 0, buffer, offset, length);
|
||||
overflowBuffer = Arrays.copyOfRange(overflowBuffer, length, overflowBuffer.length);
|
||||
return length;
|
||||
} else if (overflowBuffer.length == length) {
|
||||
System.arraycopy(overflowBuffer, 0, buffer, offset, length);
|
||||
overflowBuffer = null;
|
||||
return length;
|
||||
} else {
|
||||
System.arraycopy(overflowBuffer, 0, buffer, offset, overflowBuffer.length);
|
||||
readLength += overflowBuffer.length;
|
||||
offset += readLength;
|
||||
length -= readLength;
|
||||
overflowBuffer = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (length + totalRead > totalDataSize)
|
||||
length = (int)(totalDataSize - totalRead);
|
||||
|
||||
byte[] internalBuffer = new byte[length];
|
||||
int read = super.read(internalBuffer, 0, internalBuffer.length <= cipher.getBlockSize() ? internalBuffer.length : internalBuffer.length - cipher.getBlockSize());
|
||||
totalRead += read;
|
||||
|
||||
try {
|
||||
int outputLen = cipher.getOutputSize(read);
|
||||
|
||||
if (outputLen <= length) {
|
||||
readLength += cipher.update(internalBuffer, 0, read, buffer, offset);
|
||||
return readLength;
|
||||
}
|
||||
|
||||
byte[] transientBuffer = new byte[outputLen];
|
||||
outputLen = cipher.update(internalBuffer, 0, read, transientBuffer, 0);
|
||||
if (outputLen <= length) {
|
||||
System.arraycopy(transientBuffer, 0, buffer, offset, outputLen);
|
||||
readLength += outputLen;
|
||||
} else {
|
||||
System.arraycopy(transientBuffer, 0, buffer, offset, length);
|
||||
overflowBuffer = Arrays.copyOfRange(transientBuffer, length, outputLen);
|
||||
readLength += length;
|
||||
}
|
||||
return readLength;
|
||||
} catch (ShortBufferException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyMac(File file, Mac mac) throws FileNotFoundException, InvalidMacException {
|
||||
try {
|
||||
FileInputStream fin = new FileInputStream(file);
|
||||
int remainingData = (int) file.length() - mac.getMacLength();
|
||||
byte[] buffer = new byte[4096];
|
||||
|
||||
while (remainingData > 0) {
|
||||
int read = fin.read(buffer, 0, Math.min(buffer.length, remainingData));
|
||||
mac.update(buffer, 0, read);
|
||||
remainingData -= read;
|
||||
}
|
||||
|
||||
byte[] ourMac = mac.doFinal();
|
||||
byte[] theirMac = new byte[mac.getMacLength()];
|
||||
Util.readFully(fin, theirMac);
|
||||
|
||||
if (!Arrays.equals(ourMac, theirMac)) {
|
||||
throw new InvalidMacException("MAC doesn't match!");
|
||||
}
|
||||
} catch (IOException e1) {
|
||||
throw new InvalidMacException(e1);
|
||||
}
|
||||
}
|
||||
|
||||
private void readFully(byte[] buffer) throws IOException {
|
||||
int offset = 0;
|
||||
|
||||
for (;;) {
|
||||
int read = super.read(buffer, offset, buffer.length - offset);
|
||||
|
||||
if (read + offset < buffer.length) offset += read;
|
||||
else return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* 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
|
||||
@@ -14,15 +15,16 @@
|
||||
* 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.thoughtcrime.securesms.crypto;
|
||||
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.bouncycastle.math.ec.ECPoint;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.os.Parcel;
|
||||
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.
|
||||
*
|
||||
@@ -45,15 +47,14 @@ public class IdentityKey implements Parcelable, SerializableKey {
|
||||
}
|
||||
};
|
||||
|
||||
public static final int SIZE = 1 + 33;
|
||||
private static final int VERSION = 1;
|
||||
|
||||
private ECPublicKeyParameters publicKey;
|
||||
|
||||
public IdentityKey(ECPublicKeyParameters publicKey) {
|
||||
public static final int NIST_SIZE = 1 + ECPublicKey.KEY_SIZE;
|
||||
|
||||
private ECPublicKey publicKey;
|
||||
|
||||
public IdentityKey(ECPublicKey publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
|
||||
public IdentityKey(Parcel in) throws InvalidKeyException {
|
||||
int length = in.readInt();
|
||||
byte[] serialized = new byte[length];
|
||||
@@ -65,54 +66,45 @@ public class IdentityKey implements Parcelable, SerializableKey {
|
||||
public IdentityKey(byte[] bytes, int offset) throws InvalidKeyException {
|
||||
initializeFromSerialized(bytes, offset);
|
||||
}
|
||||
|
||||
public ECPublicKeyParameters getPublicKeyParameters() {
|
||||
return this.publicKey;
|
||||
|
||||
public ECPublicKey getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
|
||||
private void initializeFromSerialized(byte[] bytes, int offset) throws InvalidKeyException {
|
||||
int version = bytes[offset] & 0xff;
|
||||
|
||||
if (version > VERSION)
|
||||
throw new InvalidKeyException("Unsupported key version: " + version);
|
||||
|
||||
byte[] pointBytes = new byte[PublicKey.POINT_SIZE];
|
||||
System.arraycopy(bytes, offset+1, pointBytes, 0, pointBytes.length);
|
||||
|
||||
ECPoint Q;
|
||||
|
||||
try {
|
||||
Q = KeyUtil.decodePoint(pointBytes);
|
||||
} catch (RuntimeException re) {
|
||||
throw new InvalidKeyException(re);
|
||||
if ((bytes[offset] & 0xff) == 1) {
|
||||
this.publicKey = Curve.decodePoint(bytes, offset +1);
|
||||
} else {
|
||||
this.publicKey = Curve.decodePoint(bytes, offset);
|
||||
}
|
||||
|
||||
this.publicKey = new ECPublicKeyParameters(Q, KeyUtil.domainParameters);
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
byte[] encodedKey = KeyUtil.encodePoint(publicKey.getQ());
|
||||
byte[] combined = new byte[1 + encodedKey.length];
|
||||
|
||||
combined[0] = (byte)VERSION;
|
||||
System.arraycopy(encodedKey, 0, combined, 1, encodedKey.length);
|
||||
|
||||
return combined;
|
||||
if (publicKey.getType() == Curve.NIST_TYPE) {
|
||||
byte[] versionBytes = {0x01};
|
||||
byte[] encodedKey = publicKey.serialize();
|
||||
|
||||
return Util.combine(versionBytes, encodedKey);
|
||||
} else {
|
||||
return publicKey.serialize();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public String getFingerprint() {
|
||||
return Hex.toString(serialize());
|
||||
return Hex.toString(publicKey.serialize());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null) return false;
|
||||
if (!(other instanceof IdentityKey)) return false;
|
||||
return publicKey.getQ().equals(((IdentityKey)other).publicKey.getQ());
|
||||
|
||||
return publicKey.equals(((IdentityKey) other).getPublicKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return publicKey.getQ().hashCode();
|
||||
return publicKey.hashCode();
|
||||
}
|
||||
|
||||
public int describeContents() {
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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 org.whispersystems.textsecure.crypto.ecc.ECPrivateKey;
|
||||
|
||||
/**
|
||||
* Holder for public and private identity key pair.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public class IdentityKeyPair {
|
||||
|
||||
private final IdentityKey publicKey;
|
||||
private final ECPrivateKey privateKey;
|
||||
|
||||
public IdentityKeyPair(IdentityKey publicKey, ECPrivateKey privateKey) {
|
||||
this.publicKey = publicKey;
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
public IdentityKey getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public ECPrivateKey getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* 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.thoughtcrime.securesms.crypto;
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
public class InvalidKeyException extends Exception {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* 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.thoughtcrime.securesms.crypto;
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
public class InvalidMacException extends Exception {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* 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.thoughtcrime.securesms.util;
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
public class InvalidMessageException extends Exception {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* 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.thoughtcrime.securesms.crypto;
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
public class InvalidVersionException extends Exception {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* 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
|
||||
@@ -14,15 +15,15 @@
|
||||
* 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.thoughtcrime.securesms.crypto;
|
||||
|
||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
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.
|
||||
*
|
||||
@@ -31,15 +32,15 @@ import android.util.Log;
|
||||
|
||||
public class KeyPair {
|
||||
|
||||
private ECPrivateKeyParameters privateKey;
|
||||
private PublicKey publicKey;
|
||||
|
||||
private PublicKey publicKey;
|
||||
private ECPrivateKey privateKey;
|
||||
|
||||
private final MasterCipher masterCipher;
|
||||
|
||||
public KeyPair(int keyPairId, AsymmetricCipherKeyPair keyPair, MasterSecret masterSecret) {
|
||||
public KeyPair(int keyPairId, ECKeyPair keyPair, MasterSecret masterSecret) {
|
||||
this.masterCipher = new MasterCipher(masterSecret);
|
||||
this.publicKey = new PublicKey(keyPairId, (ECPublicKeyParameters)keyPair.getPublic());
|
||||
this.privateKey = (ECPrivateKeyParameters)keyPair.getPrivate();
|
||||
this.publicKey = new PublicKey(keyPairId, keyPair.getPublicKey());
|
||||
this.privateKey = keyPair.getPrivateKey();
|
||||
}
|
||||
|
||||
public KeyPair(byte[] bytes, MasterCipher masterCipher) throws InvalidKeyException {
|
||||
@@ -54,11 +55,11 @@ public class KeyPair {
|
||||
public PublicKey getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public AsymmetricCipherKeyPair getKeyPair() {
|
||||
return new AsymmetricCipherKeyPair(publicKey.getKey(), privateKey);
|
||||
|
||||
public ECPrivateKey getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
|
||||
public byte[] toBytes() {
|
||||
return serialize();
|
||||
}
|
||||
@@ -67,18 +68,14 @@ public class KeyPair {
|
||||
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(privateKeyBytes);
|
||||
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);
|
||||
byte[] combined = new byte[publicKeyBytes.length + privateKeyBytes.length];
|
||||
System.arraycopy(publicKeyBytes, 0, combined, 0, publicKeyBytes.length);
|
||||
System.arraycopy(privateKeyBytes, 0, combined, publicKeyBytes.length, privateKeyBytes.length);
|
||||
|
||||
return combined;
|
||||
byte[] privateKeyBytes = masterCipher.encryptKey(privateKey);
|
||||
return Util.combine(publicKeyBytes, privateKeyBytes);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* 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
|
||||
@@ -14,10 +15,16 @@
|
||||
* 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.thoughtcrime.securesms.crypto;
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPrivateKey;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
@@ -32,13 +39,6 @@ import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.thoughtcrime.securesms.util.InvalidMessageException;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Class that handles encryption for local storage.
|
||||
*
|
||||
@@ -70,13 +70,11 @@ public class MasterCipher {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] encryptKey(ECPrivateKeyParameters params) {
|
||||
BigInteger d = params.getD();
|
||||
byte[] dBytes = d.toByteArray();
|
||||
return encryptBytes(dBytes);
|
||||
|
||||
public byte[] encryptKey(ECPrivateKey privateKey) {
|
||||
return encryptBytes(privateKey.serialize());
|
||||
}
|
||||
|
||||
|
||||
public String encryptBody(String body) {
|
||||
return encryptAndEncodeBytes(body.getBytes());
|
||||
}
|
||||
@@ -85,13 +83,13 @@ public class MasterCipher {
|
||||
return new String(decodeAndDecryptBytes(body));
|
||||
}
|
||||
|
||||
public ECPrivateKeyParameters decryptKey(byte[] key) {
|
||||
public ECPrivateKey decryptKey(int type, byte[] key)
|
||||
throws org.whispersystems.textsecure.crypto.InvalidKeyException
|
||||
{
|
||||
try {
|
||||
BigInteger d = new BigInteger(decryptBytes(key));
|
||||
return new ECPrivateKeyParameters(d, KeyUtil.domainParameters);
|
||||
return Curve.decodePrivatePoint(type, decryptBytes(key));
|
||||
} catch (InvalidMessageException ime) {
|
||||
Log.w("bodycipher", ime);
|
||||
return null; // XXX
|
||||
throw new org.whispersystems.textsecure.crypto.InvalidKeyException(ime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,13 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import org.bouncycastle.util.Arrays;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* When a user first initializes TextSecure, a few secrets
|
||||
@@ -72,7 +71,7 @@ public class MasterSecret implements Parcelable {
|
||||
this.macKey = new SecretKeySpec(macKeyBytes, "HmacSHA1");
|
||||
|
||||
// SecretKeySpec does an internal copy in its constructor.
|
||||
Arrays.fill(encryptionKeyBytes, (byte)0x00);
|
||||
Arrays.fill(encryptionKeyBytes, (byte) 0x00);
|
||||
Arrays.fill(macKeyBytes, (byte)0x00);
|
||||
}
|
||||
|
||||
183
library/src/org/whispersystems/textsecure/crypto/PreKeyUtil.java
Normal file
183
library/src/org/whispersystems/textsecure/crypto/PreKeyUtil.java
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 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.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.thoughtcrimegson.Gson;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.ecc.Curve25519;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
|
||||
import org.whispersystems.textsecure.storage.InvalidKeyIdException;
|
||||
import org.whispersystems.textsecure.storage.PreKeyRecord;
|
||||
import org.whispersystems.textsecure.util.Medium;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
import java.io.File;
|
||||
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;
|
||||
|
||||
public class PreKeyUtil {
|
||||
|
||||
public static final int BATCH_SIZE = 100;
|
||||
|
||||
public static List<PreKeyRecord> generatePreKeys(Context context, MasterSecret masterSecret) {
|
||||
List<PreKeyRecord> records = new LinkedList<PreKeyRecord>();
|
||||
int preKeyIdOffset = getNextPreKeyId(context);
|
||||
|
||||
for (int i=0;i<BATCH_SIZE;i++) {
|
||||
int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
|
||||
ECKeyPair keyPair = Curve25519.generateKeyPair();
|
||||
PreKeyRecord record = new PreKeyRecord(context, masterSecret, preKeyId, keyPair);
|
||||
|
||||
record.save();
|
||||
records.add(record);
|
||||
}
|
||||
|
||||
setNextPreKeyId(context, (preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE);
|
||||
return records;
|
||||
}
|
||||
|
||||
public static PreKeyRecord generateLastResortKey(Context context, MasterSecret masterSecret) {
|
||||
if (PreKeyRecord.hasRecord(context, Medium.MAX_VALUE)) {
|
||||
try {
|
||||
return new PreKeyRecord(context, masterSecret, Medium.MAX_VALUE);
|
||||
} catch (InvalidKeyIdException e) {
|
||||
Log.w("PreKeyUtil", e);
|
||||
PreKeyRecord.delete(context, Medium.MAX_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
ECKeyPair keyPair = Curve25519.generateKeyPair();
|
||||
PreKeyRecord record = new PreKeyRecord(context, masterSecret, Medium.MAX_VALUE, keyPair);
|
||||
|
||||
record.save();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
private static void setNextPreKeyId(Context context, int id) {
|
||||
try {
|
||||
File nextFile = new File(getPreKeysDirectory(context), PreKeyIndex.FILE_NAME);
|
||||
FileOutputStream fout = new FileOutputStream(nextFile);
|
||||
fout.write(new Gson().toJson(new PreKeyIndex(id)).getBytes());
|
||||
fout.close();
|
||||
} catch (IOException e) {
|
||||
Log.w("PreKeyUtil", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getNextPreKeyId(Context context) {
|
||||
try {
|
||||
File nextFile = new File(getPreKeysDirectory(context), PreKeyIndex.FILE_NAME);
|
||||
|
||||
if (nextFile.exists()) {
|
||||
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
|
||||
} else {
|
||||
InputStreamReader reader = new InputStreamReader(new FileInputStream(nextFile));
|
||||
PreKeyIndex index = new Gson().fromJson(reader, PreKeyIndex.class);
|
||||
reader.close();
|
||||
return index.nextPreKeyId;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w("PreKeyUtil", e);
|
||||
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
private static File getPreKeysDirectory(Context context) {
|
||||
File directory = new File(context.getFilesDir(), PreKeyRecord.PREKEY_DIRECTORY);
|
||||
|
||||
if (!directory.exists())
|
||||
directory.mkdirs();
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
||||
private static class PreKeyRecordIdComparator implements Comparator<String> {
|
||||
@Override
|
||||
public int compare(String lhs, String rhs) {
|
||||
if (lhs.equals(PreKeyIndex.FILE_NAME)) return -1;
|
||||
else if (rhs.equals(PreKeyIndex.FILE_NAME)) return 1;
|
||||
|
||||
try {
|
||||
long lhsLong = Long.parseLong(lhs);
|
||||
long rhsLong = Long.parseLong(rhs);
|
||||
|
||||
if (lhsLong < rhsLong) return -1;
|
||||
else if (lhsLong > rhsLong) return 1;
|
||||
else return 0;
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class PreKeyIndex {
|
||||
public static final String FILE_NAME = "index.dat";
|
||||
|
||||
private int nextPreKeyId;
|
||||
|
||||
public PreKeyIndex() {}
|
||||
|
||||
public PreKeyIndex(int nextPreKeyId) {
|
||||
this.nextPreKeyId = nextPreKeyId;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* 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
|
||||
@@ -14,23 +15,24 @@
|
||||
* 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.thoughtcrime.securesms.crypto;
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||
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 org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.bouncycastle.math.ec.ECPoint;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
public class PublicKey {
|
||||
public static final int POINT_SIZE = 33;
|
||||
public static final int KEY_SIZE = 3 + POINT_SIZE;
|
||||
|
||||
private ECPublicKeyParameters publicKey;
|
||||
|
||||
public static final int KEY_SIZE = 3 + ECPublicKey.KEY_SIZE;
|
||||
|
||||
private final ECPublicKey publicKey;
|
||||
private int id;
|
||||
|
||||
public PublicKey(PublicKey publicKey) {
|
||||
@@ -40,35 +42,28 @@ public class PublicKey {
|
||||
this.publicKey = publicKey.publicKey;
|
||||
}
|
||||
|
||||
public PublicKey(int id, ECPublicKeyParameters publicKey) {
|
||||
public PublicKey(int id, ECPublicKey publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public PublicKey(byte[] bytes, int offset) throws InvalidKeyException {
|
||||
Log.w("PublicKey", "PublicKey Length: " + (bytes.length - offset));
|
||||
|
||||
if ((bytes.length - offset) < KEY_SIZE)
|
||||
throw new InvalidKeyException("Provided bytes are too short.");
|
||||
|
||||
this.id = Conversions.byteArrayToMedium(bytes, offset);
|
||||
byte[] pointBytes = new byte[POINT_SIZE];
|
||||
|
||||
System.arraycopy(bytes, offset+3, pointBytes, 0, pointBytes.length);
|
||||
|
||||
ECPoint Q;
|
||||
|
||||
try {
|
||||
Q = KeyUtil.decodePoint(pointBytes);
|
||||
} catch (RuntimeException re) {
|
||||
throw new InvalidKeyException(re);
|
||||
}
|
||||
|
||||
this.publicKey = new ECPublicKeyParameters(Q, KeyUtil.domainParameters);
|
||||
|
||||
this.id = Conversions.byteArrayToMedium(bytes, offset);
|
||||
this.publicKey = Curve.decodePoint(bytes, offset + 3);
|
||||
}
|
||||
|
||||
|
||||
public PublicKey(byte[] bytes) throws InvalidKeyException {
|
||||
this(bytes, 0);
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return publicKey.getType();
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
@@ -78,7 +73,7 @@ public class PublicKey {
|
||||
return id;
|
||||
}
|
||||
|
||||
public ECPublicKeyParameters getKey() {
|
||||
public ECPublicKey getKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
@@ -97,14 +92,11 @@ public class PublicKey {
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
byte[] complete = new byte[KEY_SIZE];
|
||||
byte[] serializedPoint = KeyUtil.encodePoint(publicKey.getQ());
|
||||
|
||||
byte[] keyIdBytes = Conversions.mediumToByteArray(id);
|
||||
byte[] serializedPoint = publicKey.serialize();
|
||||
|
||||
Log.w("PublicKey", "Serializing public key point: " + Hex.toString(serializedPoint));
|
||||
|
||||
Conversions.mediumToByteArray(complete, 0, id);
|
||||
System.arraycopy(serializedPoint, 0, complete, 3, serializedPoint.length);
|
||||
|
||||
return complete;
|
||||
}
|
||||
|
||||
return Util.combine(keyIdBytes, serializedPoint);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* 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.thoughtcrime.securesms.crypto;
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
public interface SerializableKey {
|
||||
public byte[] serialize();
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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.content.Context;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
|
||||
import org.whispersystems.textsecure.storage.RecipientDevice;
|
||||
import org.whispersystems.textsecure.storage.SessionRecordV1;
|
||||
import org.whispersystems.textsecure.storage.SessionRecordV2;
|
||||
|
||||
public abstract class SessionCipher {
|
||||
|
||||
protected static final Object SESSION_LOCK = new Object();
|
||||
|
||||
public abstract CiphertextMessage encrypt(byte[] paddedMessage);
|
||||
public abstract byte[] decrypt(byte[] decodedMessage) throws InvalidMessageException;
|
||||
public abstract int getRemoteRegistrationId();
|
||||
|
||||
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());
|
||||
} else {
|
||||
throw new AssertionError("Attempt to initialize cipher for non-existing session.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,15 +14,15 @@
|
||||
* 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.thoughtcrime.securesms.crypto;
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
public interface TransportDetails {
|
||||
public byte[] stripPaddedMessage(byte[] messageWithPadding);
|
||||
public byte[] getStrippedPaddingMessageBody(byte[] messageWithPadding);
|
||||
public byte[] getPaddedMessageBody(byte[] messageBody);
|
||||
|
||||
public byte[] encodeMessage(byte[] messageWithMac);
|
||||
public byte[] decodeMessage(byte[] encodedMessageBytes) throws IOException;
|
||||
public byte[] getEncodedMessage(byte[] messageWithMac);
|
||||
public byte[] getDecodedMessage(byte[] encodedMessageBytes) throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
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 ECPublicKey decodePoint(byte[] bytes, int offset)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
int type = bytes[offset];
|
||||
|
||||
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 byte[] calculateAgreement(ECPublicKey publicKey, ECPrivateKey privateKey)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
if (publicKey.getType() != privateKey.getType()) {
|
||||
throw new InvalidKeyException("Public and private keys must be of the same type!");
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 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.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class Curve25519 {
|
||||
|
||||
static {
|
||||
System.loadLibrary("curve25519");
|
||||
|
||||
try {
|
||||
random = SecureRandom.getInstance("SHA1PRNG");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final SecureRandom random;
|
||||
|
||||
private static native byte[] calculateAgreement(byte[] ourPrivate, byte[] theirPublic);
|
||||
private static native byte[] generatePublicKey(byte[] privateKey);
|
||||
private static native byte[] generatePrivateKey(byte[] random);
|
||||
|
||||
public static ECKeyPair generateKeyPair() {
|
||||
byte[] privateKey = generatePrivateKey();
|
||||
byte[] publicKey = generatePublicKey(privateKey);
|
||||
|
||||
return new ECKeyPair(new DjbECPublicKey(publicKey), new DjbECPrivateKey(privateKey));
|
||||
}
|
||||
|
||||
static byte[] calculateAgreement(ECPublicKey publicKey, ECPrivateKey privateKey) {
|
||||
return calculateAgreement(((DjbECPrivateKey)privateKey).getPrivateKey(),
|
||||
((DjbECPublicKey)publicKey).getPublicKey());
|
||||
}
|
||||
|
||||
static ECPublicKey decodePoint(byte[] encoded, int offset)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
int type = encoded[offset] & 0xFF;
|
||||
byte[] keyBytes = new byte[32];
|
||||
System.arraycopy(encoded, offset+1, keyBytes, 0, keyBytes.length);
|
||||
|
||||
if (type != Curve.DJB_TYPE) {
|
||||
throw new InvalidKeyException("Bad key type: " + type);
|
||||
}
|
||||
|
||||
return new DjbECPublicKey(keyBytes);
|
||||
}
|
||||
|
||||
private static byte[] generatePrivateKey() {
|
||||
byte[] privateKey = new byte[32];
|
||||
random.nextBytes(privateKey);
|
||||
|
||||
return generatePrivateKey(privateKey);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 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,6 +1,6 @@
|
||||
/**
|
||||
* 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
|
||||
@@ -10,31 +10,32 @@
|
||||
* 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.thoughtcrime.securesms.crypto;
|
||||
|
||||
public class InvalidMessageException extends Exception {
|
||||
package org.whispersystems.textsecure.crypto.ecc;
|
||||
|
||||
public InvalidMessageException() {
|
||||
// TODO Auto-generated constructor stub
|
||||
public class DjbECPrivateKey implements ECPrivateKey {
|
||||
|
||||
private final byte[] privateKey;
|
||||
|
||||
DjbECPrivateKey(byte[] privateKey) {
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
public InvalidMessageException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
// TODO Auto-generated constructor stub
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
public InvalidMessageException(Throwable throwable) {
|
||||
super(throwable);
|
||||
// TODO Auto-generated constructor stub
|
||||
@Override
|
||||
public int getType() {
|
||||
return Curve.DJB_TYPE;
|
||||
}
|
||||
|
||||
public InvalidMessageException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
// TODO Auto-generated constructor stub
|
||||
public byte[] getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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.whispersystems.textsecure.util.Util;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class DjbECPublicKey implements ECPublicKey {
|
||||
|
||||
private final byte[] publicKey;
|
||||
|
||||
DjbECPublicKey(byte[] publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
byte[] type = {Curve.DJB_TYPE};
|
||||
return Util.combine(type, publicKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return Curve.DJB_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null) return false;
|
||||
if (!(other instanceof DjbECPublicKey)) return false;
|
||||
|
||||
DjbECPublicKey that = (DjbECPublicKey)other;
|
||||
return Arrays.equals(this.publicKey, that.publicKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(publicKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ECPublicKey another) {
|
||||
return new BigInteger(publicKey).compareTo(new BigInteger(((DjbECPublicKey)another).publicKey));
|
||||
}
|
||||
|
||||
public byte[] getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public class ECKeyPair {
|
||||
|
||||
private final ECPublicKey publicKey;
|
||||
private final ECPrivateKey privateKey;
|
||||
|
||||
public ECKeyPair(ECPublicKey publicKey, ECPrivateKey privateKey) {
|
||||
this.publicKey = publicKey;
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
public ECPublicKey getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public ECPrivateKey getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public interface ECPrivateKey {
|
||||
public byte[] serialize();
|
||||
public int getType();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public interface ECPublicKey extends Comparable<ECPublicKey> {
|
||||
|
||||
public static final int KEY_SIZE = 33;
|
||||
|
||||
public byte[] serialize();
|
||||
|
||||
public int getType();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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.ECPrivateKeyParameters;
|
||||
|
||||
public class NistECPrivateKey implements ECPrivateKey {
|
||||
|
||||
private final ECPrivateKeyParameters privateKey;
|
||||
|
||||
public NistECPrivateKey(ECPrivateKeyParameters privateKey) {
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return privateKey.getD().toByteArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return Curve.NIST_TYPE;
|
||||
}
|
||||
|
||||
public ECPrivateKeyParameters getParameters() {
|
||||
return privateKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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.kdf;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class DerivedSecrets {
|
||||
|
||||
private final SecretKeySpec cipherKey;
|
||||
private final SecretKeySpec macKey;
|
||||
|
||||
public DerivedSecrets(SecretKeySpec cipherKey, SecretKeySpec macKey) {
|
||||
this.cipherKey = cipherKey;
|
||||
this.macKey = macKey;
|
||||
}
|
||||
|
||||
public SecretKeySpec getCipherKey() {
|
||||
return cipherKey;
|
||||
}
|
||||
|
||||
public SecretKeySpec getMacKey() {
|
||||
return macKey;
|
||||
}
|
||||
}
|
||||
108
library/src/org/whispersystems/textsecure/crypto/kdf/HKDF.java
Normal file
108
library/src/org/whispersystems/textsecure/crypto/kdf/HKDF.java
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 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.kdf;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class HKDF {
|
||||
|
||||
private static final int HASH_OUTPUT_SIZE = 32;
|
||||
private static final int KEY_MATERIAL_SIZE = 64;
|
||||
|
||||
private static final int CIPHER_KEYS_OFFSET = 0;
|
||||
private static final int MAC_KEYS_OFFSET = 32;
|
||||
|
||||
public DerivedSecrets deriveSecrets(byte[] inputKeyMaterial, byte[] info) {
|
||||
byte[] salt = new byte[HASH_OUTPUT_SIZE];
|
||||
return deriveSecrets(inputKeyMaterial, salt, info);
|
||||
}
|
||||
|
||||
public DerivedSecrets deriveSecrets(byte[] inputKeyMaterial, byte[] salt, byte[] info) {
|
||||
byte[] prk = extract(salt, inputKeyMaterial);
|
||||
byte[] okm = expand(prk, info, KEY_MATERIAL_SIZE);
|
||||
|
||||
SecretKeySpec cipherKey = deriveCipherKey(okm);
|
||||
SecretKeySpec macKey = deriveMacKey(okm);
|
||||
|
||||
return new DerivedSecrets(cipherKey, macKey);
|
||||
}
|
||||
|
||||
private SecretKeySpec deriveCipherKey(byte[] okm) {
|
||||
byte[] cipherKey = new byte[32];
|
||||
System.arraycopy(okm, CIPHER_KEYS_OFFSET, cipherKey, 0, cipherKey.length);
|
||||
return new SecretKeySpec(cipherKey, "AES");
|
||||
}
|
||||
|
||||
private SecretKeySpec deriveMacKey(byte[] okm) {
|
||||
byte[] macKey = new byte[32];
|
||||
System.arraycopy(okm, MAC_KEYS_OFFSET, macKey, 0, macKey.length);
|
||||
return new SecretKeySpec(macKey, "HmacSHA256");
|
||||
}
|
||||
|
||||
private byte[] extract(byte[] salt, byte[] inputKeyMaterial) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(salt, "HmacSHA256"));
|
||||
return mac.doFinal(inputKeyMaterial);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] expand(byte[] prk, byte[] info, int outputSize) {
|
||||
try {
|
||||
int iterations = (int)Math.ceil((double)outputSize/(double)HASH_OUTPUT_SIZE);
|
||||
byte[] mixin = new byte[0];
|
||||
ByteArrayOutputStream results = new ByteArrayOutputStream();
|
||||
|
||||
for (int i=0;i<iterations;i++) {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(prk, "HmacSHA256"));
|
||||
|
||||
mac.update(mixin);
|
||||
if (info != null) {
|
||||
mac.update(info);
|
||||
}
|
||||
mac.update((byte)i);
|
||||
|
||||
byte[] stepResult = mac.doFinal();
|
||||
results.write(stepResult, 0, stepResult.length);
|
||||
|
||||
mixin = stepResult;
|
||||
}
|
||||
|
||||
return results.toByteArray();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 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.kdf;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class NKDF {
|
||||
|
||||
public static final int LEGACY_CIPHER_KEY_LENGTH = 16;
|
||||
public static final int LEGACY_MAC_KEY_LENGTH = 20;
|
||||
|
||||
public DerivedSecrets deriveSecrets(byte[] sharedSecret, boolean isLowEnd)
|
||||
{
|
||||
SecretKeySpec cipherKey = deriveCipherSecret(isLowEnd, sharedSecret);
|
||||
SecretKeySpec macKey = deriveMacSecret(cipherKey);
|
||||
|
||||
return new DerivedSecrets(cipherKey, macKey);
|
||||
}
|
||||
|
||||
private SecretKeySpec deriveCipherSecret(boolean isLowEnd, byte[] sharedSecret) {
|
||||
byte[] derivedBytes = deriveBytes(sharedSecret, LEGACY_CIPHER_KEY_LENGTH * 2);
|
||||
byte[] cipherSecret = new byte[LEGACY_CIPHER_KEY_LENGTH];
|
||||
|
||||
if (isLowEnd) {
|
||||
System.arraycopy(derivedBytes, LEGACY_CIPHER_KEY_LENGTH, cipherSecret, 0, LEGACY_CIPHER_KEY_LENGTH);
|
||||
} else {
|
||||
System.arraycopy(derivedBytes, 0, cipherSecret, 0, LEGACY_CIPHER_KEY_LENGTH);
|
||||
}
|
||||
|
||||
return new SecretKeySpec(cipherSecret, "AES");
|
||||
}
|
||||
|
||||
private SecretKeySpec deriveMacSecret(SecretKeySpec key) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||
byte[] secret = md.digest(key.getEncoded());
|
||||
|
||||
return new SecretKeySpec(secret, "HmacSHA1");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalArgumentException("SHA-1 Not Supported!",e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] deriveBytes(byte[] seed, int bytesNeeded) {
|
||||
MessageDigest md;
|
||||
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.w("NKDF", e);
|
||||
throw new IllegalArgumentException("SHA-256 Not Supported!");
|
||||
}
|
||||
|
||||
int rounds = bytesNeeded / md.getDigestLength();
|
||||
|
||||
for (int i=1;i<=rounds;i++) {
|
||||
byte[] roundBytes = Conversions.intToByteArray(i);
|
||||
md.update(roundBytes);
|
||||
md.update(seed);
|
||||
}
|
||||
|
||||
return md.digest();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
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 LEGACY_WHISPER_TYPE = 1;
|
||||
public static final int CURRENT_WHISPER_TYPE = 2;
|
||||
public static final int PREKEY_WHISPER_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 byte[] serialize();
|
||||
public int getType();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package org.whispersystems.textsecure.crypto.protocol;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
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.ecc.Curve;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
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;
|
||||
|
||||
public PreKeyWhisperMessage(byte[] serialized)
|
||||
throws InvalidMessageException, InvalidVersionException
|
||||
{
|
||||
try {
|
||||
this.version = Conversions.lowBitsToInt(serialized[0]);
|
||||
|
||||
if (this.version > CiphertextMessage.CURRENT_VERSION) {
|
||||
throw new InvalidVersionException("Unknown version: " + this.version);
|
||||
}
|
||||
|
||||
WhisperProtos.PreKeyWhisperMessage preKeyWhisperMessage
|
||||
= WhisperProtos.PreKeyWhisperMessage.parseFrom(ByteString.copyFrom(serialized, 1,
|
||||
serialized.length-1));
|
||||
|
||||
if (!preKeyWhisperMessage.hasPreKeyId() ||
|
||||
!preKeyWhisperMessage.hasBaseKey() ||
|
||||
!preKeyWhisperMessage.hasIdentityKey() ||
|
||||
!preKeyWhisperMessage.hasMessage())
|
||||
{
|
||||
throw new InvalidMessageException("Incomplete message.");
|
||||
}
|
||||
|
||||
this.serialized = serialized;
|
||||
this.registrationId = preKeyWhisperMessage.getRegistrationId();
|
||||
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());
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public PreKeyWhisperMessage(int registrationId, int preKeyId, ECPublicKey baseKey,
|
||||
IdentityKey identityKey, WhisperMessageV2 message)
|
||||
{
|
||||
this.version = CiphertextMessage.CURRENT_VERSION;
|
||||
this.registrationId = registrationId;
|
||||
this.preKeyId = preKeyId;
|
||||
this.baseKey = baseKey;
|
||||
this.identityKey = identityKey;
|
||||
this.message = message;
|
||||
|
||||
byte[] versionBytes = {Conversions.intsToByteHighAndLow(CURRENT_VERSION, this.version)};
|
||||
byte[] messageBytes = WhisperProtos.PreKeyWhisperMessage.newBuilder()
|
||||
.setPreKeyId(preKeyId)
|
||||
.setBaseKey(ByteString.copyFrom(baseKey.serialize()))
|
||||
.setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
|
||||
.setMessage(ByteString.copyFrom(message.serialize()))
|
||||
.setRegistrationId(registrationId)
|
||||
.build().toByteArray();
|
||||
|
||||
this.serialized = Util.combine(versionBytes, messageBytes);
|
||||
}
|
||||
|
||||
public IdentityKey getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public int getRegistrationId() {
|
||||
return registrationId;
|
||||
}
|
||||
|
||||
public int getPreKeyId() {
|
||||
return preKeyId;
|
||||
}
|
||||
|
||||
public ECPublicKey getBaseKey() {
|
||||
return baseKey;
|
||||
}
|
||||
|
||||
public WhisperMessageV2 getWhisperMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return serialized;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return CiphertextMessage.PREKEY_WHISPER_TYPE;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package org.whispersystems.textsecure.crypto.protocol;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
||||
import org.whispersystems.textsecure.crypto.PublicKey;
|
||||
import org.whispersystems.textsecure.crypto.SessionCipherV1;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
|
||||
public class WhisperMessageV1 implements CiphertextMessage{
|
||||
|
||||
private static final int VERSION_LENGTH = 1;
|
||||
private static final int SENDER_KEY_ID_LENGTH = 3;
|
||||
private static final int RECEIVER_KEY_ID_LENGTH = 3;
|
||||
private static final int NEXT_KEY_LENGTH = PublicKey.KEY_SIZE;
|
||||
private static final int COUNTER_LENGTH = 3;
|
||||
private static final int HEADER_LENGTH = VERSION_LENGTH + SENDER_KEY_ID_LENGTH +
|
||||
RECEIVER_KEY_ID_LENGTH + COUNTER_LENGTH +
|
||||
NEXT_KEY_LENGTH;
|
||||
private static final int MAC_LENGTH = 10;
|
||||
|
||||
|
||||
private static final int VERSION_OFFSET = 0;
|
||||
private static final int SENDER_KEY_ID_OFFSET = VERSION_OFFSET + VERSION_LENGTH;
|
||||
private static final int RECEIVER_KEY_ID_OFFSET = SENDER_KEY_ID_OFFSET + SENDER_KEY_ID_LENGTH;
|
||||
private static final int NEXT_KEY_OFFSET = RECEIVER_KEY_ID_OFFSET + RECEIVER_KEY_ID_LENGTH;
|
||||
private static final int COUNTER_OFFSET = NEXT_KEY_OFFSET + NEXT_KEY_LENGTH;
|
||||
private static final int BODY_OFFSET = COUNTER_OFFSET + COUNTER_LENGTH;
|
||||
|
||||
static final int ENCRYPTED_MESSAGE_OVERHEAD = HEADER_LENGTH + MAC_LENGTH;
|
||||
|
||||
private final byte[] ciphertext;
|
||||
|
||||
public WhisperMessageV1(SessionCipherV1.SessionCipherContext sessionContext,
|
||||
byte[] ciphertextBody)
|
||||
{
|
||||
this.ciphertext = new byte[HEADER_LENGTH + ciphertextBody.length + MAC_LENGTH];
|
||||
setVersion(sessionContext.getMessageVersion(), CURRENT_VERSION);
|
||||
setSenderKeyId(sessionContext.getSenderKeyId());
|
||||
setReceiverKeyId(sessionContext.getRecipientKeyId());
|
||||
setNextKeyBytes(sessionContext.getNextKey().serialize());
|
||||
setCounter(sessionContext.getCounter());
|
||||
setBody(ciphertextBody);
|
||||
setMac(calculateMac(sessionContext.getSessionKey().getMacKey(),
|
||||
ciphertext, 0, ciphertext.length - MAC_LENGTH));
|
||||
}
|
||||
|
||||
public WhisperMessageV1(byte[] ciphertext) throws InvalidMessageException {
|
||||
this.ciphertext = ciphertext;
|
||||
|
||||
if (ciphertext.length < HEADER_LENGTH) {
|
||||
throw new InvalidMessageException("Not long enough for ciphertext header!");
|
||||
}
|
||||
|
||||
if (getCurrentVersion() > LEGACY_VERSION) {
|
||||
throw new InvalidMessageException("Received non-legacy version: " + getCurrentVersion());
|
||||
}
|
||||
}
|
||||
|
||||
public void setVersion(int current, int supported) {
|
||||
ciphertext[VERSION_OFFSET] = Conversions.intsToByteHighAndLow(current, supported);
|
||||
}
|
||||
|
||||
public int getCurrentVersion() {
|
||||
return Conversions.highBitsToInt(ciphertext[VERSION_OFFSET]);
|
||||
}
|
||||
|
||||
public int getSupportedVersion() {
|
||||
return Conversions.lowBitsToInt(ciphertext[VERSION_OFFSET]);
|
||||
}
|
||||
|
||||
public void setSenderKeyId(int senderKeyId) {
|
||||
Conversions.mediumToByteArray(ciphertext, SENDER_KEY_ID_OFFSET, senderKeyId);
|
||||
}
|
||||
|
||||
public int getSenderKeyId() {
|
||||
return Conversions.byteArrayToMedium(ciphertext, SENDER_KEY_ID_OFFSET);
|
||||
}
|
||||
|
||||
public void setReceiverKeyId(int receiverKeyId) {
|
||||
Conversions.mediumToByteArray(ciphertext, RECEIVER_KEY_ID_OFFSET, receiverKeyId);
|
||||
}
|
||||
|
||||
public int getReceiverKeyId() {
|
||||
return Conversions.byteArrayToMedium(ciphertext, RECEIVER_KEY_ID_OFFSET);
|
||||
}
|
||||
|
||||
public void setNextKeyBytes(byte[] nextKey) {
|
||||
assert(nextKey.length == NEXT_KEY_LENGTH);
|
||||
System.arraycopy(nextKey, 0, ciphertext, NEXT_KEY_OFFSET, nextKey.length);
|
||||
}
|
||||
|
||||
public byte[] getNextKeyBytes() {
|
||||
byte[] nextKeyBytes = new byte[NEXT_KEY_LENGTH];
|
||||
System.arraycopy(ciphertext, NEXT_KEY_OFFSET, nextKeyBytes, 0, nextKeyBytes.length);
|
||||
|
||||
return nextKeyBytes;
|
||||
}
|
||||
|
||||
public void setCounter(int counter) {
|
||||
Conversions.mediumToByteArray(ciphertext, COUNTER_OFFSET, counter);
|
||||
}
|
||||
|
||||
public int getCounter() {
|
||||
return Conversions.byteArrayToMedium(ciphertext, COUNTER_OFFSET);
|
||||
}
|
||||
|
||||
public void setBody(byte[] body) {
|
||||
System.arraycopy(body, 0, ciphertext, BODY_OFFSET, body.length);
|
||||
}
|
||||
|
||||
public byte[] getBody() {
|
||||
byte[] body = new byte[ciphertext.length - HEADER_LENGTH - MAC_LENGTH];
|
||||
System.arraycopy(ciphertext, BODY_OFFSET, body, 0, body.length);
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
public void setMac(byte[] mac) {
|
||||
System.arraycopy(mac, 0, ciphertext, ciphertext.length-mac.length, mac.length);
|
||||
}
|
||||
|
||||
public byte[] getMac() {
|
||||
byte[] mac = new byte[MAC_LENGTH];
|
||||
System.arraycopy(ciphertext, ciphertext.length-mac.length, mac, 0, mac.length);
|
||||
|
||||
return mac;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return CiphertextMessage.LEGACY_WHISPER_TYPE;
|
||||
}
|
||||
|
||||
public void verifyMac(SessionCipherV1.SessionCipherContext sessionContext)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
verifyMac(sessionContext.getSessionKey().getMacKey(),
|
||||
this.ciphertext, 0, this.ciphertext.length - MAC_LENGTH, getMac());
|
||||
}
|
||||
|
||||
private byte[] calculateMac(SecretKeySpec macKey, byte[] message, int offset, int length) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA1");
|
||||
mac.init(macKey);
|
||||
|
||||
mac.update(message, offset, length);
|
||||
byte[] macBytes = mac.doFinal();
|
||||
|
||||
return Util.trim(macBytes, MAC_LENGTH);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyMac(SecretKeySpec macKey, byte[] message, int offset, int length,
|
||||
byte[] receivedMac)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
byte[] localMac = calculateMac(macKey, message, offset, length);
|
||||
|
||||
Log.w("WhisperMessageV1", "Local Mac: " + Hex.toString(localMac));
|
||||
Log.w("WhisperMessageV1", "Remot Mac: " + Hex.toString(receivedMac));
|
||||
|
||||
if (!Arrays.equals(localMac, receivedMac)) {
|
||||
throw new InvalidMessageException("MAC on message does not match calculated MAC.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
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.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.NoSuchAlgorithmException;
|
||||
import java.text.ParseException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class WhisperMessageV2 implements CiphertextMessage {
|
||||
|
||||
private static final int MAC_LENGTH = 8;
|
||||
|
||||
private final ECPublicKey senderEphemeral;
|
||||
private final int counter;
|
||||
private final int previousCounter;
|
||||
private final byte[] ciphertext;
|
||||
private final byte[] serialized;
|
||||
|
||||
public WhisperMessageV2(byte[] serialized) throws InvalidMessageException {
|
||||
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) != CURRENT_VERSION) {
|
||||
throw new InvalidMessageException("Unknown version: " + Conversions.highBitsToInt(version));
|
||||
}
|
||||
|
||||
WhisperMessage whisperMessage = WhisperMessage.parseFrom(message);
|
||||
|
||||
if (!whisperMessage.hasCiphertext() ||
|
||||
!whisperMessage.hasCounter() ||
|
||||
!whisperMessage.hasEphemeralKey())
|
||||
{
|
||||
throw new InvalidMessageException("Incomplete message.");
|
||||
}
|
||||
|
||||
this.serialized = serialized;
|
||||
this.senderEphemeral = Curve.decodePoint(whisperMessage.getEphemeralKey().toByteArray(), 0);
|
||||
this.counter = whisperMessage.getCounter();
|
||||
this.previousCounter = whisperMessage.getPreviousCounter();
|
||||
this.ciphertext = whisperMessage.getCiphertext().toByteArray();
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
} catch (ParseException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WhisperMessageV2(SecretKeySpec macKey, ECPublicKey senderEphemeral,
|
||||
int counter, int previousCounter, byte[] ciphertext)
|
||||
{
|
||||
byte[] version = {Conversions.intsToByteHighAndLow(CURRENT_VERSION, CURRENT_VERSION)};
|
||||
byte[] message = WhisperMessage.newBuilder()
|
||||
.setEphemeralKey(ByteString.copyFrom(senderEphemeral.serialize()))
|
||||
.setCounter(counter)
|
||||
.setPreviousCounter(previousCounter)
|
||||
.setCiphertext(ByteString.copyFrom(ciphertext))
|
||||
.build().toByteArray();
|
||||
byte[] mac = getMac(macKey, Util.combine(version, message));
|
||||
|
||||
this.serialized = Util.combine(version, message, mac);
|
||||
this.senderEphemeral = senderEphemeral;
|
||||
this.counter = counter;
|
||||
this.previousCounter = previousCounter;
|
||||
this.ciphertext = ciphertext;
|
||||
}
|
||||
|
||||
public ECPublicKey getSenderEphemeral() {
|
||||
return senderEphemeral;
|
||||
}
|
||||
|
||||
public int getCounter() {
|
||||
return counter;
|
||||
}
|
||||
|
||||
public byte[] getBody() {
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
public void verifyMac(SecretKeySpec macKey)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
byte[][] parts = Util.split(serialized, serialized.length - MAC_LENGTH, MAC_LENGTH);
|
||||
byte[] ourMac = getMac(macKey, parts[0]);
|
||||
byte[] theirMac = parts[1];
|
||||
|
||||
if (!Arrays.equals(ourMac, theirMac)) {
|
||||
throw new InvalidMessageException("Bad Mac!");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getMac(SecretKeySpec macKey, byte[] serialized) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(macKey);
|
||||
|
||||
byte[] fullMac = mac.doFinal(serialized);
|
||||
return Util.trim(fullMac, MAC_LENGTH);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (java.security.InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return serialized;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return CiphertextMessage.CURRENT_WHISPER_TYPE;
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
||||
package org.whispersystems.textsecure.crypto.ratchet;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets;
|
||||
import org.whispersystems.textsecure.crypto.kdf.HKDF;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class ChainKey {
|
||||
|
||||
private static final byte[] MESSAGE_KEY_SEED = {0x01};
|
||||
private static final byte[] CHAIN_KEY_SEED = {0x02};
|
||||
|
||||
private final byte[] key;
|
||||
private final int index;
|
||||
|
||||
public ChainKey(byte[] key, int index) {
|
||||
this.key = key;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public byte[] getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public ChainKey getNextChainKey() {
|
||||
byte[] nextKey = getBaseMaterial(CHAIN_KEY_SEED);
|
||||
return new ChainKey(nextKey, index + 1);
|
||||
}
|
||||
|
||||
public MessageKeys getMessageKeys() {
|
||||
HKDF kdf = new HKDF();
|
||||
byte[] inputKeyMaterial = getBaseMaterial(MESSAGE_KEY_SEED);
|
||||
DerivedSecrets keyMaterial = kdf.deriveSecrets(inputKeyMaterial, "WhisperMessageKeys".getBytes());
|
||||
|
||||
return new MessageKeys(keyMaterial.getCipherKey(), keyMaterial.getMacKey(), index);
|
||||
}
|
||||
|
||||
private byte[] getBaseMaterial(byte[] seed) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(key, "HmacSHA256"));
|
||||
|
||||
return mac.doFinal(seed);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.whispersystems.textsecure.crypto.ratchet;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class MessageKeys {
|
||||
|
||||
private final SecretKeySpec cipherKey;
|
||||
private final SecretKeySpec macKey;
|
||||
private final int counter;
|
||||
|
||||
public MessageKeys(SecretKeySpec cipherKey, SecretKeySpec macKey, int counter) {
|
||||
this.cipherKey = cipherKey;
|
||||
this.macKey = macKey;
|
||||
this.counter = counter;
|
||||
}
|
||||
|
||||
public SecretKeySpec getCipherKey() {
|
||||
return cipherKey;
|
||||
}
|
||||
|
||||
public SecretKeySpec getMacKey() {
|
||||
return macKey;
|
||||
}
|
||||
|
||||
public int getCounter() {
|
||||
return counter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package org.whispersystems.textsecure.crypto.ratchet;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
||||
import org.whispersystems.textsecure.crypto.IdentityKeyPair;
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
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.kdf.DerivedSecrets;
|
||||
import org.whispersystems.textsecure.crypto.kdf.HKDF;
|
||||
import org.whispersystems.textsecure.storage.SessionRecordV2;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class RatchetingSession {
|
||||
|
||||
public static void initializeSession(SessionRecordV2 sessionRecord,
|
||||
ECKeyPair ourBaseKey,
|
||||
ECPublicKey theirBaseKey,
|
||||
ECKeyPair ourEphemeralKey,
|
||||
ECPublicKey theirEphemeralKey,
|
||||
IdentityKeyPair ourIdentityKey,
|
||||
IdentityKey theirIdentityKey)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
if (isAlice(ourBaseKey.getPublicKey(), theirBaseKey, ourEphemeralKey.getPublicKey(), theirEphemeralKey)) {
|
||||
initializeSessionAsAlice(sessionRecord, ourBaseKey, theirBaseKey, theirEphemeralKey,
|
||||
ourIdentityKey, theirIdentityKey);
|
||||
} else {
|
||||
initializeSessionAsBob(sessionRecord, ourBaseKey, theirBaseKey,
|
||||
ourEphemeralKey, ourIdentityKey, theirIdentityKey);
|
||||
}
|
||||
}
|
||||
|
||||
private static void initializeSessionAsAlice(SessionRecordV2 sessionRecord,
|
||||
ECKeyPair ourBaseKey, ECPublicKey theirBaseKey,
|
||||
ECPublicKey theirEphemeralKey,
|
||||
IdentityKeyPair ourIdentityKey,
|
||||
IdentityKey theirIdentityKey)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
sessionRecord.setRemoteIdentityKey(theirIdentityKey);
|
||||
sessionRecord.setLocalIdentityKey(ourIdentityKey.getPublicKey());
|
||||
|
||||
ECKeyPair sendingKey = Curve.generateKeyPairForType(ourIdentityKey.getPublicKey().getPublicKey().getType());
|
||||
Pair<RootKey, ChainKey> receivingChain = calculate3DHE(true, ourBaseKey, theirBaseKey, ourIdentityKey, theirIdentityKey);
|
||||
Pair<RootKey, ChainKey> sendingChain = receivingChain.first.createChain(theirEphemeralKey, sendingKey);
|
||||
|
||||
sessionRecord.addReceiverChain(theirEphemeralKey, receivingChain.second);
|
||||
sessionRecord.setSenderChain(sendingKey, sendingChain.second);
|
||||
sessionRecord.setRootKey(sendingChain.first);
|
||||
}
|
||||
|
||||
private static void initializeSessionAsBob(SessionRecordV2 sessionRecord,
|
||||
ECKeyPair ourBaseKey, ECPublicKey theirBaseKey,
|
||||
ECKeyPair ourEphemeralKey,
|
||||
IdentityKeyPair ourIdentityKey,
|
||||
IdentityKey theirIdentityKey)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
sessionRecord.setRemoteIdentityKey(theirIdentityKey);
|
||||
sessionRecord.setLocalIdentityKey(ourIdentityKey.getPublicKey());
|
||||
|
||||
Pair<RootKey, ChainKey> sendingChain = calculate3DHE(false, ourBaseKey, theirBaseKey,
|
||||
ourIdentityKey, theirIdentityKey);
|
||||
|
||||
sessionRecord.setSenderChain(ourEphemeralKey, sendingChain.second);
|
||||
sessionRecord.setRootKey(sendingChain.first);
|
||||
}
|
||||
|
||||
private static Pair<RootKey, ChainKey> calculate3DHE(boolean isAlice,
|
||||
ECKeyPair ourEphemeral, ECPublicKey theirEphemeral,
|
||||
IdentityKeyPair ourIdentity, IdentityKey theirIdentity)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
try {
|
||||
ByteArrayOutputStream secrets = new ByteArrayOutputStream();
|
||||
|
||||
if (isAlice) {
|
||||
secrets.write(Curve.calculateAgreement(theirEphemeral, ourIdentity.getPrivateKey()));
|
||||
secrets.write(Curve.calculateAgreement(theirIdentity.getPublicKey(), ourEphemeral.getPrivateKey()));
|
||||
} else {
|
||||
secrets.write(Curve.calculateAgreement(theirIdentity.getPublicKey(), ourEphemeral.getPrivateKey()));
|
||||
secrets.write(Curve.calculateAgreement(theirEphemeral, ourIdentity.getPrivateKey()));
|
||||
}
|
||||
|
||||
secrets.write(Curve.calculateAgreement(theirEphemeral, ourEphemeral.getPrivateKey()));
|
||||
|
||||
DerivedSecrets derivedSecrets = new HKDF().deriveSecrets(secrets.toByteArray(),
|
||||
"WhisperText".getBytes());
|
||||
|
||||
return new Pair<RootKey, ChainKey>(new RootKey(derivedSecrets.getCipherKey().getEncoded()),
|
||||
new ChainKey(derivedSecrets.getMacKey().getEncoded(), 0));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isAlice(ECPublicKey ourBaseKey, ECPublicKey theirBaseKey,
|
||||
ECPublicKey ourEphemeralKey, ECPublicKey theirEphemeralKey)
|
||||
{
|
||||
if (ourEphemeralKey.equals(ourBaseKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (theirEphemeralKey.equals(theirBaseKey)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isLowEnd(ourBaseKey, theirBaseKey);
|
||||
}
|
||||
|
||||
private static boolean isLowEnd(ECPublicKey ourKey, ECPublicKey theirKey) {
|
||||
return ourKey.compareTo(theirKey) < 0;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.whispersystems.textsecure.crypto.ratchet;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
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.kdf.DerivedSecrets;
|
||||
import org.whispersystems.textsecure.crypto.kdf.HKDF;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class RootKey {
|
||||
|
||||
private final byte[] key;
|
||||
|
||||
public RootKey(byte[] key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public byte[] getKeyBytes() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public Pair<RootKey, ChainKey> createChain(ECPublicKey theirEphemeral, ECKeyPair ourEphemeral)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
HKDF kdf = new HKDF();
|
||||
byte[] sharedSecret = Curve.calculateAgreement(theirEphemeral, ourEphemeral.getPrivateKey());
|
||||
DerivedSecrets keys = kdf.deriveSecrets(sharedSecret, key, "WhisperRatchet".getBytes());
|
||||
RootKey newRootKey = new RootKey(keys.getCipherKey().getEncoded());
|
||||
ChainKey newChainKey = new ChainKey(keys.getMacKey().getEncoded(), 0);
|
||||
|
||||
return new Pair<RootKey, ChainKey>(newRootKey, newChainKey);
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,9 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.directory;
|
||||
package org.whispersystems.textsecure.directory;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
@@ -0,0 +1,255 @@
|
||||
package org.whispersystems.textsecure.directory;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.net.Uri;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Phone;
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.push.ContactTokenDetails;
|
||||
import org.whispersystems.textsecure.util.InvalidNumberException;
|
||||
import org.whispersystems.textsecure.util.PhoneNumberFormatter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class Directory {
|
||||
|
||||
private static final int INTRODUCED_CHANGE_FROM_TOKEN_TO_E164_NUMBER = 2;
|
||||
|
||||
private static final String DATABASE_NAME = "whisper_directory.db";
|
||||
private static final int DATABASE_VERSION = 2;
|
||||
|
||||
private static final String TABLE_NAME = "directory";
|
||||
private static final String ID = "_id";
|
||||
private static final String NUMBER = "number";
|
||||
private static final String REGISTERED = "registered";
|
||||
private static final String RELAY = "relay";
|
||||
private static final String SUPPORTS_SMS = "supports_sms";
|
||||
private static final String TIMESTAMP = "timestamp";
|
||||
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY, " +
|
||||
NUMBER + " TEXT UNIQUE, " +
|
||||
REGISTERED + " INTEGER, " +
|
||||
RELAY + " TEXT, " +
|
||||
SUPPORTS_SMS + " INTEGER, " +
|
||||
TIMESTAMP + " INTEGER);";
|
||||
|
||||
private static final Object instanceLock = new Object();
|
||||
private static volatile Directory instance;
|
||||
|
||||
public static Directory getInstance(Context context) {
|
||||
if (instance == null) {
|
||||
synchronized (instanceLock) {
|
||||
if (instance == null) {
|
||||
instance = new Directory(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private final DatabaseHelper databaseHelper;
|
||||
private final Context context;
|
||||
|
||||
private Directory(Context context) {
|
||||
this.context = context;
|
||||
this.databaseHelper = new DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
public boolean isSmsFallbackSupported(String e164number) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = db.query(TABLE_NAME, new String[] {SUPPORTS_SMS}, NUMBER + " = ?",
|
||||
new String[]{e164number}, null, null, null);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getInt(0) == 1;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isActiveNumber(String e164number) throws NotInDirectoryException {
|
||||
if (e164number == null || e164number.length() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = db.query(TABLE_NAME,
|
||||
new String[]{REGISTERED}, NUMBER + " = ?",
|
||||
new String[] {e164number}, null, null, null);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getInt(0) == 1;
|
||||
} else {
|
||||
throw new NotInDirectoryException();
|
||||
}
|
||||
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
public String getRelay(String e164number) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = database.query(TABLE_NAME, null, NUMBER + " = ?", new String[]{e164number}, null, null, null);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getString(cursor.getColumnIndexOrThrow(RELAY));
|
||||
}
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
public void setNumber(ContactTokenDetails token, boolean active) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(NUMBER, token.getNumber());
|
||||
values.put(RELAY, token.getRelay());
|
||||
values.put(REGISTERED, active ? 1 : 0);
|
||||
values.put(SUPPORTS_SMS, token.isSupportsSms() ? 1 : 0);
|
||||
values.put(TIMESTAMP, System.currentTimeMillis());
|
||||
db.replace(TABLE_NAME, null, values);
|
||||
}
|
||||
|
||||
public void setNumbers(List<ContactTokenDetails> activeTokens, Collection<String> inactiveTokens) {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
for (ContactTokenDetails token : activeTokens) {
|
||||
Log.w("Directory", "Adding active token: " + token);
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(NUMBER, token.getNumber());
|
||||
values.put(REGISTERED, 1);
|
||||
values.put(TIMESTAMP, timestamp);
|
||||
values.put(RELAY, token.getRelay());
|
||||
values.put(SUPPORTS_SMS, token.isSupportsSms() ? 1 : 0);
|
||||
db.replace(TABLE_NAME, null, values);
|
||||
}
|
||||
|
||||
for (String token : inactiveTokens) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(NUMBER, token);
|
||||
values.put(REGISTERED, 0);
|
||||
values.put(TIMESTAMP, timestamp);
|
||||
db.replace(TABLE_NAME, null, values);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public Set<String> getPushEligibleContactNumbers(String localNumber) {
|
||||
final Uri uri = Phone.CONTENT_URI;
|
||||
final Set<String> results = new HashSet<String>();
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = context.getContentResolver().query(uri, new String[] {Phone.NUMBER}, null, null, null);
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
final String rawNumber = cursor.getString(0);
|
||||
if (rawNumber != null) {
|
||||
try {
|
||||
final String e164Number = PhoneNumberFormatter.formatNumber(rawNumber, localNumber);
|
||||
results.add(e164Number);
|
||||
} catch (InvalidNumberException e) {
|
||||
Log.w("Directory", "Invalid number: " + rawNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
|
||||
final SQLiteDatabase readableDb = databaseHelper.getReadableDatabase();
|
||||
if (readableDb != null) {
|
||||
cursor = readableDb.query(TABLE_NAME, new String[]{NUMBER},
|
||||
null, null, null, null, null);
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
results.add(cursor.getString(0));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getActiveNumbers() {
|
||||
final List<String> results = new ArrayList<String>();
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[]{NUMBER},
|
||||
REGISTERED + " = 1", null, null, null, null);
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
results.add(cursor.getString(0));
|
||||
}
|
||||
return results;
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
private class DatabaseHelper extends SQLiteOpenHelper {
|
||||
|
||||
public DatabaseHelper(Context context, String name,
|
||||
SQLiteDatabase.CursorFactory factory,
|
||||
int version)
|
||||
{
|
||||
super(context, name, factory, version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(CREATE_TABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
if (oldVersion < INTRODUCED_CHANGE_FROM_TOKEN_TO_E164_NUMBER) {
|
||||
db.execSQL("DROP TABLE directory;");
|
||||
db.execSQL("CREATE TABLE directory ( _id INTEGER PRIMARY KEY, " +
|
||||
"number TEXT UNIQUE, " +
|
||||
"registered INTEGER, " +
|
||||
"relay TEXT, " +
|
||||
"supports_sms INTEGER, " +
|
||||
"timestamp INTEGER);");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.directory;
|
||||
package org.whispersystems.textsecure.directory;
|
||||
|
||||
public class DirectoryDescriptor {
|
||||
private String version;
|
||||
@@ -0,0 +1,4 @@
|
||||
package org.whispersystems.textsecure.directory;
|
||||
|
||||
public class NotInDirectoryException extends Throwable {
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.directory;
|
||||
package org.whispersystems.textsecure.directory;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
@@ -23,7 +23,6 @@ import android.util.Log;
|
||||
import com.google.thoughtcrimegson.Gson;
|
||||
import com.google.thoughtcrimegson.JsonParseException;
|
||||
import com.google.thoughtcrimegson.annotations.SerializedName;
|
||||
import org.thoughtcrime.securesms.util.PhoneNumberFormatter;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
@@ -33,6 +32,8 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
/**
|
||||
* Handles providing lookups, serializing, and deserializing the RedPhone directory.
|
||||
@@ -75,14 +76,57 @@ public class NumberFilter {
|
||||
if (bloomFilter == null) return false;
|
||||
else if (number == null || number.length() == 0) return false;
|
||||
|
||||
return new BloomFilter(bloomFilter, hashCount).contains(PhoneNumberFormatter.formatNumber(context, number));
|
||||
return new BloomFilter(bloomFilter, hashCount).contains(number);
|
||||
} catch (IOException ioe) {
|
||||
Log.w("NumberFilter", ioe);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void update(File bloomFilter, long capacity, int hashCount, String version)
|
||||
public synchronized boolean containsNumbers(List<String> numbers) {
|
||||
try {
|
||||
if (bloomFilter == null) return false;
|
||||
if (numbers == null || numbers.size() == 0) return false;
|
||||
|
||||
BloomFilter filter = new BloomFilter(bloomFilter, hashCount);
|
||||
|
||||
for (String number : numbers) {
|
||||
if (!filter.contains(number)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (IOException ioe) {
|
||||
Log.w("NumberFilter", ioe);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void update(DirectoryDescriptor descriptor, File compressedData) {
|
||||
try {
|
||||
File uncompressed = File.createTempFile("directory", ".dat", context.getFilesDir());
|
||||
FileInputStream fin = new FileInputStream (compressedData);
|
||||
GZIPInputStream gin = new GZIPInputStream(fin);
|
||||
FileOutputStream out = new FileOutputStream(uncompressed);
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
int read;
|
||||
|
||||
while ((read = gin.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
|
||||
out.close();
|
||||
compressedData.delete();
|
||||
|
||||
update(uncompressed, descriptor.getCapacity(), descriptor.getHashCount(), descriptor.getVersion());
|
||||
} catch (IOException ioe) {
|
||||
Log.w("NumberFilter", ioe);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void update(File bloomFilter, long capacity, int hashCount, String version)
|
||||
{
|
||||
if (this.bloomFilter != null)
|
||||
this.bloomFilter.delete();
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
public class AccountAttributes {
|
||||
|
||||
private String signalingKey;
|
||||
private boolean supportsSms;
|
||||
private int registrationId;
|
||||
|
||||
public AccountAttributes(String signalingKey, boolean supportsSms, int registrationId) {
|
||||
this.signalingKey = signalingKey;
|
||||
this.supportsSms = supportsSms;
|
||||
this.registrationId = registrationId;
|
||||
}
|
||||
|
||||
public AccountAttributes() {}
|
||||
|
||||
public String getSignalingKey() {
|
||||
return signalingKey;
|
||||
}
|
||||
|
||||
public boolean isSupportsSms() {
|
||||
return supportsSms;
|
||||
}
|
||||
|
||||
public int getRegistrationId() {
|
||||
return registrationId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class AuthorizationFailedException extends IOException {
|
||||
public AuthorizationFailedException(String s) {
|
||||
super(s);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import com.google.thoughtcrimegson.Gson;
|
||||
|
||||
public class ContactTokenDetails {
|
||||
|
||||
private String token;
|
||||
private String relay;
|
||||
private String number;
|
||||
private boolean supportsSms;
|
||||
|
||||
public ContactTokenDetails() {}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public String getRelay() {
|
||||
return relay;
|
||||
}
|
||||
|
||||
public boolean isSupportsSms() {
|
||||
return supportsSms;
|
||||
}
|
||||
|
||||
public void setNumber(String number) {
|
||||
this.number = number;
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
return number;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ContactTokenDetailsList {
|
||||
|
||||
private List<ContactTokenDetails> contacts;
|
||||
|
||||
public ContactTokenDetailsList() {}
|
||||
|
||||
public List<ContactTokenDetails> getContacts() {
|
||||
return contacts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ContactTokenList {
|
||||
|
||||
private List<String> contacts;
|
||||
|
||||
public ContactTokenList(List<String> contacts) {
|
||||
this.contacts = contacts;
|
||||
}
|
||||
|
||||
public ContactTokenList() {}
|
||||
|
||||
public List<String> getContacts() {
|
||||
return contacts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.InvalidVersionException;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
import org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class IncomingEncryptedPushMessage {
|
||||
|
||||
private static final int SUPPORTED_VERSION = 1;
|
||||
private static final int CIPHER_KEY_SIZE = 32;
|
||||
private static final int MAC_KEY_SIZE = 20;
|
||||
private static final int MAC_SIZE = 10;
|
||||
|
||||
private static final int VERSION_OFFSET = 0;
|
||||
private static final int VERSION_LENGTH = 1;
|
||||
private static final int IV_OFFSET = VERSION_OFFSET + VERSION_LENGTH;
|
||||
private static final int IV_LENGTH = 16;
|
||||
private static final int CIPHERTEXT_OFFSET = IV_OFFSET + IV_LENGTH;
|
||||
|
||||
private final IncomingPushMessage incomingPushMessage;
|
||||
|
||||
public IncomingEncryptedPushMessage(String message, String signalingKey)
|
||||
throws IOException, InvalidVersionException
|
||||
{
|
||||
byte[] ciphertext = Base64.decode(message);
|
||||
|
||||
if (ciphertext.length < VERSION_LENGTH || ciphertext[VERSION_OFFSET] != SUPPORTED_VERSION)
|
||||
throw new InvalidVersionException("Unsupported version!");
|
||||
|
||||
SecretKeySpec cipherKey = getCipherKey(signalingKey);
|
||||
SecretKeySpec macKey = getMacKey(signalingKey);
|
||||
|
||||
verifyMac(ciphertext, macKey);
|
||||
|
||||
byte[] plaintext = getPlaintext(ciphertext, cipherKey);
|
||||
IncomingPushMessageSignal signal = IncomingPushMessageSignal.parseFrom(plaintext);
|
||||
|
||||
this.incomingPushMessage = new IncomingPushMessage(signal);
|
||||
}
|
||||
|
||||
public IncomingPushMessage getIncomingPushMessage() {
|
||||
return incomingPushMessage;
|
||||
}
|
||||
|
||||
private byte[] getPlaintext(byte[] ciphertext, SecretKeySpec cipherKey) throws IOException {
|
||||
try {
|
||||
byte[] ivBytes = new byte[IV_LENGTH];
|
||||
System.arraycopy(ciphertext, IV_OFFSET, ivBytes, 0, ivBytes.length);
|
||||
IvParameterSpec iv = new IvParameterSpec(ivBytes);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, cipherKey, iv);
|
||||
|
||||
return cipher.doFinal(ciphertext, CIPHERTEXT_OFFSET,
|
||||
ciphertext.length - VERSION_LENGTH - IV_LENGTH - MAC_SIZE);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (BadPaddingException e) {
|
||||
Log.w("IncomingEncryptedPushMessage", e);
|
||||
throw new IOException("Bad padding?");
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyMac(byte[] ciphertext, SecretKeySpec macKey) throws IOException {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(macKey);
|
||||
|
||||
if (ciphertext.length < MAC_SIZE + 1)
|
||||
throw new IOException("Invalid MAC!");
|
||||
|
||||
mac.update(ciphertext, 0, ciphertext.length - MAC_SIZE);
|
||||
|
||||
byte[] ourMacFull = mac.doFinal();
|
||||
byte[] ourMacBytes = new byte[MAC_SIZE];
|
||||
System.arraycopy(ourMacFull, 0, ourMacBytes, 0, ourMacBytes.length);
|
||||
|
||||
byte[] theirMacBytes = new byte[MAC_SIZE];
|
||||
System.arraycopy(ciphertext, ciphertext.length-MAC_SIZE, theirMacBytes, 0, theirMacBytes.length);
|
||||
|
||||
Log.w("IncomingEncryptedPushMessage", "Our MAC: " + Hex.toString(ourMacBytes));
|
||||
Log.w("IncomingEncryptedPushMessage", "Thr MAC: " + Hex.toString(theirMacBytes));
|
||||
|
||||
if (!Arrays.equals(ourMacBytes, theirMacBytes)) {
|
||||
throw new IOException("Invalid MAC compare!");
|
||||
}
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private SecretKeySpec getCipherKey(String signalingKey) throws IOException {
|
||||
byte[] signalingKeyBytes = Base64.decode(signalingKey);
|
||||
byte[] cipherKey = new byte[CIPHER_KEY_SIZE];
|
||||
System.arraycopy(signalingKeyBytes, 0, cipherKey, 0, cipherKey.length);
|
||||
|
||||
return new SecretKeySpec(cipherKey, "AES");
|
||||
}
|
||||
|
||||
|
||||
private SecretKeySpec getMacKey(String signalingKey) throws IOException {
|
||||
byte[] signalingKeyBytes = Base64.decode(signalingKey);
|
||||
byte[] macKey = new byte[MAC_KEY_SIZE];
|
||||
System.arraycopy(signalingKeyBytes, CIPHER_KEY_SIZE, macKey, 0, macKey.length);
|
||||
|
||||
return new SecretKeySpec(macKey, "HmacSHA256");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal;
|
||||
|
||||
public class IncomingPushMessage implements Parcelable {
|
||||
|
||||
public static final Parcelable.Creator<IncomingPushMessage> CREATOR = new Parcelable.Creator<IncomingPushMessage>() {
|
||||
@Override
|
||||
public IncomingPushMessage createFromParcel(Parcel in) {
|
||||
return new IncomingPushMessage(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IncomingPushMessage[] newArray(int size) {
|
||||
return new IncomingPushMessage[size];
|
||||
}
|
||||
};
|
||||
|
||||
private int type;
|
||||
private String source;
|
||||
private int sourceDevice;
|
||||
private byte[] message;
|
||||
private long timestamp;
|
||||
private String relay;
|
||||
|
||||
private IncomingPushMessage(IncomingPushMessage message, byte[] body) {
|
||||
this.type = message.type;
|
||||
this.source = message.source;
|
||||
this.sourceDevice = message.sourceDevice;
|
||||
this.timestamp = message.timestamp;
|
||||
this.relay = message.relay;
|
||||
this.message = body;
|
||||
}
|
||||
|
||||
public IncomingPushMessage(IncomingPushMessageSignal signal) {
|
||||
this.type = signal.getType().getNumber();
|
||||
this.source = signal.getSource();
|
||||
this.sourceDevice = signal.getSourceDevice();
|
||||
this.message = signal.getMessage().toByteArray();
|
||||
this.timestamp = signal.getTimestamp();
|
||||
this.relay = signal.getRelay();
|
||||
}
|
||||
|
||||
public IncomingPushMessage(Parcel in) {
|
||||
this.type = in.readInt();
|
||||
this.source = in.readString();
|
||||
this.sourceDevice = in.readInt();
|
||||
|
||||
if (in.readInt() == 1) {
|
||||
this.relay = in.readString();
|
||||
}
|
||||
|
||||
this.message = new byte[in.readInt()];
|
||||
in.readByteArray(this.message);
|
||||
this.timestamp = in.readLong();
|
||||
}
|
||||
|
||||
public IncomingPushMessage(int type, String source, int sourceDevice,
|
||||
byte[] body, long timestamp)
|
||||
{
|
||||
this.type = type;
|
||||
this.source = source;
|
||||
this.sourceDevice = sourceDevice;
|
||||
this.message = body;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public String getRelay() {
|
||||
return relay;
|
||||
}
|
||||
|
||||
public long getTimestampMillis() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public String getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public int getSourceDevice() {
|
||||
return sourceDevice;
|
||||
}
|
||||
|
||||
public byte[] getBody() {
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(type);
|
||||
dest.writeString(source);
|
||||
dest.writeInt(sourceDevice);
|
||||
dest.writeInt(relay == null ? 0 : 1);
|
||||
if (relay != null) {
|
||||
dest.writeString(relay);
|
||||
}
|
||||
dest.writeInt(message.length);
|
||||
dest.writeByteArray(message);
|
||||
dest.writeLong(timestamp);
|
||||
}
|
||||
|
||||
public IncomingPushMessage withBody(byte[] body) {
|
||||
return new IncomingPushMessage(this, body);
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public boolean isSecureMessage() {
|
||||
return getType() == IncomingPushMessageSignal.Type.CIPHERTEXT_VALUE;
|
||||
}
|
||||
|
||||
public boolean isPreKeyBundle() {
|
||||
return getType() == IncomingPushMessageSignal.Type.PREKEY_BUNDLE_VALUE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MismatchedDevices {
|
||||
private List<Integer> missingDevices;
|
||||
|
||||
private List<Integer> extraDevices;
|
||||
|
||||
public List<Integer> getMissingDevices() {
|
||||
return missingDevices;
|
||||
}
|
||||
|
||||
public List<Integer> getExtraDevices() {
|
||||
return extraDevices;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class MismatchedDevicesException extends IOException {
|
||||
|
||||
private final MismatchedDevices mismatchedDevices;
|
||||
|
||||
public MismatchedDevicesException(MismatchedDevices mismatchedDevices) {
|
||||
this.mismatchedDevices = mismatchedDevices;
|
||||
}
|
||||
|
||||
public MismatchedDevices getMismatchedDevices() {
|
||||
return mismatchedDevices;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class NotFoundException extends IOException {
|
||||
public NotFoundException(String s) {
|
||||
super(s);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
|
||||
public class OutgoingPushMessage {
|
||||
|
||||
private int type;
|
||||
private int destinationDeviceId;
|
||||
private int destinationRegistrationId;
|
||||
private String body;
|
||||
|
||||
public OutgoingPushMessage(PushAddress address, PushBody body) {
|
||||
this.type = body.getType();
|
||||
this.destinationDeviceId = address.getDeviceId();
|
||||
this.destinationRegistrationId = body.getRemoteRegistrationId();
|
||||
this.body = Base64.encodeBytes(body.getBody());
|
||||
}
|
||||
|
||||
public int getDestinationDeviceId() {
|
||||
return destinationDeviceId;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public int getDestinationRegistrationId() {
|
||||
return destinationRegistrationId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class OutgoingPushMessageList {
|
||||
|
||||
private String destination;
|
||||
|
||||
private String relay;
|
||||
|
||||
private List<OutgoingPushMessage> messages;
|
||||
|
||||
public OutgoingPushMessageList(String destination, String relay, List<OutgoingPushMessage> messages) {
|
||||
this.destination = destination;
|
||||
this.relay = relay;
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
public String getDestination() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public List<OutgoingPushMessage> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public String getRelay() {
|
||||
return relay;
|
||||
}
|
||||
}
|
||||
126
library/src/org/whispersystems/textsecure/push/PreKeyEntity.java
Normal file
126
library/src/org/whispersystems/textsecure/push/PreKeyEntity.java
Normal file
@@ -0,0 +1,126 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import com.google.thoughtcrimegson.GsonBuilder;
|
||||
import com.google.thoughtcrimegson.JsonDeserializationContext;
|
||||
import com.google.thoughtcrimegson.JsonDeserializer;
|
||||
import com.google.thoughtcrimegson.JsonElement;
|
||||
import com.google.thoughtcrimegson.JsonParseException;
|
||||
import com.google.thoughtcrimegson.JsonPrimitive;
|
||||
import com.google.thoughtcrimegson.JsonSerializationContext;
|
||||
import com.google.thoughtcrimegson.JsonSerializer;
|
||||
import com.google.thoughtcrimegson.annotations.Expose;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
public class PreKeyEntity {
|
||||
|
||||
@Expose(serialize = false)
|
||||
private int deviceId;
|
||||
|
||||
private int keyId;
|
||||
private ECPublicKey publicKey;
|
||||
private IdentityKey identityKey;
|
||||
private int registrationId;
|
||||
|
||||
public PreKeyEntity(int keyId, ECPublicKey publicKey, IdentityKey identityKey) {
|
||||
this.keyId = keyId;
|
||||
this.publicKey = publicKey;
|
||||
this.identityKey = identityKey;
|
||||
this.registrationId = registrationId;
|
||||
}
|
||||
|
||||
public int getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public int getKeyId() {
|
||||
return keyId;
|
||||
}
|
||||
|
||||
public ECPublicKey getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public IdentityKey getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public int getRegistrationId() {
|
||||
return registrationId;
|
||||
}
|
||||
|
||||
public static String toJson(PreKeyEntity entity) {
|
||||
return getBuilder().create().toJson(entity);
|
||||
}
|
||||
|
||||
public static PreKeyEntity fromJson(String encoded) {
|
||||
return getBuilder().create().fromJson(encoded, PreKeyEntity.class);
|
||||
}
|
||||
|
||||
public static GsonBuilder getBuilder() {
|
||||
GsonBuilder builder = new GsonBuilder();
|
||||
builder.registerTypeAdapter(ECPublicKey.class, new ECPublicKeyJsonAdapter());
|
||||
builder.registerTypeAdapter(IdentityKey.class, new IdentityKeyJsonAdapter());
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
private static class ECPublicKeyJsonAdapter
|
||||
implements JsonSerializer<ECPublicKey>, JsonDeserializer<ECPublicKey>
|
||||
{
|
||||
@Override
|
||||
public JsonElement serialize(ECPublicKey preKeyPublic, Type type,
|
||||
JsonSerializationContext jsonSerializationContext)
|
||||
{
|
||||
return new JsonPrimitive(Base64.encodeBytesWithoutPadding(preKeyPublic.serialize()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ECPublicKey deserialize(JsonElement jsonElement, Type type,
|
||||
JsonDeserializationContext jsonDeserializationContext)
|
||||
throws JsonParseException
|
||||
{
|
||||
try {
|
||||
return Curve.decodePoint(Base64.decodeWithoutPadding(jsonElement.getAsJsonPrimitive().getAsString()), 0);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new JsonParseException(e);
|
||||
} catch (IOException e) {
|
||||
throw new JsonParseException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class IdentityKeyJsonAdapter
|
||||
implements JsonSerializer<IdentityKey>, JsonDeserializer<IdentityKey>
|
||||
{
|
||||
@Override
|
||||
public JsonElement serialize(IdentityKey identityKey, Type type,
|
||||
JsonSerializationContext jsonSerializationContext)
|
||||
{
|
||||
return new JsonPrimitive(Base64.encodeBytesWithoutPadding(identityKey.serialize()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityKey deserialize(JsonElement jsonElement, Type type,
|
||||
JsonDeserializationContext jsonDeserializationContext)
|
||||
throws JsonParseException
|
||||
{
|
||||
try {
|
||||
return new IdentityKey(Base64.decodeWithoutPadding(jsonElement.getAsJsonPrimitive().getAsString()), 0);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new JsonParseException(e);
|
||||
} catch (IOException e) {
|
||||
throw new JsonParseException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PreKeyList {
|
||||
|
||||
private PreKeyEntity lastResortKey;
|
||||
private List<PreKeyEntity> keys;
|
||||
|
||||
public PreKeyList(PreKeyEntity lastResortKey, List<PreKeyEntity> keys) {
|
||||
this.keys = keys;
|
||||
this.lastResortKey = lastResortKey;
|
||||
}
|
||||
|
||||
public List<PreKeyEntity> getKeys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
public static String toJson(PreKeyList entity) {
|
||||
return PreKeyEntity.getBuilder().create().toJson(entity);
|
||||
}
|
||||
|
||||
public static PreKeyList fromJson(String serialized) {
|
||||
return PreKeyEntity.getBuilder().create().fromJson(serialized, PreKeyList.class);
|
||||
}
|
||||
|
||||
public PreKeyEntity getLastResortKey() {
|
||||
return lastResortKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.whispersystems.textsecure.directory.Directory;
|
||||
import org.whispersystems.textsecure.storage.RecipientDevice;
|
||||
|
||||
public class PushAddress extends RecipientDevice {
|
||||
|
||||
private final String e164number;
|
||||
private final String relay;
|
||||
|
||||
private PushAddress(long recipientId, String e164number, int deviceId, String relay) {
|
||||
super(recipientId, deviceId);
|
||||
this.e164number = e164number;
|
||||
this.relay = relay;
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
return e164number;
|
||||
}
|
||||
|
||||
public String getRelay() {
|
||||
return relay;
|
||||
}
|
||||
|
||||
public static PushAddress create(Context context, long recipientId, String e164number, int deviceId) {
|
||||
String relay = Directory.getInstance(context).getRelay(e164number);
|
||||
return new PushAddress(recipientId, e164number, deviceId, relay);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
public class PushAttachmentData {
|
||||
|
||||
private final String contentType;
|
||||
private final byte[] data;
|
||||
|
||||
public PushAttachmentData(String contentType, byte[] data) {
|
||||
this.contentType = contentType;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
public class PushAttachmentPointer implements Parcelable {
|
||||
|
||||
public static final Parcelable.Creator<PushAttachmentPointer> CREATOR = new Parcelable.Creator<PushAttachmentPointer>() {
|
||||
@Override
|
||||
public PushAttachmentPointer createFromParcel(Parcel in) {
|
||||
return new PushAttachmentPointer(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PushAttachmentPointer[] newArray(int size) {
|
||||
return new PushAttachmentPointer[size];
|
||||
}
|
||||
};
|
||||
|
||||
private final String contentType;
|
||||
private final long id;
|
||||
private final byte[] key;
|
||||
|
||||
public PushAttachmentPointer(String contentType, long id, byte[] key) {
|
||||
this.contentType = contentType;
|
||||
this.id = id;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public PushAttachmentPointer(Parcel in) {
|
||||
this.contentType = in.readString();
|
||||
this.id = in.readLong();
|
||||
|
||||
int keyLength = in.readInt();
|
||||
this.key = new byte[keyLength];
|
||||
in.readByteArray(this.key);
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public byte[] getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(contentType);
|
||||
dest.writeLong(id);
|
||||
dest.writeInt(this.key.length);
|
||||
dest.writeByteArray(this.key);
|
||||
}
|
||||
}
|
||||
26
library/src/org/whispersystems/textsecure/push/PushBody.java
Normal file
26
library/src/org/whispersystems/textsecure/push/PushBody.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
public class PushBody {
|
||||
|
||||
private final int type;
|
||||
private final int remoteRegistrationId;
|
||||
private final byte[] body;
|
||||
|
||||
public PushBody(int type, int remoteRegistrationId, byte[] body) {
|
||||
this.type = type;
|
||||
this.remoteRegistrationId = remoteRegistrationId;
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public byte[] getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public int getRemoteRegistrationId() {
|
||||
return remoteRegistrationId;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
package org.thoughtcrime.securesms.gcm;
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GcmMessageResponse {
|
||||
public class PushMessageResponse {
|
||||
private List<String> success;
|
||||
private List<String> failure;
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.thoughtcrimegson.Gson;
|
||||
import com.google.thoughtcrimegson.JsonParseException;
|
||||
|
||||
import org.apache.http.conn.ssl.StrictHostnameVerifier;
|
||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
||||
import org.whispersystems.textsecure.storage.PreKeyRecord;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
|
||||
public class PushServiceSocket {
|
||||
|
||||
private static final String CREATE_ACCOUNT_SMS_PATH = "/v1/accounts/sms/code/%s";
|
||||
private static final String CREATE_ACCOUNT_VOICE_PATH = "/v1/accounts/voice/code/%s";
|
||||
private static final String VERIFY_ACCOUNT_PATH = "/v1/accounts/code/%s";
|
||||
private static final String REGISTER_GCM_PATH = "/v1/accounts/gcm/";
|
||||
private static final String PREKEY_PATH = "/v1/keys/%s";
|
||||
private static final String PREKEY_DEVICE_PATH = "/v1/keys/%s/%s";
|
||||
|
||||
private static final String DIRECTORY_TOKENS_PATH = "/v1/directory/tokens";
|
||||
private static final String DIRECTORY_VERIFY_PATH = "/v1/directory/%s";
|
||||
private static final String MESSAGE_PATH = "/v1/messages/%s";
|
||||
private static final String ATTACHMENT_PATH = "/v1/attachments/%s";
|
||||
|
||||
private static final boolean ENFORCE_SSL = true;
|
||||
|
||||
private final Context context;
|
||||
private final String serviceUrl;
|
||||
private final String localNumber;
|
||||
private final String password;
|
||||
private final TrustManagerFactory trustManagerFactory;
|
||||
|
||||
public PushServiceSocket(Context context, String serviceUrl, TrustStore trustStore,
|
||||
String localNumber, String password)
|
||||
{
|
||||
this.context = context.getApplicationContext();
|
||||
this.serviceUrl = serviceUrl;
|
||||
this.localNumber = localNumber;
|
||||
this.password = password;
|
||||
this.trustManagerFactory = initializeTrustManagerFactory(trustStore);
|
||||
}
|
||||
|
||||
public void createAccount(boolean voice) throws IOException {
|
||||
String path = voice ? CREATE_ACCOUNT_VOICE_PATH : CREATE_ACCOUNT_SMS_PATH;
|
||||
makeRequest(String.format(path, localNumber), "GET", null);
|
||||
}
|
||||
|
||||
public void verifyAccount(String verificationCode, String signalingKey,
|
||||
boolean supportsSms, int registrationId)
|
||||
throws IOException
|
||||
{
|
||||
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, supportsSms, registrationId);
|
||||
makeRequest(String.format(VERIFY_ACCOUNT_PATH, verificationCode),
|
||||
"PUT", new Gson().toJson(signalingKeyEntity));
|
||||
}
|
||||
|
||||
public void registerGcmId(String gcmRegistrationId) throws IOException {
|
||||
GcmRegistrationId registration = new GcmRegistrationId(gcmRegistrationId);
|
||||
makeRequest(REGISTER_GCM_PATH, "PUT", new Gson().toJson(registration));
|
||||
}
|
||||
|
||||
public void unregisterGcmId() throws IOException {
|
||||
makeRequest(REGISTER_GCM_PATH, "DELETE", null);
|
||||
}
|
||||
|
||||
public void sendMessage(OutgoingPushMessageList bundle)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
makeRequest(String.format(MESSAGE_PATH, bundle.getDestination()), "PUT", new Gson().toJson(bundle));
|
||||
} catch (NotFoundException nfe) {
|
||||
throw new UnregisteredUserException(bundle.getDestination(), nfe);
|
||||
}
|
||||
}
|
||||
|
||||
public void registerPreKeys(IdentityKey identityKey,
|
||||
PreKeyRecord lastResortKey,
|
||||
List<PreKeyRecord> records)
|
||||
throws IOException
|
||||
{
|
||||
List<PreKeyEntity> entities = new LinkedList<PreKeyEntity>();
|
||||
|
||||
for (PreKeyRecord record : records) {
|
||||
PreKeyEntity entity = new PreKeyEntity(record.getId(),
|
||||
record.getKeyPair().getPublicKey(),
|
||||
identityKey);
|
||||
|
||||
entities.add(entity);
|
||||
}
|
||||
|
||||
PreKeyEntity lastResortEntity = new PreKeyEntity(lastResortKey.getId(),
|
||||
lastResortKey.getKeyPair().getPublicKey(),
|
||||
identityKey);
|
||||
|
||||
|
||||
makeRequest(String.format(PREKEY_PATH, ""), "PUT",
|
||||
PreKeyList.toJson(new PreKeyList(lastResortEntity, entities)));
|
||||
}
|
||||
|
||||
public List<PreKeyEntity> getPreKeys(PushAddress destination) throws IOException {
|
||||
try {
|
||||
String deviceId = String.valueOf(destination.getDeviceId());
|
||||
|
||||
if (deviceId.equals("1"))
|
||||
deviceId = "*";
|
||||
|
||||
String path = String.format(PREKEY_DEVICE_PATH, destination.getNumber(), deviceId);
|
||||
|
||||
if (!Util.isEmpty(destination.getRelay())) {
|
||||
path = path + "?relay=" + destination.getRelay();
|
||||
}
|
||||
|
||||
String responseText = makeRequest(path, "GET", null);
|
||||
PreKeyList response = PreKeyList.fromJson(responseText);
|
||||
|
||||
return response.getKeys();
|
||||
} catch (JsonParseException e) {
|
||||
throw new IOException(e);
|
||||
} catch (NotFoundException nfe) {
|
||||
throw new UnregisteredUserException(destination.getNumber(), nfe);
|
||||
}
|
||||
}
|
||||
|
||||
public PreKeyEntity getPreKey(PushAddress destination) throws IOException {
|
||||
try {
|
||||
String path = String.format(PREKEY_DEVICE_PATH, destination.getNumber(),
|
||||
String.valueOf(destination.getDeviceId()));
|
||||
|
||||
if (!Util.isEmpty(destination.getRelay())) {
|
||||
path = path + "?relay=" + destination.getRelay();
|
||||
}
|
||||
|
||||
String responseText = makeRequest(path, "GET", null);
|
||||
PreKeyList response = PreKeyList.fromJson(responseText);
|
||||
|
||||
if (response.getKeys() == null || response.getKeys().size() < 1)
|
||||
throw new IOException("Empty prekey list");
|
||||
|
||||
return response.getKeys().get(0);
|
||||
} catch (JsonParseException e) {
|
||||
throw new IOException(e);
|
||||
} catch (NotFoundException nfe) {
|
||||
throw new UnregisteredUserException(destination.getNumber(), nfe);
|
||||
}
|
||||
}
|
||||
|
||||
public long sendAttachment(PushAttachmentData attachment) throws IOException {
|
||||
String response = makeRequest(String.format(ATTACHMENT_PATH, ""), "GET", null);
|
||||
AttachmentDescriptor attachmentKey = new Gson().fromJson(response, AttachmentDescriptor.class);
|
||||
|
||||
if (attachmentKey == null || attachmentKey.getLocation() == null) {
|
||||
throw new IOException("Server failed to allocate an attachment key!");
|
||||
}
|
||||
|
||||
Log.w("PushServiceSocket", "Got attachment content location: " + attachmentKey.getLocation());
|
||||
|
||||
uploadExternalFile("PUT", attachmentKey.getLocation(), attachment.getData());
|
||||
|
||||
return attachmentKey.getId();
|
||||
}
|
||||
|
||||
public File retrieveAttachment(String relay, long attachmentId) throws IOException {
|
||||
String path = String.format(ATTACHMENT_PATH, String.valueOf(attachmentId));
|
||||
|
||||
if (!Util.isEmpty(relay)) {
|
||||
path = path + "?relay=" + relay;
|
||||
}
|
||||
|
||||
String response = makeRequest(path, "GET", null);
|
||||
AttachmentDescriptor descriptor = new Gson().fromJson(response, AttachmentDescriptor.class);
|
||||
|
||||
Log.w("PushServiceSocket", "Attachment: " + attachmentId + " is at: " + descriptor.getLocation());
|
||||
|
||||
File attachment = File.createTempFile("attachment", ".tmp", context.getFilesDir());
|
||||
attachment.deleteOnExit();
|
||||
|
||||
downloadExternalFile(descriptor.getLocation(), attachment);
|
||||
|
||||
return attachment;
|
||||
}
|
||||
|
||||
public List<ContactTokenDetails> retrieveDirectory(Set<String> contactTokens) {
|
||||
try {
|
||||
ContactTokenList contactTokenList = new ContactTokenList(new LinkedList(contactTokens));
|
||||
String response = makeRequest(DIRECTORY_TOKENS_PATH, "PUT", new Gson().toJson(contactTokenList));
|
||||
ContactTokenDetailsList activeTokens = new Gson().fromJson(response, ContactTokenDetailsList.class);
|
||||
|
||||
return activeTokens.getContacts();
|
||||
} catch (IOException ioe) {
|
||||
Log.w("PushServiceSocket", ioe);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public ContactTokenDetails getContactTokenDetails(String contactToken) throws IOException {
|
||||
try {
|
||||
String response = makeRequest(String.format(DIRECTORY_VERIFY_PATH, contactToken), "GET", null);
|
||||
return new Gson().fromJson(response, ContactTokenDetails.class);
|
||||
} catch (NotFoundException nfe) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void downloadExternalFile(String url, File localDestination)
|
||||
throws IOException
|
||||
{
|
||||
URL downloadUrl = new URL(url);
|
||||
HttpURLConnection connection = (HttpURLConnection) downloadUrl.openConnection();
|
||||
connection.setRequestProperty("Content-Type", "application/octet-stream");
|
||||
connection.setRequestMethod("GET");
|
||||
connection.setDoInput(true);
|
||||
|
||||
try {
|
||||
if (connection.getResponseCode() != 200) {
|
||||
throw new IOException("Bad response: " + connection.getResponseCode());
|
||||
}
|
||||
|
||||
OutputStream output = new FileOutputStream(localDestination);
|
||||
InputStream input = connection.getInputStream();
|
||||
byte[] buffer = new byte[4096];
|
||||
int read;
|
||||
|
||||
while ((read = input.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, read);
|
||||
}
|
||||
|
||||
output.close();
|
||||
Log.w("PushServiceSocket", "Downloaded: " + url + " to: " + localDestination.getAbsolutePath());
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private void uploadExternalFile(String method, String url, byte[] data)
|
||||
throws IOException
|
||||
{
|
||||
URL uploadUrl = new URL(url);
|
||||
HttpsURLConnection connection = (HttpsURLConnection) uploadUrl.openConnection();
|
||||
connection.setDoOutput(true);
|
||||
connection.setRequestMethod(method);
|
||||
connection.setRequestProperty("Content-Type", "application/octet-stream");
|
||||
connection.connect();
|
||||
|
||||
try {
|
||||
OutputStream out = connection.getOutputStream();
|
||||
out.write(data);
|
||||
out.close();
|
||||
|
||||
if (connection.getResponseCode() != 200) {
|
||||
throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage());
|
||||
}
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private String makeRequest(String urlFragment, String method, String body)
|
||||
throws IOException
|
||||
{
|
||||
HttpURLConnection connection = makeBaseRequest(urlFragment, method, body);
|
||||
String response = Util.readFully(connection.getInputStream());
|
||||
|
||||
connection.disconnect();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private HttpURLConnection makeBaseRequest(String urlFragment, String method, String body)
|
||||
throws IOException
|
||||
{
|
||||
HttpURLConnection connection = getConnection(urlFragment, method);
|
||||
|
||||
if (body != null) {
|
||||
connection.setDoOutput(true);
|
||||
}
|
||||
|
||||
connection.connect();
|
||||
|
||||
if (body != null) {
|
||||
Log.w("PushServiceSocket", method + " -- " + body);
|
||||
OutputStream out = connection.getOutputStream();
|
||||
out.write(body.getBytes());
|
||||
out.close();
|
||||
}
|
||||
|
||||
if (connection.getResponseCode() == 413) {
|
||||
connection.disconnect();
|
||||
throw new RateLimitException("Rate limit exceeded: " + connection.getResponseCode());
|
||||
}
|
||||
|
||||
if (connection.getResponseCode() == 401 || connection.getResponseCode() == 403) {
|
||||
connection.disconnect();
|
||||
throw new AuthorizationFailedException("Authorization failed!");
|
||||
}
|
||||
|
||||
if (connection.getResponseCode() == 404) {
|
||||
connection.disconnect();
|
||||
throw new NotFoundException("Not found");
|
||||
}
|
||||
|
||||
if (connection.getResponseCode() == 409) {
|
||||
String response = Util.readFully(connection.getErrorStream());
|
||||
throw new MismatchedDevicesException(new Gson().fromJson(response, MismatchedDevices.class));
|
||||
}
|
||||
|
||||
if (connection.getResponseCode() == 410) {
|
||||
String response = Util.readFully(connection.getErrorStream());
|
||||
throw new StaleDevicesException(new Gson().fromJson(response, StaleDevices.class));
|
||||
}
|
||||
|
||||
if (connection.getResponseCode() != 200 && connection.getResponseCode() != 204) {
|
||||
throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage());
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
private HttpURLConnection getConnection(String urlFragment, String method) throws IOException {
|
||||
try {
|
||||
SSLContext context = SSLContext.getInstance("TLS");
|
||||
context.init(null, trustManagerFactory.getTrustManagers(), null);
|
||||
|
||||
URL url = new URL(String.format("%s%s", serviceUrl, urlFragment));
|
||||
Log.w("PushServiceSocket", "Push service URL: " + serviceUrl);
|
||||
Log.w("PushServiceSocket", "Opening URL: " + url);
|
||||
|
||||
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
|
||||
|
||||
if (ENFORCE_SSL) {
|
||||
((HttpsURLConnection)connection).setSSLSocketFactory(context.getSocketFactory());
|
||||
((HttpsURLConnection)connection).setHostnameVerifier(new StrictHostnameVerifier());
|
||||
}
|
||||
|
||||
connection.setRequestMethod(method);
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
|
||||
if (password != null) {
|
||||
connection.setRequestProperty("Authorization", getAuthorizationHeader());
|
||||
}
|
||||
|
||||
return connection;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (KeyManagementException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (MalformedURLException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getAuthorizationHeader() {
|
||||
try {
|
||||
return "Basic " + Base64.encodeBytes((localNumber + ":" + password).getBytes("UTF-8"));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private TrustManagerFactory initializeTrustManagerFactory(TrustStore trustStore) {
|
||||
try {
|
||||
InputStream keyStoreInputStream = trustStore.getKeyStoreInputStream();
|
||||
KeyStore keyStore = KeyStore.getInstance("BKS");
|
||||
|
||||
keyStore.load(keyStoreInputStream, trustStore.getKeyStorePassword().toCharArray());
|
||||
|
||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509");
|
||||
trustManagerFactory.init(keyStore);
|
||||
|
||||
return trustManagerFactory;
|
||||
} catch (KeyStoreException kse) {
|
||||
throw new AssertionError(kse);
|
||||
} catch (CertificateException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (IOException ioe) {
|
||||
throw new AssertionError(ioe);
|
||||
}
|
||||
}
|
||||
|
||||
private static class GcmRegistrationId {
|
||||
private String gcmRegistrationId;
|
||||
|
||||
public GcmRegistrationId() {}
|
||||
|
||||
public GcmRegistrationId(String gcmRegistrationId) {
|
||||
this.gcmRegistrationId = gcmRegistrationId;
|
||||
}
|
||||
}
|
||||
|
||||
private static class AttachmentDescriptor {
|
||||
private long id;
|
||||
private String location;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getLocation() {
|
||||
return location;
|
||||
}
|
||||
}
|
||||
|
||||
public interface TrustStore {
|
||||
public InputStream getKeyStoreInputStream();
|
||||
public String getKeyStorePassword();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.TransportDetails;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class PushTransportDetails implements TransportDetails {
|
||||
@Override
|
||||
public byte[] getStrippedPaddingMessageBody(byte[] messageWithPadding) {
|
||||
return messageWithPadding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getPaddedMessageBody(byte[] messageBody) {
|
||||
return messageBody;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getEncodedMessage(byte[] messageWithMac) {
|
||||
return messageWithMac;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getDecodedMessage(byte[] encodedMessageBytes) throws IOException {
|
||||
return encodedMessageBytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class RateLimitException extends IOException {
|
||||
public RateLimitException(String s) {
|
||||
super(s);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user