mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-16 16:06:08 +00:00
Compare commits
343 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbf02603ce | ||
|
|
5caec4a146 | ||
|
|
dc77c43435 | ||
|
|
827298d1a2 | ||
|
|
48f5b932f7 | ||
|
|
90169e9468 | ||
|
|
b5fe378bc9 | ||
|
|
0f53c9d170 | ||
|
|
3474950830 | ||
|
|
39ee363150 | ||
|
|
411e3ceff6 | ||
|
|
b7f8c3b3d3 | ||
|
|
3efa8e6899 | ||
|
|
948f481530 | ||
|
|
716fdefa4c | ||
|
|
2dc893730a | ||
|
|
1af99ce155 | ||
|
|
0850f1b0f1 | ||
|
|
1ddc45fd9c | ||
|
|
977765c80f | ||
|
|
4b2d07ab35 | ||
|
|
003ebe6364 | ||
|
|
2f7c005c23 | ||
|
|
367b481d07 | ||
|
|
5cd8c922d2 | ||
|
|
a2fe8a9d5c | ||
|
|
ee921a8f49 | ||
|
|
e0394b4481 | ||
|
|
de100f5be7 | ||
|
|
a020a57be6 | ||
|
|
28f1a0a636 | ||
|
|
5a807ffc28 | ||
|
|
757cb1c846 | ||
|
|
4f066757e7 | ||
|
|
633aa9b057 | ||
|
|
a5c26b2e16 | ||
|
|
ca561d76ff | ||
|
|
107d999ee7 | ||
|
|
0f6c7660cb | ||
|
|
8ea4db03db | ||
|
|
81ee9e31c5 | ||
|
|
a3e900ecbe | ||
|
|
384fb3b2b5 | ||
|
|
d795aa30b3 | ||
|
|
bac4d63312 | ||
|
|
bf60f90019 | ||
|
|
852ca2ac05 | ||
|
|
038bebfdbb | ||
|
|
3b25b87aa8 | ||
|
|
5a62856e46 | ||
|
|
37a52df4e6 | ||
|
|
d3148b6766 | ||
|
|
6fb85aff6d | ||
|
|
ed45067227 | ||
|
|
0015711759 | ||
|
|
15390e477e | ||
|
|
a8c23413ba | ||
|
|
ce68429a9b | ||
|
|
1a9a88a5a1 | ||
|
|
7987362c25 | ||
|
|
5d42110d6c | ||
|
|
dec7fd4c8a | ||
|
|
82df23dd41 | ||
|
|
ce710b378f | ||
|
|
20fd881613 | ||
|
|
5fa429b0d5 | ||
|
|
630dce04fc | ||
|
|
0da1d8818e | ||
|
|
c84285c639 | ||
|
|
5a525a2e58 | ||
|
|
dda8a214a4 | ||
|
|
bd167cbb17 | ||
|
|
a0aaa7d724 | ||
|
|
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 | ||
|
|
89ae5ed4ed |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -5,7 +5,7 @@ project.properties
|
||||
bin/
|
||||
gen/
|
||||
.idea/
|
||||
TextSecure.iml
|
||||
*.iml
|
||||
out
|
||||
tests
|
||||
lint.xml
|
||||
@@ -17,6 +17,5 @@ build-log.xml
|
||||
.gradle
|
||||
build
|
||||
signing.properties
|
||||
gradle
|
||||
gradlew
|
||||
gradlew.bat
|
||||
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,kn_IN:kn-rIN,cs_CZ:cs
|
||||
lang_map = fr_CA:fr-rCA,pt_BR:pt-rBR,pt_PT:pt,zh_CN:zh-rCN,zh_HK:zh-rHK,zh_TW:zh-rTW,da_DK:da-rDK,de_DE:de,tr_TR:tr,fr_FR:fr,es_ES:es,hu_HU:hu,sv_SE:sv-rSE,bg_BG:bg,el_GR:el,kn_IN:kn-rIN,cs_CZ:cs
|
||||
|
||||
|
||||
[textsecure-official.master]
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.thoughtcrime.securesms"
|
||||
android:versionCode="60"
|
||||
android:versionName="1.0.4">
|
||||
android:versionCode="67"
|
||||
android:versionName="2.0.4">
|
||||
|
||||
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="19"/>
|
||||
<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"/>
|
||||
@@ -39,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">
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<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" />
|
||||
@@ -63,17 +63,25 @@
|
||||
<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"/>
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
</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"/>
|
||||
|
||||
@@ -85,16 +93,19 @@
|
||||
<activity android:name=".MmsPreferencesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ConversationListActivity"
|
||||
<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"
|
||||
@@ -121,6 +132,17 @@
|
||||
|
||||
<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"
|
||||
@@ -139,10 +161,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"/>
|
||||
@@ -166,10 +184,27 @@
|
||||
<activity android:name=".RegistrationProgressActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".LogSubmitActivity"
|
||||
android:label="@string/AndroidManifest__log_submit"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".DummyActivity"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:enabled="true"
|
||||
android:allowTaskReparenting="true"
|
||||
android:noHistory="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:alwaysRetainTaskState="false"
|
||||
android:stateNotNeeded="true"
|
||||
android:clearTaskOnLaunch="true"
|
||||
android:finishOnTaskLaunch="true" />
|
||||
|
||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||
<service android:enabled="true" android:name=".service.KeyCachingService"/>
|
||||
<service android:enabled="true" android:name=".service.SendReceiveService"/>
|
||||
<service android:enabled="true" android:name=".service.RegistrationService"/>
|
||||
<service android:enabled="true" android:name=".service.DirectoryRefreshService"/>
|
||||
<service android:enabled="true" android:name=".gcm.GcmIntentService"/>
|
||||
|
||||
<service android:name=".service.QuickResponseService"
|
||||
@@ -234,14 +269,6 @@
|
||||
<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"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".notifications.MarkReadReceiver"
|
||||
android:enabled="true"
|
||||
@@ -255,6 +282,20 @@
|
||||
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>
|
||||
|
||||
|
||||
17
BUILDING.md
17
BUILDING.md
@@ -2,8 +2,21 @@ Building TextSecure
|
||||
===================
|
||||
|
||||
1. Ensure the 'Android Support Repository' is installed from the Android SDK manager.
|
||||
1. Ensure gradle >= 1.8 is installed.
|
||||
|
||||
Execute Gradle:
|
||||
|
||||
gradle build
|
||||
./gradlew build
|
||||
|
||||
Re-building native components
|
||||
-----------------------------
|
||||
|
||||
Note: This step is optional; native components are contained as binaries (see [library/libs](library/libs)).
|
||||
|
||||
1. Ensure that the Android NDK is installed.
|
||||
|
||||
Execute ndk-build:
|
||||
|
||||
cd library
|
||||
ndk-build
|
||||
|
||||
Afterwards, execute Gradle as above to re-create the APK.
|
||||
|
||||
@@ -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.
18
build.gradle
18
build.gradle
@@ -3,7 +3,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:0.6.+'
|
||||
classpath 'com.android.tools.build:gradle:0.8.+'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,18 +21,18 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
compile 'com.actionbarsherlock:actionbarsherlock:4.4.0@aar'
|
||||
compile 'com.googlecode.libphonenumber:libphonenumber:5.3'
|
||||
compile 'com.android.support:support-v4:18.0.0'
|
||||
compile 'org.whispersystems:gson:2.1'
|
||||
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'
|
||||
buildToolsVersion '19.0.2'
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 8
|
||||
minSdkVersion 9
|
||||
targetSdkVersion 19
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ android {
|
||||
manifest.srcFile 'AndroidManifest.xml'
|
||||
java.srcDirs = ['src']
|
||||
resources.srcDirs = ['src']
|
||||
aild.srcDirs = ['src']
|
||||
aidl.srcDirs = ['src']
|
||||
renderscript.srcDirs = ['src']
|
||||
res.srcDirs = ['res']
|
||||
assets.srcDirs = ['assets']
|
||||
@@ -58,6 +58,10 @@ android {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
def Properties props = new Properties()
|
||||
|
||||
3
contributing.md
Normal file
3
contributing.md
Normal file
@@ -0,0 +1,3 @@
|
||||
##Translations
|
||||
|
||||
Please do not submit issues or pull requests for translation fixes. Anyone can update the translations in [Transifex](https://www.transifex.com/projects/p/textsecure-official/). Please submit your corrections there.
|
||||
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'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 19
|
||||
buildToolsVersion '19.0.2'
|
||||
|
||||
android {
|
||||
sourceSets {
|
||||
main {
|
||||
manifest.srcFile 'AndroidManifest.xml'
|
||||
java.srcDirs = ['src']
|
||||
resources.srcDirs = ['src']
|
||||
aidl.srcDirs = ['src']
|
||||
renderscript.srcDirs = ['src']
|
||||
res.srcDirs = ['res']
|
||||
assets.srcDirs = ['assets']
|
||||
jniLibs.srcDirs = ['libs']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
1
library/jni/Application.mk
Normal file
1
library/jni/Application.mk
Normal file
@@ -0,0 +1 @@
|
||||
APP_ABI := armeabi armeabi-v7a x86
|
||||
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-v7a/libcurve25519.so
Executable file
BIN
library/libs/armeabi-v7a/libcurve25519.so
Executable file
Binary file not shown.
BIN
library/libs/armeabi/libcurve25519.so
Executable file
BIN
library/libs/armeabi/libcurve25519.so
Executable file
Binary file not shown.
BIN
library/libs/x86/libcurve25519.so
Executable file
BIN
library/libs/x86/libcurve25519.so
Executable 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,6 @@
|
||||
package org.whispersystems.textsecure.push;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ExpectationFailedException extends IOException {
|
||||
}
|
||||
@@ -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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user