Compare commits

...

114 Commits

Author SHA1 Message Date
Moxie Marlinspike
c1fe585ffb Bump version to 2.6.3
// FREEBIE
2015-03-09 13:32:11 -07:00
Moxie Marlinspike
078df0f029 Update key conflict message in place.
// FREEBIE
2015-03-09 13:15:24 -07:00
Moxie Marlinspike
9f9293cd61 Pending indicator was not cleared on recycled views.
// FREEBIE
2015-03-09 13:14:39 -07:00
Moxie Marlinspike
4e87513867 Bump version to 2.6.2
// FREEBIE
2015-03-08 11:45:56 -07:00
Moxie Marlinspike
c05aca19ae Minimal-diff change to fix NPE.
Fixes #2623

// FREEBIE
2015-03-08 11:37:18 -07:00
Moxie Marlinspike
db2c7fabe1 Bump version to 2.6.1
// FREEBIE
2015-03-07 09:28:28 -08:00
Moxie Marlinspike
e7eb0ec2b3 Fix for missing messages bug
Fixes #2609

// FREEBIE
2015-03-07 09:02:59 -08:00
Moxie Marlinspike
da75268545 Temporary fix until jake fixes this shit
// FREEBIE
2015-03-07 09:02:38 -08:00
Moxie Marlinspike
49ab1e7bd5 Bump version to 2.6.0
// FREEBIE
2015-03-06 11:07:53 -08:00
Moxie Marlinspike
b5a2d7882a Updated language translations.
// FREEBIE
2015-03-06 10:10:34 -08:00
Moxie Marlinspike
534df06794 Support granular "custom" MMS preferences.
1) Make each MMS preference an individual choice between custom
   and default.

2) Display default values.

Closes #2487

// FREEBIE
2015-03-05 18:42:34 -08:00
agrajaghh
e31ddf0599 remove unused icons
Closes #2575

//FREEBIE
2015-03-05 18:36:10 -08:00
Rhodey Orbits
ce22e54359 Update message timesamps on resume.
Fixes #2519.
Closes #2593

// FREEBIE
2015-03-05 18:20:39 -08:00
Rhodey Orbits
7ce7ec56eb 1) created a new string constant for 'Passphrase'.
2) modified 'App protection' preference group summary to be more verbose.
3) dropped the colons from 'Appearance' preference group summary.
4) dropped the colons from and 'SMS and MMS' preference group summary.
2015-03-05 18:20:39 -08:00
agrajaghh
2b39728ea0 fix icons in import/export dialog
Closes #2574

//FREEBIE
2015-03-05 17:35:25 -08:00
Jake McGinty
2af5c7f02e fix "verify" button spacing in details
Fixes #2566
// FREEBIE
2015-03-05 17:07:07 -08:00
Jake McGinty
d6e0199e05 fix MessageDetailsActivity home button
// FREEBIE
2015-03-05 17:07:07 -08:00
Moxie Marlinspike
301ed0b255 Move passphrase disabled preference set to after secret generation.
Fixes #2588
2015-03-05 13:09:18 -08:00
Moxie Marlinspike
ac5306be5b Fix locale names
// FREEBIE

Closes #2564
2015-03-05 11:45:09 -08:00
agrajaghh
9de62d0286 replace icons in the add attachment dialog
Closes #2535
2015-03-03 17:14:31 -08:00
agrajaghh
067b14d551 make some strings translateable
closes #2567

//FREEBIE
2015-03-03 17:09:07 -08:00
agrajaghh
03110c9870 remove ConfirmIdentityActivity
Closes #2571

//FREEBIE
2015-03-03 17:06:24 -08:00
agrajaghh
a2b5bf54c8 remove PassphraseTimeoutPreference since its no longer used
closes #2565

//FREEBIE
2015-03-03 17:03:17 -08:00
Moxie Marlinspike
5602a3dfc0 Update to latest version of libtextsecure for simplified interface.
1) Switch to new TextSecureAddress addressing, rather than mixing
   long-based recipient IDs into libtextsecure.

2) Get rid of RecipientFormattingException throws in calls to
   RecipientFactory.

Closes #2570
2015-03-03 12:25:09 -08:00
Moxie Marlinspike
ed5b3f8679 Break out libtextsecure
// FREEBIE

Closes #2542
2015-03-02 08:25:19 -08:00
Moxie Marlinspike
7bf7acb1ff Dependency verification.
// FREEBIE

Closes #2546
2015-02-28 09:40:52 -08:00
Moxie Marlinspike
ffa5dc549b Add libtextsecure README
// FREEBIE
2015-02-27 16:58:19 -08:00
Moxie Marlinspike
a59feb7ad4 Rename PushAddress to TextSecureAddress
// FREEBIE
2015-02-27 16:57:51 -08:00
Jake McGinty
753a1c7219 add FAB content description
// FREEBIE
2015-02-27 16:31:01 -08:00
Moxie Marlinspike
004f050741 libtextsecure javadoc and minor API refactoring
// FREEBIE
2015-02-27 15:35:18 -08:00
Jake McGinty
fcde642563 Fix invalid transportation options
1) Accounts in the directory that don't have supports_sms can't
   get encrypted SMS.
2) Push groups can't receive insecure SMS

// FREEBIE

Closes #2521
2015-02-27 13:00:18 -08:00
Jake McGinty
ac0a1c0bc1 Conflicts UI touch-ups:
1) Fix i18n and layout naming.
2) Error header for message details.
3) Add back button to message details.

// FREEBIE

Closes #2593
2015-02-27 12:26:35 -08:00
Moxie Marlinspike
3f93731300 Adapt Conflicts UI to work with single-recipient failures. 2015-02-27 12:26:09 -08:00
Moxie Marlinspike
00d7b5c284 Better UX handling on identity key mismatches.
1) Migrate from GSON to Jackson everywhere.

2) Add support for storing identity key conflicts on message rows.

3) Add limited support for surfacing identity key conflicts in UI.
2015-02-27 12:26:09 -08:00
Jake McGinty
4397b55ceb fix padding problem in compose text
// FREEBIE
2015-02-25 14:53:32 -08:00
Martin Stone
b87abcc2c1 Update lockscreen image to new (blue) logo.
Fixes #2426
Closes #2502
2015-02-24 16:45:12 -08:00
Sebastian Kürten
79dbf2746f Added new media icons for audio/video
Added icons new icons, deleted an old one.
Using new icons in AudioSlide/VideoSlide.

Closes #2304
2015-02-23 13:13:39 -08:00
agrajaghh
a4bd9fae9a make "repeat alerts" setting translatable
Fixes #2511
Closes #2516

 //FREEBIE
2015-02-23 09:54:28 -08:00
Moxie Marlinspike
d4493e700e Add GroupId for received messages.
// FREEBIE
2015-02-23 09:53:32 -08:00
Moxie Marlinspike
83d65228e9 Fix for race conditioned caused by OkHttpClient NPE.
We catch OkHttpClient exceptions to deal with bugs in their code,
but in some cases that was leaving our state information in a
bad situation.

// FREEBIE
2015-02-23 09:53:19 -08:00
Moxie Marlinspike
8a2caeef3d Fix for OKH IllegalStateException.
// FREEBIE

Fixes #2501
2015-02-20 12:45:42 -08:00
Jake McGinty
6280ef4ab8 Revert "set appcompat menu class to obfuscate-only in PG"
This reverts commit 899afa2092.

Was able to reproduce a crash with this in second test that was actually
resolved in the previous proguard. Reverting.
2015-02-19 16:27:39 -08:00
Jake McGinty
899afa2092 set appcompat menu class to obfuscate-only in PG
keeping it on the safe side - tested on problematic samsung devices, and
it looks like it works fine.
// FREEBIE
2015-02-19 15:36:57 -08:00
Moxie Marlinspike
7c6b965969 Shrink some SC classes for Gingerbread dexopt limit.
// FREEBIE
2015-02-19 12:32:54 -08:00
Jake McGinty
5fac189736 "All images" view for conversations
// FREEBIE
2015-02-18 15:47:47 -08:00
Moxie Marlinspike
d3271f548c Support for retrieving stored messages via websocket.
1) When registering with server, indicate that the server should
   store messages and send notifications.

2) Process notification GCM messages, and connect to the server
   to retrieve actual message content.
2015-02-18 11:09:01 -08:00
Jake McGinty
023195dd4b make send button focusable
Fixes #2494
// FREEBIE
2015-02-17 17:55:18 -08:00
Moxie Marlinspike
299dd86c77 Support matching to account for telcos inserting SMS prefixes.
// FREEBIE

Fixes #1944
2015-02-16 20:25:19 -08:00
Jake McGinty
f818cfa32b Unjank character count indicators
Fixes #1841
// FREEBIE
2015-02-16 13:25:44 -08:00
Jake McGinty
fc21d2038f Add first Espresso interaction test
// FREEBIE
2015-02-16 13:08:28 -08:00
Jake McGinty
8277e4e102 Fix NPE when InetAddress.getByName() is null
Resolves #2189
// FREEBIE
2015-02-16 13:07:59 -08:00
Jake McGinty
242dbef54e Prevent NPE when MMS's FROM PduHeader is missing
Fixes #1448
// FREEBIE
2015-02-16 13:07:30 -08:00
Jake McGinty
9ffd84ce05 Increase readability of recv date in dark theme
Fixes #1483
// FREEBIE
2015-02-15 22:09:02 -08:00
Moxie Marlinspike
cb37ce0f5f Fix README typo
// FREEBIE

Closes #2396
2015-02-15 14:41:36 -08:00
Andy Martin
7ab739b22b clear the current text box when deleting a draft-only thread.
otherwise the draft will be immediately re-saved (preventing
thread deletion).

// FREEBIE

Closes #2474
Fixes #2473
2015-02-15 09:41:55 -08:00
Moxie Marlinspike
224b06f05a Theme the HMS picker.
// FREEBIE

Closes #2353
2015-02-13 15:06:50 -08:00
McLoo
d36d33f837 Stop crash on pasting long numbers as country code
//FREEBIE

Closes #2311
2015-02-13 14:38:35 -08:00
Moxie Marlinspike
724dced820 Define a max message length to display.
// FREEBIE

Closes #2294
2015-02-13 14:32:11 -08:00
Sam Lanning
b8ca0e57ad Only initialise EmojiGrid when not in edit mode
Suppress initialisation of the emoji_grid_layout.xml when the
EmojiDrawer is being used in a layout where the editor is shown,
 to prevent an NPE when an editor is being used.

// FREEBIE

Closes #2288
2015-02-13 14:23:05 -08:00
AsamK
fe5fc411a7 Save drafts for recipients without an existing thread
- Save drafts without thread in draft database
- Clear drafts when deleting conversation(s), otherwise
  the drafts stay in the database and will reappear when a thread
  with the same threadId is created

Closes #1510
2015-02-13 14:16:19 -08:00
agrajaghh
20d43bbfcb add icons to the preference categories
Closes #2361
2015-02-13 11:18:04 -08:00
Jake McGinty
442f56cdc3 prevent NPE in GroupCreateActivity
Occurs when a returned Recipient is null when returning from contact
selection activity.

Fixes #2466
// FREEBIE
2015-02-12 13:36:47 -08:00
Moxie Marlinspike
35dabaf1e5 Bump version to 2.5.3
// FREEBIE
2015-02-12 13:23:53 -08:00
Jake McGinty
1aec794a34 widen LGE menu workaround constraints
// FREEBIE
2015-02-12 13:16:03 -08:00
Jake McGinty
5bd8d6c69d Check the part stream is non-null before decoding
// FREEBIE

Closes #2459
2015-02-12 10:32:53 -08:00
Moxie Marlinspike
aa26785c00 Reinclude SC provider for consistency.
// FREEBIE

Fixes #2461
2015-02-12 10:30:45 -08:00
Moxie Marlinspike
f450b37cfd Bump version to 2.5.2
// FREEBIE
2015-02-11 23:24:10 -08:00
Jake McGinty
b88e470594 prevent recipient listener Activity context leak
Resolves #2372
Resolves #2425
// FREEBIE
2015-02-11 12:50:20 -08:00
Jake McGinty
f38677794a stop giving long-living static objects Activity contexts
// FREEBIE
2015-02-11 12:50:19 -08:00
Jake McGinty
ac4db41435 lower memory consumption from previews
// FREEBIE
2015-02-11 12:50:19 -08:00
Jake McGinty
3a9d521ffe Workaround LG-related menu issues, pt. 2
fixes #2444
// FREEBIE
2015-02-11 12:44:40 -08:00
Jake McGinty
b1bf33b13b Fix SaveAttachmentTask NPE
resolves #2454
// FREEBIE
2015-02-11 11:42:36 -08:00
Moxie Marlinspike
352418d2d7 Don't notify on MMS delivery failure if message has been deleted.
// FREEBIE

Fixes #2453
2015-02-11 10:29:05 -08:00
Moxie Marlinspike
fb523985ed Bump version to 2.5.1
// FREEBIE
2015-02-09 13:45:41 -08:00
Moxie Marlinspike
7ffe6f053c Merge pull request #2435 from mcginty/dark-actionmode-popup
Fix white-on-white ActionMode popup in dark theme
2015-02-09 12:07:54 -08:00
Jake McGinty
ea77191b70 Fix white-on-white ActionMode popup
// FREEBIE
2015-02-09 12:00:52 -08:00
Moxie Marlinspike
e0b882d4d2 Merge pull request #2434 from mcginty/plurals
move plurals to strings.xml
2015-02-09 11:24:58 -08:00
Jake McGinty
10a74d6d08 move plurals to strings.xml
Resolves #2421
// FREEBIE
2015-02-09 11:19:30 -08:00
Jake McGinty
f092e85b62 Workaround for LGE-related NPEs
See: https://code.google.com/p/android/issues/detail?id=78154

Fixes #2424
Closes #2428

// FREEBIE
2015-02-09 09:15:44 -08:00
Moxie Marlinspike
8aa0f15740 Bump version to 2.5.0
// FREEBIE
2015-02-06 16:42:04 -08:00
Moxie Marlinspike
18d4d4de24 Update language translations
// FREEBIE
2015-02-06 16:21:16 -08:00
Moxie Marlinspike
b3c42dee7e Merge pull request #2416 from mcginty/better-relative-dates
more succinct relative dates
2015-02-06 16:20:23 -08:00
Moxie Marlinspike
5836f35291 Merge pull request #2400 from mcginty/mms-bad-base64-fix
Fix NPE when encrypted MMS has bad base64 encoding
2015-02-06 16:19:20 -08:00
Jake McGinty
dbd8a4083c Fix NPE when encrypted MMS has bad base64 encoding
// FREEBIE
2015-02-06 13:01:36 -08:00
Jake McGinty
0fd52ad1fe more succinct relative dates
// FREEBIE
2015-02-06 13:00:21 -08:00
Moxie Marlinspike
8031c788d9 Merge pull request #2415 from mcginty/empty-group-updates
fix empty group update messages
2015-02-05 19:49:09 -08:00
Moxie Marlinspike
10a29db93d Merge pull request #2414 from mcginty/unread-readability
increase unread visibility in conv list
2015-02-05 19:46:31 -08:00
Jake McGinty
8f3f2e6921 fix empty group update messages
// FREEBIE
2015-02-05 19:38:08 -08:00
Jake McGinty
65c262acef increase unread visibility in conv list
// FREEBIE
2015-02-05 18:41:54 -08:00
AsamK
d586893402 Also crop contact photos in ShareList
// FREEBIE
2015-02-05 14:14:48 -08:00
Jake McGinty
ba62e018db Refine icon
1) fix glow
2) improve shadows
3) fix density-inconsistent assets
2015-01-28 12:52:51 -10:00
Moxie Marlinspike
57c17e705f Fix provisioning dialog strings.
// FREEBIE

Closes #2368
2015-01-24 10:16:33 -08:00
Jake McGinty
2e253fb6a8 provisioning dialog rework
// FREEBIE
2015-01-24 10:15:47 -08:00
Moxie Marlinspike
0c32001fe4 Support for multi-device provisioning flow.
// FREEBIE
2015-01-24 10:15:47 -08:00
Jake McGinty
48f6c2c526 Fix 'indicator tab' coloring
Was previously white-on-near-white
// FREEBIE
2015-01-23 10:59:22 -10:00
Jake McGinty
39fd1e8f46 fix incoming lock asset
// FREEBIE
2015-01-21 20:06:05 -10:00
Jake McGinty
810abe0275 material actionbar and fab
// FREEBIE
2015-01-21 19:01:23 -10:00
Jake McGinty
62816ee51a rounded ImageView instead of Bitmap, crop-select
// FREEBIE
2015-01-21 17:39:54 -10:00
Calvin Hu
a0599c1639 add theme to import/export activity
closes #2359
2015-01-16 10:41:01 -10:00
Jake McGinty
8a4a1b385d libpastelog 1.0.4
// FREEBIE
2015-01-15 13:06:42 -10:00
Moxie Marlinspike
5def8ba78d Bump version to 2.4.2
// FREEBIE
2015-01-14 13:51:53 -08:00
Jake McGinty
42da687602 Revert "add theme to import/export activity"
This reverts commit e64c067636.
2015-01-14 10:23:10 -10:00
Calvin Hu
e64c067636 add theme to import/export activity 2015-01-14 10:03:02 -10:00
Calvin Hu
c89fbabbf3 refresh contacts database on new db query 2015-01-13 15:16:30 -10:00
Moxie Marlinspike
778b8b490c Merge pull request #2346 from mcginty/thumbnail-early-recycle
fix early recycling of thumbnail
2015-01-13 16:21:15 -08:00
Jake McGinty
bade52d748 fix early recycling of thumbnail
// FREEBIE
2015-01-13 14:17:03 -10:00
Moxie Marlinspike
abc322b075 Bump version to 2.4.1
// FREEBIE
2015-01-13 16:01:58 -08:00
Moxie Marlinspike
9a31c5961a Merge pull request #2341 from mcginty/mms-recv-null-text
don't fail on null received text parts
2015-01-13 12:35:22 -08:00
Jake McGinty
f536e45378 don't fail on null received text parts
Fixes #2340
// FREEBIE
2015-01-13 10:15:18 -10:00
Moxie Marlinspike
18961e6369 Merge pull request #2339 from mcginty/content-security-exception
catch content security exception
2015-01-13 12:02:24 -08:00
Jake McGinty
1505ec8c35 catch ContentProvider SecurityExceptions to avoid crashes
// FREEBIE
2015-01-13 10:00:19 -10:00
Jake McGinty
d41efdbd1c prepareMessageMedia before we encrypt
and fail more nicely when pdu composition fails

// FREEBIE

Closes #2338
2015-01-13 11:59:03 -08:00
Jake McGinty
156cb4034e don't copy data when no dataUri
Fixes #2336
// FREEBIE
2015-01-13 11:57:46 -08:00
Moxie Marlinspike
d2e188ee62 Bump version to 2.4.0
// FREEBIE
2015-01-13 11:54:36 -08:00
696 changed files with 8915 additions and 57181 deletions

View File

@@ -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,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,sr:sr
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,sr:sr,he:iw,id:in
[textsecure-official.master]

View File

@@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.thoughtcrime.securesms"
android:versionCode="87"
android:versionName="2.3.3">
android:versionCode="98"
android:versionName="2.6.3">
<permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"
android:label="Access to TextSecure Secrets"
@@ -90,7 +90,18 @@
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".MmsPreferencesActivity"
<activity android:name=".DeviceProvisioningActivity"
android:theme="@style/TextSecure.DialogActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tsdevice"/>
</intent-filter>
</activity>
<activity android:name=".preferences.MmsPreferencesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ShareActivity"
@@ -110,10 +121,16 @@
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".GroupCreateActivity"
android:windowSoftInputMode="stateVisible"
<activity android:name=".MessageDetailsActivity"
android:label="Message Details"
android:windowSoftInputMode="stateUnchanged"
android:launchMode="singleTask"
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.AppCompat.Light.DarkActionBar"
android:launchMode="singleTask"
@@ -190,6 +207,11 @@
<activity android:name=".MediaPreviewActivity"
android:label="@string/AndroidManifest__media_preview"
android:windowSoftInputMode="stateHidden"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".MediaOverviewActivity"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -208,9 +230,12 @@
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name="com.soundcloud.android.crop.CropImageActivity" />
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:name=".service.KeyCachingService"/>
<service android:enabled="true" android:name=".service.RegistrationService"/>
<service android:enabled="true" android:name=".service.MessageRetrievalService"/>
<service android:name=".service.QuickResponseService"
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
@@ -311,6 +336,5 @@
<action android:name="org.thoughtcrime.securesms.MessageNotifier.DELETE_REMINDER_ACTION"/>
</intent-filter>
</receiver>
<uses-library android:name="android.test.runner" />
</application>
</manifest>

View File

@@ -30,6 +30,18 @@ The following steps should help you (re)build TextSecure from the command line.
./gradlew build
Visual assets
----------------------
Source assets tend to be large binary blobs, which are best stored outside of git repositories. We host ours in a [Pixelapse repository](https://www.pixelapse.com/openwhispersystems/projects/signal-android/). Some source files are SVGs that can be auto-colored and sized using a tool like [android-res-utils](https://github.com/sebkur/android-res-utils).
Sample command for generating our audio placeholder image:
```bash
pngs_from_svg.py ic_audio.svg /path/to/TextSecure/res/ 150 "#000" 0.54 _light
pngs_from_svg.py ic_audio.svg /path/to/TextSecure/res/ 150 "#fff" 1.0 _dark
```
Setting up a development environment
------------------------------------

View File

@@ -1,6 +1,6 @@
# TextSecure [![Build Status](https://travis-ci.org/WhisperSystems/TextSecure.svg?branch=master)](https://travis-ci.org/WhisperSystems/TextSecure)
TextSecure is a messaging app for easy private communicate with friends.
TextSecure is a messaging app for simple private communication with friends.
TextSecure can use either data (WiFi/3G/4G) or SMS to communicate securely, and all TextSecure
messages can also be encrypted locally on your device.

View File

@@ -0,0 +1,24 @@
package org.thoughtcrime.securesms;
import static android.support.test.espresso.Espresso.*;
import static android.support.test.espresso.action.ViewActions.*;
import static android.support.test.espresso.matcher.ViewMatchers.*;
import static android.support.test.espresso.assertion.ViewAssertions.*;
import android.test.suitebuilder.annotation.LargeTest;
@LargeTest
public class RegistrationActivityTest extends RoutedInstrumentationTestCase {
private final static String TAG = RegistrationActivityTest.class.getSimpleName();
public RegistrationActivityTest() {
super();
}
@SuppressWarnings("unchecked")
public void testRegistrationButtons() throws Exception {
waitOn(RegistrationActivity.class);
onView(withId(R.id.registerButton)).check(matches(isDisplayed()));
onView(withId(R.id.skipButton)).check(matches(isDisplayed())).perform(click());
}
}

View File

@@ -0,0 +1,37 @@
package org.thoughtcrime.securesms;
import android.app.Activity;
import android.app.Instrumentation.ActivityMonitor;
import android.preference.PreferenceManager;
import android.test.ActivityInstrumentationTestCase2;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
public class RoutedInstrumentationTestCase extends ActivityInstrumentationTestCase2<RoutingActivity> {
private static final String TAG = RoutedInstrumentationTestCase.class.getSimpleName();
public RoutedInstrumentationTestCase() {
super(RoutingActivity.class);
}
@Override
public void setUp() throws Exception {
System.setProperty("dexmaker.dexcache", getInstrumentation().getTargetContext().getCacheDir().getPath());
super.setUp();
clearSharedPrefs();
getActivity();
}
protected void clearSharedPrefs() {
PreferenceManager.getDefaultSharedPreferences(getInstrumentation().getTargetContext())
.edit().clear().commit();
getInstrumentation().getTargetContext().getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
.edit().clear().commit();
}
protected static void waitOn(Class<? extends Activity> clazz) {
Log.w(TAG, "waiting for " + clazz.getName());
new ActivityMonitor(clazz.getName(), null, true).waitForActivityWithTimeout(10000);
}
}

View File

@@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.database;
import org.thoughtcrime.securesms.TextSecureTestCase;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThat;
public class CanonicalAddressDatabaseTest extends TextSecureTestCase {
private static final String AMBIGUOUS_NUMBER = "222-3333";

View File

@@ -1,13 +1,11 @@
package org.thoughtcrime.securesms.jobs;
import android.test.AndroidTestCase;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.thoughtcrime.securesms.TextSecureTestCase;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.whispersystems.textsecure.api.TextSecureMessageSender;
import org.whispersystems.textsecure.api.push.PushAddress;
import org.whispersystems.textsecure.api.push.TextSecureAddress;
import org.whispersystems.textsecure.api.push.exceptions.NotFoundException;
import org.whispersystems.textsecure.api.push.exceptions.PushNetworkException;
@@ -38,7 +36,7 @@ public class DeliveryReceiptJobTest extends TextSecureTestCase {
deliveryReceiptJob.onRun();
ArgumentCaptor<PushAddress> captor = ArgumentCaptor.forClass(PushAddress.class);
ArgumentCaptor<TextSecureAddress> captor = ArgumentCaptor.forClass(TextSecureAddress.class);
verify(textSecureMessageSender).sendDeliveryReceipt(captor.capture(), eq(timestamp));
assertTrue(captor.getValue().getRelay().equals("foo"));
@@ -51,7 +49,7 @@ public class DeliveryReceiptJobTest extends TextSecureTestCase {
Mockito.doThrow(new PushNetworkException("network error"))
.when(textSecureMessageSender)
.sendDeliveryReceipt(any(PushAddress.class), eq(timestamp));
.sendDeliveryReceipt(any(TextSecureAddress.class), eq(timestamp));
DeliveryReceiptJob deliveryReceiptJob = new DeliveryReceiptJob(getContext(),
@@ -70,7 +68,7 @@ public class DeliveryReceiptJobTest extends TextSecureTestCase {
Mockito.doThrow(new NotFoundException("not found"))
.when(textSecureMessageSender)
.sendDeliveryReceipt(any(PushAddress.class), eq(timestamp));
.sendDeliveryReceipt(any(TextSecureAddress.class), eq(timestamp));
try {
deliveryReceiptJob.onRun();

View File

@@ -7,7 +7,8 @@ import junit.framework.AssertionFailedError;
import org.thoughtcrime.securesms.TextSecureTestCase;
import org.whispersystems.textsecure.api.util.InvalidNumberException;
import org.whispersystems.textsecure.api.util.PhoneNumberFormatter;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThat;
public class PhoneNumberFormatterTest extends TextSecureTestCase {
private static final String LOCAL_NUMBER = "+15555555555";

View File

@@ -1,9 +0,0 @@
package org.thoughtcrime.securesms.util;
import android.test.AndroidTestCase;
import static org.fest.assertions.api.Assertions.assertThat;
public class UtilTest extends AndroidTestCase {
}

48
artwork/ic_contact.svg Normal file
View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg2"
sodipodi:docname="ic_contact.svg"
inkscape:version="0.91pre3 r13670"
x="0px"
y="0px"
width="48px"
height="48px"
viewBox="0 0 48 48"
enable-background="new 0 0 48 48"
xml:space="preserve"><metadata
id="metadata8"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs6" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="798"
inkscape:window-height="480"
id="namedview4"
showgrid="false"
inkscape:zoom="4.9166667"
inkscape:cx="24"
inkscape:cy="24"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><path
id="path4"
d="M38,6H10c-2.21,0-4,1.79-4,4v28c0,2.21,1.79,4,4,4h28c2.21,0,4-1.79,4-4V10C42,7.79,40.21,6,38,6z M24,12 c3.31,0,6,2.69,6,6c0,3.32-2.69,6-6,6s-6-2.68-6-6C18,14.69,20.69,12,24,12z M36,36H12v-2c0-4,8-6.2,12-6.2S36,30,36,34V36z"
style="opacity:1;fill:#000000" /></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

BIN
artwork/logo-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -5,13 +5,12 @@ buildscript {
}
}
dependencies {
classpath 'com.android.tools.build:gradle:1.0.0'
classpath 'com.android.tools.build:gradle:1.0.1'
classpath files('libs/gradle-witness.jar')
}
}
apply plugin: 'com.android.application'
apply from: 'strip_play_services.gradle'
apply plugin: 'witness'
repositories {
@@ -21,73 +20,100 @@ repositories {
maven {
url "https://raw.github.com/whispersystems/maven/master/preferencefragment/releases/"
}
maven {
url "https://raw.github.com/whispersystems/maven/master/gson/releases/"
}
maven {
url "https://raw.github.com/whispersystems/maven/master/smil/releases/"
}
maven {
url "https://raw.github.com/whispersystems/maven/master/shortcutbadger/releases/"
}
jcenter()
mavenLocal()
}
dependencies {
compile 'me.leolin:ShortcutBadger:1.0.2-WS2'
compile 'se.emilsjolander:stickylistheaders:2.2.0'
compile 'com.google.android.gms:play-services:6.1.71'
compile 'com.google.android.gms:play-services-base:6.5.87'
compile 'com.astuetz:pagerslidingtabstrip:1.0.1'
compile 'org.w3c:smil:1.0.0'
compile 'org.apache.httpcomponents:httpclient-android:4.3.5'
compile 'com.github.chrisbanes.photoview:library:1.2.3'
compile 'com.android.support:appcompat-v7:20.0.0'
compile 'com.madgag.spongycastle:prov:1.51.0.0'
compile 'com.makeramen:roundedimageview:1.5.0'
compile 'com.afollestad:material-dialogs:0.6.1.5'
compile 'com.soundcloud.android:android-crop:0.9.10@aar'
compile 'com.android.support:appcompat-v7:21.0.3'
compile 'com.android.support:recyclerview-v7:21.0.3'
compile 'com.melnykov:floatingactionbutton:1.1.0'
compile 'com.google.zxing:android-integration:3.1.0'
compile ('com.android.support:support-v4-preferencefragment:1.0.0@aar'){
exclude module: 'support-v4'
}
compile 'com.squareup.dagger:dagger:1.2.2'
compile ("com.doomonafireball.betterpickers:library:1.5.2") {
compile ("com.doomonafireball.betterpickers:library:1.5.3") {
exclude group: 'com.android.support', module: 'support-v4'
}
compile 'com.madgag.spongycastle:prov:1.51.0.0'
provided 'com.squareup.dagger:dagger-compiler:1.2.2'
compile 'org.whispersystems:jobmanager:0.10.0'
compile 'org.whispersystems:libpastelog:1.0.2'
compile 'org.whispersystems:libpastelog:1.0.4'
compile 'org.whispersystems:textsecure-android:1.1.0'
androidTestCompile 'com.squareup:fest-android:1.0.8'
androidTestCompile 'com.google.dexmaker:dexmaker:1.1'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.1'
androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
compile project(':libtextsecure')
androidTestCompile ('org.assertj:assertj-core:1.7.1') {
exclude group: 'org.hamcrest', module: 'hamcrest-core'
}
androidTestCompile ('com.squareup.assertj:assertj-android:1.0.0') {
exclude group: 'org.hamcrest', module: 'hamcrest-core'
}
androidTestCompile ('com.android.support.test.espresso:espresso-core:2.0') {
exclude group: 'javax.inject'
}
androidTestCompile 'com.android.support.test:testing-support-lib:0.1'
}
dependencyVerification {
verify = [
'me.leolin:ShortcutBadger:027977c718035e5997035e04e05152d6c72d94df645e8b7099a274ada722bd14',
'se.emilsjolander:stickylistheaders:89146b46c96fea0e40200474a2625cda10fe94891e4128f53cdb42375091b9b6',
'com.google.android.gms:play-services:32e7d1834a1cf8fa4b17e8d359db580c286e26c1eefbf84fdb9996eac8d74919',
'com.google.android.gms:play-services-base:832cb6b3130e871db6a412c4ab585656dbcc5e7948101f190186757785703f75',
'com.astuetz:pagerslidingtabstrip:f1641396732c7132a7abb837e482e5ee2b0ebb8d10813fc52bbaec2c15c184c2',
'org.w3c:smil:085dc40f2bb249651578bfa07499fd08b16ad0886dbe2c4078586a408da62f9b',
'org.apache.httpcomponents:httpclient-android:6f56466a9bd0d42934b90bfbfe9977a8b654c058bf44a12bdc2877c4e1f033f1',
'com.github.chrisbanes.photoview:library:8b5344e206f125e7ba9d684008f36c4992d03853c57e5814125f88496126e3cc',
'com.android.support:appcompat-v7:736f576ab0b68d27bdf18b1e7931566e6d8254b73965175313e87f8866b91547',
'com.madgag.spongycastle:prov:b8c3fec3a59aac1aa04ccf4dad7179351e54ef7672f53f508151b614c131398a',
'com.makeramen:roundedimageview:7dda2e78c406760e5c356ccce59b0df46b5b171cf18abb891998594405021548',
'com.afollestad:material-dialogs:ccb013e6572c86cfcca433855cf0dbfbff9b5e7bb9d1f504b761a6bc6f467b60',
'com.soundcloud.android:android-crop:ffd4b973cf6e97f7d64118a0dc088df50e9066fd5634fe6911dd0c0c5d346177',
'com.android.support:appcompat-v7:5dbeb5316d0a6027d646ae552804c3baa5e3bd53f7f33db50904d51505c8a0e5',
'com.android.support:recyclerview-v7:e525ad3f33c84bb12b73d2dc975b55364a53f0f2d0697e043efba59ba73e22d2',
'com.melnykov:floatingactionbutton:0679ad9f7d61eb7aeab91e8dc56358cdedd5b1c1b9c48464499ffa05c40d3985',
'com.google.zxing:android-integration:89e56aadf1164bd71e57949163c53abf90af368b51669c0d4a47a163335f95c4',
'com.android.support:support-v4-preferencefragment:5470f5872514a6226fa1fc6f4e000991f38805691c534cf0bd2778911fc773ad',
'com.squareup.dagger:dagger:789aca24537022e49f91fc6444078d9de8f1dd99e1bfb090f18491b186967883',
'com.doomonafireball.betterpickers:library:1c2dd66b0f4555e9e68427b28eed8826b7cd2e7074b8c038c88836a6aa9abe64',
'org.whispersystems:libpastelog:9798b3c93a91082c2c68542ce5b5c182e18556aebdcb7c8cebbd89eb48ac4047',
'com.doomonafireball.betterpickers:library:132ecd685c95a99e7377c4e27bfadbb2d7ed0bea995944060cd62d4369fdaf3d',
'com.madgag.spongycastle:prov:b8c3fec3a59aac1aa04ccf4dad7179351e54ef7672f53f508151b614c131398a',
'org.whispersystems:jobmanager:01f35586c43aa3806f1c18d3d6a5a972def98103ba1a5a9ca3eec08d15f974b7',
'com.android.support:support-v4:81f2b1c2c94efd5a4ec7fcd97b6cdcd00e87a933905c5c86103c7319eb024572',
'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f',
'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
'org.whispersystems:libpastelog:3ccf00fe1597eb8ca1e5de99b17fc225387a1b80b5bbc00ec1bc4d4f3ea9cdde',
'org.whispersystems:textsecure-android:d2a8630a796222459163effba01456a322f6f15a63668393fb1203f89180a21a',
'com.android.support:support-annotations:fdee2354787ef66b268e75958de3f7f6c4f8f325510a6dac9f49c929f83a63de',
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
'org.whispersystems:gson:08f4f7498455d1539c9233e5aac18e9b1805815ef29221572996508eb512fe51',
'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74',
'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f',
'org.whispersystems:axolotl-android:68e81fcb2d54626925574740263b8c2501e96f557dabedeeba7fa414deccef99',
'org.whispersystems:textsecure-java:f552a3cfee5ba111057c700cbe5208a4c7fe31488d17ddf49f01dd974026b00d',
'org.whispersystems:axolotl-java:4af04115903cbc5581b32ce82d0518a704dd039f48138d26eb518fa3a2bfee91',
'org.whispersystems:curve25519-android:3c29a4131a69b0d16baaa3d707678deb39602c3a3ffd75805ce7f9db252e5d0d',
'com.googlecode.libphonenumber:libphonenumber:eba17eae81dd622ea89a00a3a8c025b2f25d342e0d9644c5b62e16f15687c3ab',
'org.whispersystems:axolotl-android:7617256d05aaecd7b5475cd55e42773d7079167a22ca48512bcb0f84f8473cc9',
'com.android.support:support-annotations:1aa96ef0cc4a445bfc2f93ccf762305bc57fa107b12afe9d11f3863ae8a11036',
'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74',
'com.squareup.okhttp:okhttp:89b7f63e2e5b6c410266abc14f50fe52ea8d2d8a57260829e499b1cd9f0e61af',
'com.fasterxml.jackson.core:jackson-databind:835097bcdd11f5bc8a08378c70d4c8054dfa4b911691cc2752063c75534d198d',
'org.whispersystems:curve25519-java:9ccef8f5aba05d9942336f023c589d6278b4f9135bdc34a7bade1f4e7ad65fa3',
'com.squareup.okio:okio:5e1098bd3fdee4c3347f5ab815b40ba851e4ab1b348c5e49a5b0362f0ce6e978',
'com.fasterxml.jackson.core:jackson-annotations:0ca408c24202a7626ec8b861e99d85eca5e38b73311dd6dd12e3e9deecc3fe94',
'com.fasterxml.jackson.core:jackson-core:cbf4604784b4de226262845447a1ad3bb38a6728cebe86562e2c5afada8be2c0',
'com.android.support:support-v4:703572d3015a088cc5604b7e38885af3d307c829d0c5ceaf8654ff41c71cd160',
]
}
@@ -95,10 +121,15 @@ android {
compileSdkVersion 21
buildToolsVersion '21.1.2'
dexOptions {
javaMaxHeapSize "4g"
}
defaultConfig {
minSdkVersion 9
targetSdkVersion 19
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
buildConfigField "long", "BUILD_TIMESTAMP", System.currentTimeMillis() + "L"
}
@@ -107,16 +138,25 @@ android {
targetCompatibility JavaVersion.VERSION_1_7
}
packagingOptions {
exclude 'LICENSE.txt'
exclude 'LICENSE'
exclude 'NOTICE'
exclude 'asm-license.txt'
}
signingConfigs {
release
}
buildTypes {
debug {
minifyEnabled false
minifyEnabled true
proguardFiles 'proguard.cfg'
}
release {
minifyEnabled false
minifyEnabled true
proguardFiles 'proguard.cfg'
signingConfig signingConfigs.release
}
}
@@ -139,6 +179,10 @@ android {
}
}
packagingOptions {
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
}
lintOptions {
abortOnError false

View File

@@ -1,54 +0,0 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.0.0'
}
}
apply plugin: 'com.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.5.0'
compile 'com.googlecode.libphonenumber:libphonenumber:6.1'
compile 'org.whispersystems:gson:2.2.4'
compile 'org.whispersystems:axolotl-android:1.0.0'
}
android {
compileSdkVersion 21
buildToolsVersion '21.1.2'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
}
tasks.whenTaskAdded { task ->
if (task.name.equals("lint")) {
task.enabled = false
}
}
version '0.1'
group 'org.whispersystems.textsecure'
archivesBaseName = 'libtextsecure'
uploadArchives {
repositories {
mavenDeployer {
repository(url: mavenLocal().getUrl())
}
}
}

View File

@@ -1,53 +0,0 @@
package textsecure;
option java_package = "org.whispersystems.textsecure.internal.push";
option java_outer_classname = "PushMessageProtos";
message IncomingPushMessageSignal {
enum Type {
UNKNOWN = 0;
CIPHERTEXT = 1;
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
PLAINTEXT = 4;
RECEIPT = 5;
}
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;
}

View File

@@ -1,3 +0,0 @@
all:
protoc --java_out=../src/main/java/ IncomingPushMessageSignal.proto

View File

@@ -1,35 +0,0 @@
package org.whispersystems.textsecure.push;
import android.test.AndroidTestCase;
import org.whispersystems.textsecure.internal.push.PushTransportDetails;
public class PushTransportDetailsTest extends AndroidTestCase {
private final PushTransportDetails transportV2 = new PushTransportDetails(2);
private final PushTransportDetails transportV3 = new PushTransportDetails(3);
public void testV3Padding() {
for (int i=0;i<159;i++) {
byte[] message = new byte[i];
assertEquals(transportV3.getPaddedMessageBody(message).length, 159);
}
for (int i=159;i<319;i++) {
byte[] message = new byte[i];
assertEquals(transportV3.getPaddedMessageBody(message).length, 319);
}
for (int i=319;i<479;i++) {
byte[] message = new byte[i];
assertEquals(transportV3.getPaddedMessageBody(message).length, 479);
}
}
public void testV2Padding() {
for (int i=0;i<480;i++) {
byte[] message = new byte[i];
assertTrue(transportV2.getPaddedMessageBody(message).length == message.length);
}
}
}

View File

@@ -1,8 +0,0 @@
<?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>

View File

@@ -1,97 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.push.ContactTokenDetails;
import org.whispersystems.textsecure.api.push.TrustStore;
import org.whispersystems.textsecure.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.textsecure.api.push.exceptions.PushNetworkException;
import org.whispersystems.textsecure.internal.push.PushServiceSocket;
import org.whispersystems.textsecure.api.push.SignedPreKeyEntity;
import java.io.IOException;
import java.util.List;
import java.util.Set;
public class TextSecureAccountManager {
private final PushServiceSocket pushServiceSocket;
public TextSecureAccountManager(String url, TrustStore trustStore,
String user, String password)
{
this.pushServiceSocket = new PushServiceSocket(url, trustStore, user, password);
}
public void setGcmId(Optional<String> gcmRegistrationId) throws IOException {
if (gcmRegistrationId.isPresent()) {
this.pushServiceSocket.registerGcmId(gcmRegistrationId.get());
} else {
this.pushServiceSocket.unregisterGcmId();
}
}
public void requestSmsVerificationCode() throws IOException {
this.pushServiceSocket.createAccount(false);
}
public void requestVoiceVerificationCode() throws IOException {
this.pushServiceSocket.createAccount(true);
}
public void verifyAccount(String verificationCode, String signalingKey,
boolean supportsSms, int axolotlRegistrationId)
throws IOException
{
this.pushServiceSocket.verifyAccount(verificationCode, signalingKey,
supportsSms, axolotlRegistrationId);
}
public void setPreKeys(IdentityKey identityKey, PreKeyRecord lastResortKey,
SignedPreKeyRecord signedPreKey, List<PreKeyRecord> oneTimePreKeys)
throws IOException
{
this.pushServiceSocket.registerPreKeys(identityKey, lastResortKey, signedPreKey, oneTimePreKeys);
}
public int getPreKeysCount() throws IOException {
return this.pushServiceSocket.getAvailablePreKeys();
}
public void setSignedPreKey(SignedPreKeyRecord signedPreKey) throws IOException {
this.pushServiceSocket.setCurrentSignedPreKey(signedPreKey);
}
public SignedPreKeyEntity getSignedPreKey() throws IOException {
return this.pushServiceSocket.getCurrentSignedPreKey();
}
public Optional<ContactTokenDetails> getContact(String contactToken) throws IOException {
return Optional.fromNullable(this.pushServiceSocket.getContactTokenDetails(contactToken));
}
public List<ContactTokenDetails> getContacts(Set<String> contactTokens)
throws IOException
{
return this.pushServiceSocket.retrieveDirectory(contactTokens);
}
}

View File

@@ -1,46 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer;
import org.whispersystems.textsecure.api.push.TrustStore;
import org.whispersystems.textsecure.internal.push.PushServiceSocket;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
public class TextSecureMessageReceiver {
private final PushServiceSocket socket;
public TextSecureMessageReceiver(String url, TrustStore trustStore,
String user, String password)
{
this.socket = new PushServiceSocket(url, trustStore, user, password);
}
public InputStream retrieveAttachment(TextSecureAttachmentPointer pointer, File destination)
throws IOException, InvalidMessageException
{
socket.retrieveAttachment(pointer.getRelay().orNull(), pointer.getId(), destination);
return new AttachmentCipherInputStream(destination, pointer.getKey());
}
}

View File

@@ -1,323 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api;
import android.util.Log;
import com.google.protobuf.ByteString;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.SessionBuilder;
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.libaxolotl.state.PreKeyBundle;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.crypto.TextSecureCipher;
import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException;
import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentStream;
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
import org.whispersystems.textsecure.api.push.PushAddress;
import org.whispersystems.textsecure.api.push.TrustStore;
import org.whispersystems.textsecure.internal.push.MismatchedDevices;
import org.whispersystems.textsecure.internal.push.OutgoingPushMessage;
import org.whispersystems.textsecure.internal.push.OutgoingPushMessageList;
import org.whispersystems.textsecure.internal.push.PushAttachmentData;
import org.whispersystems.textsecure.internal.push.PushBody;
import org.whispersystems.textsecure.internal.push.PushServiceSocket;
import org.whispersystems.textsecure.internal.push.StaleDevices;
import org.whispersystems.textsecure.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.textsecure.internal.push.exceptions.MismatchedDevicesException;
import org.whispersystems.textsecure.internal.push.exceptions.StaleDevicesException;
import org.whispersystems.textsecure.internal.util.Util;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.IncomingPushMessageSignal.Type;
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent;
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.AttachmentPointer;
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.GroupContext;
public class TextSecureMessageSender {
private static final String TAG = TextSecureMessageSender.class.getSimpleName();
private final PushServiceSocket socket;
private final AxolotlStore store;
private final Optional<EventListener> eventListener;
public TextSecureMessageSender(String url, TrustStore trustStore,
String user, String password, AxolotlStore store,
Optional<EventListener> eventListener)
{
this.socket = new PushServiceSocket(url, trustStore, user, password);
this.store = store;
this.eventListener = eventListener;
}
public void sendDeliveryReceipt(PushAddress recipient, long messageId) throws IOException {
this.socket.sendReceipt(recipient.getNumber(), messageId, recipient.getRelay());
}
public void sendMessage(PushAddress recipient, TextSecureMessage message)
throws UntrustedIdentityException, IOException
{
byte[] content = createMessageContent(message);
sendMessage(recipient, message.getTimestamp(), content);
if (message.isEndSession()) {
store.deleteAllSessions(recipient.getRecipientId());
if (eventListener.isPresent()) {
eventListener.get().onSecurityEvent(recipient.getRecipientId());
}
}
}
public void sendMessage(List<PushAddress> recipients, TextSecureMessage message)
throws IOException, EncapsulatedExceptions
{
byte[] content = createMessageContent(message);
sendMessage(recipients, message.getTimestamp(), content);
}
private byte[] createMessageContent(TextSecureMessage message) throws IOException {
PushMessageContent.Builder builder = PushMessageContent.newBuilder();
List<AttachmentPointer> pointers = createAttachmentPointers(message.getAttachments());
if (!pointers.isEmpty()) {
builder.addAllAttachments(pointers);
}
if (message.getBody().isPresent()) {
builder.setBody(message.getBody().get());
}
if (message.getGroupInfo().isPresent()) {
builder.setGroup(createGroupContent(message.getGroupInfo().get()));
}
if (message.isEndSession()) {
builder.setFlags(PushMessageContent.Flags.END_SESSION_VALUE);
}
return builder.build().toByteArray();
}
private GroupContext createGroupContent(TextSecureGroup group) throws IOException {
GroupContext.Builder builder = GroupContext.newBuilder();
builder.setId(ByteString.copyFrom(group.getGroupId()));
if (group.getType() != TextSecureGroup.Type.DELIVER) {
if (group.getType() == TextSecureGroup.Type.UPDATE) builder.setType(GroupContext.Type.UPDATE);
else if (group.getType() == TextSecureGroup.Type.QUIT) builder.setType(GroupContext.Type.QUIT);
else throw new AssertionError("Unknown type: " + group.getType());
if (group.getName().isPresent()) builder.setName(group.getName().get());
if (group.getMembers().isPresent()) builder.addAllMembers(group.getMembers().get());
if (group.getAvatar().isPresent() && group.getAvatar().get().isStream()) {
AttachmentPointer pointer = createAttachmentPointer(group.getAvatar().get().asStream());
builder.setAvatar(pointer);
}
} else {
builder.setType(GroupContext.Type.DELIVER);
}
return builder.build();
}
private void sendMessage(List<PushAddress> recipients, long timestamp, byte[] content)
throws IOException, EncapsulatedExceptions
{
List<UntrustedIdentityException> untrustedIdentities = new LinkedList<>();
List<UnregisteredUserException> unregisteredUsers = new LinkedList<>();
for (PushAddress recipient : recipients) {
try {
sendMessage(recipient, timestamp, content);
} catch (UntrustedIdentityException e) {
Log.w(TAG, e);
untrustedIdentities.add(e);
} catch (UnregisteredUserException e) {
Log.w(TAG, e);
unregisteredUsers.add(e);
}
}
if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty()) {
throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers);
}
}
private void sendMessage(PushAddress recipient, long timestamp, byte[] content)
throws UntrustedIdentityException, IOException
{
for (int i=0;i<3;i++) {
try {
OutgoingPushMessageList messages = getEncryptedMessages(socket, recipient, timestamp, content);
socket.sendMessage(messages);
return;
} catch (MismatchedDevicesException mde) {
Log.w(TAG, mde);
handleMismatchedDevices(socket, recipient, mde.getMismatchedDevices());
} catch (StaleDevicesException ste) {
Log.w(TAG, ste);
handleStaleDevices(recipient, ste.getStaleDevices());
}
}
}
private List<AttachmentPointer> createAttachmentPointers(Optional<List<TextSecureAttachment>> attachments) throws IOException {
List<AttachmentPointer> pointers = new LinkedList<>();
if (!attachments.isPresent() || attachments.get().isEmpty()) {
Log.w(TAG, "No attachments present...");
return pointers;
}
for (TextSecureAttachment attachment : attachments.get()) {
if (attachment.isStream()) {
Log.w(TAG, "Found attachment, creating pointer...");
pointers.add(createAttachmentPointer(attachment.asStream()));
}
}
return pointers;
}
private AttachmentPointer createAttachmentPointer(TextSecureAttachmentStream attachment)
throws IOException
{
byte[] attachmentKey = Util.getSecretBytes(64);
PushAttachmentData attachmentData = new PushAttachmentData(attachment.getContentType(),
attachment.getInputStream(),
attachment.getLength(),
attachmentKey);
long attachmentId = socket.sendAttachment(attachmentData);
return AttachmentPointer.newBuilder()
.setContentType(attachment.getContentType())
.setId(attachmentId)
.setKey(ByteString.copyFrom(attachmentKey))
.build();
}
private OutgoingPushMessageList getEncryptedMessages(PushServiceSocket socket,
PushAddress recipient,
long timestamp,
byte[] plaintext)
throws IOException, UntrustedIdentityException
{
PushBody masterBody = getEncryptedMessage(socket, recipient, plaintext);
List<OutgoingPushMessage> messages = new LinkedList<>();
messages.add(new OutgoingPushMessage(recipient, masterBody));
for (int deviceId : store.getSubDeviceSessions(recipient.getRecipientId())) {
PushAddress device = new PushAddress(recipient.getRecipientId(), recipient.getNumber(), deviceId, recipient.getRelay());
PushBody body = getEncryptedMessage(socket, device, plaintext);
messages.add(new OutgoingPushMessage(device, body));
}
return new OutgoingPushMessageList(recipient.getNumber(), timestamp, recipient.getRelay(), messages);
}
private PushBody getEncryptedMessage(PushServiceSocket socket, PushAddress recipient, byte[] plaintext)
throws IOException, UntrustedIdentityException
{
if (!store.containsSession(recipient.getRecipientId(), recipient.getDeviceId())) {
try {
List<PreKeyBundle> preKeys = socket.getPreKeys(recipient);
for (PreKeyBundle preKey : preKeys) {
try {
SessionBuilder sessionBuilder = new SessionBuilder(store, recipient.getRecipientId(), recipient.getDeviceId());
sessionBuilder.process(preKey);
} catch (org.whispersystems.libaxolotl.UntrustedIdentityException e) {
throw new UntrustedIdentityException("Untrusted identity key!", recipient.getNumber(), preKey.getIdentityKey());
}
}
if (eventListener.isPresent()) {
eventListener.get().onSecurityEvent(recipient.getRecipientId());
}
} catch (InvalidKeyException e) {
throw new IOException(e);
}
}
TextSecureCipher cipher = new TextSecureCipher(store, recipient.getRecipientId(), recipient.getDeviceId());
CiphertextMessage message = cipher.encrypt(plaintext);
int remoteRegistrationId = cipher.getRemoteRegistrationId();
if (message.getType() == CiphertextMessage.PREKEY_TYPE) {
return new PushBody(Type.PREKEY_BUNDLE_VALUE, remoteRegistrationId, message.serialize());
} else if (message.getType() == CiphertextMessage.WHISPER_TYPE) {
return new PushBody(Type.CIPHERTEXT_VALUE, remoteRegistrationId, message.serialize());
} else {
throw new AssertionError("Unknown ciphertext type: " + message.getType());
}
}
private void handleMismatchedDevices(PushServiceSocket socket, PushAddress recipient,
MismatchedDevices mismatchedDevices)
throws IOException, UntrustedIdentityException
{
try {
for (int extraDeviceId : mismatchedDevices.getExtraDevices()) {
store.deleteSession(recipient.getRecipientId(), extraDeviceId);
}
for (int missingDeviceId : mismatchedDevices.getMissingDevices()) {
PushAddress device = new PushAddress(recipient.getRecipientId(), recipient.getNumber(),
missingDeviceId, recipient.getRelay());
PreKeyBundle preKey = socket.getPreKey(device);
try {
SessionBuilder sessionBuilder = new SessionBuilder(store, device.getRecipientId(), device.getDeviceId());
sessionBuilder.process(preKey);
} catch (org.whispersystems.libaxolotl.UntrustedIdentityException e) {
throw new UntrustedIdentityException("Untrusted identity key!", recipient.getNumber(), preKey.getIdentityKey());
}
}
} catch (InvalidKeyException e) {
throw new IOException(e);
}
}
private void handleStaleDevices(PushAddress recipient, StaleDevices staleDevices) {
long recipientId = recipient.getRecipientId();
for (int staleDeviceId : staleDevices.getStaleDevices()) {
store.deleteSession(recipientId, staleDeviceId);
}
}
public static interface EventListener {
public void onSecurityEvent(long recipientId);
}
}

View File

@@ -1,220 +0,0 @@
/**
* Copyright (C) 2013-2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.crypto;
import org.whispersystems.libaxolotl.InvalidMacException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.internal.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 static final int CIPHER_KEY_SIZE = 32;
private static final int MAC_KEY_SIZE = 32;
private Cipher cipher;
private boolean done;
private long totalDataSize;
private long totalRead;
private byte[] overflowBuffer;
public AttachmentCipherInputStream(File file, byte[] combinedKeyMaterial)
throws IOException, InvalidMessageException
{
super(file);
try {
byte[][] parts = Util.split(combinedKeyMaterial, CIPHER_KEY_SIZE, MAC_KEY_SIZE);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(parts[1], "HmacSHA256"));
if (file.length() <= BLOCK_SIZE + mac.getMacLength()) {
throw new InvalidMessageException("Message shorter than crypto overhead!");
}
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 | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
} catch (InvalidMacException e) {
throw new InvalidMessageException(e);
}
}
@Override
public int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
@Override
public int read(byte[] buffer, int offset, int length) throws IOException {
if (totalRead != totalDataSize) return readIncremental(buffer, offset, length);
else if (!done) return readFinal(buffer, offset, length);
else return -1;
}
@Override
public boolean markSupported() {
return false;
}
@Override
public long skip(long byteCount) throws IOException {
long skipped = 0L;
while (skipped < byteCount) {
byte[] buf = new byte[Math.min(4096, (int)(byteCount-skipped))];
int read = read(buf);
skipped += read;
}
return skipped;
}
private int readFinal(byte[] buffer, int offset, int length) throws IOException {
try {
int flourish = cipher.doFinal(buffer, offset);
done = true;
return flourish;
} catch (IllegalBlockSizeException | BadPaddingException | ShortBufferException e) {
throw new IOException(e);
}
}
private int readIncremental(byte[] buffer, int offset, int length) throws IOException {
int readLength = 0;
if (null != overflowBuffer) {
if (overflowBuffer.length > length) {
System.arraycopy(overflowBuffer, 0, buffer, offset, length);
overflowBuffer = Arrays.copyOfRange(overflowBuffer, length, overflowBuffer.length);
return length;
} else if (overflowBuffer.length == length) {
System.arraycopy(overflowBuffer, 0, buffer, offset, length);
overflowBuffer = null;
return length;
} else {
System.arraycopy(overflowBuffer, 0, buffer, offset, overflowBuffer.length);
readLength += overflowBuffer.length;
offset += readLength;
length -= readLength;
overflowBuffer = null;
}
}
if (length + totalRead > totalDataSize)
length = (int)(totalDataSize - totalRead);
byte[] internalBuffer = new byte[length];
int read = super.read(internalBuffer, 0, internalBuffer.length <= cipher.getBlockSize() ? internalBuffer.length : internalBuffer.length - cipher.getBlockSize());
totalRead += read;
try {
int outputLen = cipher.getOutputSize(read);
if (outputLen <= length) {
readLength += cipher.update(internalBuffer, 0, read, buffer, offset);
return readLength;
}
byte[] transientBuffer = new byte[outputLen];
outputLen = cipher.update(internalBuffer, 0, read, transientBuffer, 0);
if (outputLen <= length) {
System.arraycopy(transientBuffer, 0, buffer, offset, outputLen);
readLength += outputLen;
} else {
System.arraycopy(transientBuffer, 0, buffer, offset, length);
overflowBuffer = Arrays.copyOfRange(transientBuffer, length, outputLen);
readLength += length;
}
return readLength;
} catch (ShortBufferException e) {
throw new AssertionError(e);
}
}
private 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;
}
}
}

View File

@@ -1,121 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.crypto;
import org.whispersystems.textsecure.internal.util.Util;
import java.io.IOException;
import java.io.OutputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
public class AttachmentCipherOutputStream extends OutputStream {
private final Cipher cipher;
private final Mac mac;
private final OutputStream outputStream;
private long ciphertextLength = 0;
public AttachmentCipherOutputStream(byte[] combinedKeyMaterial,
OutputStream outputStream)
throws IOException
{
try {
this.outputStream = outputStream;
this.cipher = initializeCipher();
this.mac = initializeMac();
byte[][] keyParts = Util.split(combinedKeyMaterial, 32, 32);
this.cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(keyParts[0], "AES"));
this.mac.init(new SecretKeySpec(keyParts[1], "HmacSHA256"));
mac.update(cipher.getIV());
outputStream.write(cipher.getIV());
ciphertextLength += cipher.getIV().length;
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
@Override
public void write(byte[] buffer) throws IOException {
write(buffer, 0, buffer.length);
}
@Override
public void write(byte[] buffer, int offset, int length) throws IOException {
byte[] ciphertext = cipher.update(buffer, offset, length);
if (ciphertext != null) {
mac.update(ciphertext);
outputStream.write(ciphertext);
ciphertextLength += ciphertext.length;
}
}
@Override
public void write(int b) {
throw new AssertionError("NYI");
}
@Override
public void flush() throws IOException {
try {
byte[] ciphertext = cipher.doFinal();
byte[] auth = mac.doFinal(ciphertext);
outputStream.write(ciphertext);
outputStream.write(auth);
ciphertextLength += ciphertext.length;
ciphertextLength += auth.length;
outputStream.flush();
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
public static long getCiphertextLength(long plaintextLength) {
return 16 + (((plaintextLength / 16) +1) * 16) + 32;
}
private Mac initializeMac() {
try {
return Mac.getInstance("HmacSHA256");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private Cipher initializeCipher() {
try {
return Cipher.getInstance("AES/CBC/PKCS5Padding");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -1,148 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.crypto;
import com.google.protobuf.InvalidProtocolBufferException;
import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.NoSessionException;
import org.whispersystems.libaxolotl.SessionCipher;
import org.whispersystems.libaxolotl.UntrustedIdentityException;
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
import org.whispersystems.libaxolotl.protocol.WhisperMessage;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer;
import org.whispersystems.textsecure.api.messages.TextSecureEnvelope;
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
import org.whispersystems.textsecure.internal.push.PushTransportDetails;
import java.util.LinkedList;
import java.util.List;
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent;
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.GroupContext.Type.DELIVER;
public class TextSecureCipher {
private final SessionCipher sessionCipher;
public TextSecureCipher(AxolotlStore axolotlStore, long recipientId, int deviceId) {
this.sessionCipher = new SessionCipher(axolotlStore, recipientId, deviceId);
}
public CiphertextMessage encrypt(byte[] unpaddedMessage) {
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion());
return sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
}
public TextSecureMessage decrypt(TextSecureEnvelope envelope)
throws InvalidVersionException, InvalidMessageException, InvalidKeyException,
DuplicateMessageException, InvalidKeyIdException, UntrustedIdentityException,
LegacyMessageException, NoSessionException
{
try {
byte[] paddedMessage;
if (envelope.isPreKeyWhisperMessage()) {
paddedMessage = sessionCipher.decrypt(new PreKeyWhisperMessage(envelope.getMessage()));
} else if (envelope.isWhisperMessage()) {
paddedMessage = sessionCipher.decrypt(new WhisperMessage(envelope.getMessage()));
} else if (envelope.isPlaintext()) {
paddedMessage = envelope.getMessage();
} else {
throw new InvalidMessageException("Unknown type: " + envelope.getType());
}
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion());
PushMessageContent content = PushMessageContent.parseFrom(transportDetails.getStrippedPaddingMessageBody(paddedMessage));
return createTextSecureMessage(envelope, content);
} catch (InvalidProtocolBufferException e) {
throw new InvalidMessageException(e);
}
}
public int getRemoteRegistrationId() {
return sessionCipher.getRemoteRegistrationId();
}
private TextSecureMessage createTextSecureMessage(TextSecureEnvelope envelope, PushMessageContent content) {
TextSecureGroup groupInfo = createGroupInfo(envelope, content);
List<TextSecureAttachment> attachments = new LinkedList<>();
boolean endSession = ((content.getFlags() & PushMessageContent.Flags.END_SESSION_VALUE) != 0);
boolean secure = envelope.isWhisperMessage() || envelope.isPreKeyWhisperMessage();
for (PushMessageContent.AttachmentPointer pointer : content.getAttachmentsList()) {
attachments.add(new TextSecureAttachmentPointer(pointer.getId(),
pointer.getContentType(),
pointer.getKey().toByteArray(),
envelope.getRelay()));
}
return new TextSecureMessage(envelope.getTimestamp(), groupInfo, attachments,
content.getBody(), secure, endSession);
}
private TextSecureGroup createGroupInfo(TextSecureEnvelope envelope, PushMessageContent content) {
if (!content.hasGroup()) return null;
TextSecureGroup.Type type;
switch (content.getGroup().getType()) {
case DELIVER: type = TextSecureGroup.Type.DELIVER; break;
case UPDATE: type = TextSecureGroup.Type.UPDATE; break;
case QUIT: type = TextSecureGroup.Type.QUIT; break;
default: type = TextSecureGroup.Type.UNKNOWN; break;
}
if (content.getGroup().getType() != DELIVER) {
String name = null;
List<String> members = null;
TextSecureAttachmentPointer avatar = null;
if (content.getGroup().hasName()) {
name = content.getGroup().getName();
}
if (content.getGroup().getMembersCount() > 0) {
members = content.getGroup().getMembersList();
}
if (content.getGroup().hasAvatar()) {
avatar = new TextSecureAttachmentPointer(content.getGroup().getAvatar().getId(),
content.getGroup().getAvatar().getContentType(),
content.getGroup().getAvatar().getKey().toByteArray(),
envelope.getRelay());
}
return new TextSecureGroup(type, content.getGroup().getId().toByteArray(), name, members, avatar);
}
return new TextSecureGroup(content.getGroup().getId().toByteArray());
}
}

View File

@@ -1,44 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.crypto;
import org.whispersystems.libaxolotl.IdentityKey;
public class UntrustedIdentityException extends Exception {
private final IdentityKey identityKey;
private final String e164number;
public UntrustedIdentityException(String s, String e164number, IdentityKey identityKey) {
super(s);
this.e164number = e164number;
this.identityKey = identityKey;
}
public UntrustedIdentityException(UntrustedIdentityException e) {
this(e.getMessage(), e.getE164Number(), e.getIdentityKey());
}
public IdentityKey getIdentityKey() {
return identityKey;
}
public String getE164Number() {
return e164number;
}
}

View File

@@ -1,41 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.messages;
public abstract class TextSecureAttachment {
private final String contentType;
protected TextSecureAttachment(String contentType) {
this.contentType = contentType;
}
public String getContentType() {
return contentType;
}
public abstract boolean isStream();
public abstract boolean isPointer();
public TextSecureAttachmentStream asStream() {
return (TextSecureAttachmentStream)this;
}
public TextSecureAttachmentPointer asPointer() {
return (TextSecureAttachmentPointer)this;
}
}

View File

@@ -1,55 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.messages;
import org.whispersystems.libaxolotl.util.guava.Optional;
public class TextSecureAttachmentPointer extends TextSecureAttachment {
private final long id;
private final byte[] key;
private final Optional<String> relay;
public TextSecureAttachmentPointer(long id, String contentType, byte[] key, String relay) {
super(contentType);
this.id = id;
this.key = key;
this.relay = Optional.fromNullable(relay);
}
public long getId() {
return id;
}
public byte[] getKey() {
return key;
}
@Override
public boolean isStream() {
return false;
}
@Override
public boolean isPointer() {
return true;
}
public Optional<String> getRelay() {
return relay;
}
}

View File

@@ -1,49 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.messages;
import java.io.InputStream;
public class TextSecureAttachmentStream extends TextSecureAttachment {
private final InputStream inputStream;
private final long length;
public TextSecureAttachmentStream(InputStream inputStream, String contentType, long length) {
super(contentType);
this.inputStream = inputStream;
this.length = length;
}
@Override
public boolean isStream() {
return true;
}
@Override
public boolean isPointer() {
return false;
}
public InputStream getInputStream() {
return inputStream;
}
public long getLength() {
return length;
}
}

View File

@@ -1,193 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.messages;
import android.util.Log;
import com.google.protobuf.ByteString;
import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.textsecure.internal.push.PushMessageProtos.IncomingPushMessageSignal;
import org.whispersystems.textsecure.internal.util.Base64;
import org.whispersystems.textsecure.internal.util.Hex;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class TextSecureEnvelope {
private static final String TAG = TextSecureEnvelope.class.getSimpleName();
private static final int SUPPORTED_VERSION = 1;
private static final int CIPHER_KEY_SIZE = 32;
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 IncomingPushMessageSignal signal;
public TextSecureEnvelope(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);
this.signal = IncomingPushMessageSignal.parseFrom(getPlaintext(ciphertext, cipherKey));
}
public TextSecureEnvelope(int type, String source, int sourceDevice,
String relay, long timestamp, byte[] message)
{
this.signal = IncomingPushMessageSignal.newBuilder()
.setType(IncomingPushMessageSignal.Type.valueOf(type))
.setSource(source)
.setSourceDevice(sourceDevice)
.setRelay(relay)
.setTimestamp(timestamp)
.setMessage(ByteString.copyFrom(message))
.build();
}
public String getSource() {
return signal.getSource();
}
public int getSourceDevice() {
return signal.getSourceDevice();
}
public int getType() {
return signal.getType().getNumber();
}
public String getRelay() {
return signal.getRelay();
}
public long getTimestamp() {
return signal.getTimestamp();
}
public byte[] getMessage() {
return signal.getMessage().toByteArray();
}
public boolean isWhisperMessage() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.CIPHERTEXT_VALUE;
}
public boolean isPreKeyWhisperMessage() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.PREKEY_BUNDLE_VALUE;
}
public boolean isPlaintext() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.PLAINTEXT_VALUE;
}
public boolean isReceipt() {
return signal.getType().getNumber() == IncomingPushMessageSignal.Type.RECEIPT_VALUE;
}
private byte[] getPlaintext(byte[] ciphertext, SecretKeySpec cipherKey) throws IOException {
try {
byte[] ivBytes = new byte[IV_LENGTH];
System.arraycopy(ciphertext, IV_OFFSET, ivBytes, 0, ivBytes.length);
IvParameterSpec iv = new IvParameterSpec(ivBytes);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, cipherKey, iv);
return cipher.doFinal(ciphertext, CIPHERTEXT_OFFSET,
ciphertext.length - VERSION_LENGTH - IV_LENGTH - MAC_SIZE);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
Log.w(TAG, e);
throw new IOException("Bad padding?");
}
}
private void verifyMac(byte[] ciphertext, SecretKeySpec macKey) throws IOException {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(macKey);
if (ciphertext.length < MAC_SIZE + 1)
throw new IOException("Invalid MAC!");
mac.update(ciphertext, 0, ciphertext.length - MAC_SIZE);
byte[] ourMacFull = mac.doFinal();
byte[] ourMacBytes = new byte[MAC_SIZE];
System.arraycopy(ourMacFull, 0, ourMacBytes, 0, ourMacBytes.length);
byte[] theirMacBytes = new byte[MAC_SIZE];
System.arraycopy(ciphertext, ciphertext.length-MAC_SIZE, theirMacBytes, 0, theirMacBytes.length);
Log.w(TAG, "Our MAC: " + Hex.toString(ourMacBytes));
Log.w(TAG, "Thr MAC: " + Hex.toString(theirMacBytes));
if (!Arrays.equals(ourMacBytes, theirMacBytes)) {
throw new IOException("Invalid MAC compare!");
}
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
private SecretKeySpec getCipherKey(String signalingKey) throws IOException {
byte[] signalingKeyBytes = Base64.decode(signalingKey);
byte[] cipherKey = new byte[CIPHER_KEY_SIZE];
System.arraycopy(signalingKeyBytes, 0, cipherKey, 0, cipherKey.length);
return new SecretKeySpec(cipherKey, "AES");
}
private SecretKeySpec getMacKey(String signalingKey) throws IOException {
byte[] signalingKeyBytes = Base64.decode(signalingKey);
byte[] macKey = new byte[MAC_KEY_SIZE];
System.arraycopy(signalingKeyBytes, CIPHER_KEY_SIZE, macKey, 0, macKey.length);
return new SecretKeySpec(macKey, "HmacSHA256");
}
}

View File

@@ -1,74 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.messages;
import org.whispersystems.libaxolotl.util.guava.Optional;
import java.util.List;
public class TextSecureGroup {
public enum Type {
UNKNOWN,
UPDATE,
DELIVER,
QUIT
}
private final byte[] groupId;
private final Type type;
private final Optional<String> name;
private final Optional<List<String>> members;
private final Optional<TextSecureAttachment> avatar;
public TextSecureGroup(byte[] groupId) {
this(Type.DELIVER, groupId, null, null, null);
}
public TextSecureGroup(Type type, byte[] groupId, String name,
List<String> members,
TextSecureAttachment avatar)
{
this.type = type;
this.groupId = groupId;
this.name = Optional.fromNullable(name);
this.members = Optional.fromNullable(members);
this.avatar = Optional.fromNullable(avatar);
}
public byte[] getGroupId() {
return groupId;
}
public Type getType() {
return type;
}
public Optional<String> getName() {
return name;
}
public Optional<List<String>> getMembers() {
return members;
}
public Optional<TextSecureAttachment> getAvatar() {
return avatar;
}
}

View File

@@ -1,85 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.messages;
import org.whispersystems.libaxolotl.util.guava.Optional;
import java.util.List;
public class TextSecureMessage {
private final long timestamp;
private final Optional<List<TextSecureAttachment>> attachments;
private final Optional<String> body;
private final Optional<TextSecureGroup> group;
private final boolean secure;
private final boolean endSession;
public TextSecureMessage(long timestamp, String body) {
this(timestamp, null, body);
}
public TextSecureMessage(long timestamp, List<TextSecureAttachment> attachments, String body) {
this(timestamp, null, attachments, body);
}
public TextSecureMessage(long timestamp, TextSecureGroup group, List<TextSecureAttachment> attachments, String body) {
this(timestamp, group, attachments, body, true, false);
}
public TextSecureMessage(long timestamp, TextSecureGroup group, List<TextSecureAttachment> attachments, String body, boolean secure, boolean endSession) {
this.timestamp = timestamp;
this.body = Optional.fromNullable(body);
this.group = Optional.fromNullable(group);
this.secure = secure;
this.endSession = endSession;
if (attachments != null && !attachments.isEmpty()) {
this.attachments = Optional.of(attachments);
} else {
this.attachments = Optional.absent();
}
}
public long getTimestamp() {
return timestamp;
}
public Optional<List<TextSecureAttachment>> getAttachments() {
return attachments;
}
public Optional<String> getBody() {
return body;
}
public Optional<TextSecureGroup> getGroupInfo() {
return group;
}
public boolean isSecure() {
return secure;
}
public boolean isEndSession() {
return endSession;
}
public boolean isGroupUpdate() {
return group.isPresent() && group.get().getType() != TextSecureGroup.Type.DELIVER;
}
}

View File

@@ -1,47 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.push;
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;
}
}

View File

@@ -1,50 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.push;
public class PushAddress {
public static final int DEFAULT_DEVICE_ID = 1;
private final long recipientId;
private final String e164number;
private final int deviceId;
private final String relay;
public PushAddress(long recipientId, String e164number, int deviceId, String relay) {
this.recipientId = recipientId;
this.e164number = e164number;
this.deviceId = deviceId;
this.relay = relay;
}
public String getNumber() {
return e164number;
}
public String getRelay() {
return relay;
}
public long getRecipientId() {
return recipientId;
}
public int getDeviceId() {
return deviceId;
}
}

View File

@@ -1,88 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.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 org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.textsecure.internal.util.Base64;
import org.whispersystems.textsecure.internal.push.PreKeyEntity;
import java.io.IOException;
import java.lang.reflect.Type;
public class SignedPreKeyEntity extends PreKeyEntity {
private byte[] signature;
public SignedPreKeyEntity() {}
public SignedPreKeyEntity(int keyId, ECPublicKey publicKey, byte[] signature) {
super(keyId, publicKey);
this.signature = signature;
}
public byte[] getSignature() {
return signature;
}
public static String toJson(SignedPreKeyEntity entity) {
GsonBuilder builder = new GsonBuilder();
return forBuilder(builder).create().toJson(entity);
}
public static SignedPreKeyEntity fromJson(String serialized) {
GsonBuilder builder = new GsonBuilder();
return forBuilder(builder).create().fromJson(serialized, SignedPreKeyEntity.class);
}
public static GsonBuilder forBuilder(GsonBuilder builder) {
return PreKeyEntity.forBuilder(builder)
.registerTypeAdapter(byte[].class, new ByteArrayJsonAdapter());
}
private static class ByteArrayJsonAdapter
implements JsonSerializer<byte[]>, JsonDeserializer<byte[]>
{
@Override
public JsonElement serialize(byte[] signature, Type type,
JsonSerializationContext jsonSerializationContext)
{
return new JsonPrimitive(Base64.encodeBytesWithoutPadding(signature));
}
@Override
public byte[] deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext jsonDeserializationContext)
throws JsonParseException
{
try {
return Base64.decodeWithoutPadding(jsonElement.getAsJsonPrimitive().getAsString());
} catch (IOException e) {
throw new JsonParseException(e);
}
}
}
}

View File

@@ -1,25 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.push;
import java.io.InputStream;
public interface TrustStore {
public InputStream getKeyStoreInputStream();
public String getKeyStorePassword();
}

View File

@@ -1,23 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.push.exceptions;
public class AuthorizationFailedException extends NonSuccessfulResponseCodeException {
public AuthorizationFailedException(String s) {
super(s);
}
}

View File

@@ -1,42 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.push.exceptions;
import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException;
import java.util.List;
public class EncapsulatedExceptions extends Throwable {
private final List<UntrustedIdentityException> untrustedIdentityExceptions;
private final List<UnregisteredUserException> unregisteredUserExceptions;
public EncapsulatedExceptions(List<UntrustedIdentityException> untrustedIdentities,
List<UnregisteredUserException> unregisteredUsers)
{
this.untrustedIdentityExceptions = untrustedIdentities;
this.unregisteredUserExceptions = unregisteredUsers;
}
public List<UntrustedIdentityException> getUntrustedIdentityExceptions() {
return untrustedIdentityExceptions;
}
public List<UnregisteredUserException> getUnregisteredUserExceptions() {
return unregisteredUserExceptions;
}
}

View File

@@ -1,20 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.push.exceptions;
public class ExpectationFailedException extends NonSuccessfulResponseCodeException {
}

View File

@@ -1,30 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.push.exceptions;
import java.io.IOException;
public class NonSuccessfulResponseCodeException extends IOException {
public NonSuccessfulResponseCodeException() {
super();
}
public NonSuccessfulResponseCodeException(String s) {
super(s);
}
}

View File

@@ -1,23 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.push.exceptions;
public class NotFoundException extends NonSuccessfulResponseCodeException {
public NotFoundException(String s) {
super(s);
}
}

View File

@@ -1,23 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.push.exceptions;
public class RateLimitException extends NonSuccessfulResponseCodeException {
public RateLimitException(String s) {
super(s);
}
}

View File

@@ -1,23 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.util;
public class InvalidNumberException extends Throwable {
public InvalidNumberException(String s) {
super(s);
}
}

View File

@@ -1,137 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.api.util;
import android.util.Log;
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat;
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
import java.util.Locale;
/**
* Phone number formats are a pain.
*
* @author Moxie Marlinspike
*
*/
public class PhoneNumberFormatter {
private static final String TAG = PhoneNumberFormatter.class.getSimpleName();
public static boolean isValidNumber(String number) {
return number.matches("^\\+[0-9]{10,}");
}
private static String impreciseFormatNumber(String number, String localNumber)
throws InvalidNumberException
{
number = number.replaceAll("[^0-9+]", "");
if (number.charAt(0) == '+')
return number;
if (localNumber.charAt(0) == '+')
localNumber = localNumber.substring(1);
if (localNumber.length() == number.length() || number.length() > localNumber.length())
return "+" + number;
int difference = localNumber.length() - number.length();
return "+" + localNumber.substring(0, difference) + number;
}
public static String formatNumberInternational(String number) {
try {
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
PhoneNumber parsedNumber = util.parse(number, null);
return util.format(parsedNumber, PhoneNumberFormat.INTERNATIONAL);
} catch (NumberParseException e) {
Log.w(TAG, e);
return number;
}
}
public static String formatNumber(String number, String localNumber)
throws InvalidNumberException
{
if (number.contains("@")) {
throw new InvalidNumberException("Possible attempt to use email address.");
}
number = number.replaceAll("[^0-9+]", "");
if (number.length() == 0) {
throw new InvalidNumberException("No valid characters found.");
}
if (number.charAt(0) == '+')
return number;
try {
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
PhoneNumber localNumberObject = util.parse(localNumber, null);
String localCountryCode = util.getRegionCodeForNumber(localNumberObject);
Log.w(TAG, "Got local CC: " + localCountryCode);
PhoneNumber numberObject = util.parse(number, localCountryCode);
return util.format(numberObject, PhoneNumberFormat.E164);
} catch (NumberParseException e) {
Log.w(TAG, e);
return impreciseFormatNumber(number, localNumber);
}
}
public static String getRegionDisplayName(String regionCode) {
return (regionCode == null || regionCode.equals("ZZ") || regionCode.equals(PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY))
? "Unknown country" : new Locale("", regionCode).getDisplayCountry(Locale.getDefault());
}
public static String formatE164(String countryCode, String number) {
try {
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
int parsedCountryCode = Integer.parseInt(countryCode);
PhoneNumber parsedNumber = util.parse(number,
util.getRegionCodeForCountryCode(parsedCountryCode));
return util.format(parsedNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
} catch (NumberParseException | NumberFormatException npe) {
Log.w(TAG, npe);
}
return "+" +
countryCode.replaceAll("[^0-9]", "").replaceAll("^0*", "") +
number.replaceAll("[^0-9]", "");
}
public static String getInternationalFormatFromE164(String e164number) {
try {
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
PhoneNumber parsedNumber = util.parse(e164number, null);
return util.format(parsedNumber, PhoneNumberFormat.INTERNATIONAL);
} catch (NumberParseException e) {
Log.w(TAG, e);
return e164number;
}
}
}

View File

@@ -1,44 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.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;
}
}

View File

@@ -1,32 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.push;
import org.whispersystems.textsecure.api.push.ContactTokenDetails;
import java.util.List;
public class ContactTokenDetailsList {
private List<ContactTokenDetails> contacts;
public ContactTokenDetailsList() {}
public List<ContactTokenDetails> getContacts() {
return contacts;
}
}

View File

@@ -1,34 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.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;
}
}

View File

@@ -1,33 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.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;
}
}

View File

@@ -1,52 +0,0 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.push;
import org.whispersystems.textsecure.api.push.PushAddress;
import org.whispersystems.textsecure.internal.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;
}
}

View File

@@ -1,55 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.push;
import java.util.List;
public class OutgoingPushMessageList {
private String destination;
private String relay;
private long timestamp;
private List<OutgoingPushMessage> messages;
public OutgoingPushMessageList(String destination, long timestamp, String relay,
List<OutgoingPushMessage> messages)
{
this.timestamp = timestamp;
this.destination = destination;
this.relay = relay;
this.messages = messages;
}
public String getDestination() {
return destination;
}
public List<OutgoingPushMessage> getMessages() {
return messages;
}
public String getRelay() {
return relay;
}
public long getTimestamp() {
return timestamp;
}
}

View File

@@ -1,84 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.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 org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.textsecure.internal.util.Base64;
import java.io.IOException;
import java.lang.reflect.Type;
public class PreKeyEntity {
private int keyId;
private ECPublicKey publicKey;
public PreKeyEntity() {}
public PreKeyEntity(int keyId, ECPublicKey publicKey) {
this.keyId = keyId;
this.publicKey = publicKey;
}
public int getKeyId() {
return keyId;
}
public ECPublicKey getPublicKey() {
return publicKey;
}
public static GsonBuilder forBuilder(GsonBuilder builder) {
return builder.registerTypeAdapter(ECPublicKey.class, new ECPublicKeyJsonAdapter());
}
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 | IOException e) {
throw new JsonParseException(e);
}
}
}
}

View File

@@ -1,81 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.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 org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.textsecure.internal.util.Base64;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;
public class PreKeyResponse {
private IdentityKey identityKey;
private List<PreKeyResponseItem> devices;
public IdentityKey getIdentityKey() {
return identityKey;
}
public List<PreKeyResponseItem> getDevices() {
return devices;
}
public static PreKeyResponse fromJson(String serialized) {
GsonBuilder builder = new GsonBuilder();
return PreKeyResponseItem.forBuilder(builder)
.registerTypeAdapter(IdentityKey.class, new IdentityKeyJsonAdapter())
.create().fromJson(serialized, PreKeyResponse.class);
}
public 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 | IOException e) {
throw new JsonParseException(e);
}
}
}
}

View File

@@ -1,49 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.push;
import com.google.thoughtcrimegson.GsonBuilder;
import org.whispersystems.textsecure.api.push.SignedPreKeyEntity;
public class PreKeyResponseItem {
private int deviceId;
private int registrationId;
private SignedPreKeyEntity signedPreKey;
private PreKeyEntity preKey;
public int getDeviceId() {
return deviceId;
}
public int getRegistrationId() {
return registrationId;
}
public SignedPreKeyEntity getSignedPreKey() {
return signedPreKey;
}
public PreKeyEntity getPreKey() {
return preKey;
}
public static GsonBuilder forBuilder(GsonBuilder builder) {
return SignedPreKeyEntity.forBuilder(builder);
}
}

View File

@@ -1,33 +0,0 @@
package org.whispersystems.textsecure.internal.push;
import com.google.thoughtcrimegson.GsonBuilder;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.textsecure.api.push.SignedPreKeyEntity;
import java.util.List;
public class PreKeyState {
private IdentityKey identityKey;
private List<PreKeyEntity> preKeys;
private PreKeyEntity lastResortKey;
private SignedPreKeyEntity signedPreKey;
public PreKeyState(List<PreKeyEntity> preKeys, PreKeyEntity lastResortKey,
SignedPreKeyEntity signedPreKey, IdentityKey identityKey)
{
this.preKeys = preKeys;
this.lastResortKey = lastResortKey;
this.signedPreKey = signedPreKey;
this.identityKey = identityKey;
}
public static String toJson(PreKeyState state) {
GsonBuilder builder = new GsonBuilder();
return SignedPreKeyEntity.forBuilder(builder)
.registerTypeAdapter(IdentityKey.class, new PreKeyResponse.IdentityKeyJsonAdapter())
.create().toJson(state);
}
}

View File

@@ -1,28 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.push;
public class PreKeyStatus {
private int count;
public PreKeyStatus() {}
public int getCount() {
return count;
}
}

View File

@@ -1,50 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.push;
import java.io.InputStream;
public class PushAttachmentData {
private final String contentType;
private final InputStream data;
private final long dataSize;
private final byte[] key;
public PushAttachmentData(String contentType, InputStream data, long dataSize, byte[] key) {
this.contentType = contentType;
this.data = data;
this.dataSize = dataSize;
this.key = key;
}
public String getContentType() {
return contentType;
}
public InputStream getData() {
return data;
}
public long getDataSize() {
return dataSize;
}
public byte[] getKey() {
return key;
}
}

View File

@@ -1,42 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.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;
}
}

View File

@@ -1,568 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.push;
import android.util.Log;
import com.google.thoughtcrimegson.Gson;
import com.google.thoughtcrimegson.JsonParseException;
import org.apache.http.conn.ssl.StrictHostnameVerifier;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.state.PreKeyBundle;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.textsecure.api.push.PushAddress;
import org.whispersystems.textsecure.api.crypto.AttachmentCipherOutputStream;
import org.whispersystems.textsecure.api.push.ContactTokenDetails;
import org.whispersystems.textsecure.api.push.SignedPreKeyEntity;
import org.whispersystems.textsecure.api.push.TrustStore;
import org.whispersystems.textsecure.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.textsecure.internal.push.exceptions.MismatchedDevicesException;
import org.whispersystems.textsecure.internal.push.exceptions.StaleDevicesException;
import org.whispersystems.textsecure.internal.util.Base64;
import org.whispersystems.textsecure.internal.util.Util;
import org.whispersystems.textsecure.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.textsecure.api.push.exceptions.ExpectationFailedException;
import org.whispersystems.textsecure.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.textsecure.api.push.exceptions.NotFoundException;
import org.whispersystems.textsecure.api.push.exceptions.PushNetworkException;
import org.whispersystems.textsecure.api.push.exceptions.RateLimitException;
import org.whispersystems.textsecure.internal.util.BlacklistingTrustManager;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
/**
*
* Network interface to the TextSecure server API.
*
* @author Moxie Marlinspike
*/
public class PushServiceSocket {
private static final String CREATE_ACCOUNT_SMS_PATH = "/v1/accounts/sms/code/%s";
private static final String CREATE_ACCOUNT_VOICE_PATH = "/v1/accounts/voice/code/%s";
private static final String VERIFY_ACCOUNT_PATH = "/v1/accounts/code/%s";
private static final String REGISTER_GCM_PATH = "/v1/accounts/gcm/";
private static final String PREKEY_METADATA_PATH = "/v2/keys/";
private static final String PREKEY_PATH = "/v2/keys/%s";
private static final String PREKEY_DEVICE_PATH = "/v2/keys/%s/%s";
private static final String SIGNED_PREKEY_PATH = "/v2/keys/signed";
private static final String DIRECTORY_TOKENS_PATH = "/v1/directory/tokens";
private static final String DIRECTORY_VERIFY_PATH = "/v1/directory/%s";
private static final String MESSAGE_PATH = "/v1/messages/%s";
private static final String RECEIPT_PATH = "/v1/receipt/%s/%d";
private static final String ATTACHMENT_PATH = "/v1/attachments/%s";
private static final boolean ENFORCE_SSL = true;
private final String serviceUrl;
private final String localNumber;
private final String password;
private final TrustManager[] trustManagers;
public PushServiceSocket(String serviceUrl, TrustStore trustStore,
String localNumber, String password)
{
this.serviceUrl = serviceUrl;
this.localNumber = localNumber;
this.password = password;
this.trustManagers = initializeTrustManager(trustStore);
}
public void createAccount(boolean voice) throws IOException {
String path = voice ? CREATE_ACCOUNT_VOICE_PATH : CREATE_ACCOUNT_SMS_PATH;
makeRequest(String.format(path, localNumber), "GET", null);
}
public void verifyAccount(String verificationCode, String signalingKey,
boolean supportsSms, int registrationId)
throws IOException
{
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, supportsSms, registrationId);
makeRequest(String.format(VERIFY_ACCOUNT_PATH, verificationCode),
"PUT", new Gson().toJson(signalingKeyEntity));
}
public void sendReceipt(String destination, long messageId, String relay) throws IOException {
String path = String.format(RECEIPT_PATH, destination, messageId);
if (!Util.isEmpty(relay)) {
path += "?relay=" + relay;
}
makeRequest(path, "PUT", null);
}
public void registerGcmId(String gcmRegistrationId) throws IOException {
GcmRegistrationId registration = new GcmRegistrationId(gcmRegistrationId);
makeRequest(REGISTER_GCM_PATH, "PUT", new Gson().toJson(registration));
}
public void unregisterGcmId() throws IOException {
makeRequest(REGISTER_GCM_PATH, "DELETE", null);
}
public void sendMessage(OutgoingPushMessageList bundle)
throws IOException
{
try {
makeRequest(String.format(MESSAGE_PATH, bundle.getDestination()), "PUT", new Gson().toJson(bundle));
} catch (NotFoundException nfe) {
throw new UnregisteredUserException(bundle.getDestination(), nfe);
}
}
public void registerPreKeys(IdentityKey identityKey,
PreKeyRecord lastResortKey,
SignedPreKeyRecord signedPreKey,
List<PreKeyRecord> records)
throws IOException
{
List<PreKeyEntity> entities = new LinkedList<>();
for (PreKeyRecord record : records) {
PreKeyEntity entity = new PreKeyEntity(record.getId(),
record.getKeyPair().getPublicKey());
entities.add(entity);
}
PreKeyEntity lastResortEntity = new PreKeyEntity(lastResortKey.getId(),
lastResortKey.getKeyPair().getPublicKey());
SignedPreKeyEntity signedPreKeyEntity = new SignedPreKeyEntity(signedPreKey.getId(),
signedPreKey.getKeyPair().getPublicKey(),
signedPreKey.getSignature());
makeRequest(String.format(PREKEY_PATH, ""), "PUT",
PreKeyState.toJson(new PreKeyState(entities, lastResortEntity,
signedPreKeyEntity, identityKey)));
}
public int getAvailablePreKeys() throws IOException {
String responseText = makeRequest(PREKEY_METADATA_PATH, "GET", null);
PreKeyStatus preKeyStatus = new Gson().fromJson(responseText, PreKeyStatus.class);
return preKeyStatus.getCount();
}
public List<PreKeyBundle> getPreKeys(PushAddress destination) throws IOException {
try {
String deviceId = String.valueOf(destination.getDeviceId());
if (deviceId.equals("1"))
deviceId = "*";
String path = String.format(PREKEY_DEVICE_PATH, destination.getNumber(), deviceId);
if (!Util.isEmpty(destination.getRelay())) {
path = path + "?relay=" + destination.getRelay();
}
String responseText = makeRequest(path, "GET", null);
PreKeyResponse response = PreKeyResponse.fromJson(responseText);
List<PreKeyBundle> bundles = new LinkedList<>();
for (PreKeyResponseItem device : response.getDevices()) {
ECPublicKey preKey = null;
ECPublicKey signedPreKey = null;
byte[] signedPreKeySignature = null;
int preKeyId = -1;
int signedPreKeyId = -1;
if (device.getSignedPreKey() != null) {
signedPreKey = device.getSignedPreKey().getPublicKey();
signedPreKeyId = device.getSignedPreKey().getKeyId();
signedPreKeySignature = device.getSignedPreKey().getSignature();
}
if (device.getPreKey() != null) {
preKeyId = device.getPreKey().getKeyId();
preKey = device.getPreKey().getPublicKey();
}
bundles.add(new PreKeyBundle(device.getRegistrationId(), device.getDeviceId(), preKeyId,
preKey, signedPreKeyId, signedPreKey, signedPreKeySignature,
response.getIdentityKey()));
}
return bundles;
} catch (JsonParseException e) {
throw new IOException(e);
} catch (NotFoundException nfe) {
throw new UnregisteredUserException(destination.getNumber(), nfe);
}
}
public PreKeyBundle getPreKey(PushAddress destination) throws IOException {
try {
String path = String.format(PREKEY_DEVICE_PATH, destination.getNumber(),
String.valueOf(destination.getDeviceId()));
if (!Util.isEmpty(destination.getRelay())) {
path = path + "?relay=" + destination.getRelay();
}
String responseText = makeRequest(path, "GET", null);
PreKeyResponse response = PreKeyResponse.fromJson(responseText);
if (response.getDevices() == null || response.getDevices().size() < 1)
throw new IOException("Empty prekey list");
PreKeyResponseItem device = response.getDevices().get(0);
ECPublicKey preKey = null;
ECPublicKey signedPreKey = null;
byte[] signedPreKeySignature = null;
int preKeyId = -1;
int signedPreKeyId = -1;
if (device.getPreKey() != null) {
preKeyId = device.getPreKey().getKeyId();
preKey = device.getPreKey().getPublicKey();
}
if (device.getSignedPreKey() != null) {
signedPreKeyId = device.getSignedPreKey().getKeyId();
signedPreKey = device.getSignedPreKey().getPublicKey();
signedPreKeySignature = device.getSignedPreKey().getSignature();
}
return new PreKeyBundle(device.getRegistrationId(), device.getDeviceId(), preKeyId, preKey,
signedPreKeyId, signedPreKey, signedPreKeySignature, response.getIdentityKey());
} catch (JsonParseException e) {
throw new IOException(e);
} catch (NotFoundException nfe) {
throw new UnregisteredUserException(destination.getNumber(), nfe);
}
}
public SignedPreKeyEntity getCurrentSignedPreKey() throws IOException {
try {
String responseText = makeRequest(SIGNED_PREKEY_PATH, "GET", null);
return SignedPreKeyEntity.fromJson(responseText);
} catch (NotFoundException e) {
Log.w("PushServiceSocket", e);
return null;
}
}
public void setCurrentSignedPreKey(SignedPreKeyRecord signedPreKey) throws IOException {
SignedPreKeyEntity signedPreKeyEntity = new SignedPreKeyEntity(signedPreKey.getId(),
signedPreKey.getKeyPair().getPublicKey(),
signedPreKey.getSignature());
makeRequest(SIGNED_PREKEY_PATH, "PUT", SignedPreKeyEntity.toJson(signedPreKeyEntity));
}
public long sendAttachment(PushAttachmentData attachment) throws IOException {
String response = makeRequest(String.format(ATTACHMENT_PATH, ""), "GET", null);
AttachmentDescriptor attachmentKey = new Gson().fromJson(response, AttachmentDescriptor.class);
if (attachmentKey == null || attachmentKey.getLocation() == null) {
throw new IOException("Server failed to allocate an attachment key!");
}
Log.w("PushServiceSocket", "Got attachment content location: " + attachmentKey.getLocation());
uploadAttachment("PUT", attachmentKey.getLocation(), attachment.getData(),
attachment.getDataSize(), attachment.getKey());
return attachmentKey.getId();
}
public void retrieveAttachment(String relay, long attachmentId, File destination) throws IOException {
String path = String.format(ATTACHMENT_PATH, String.valueOf(attachmentId));
if (!Util.isEmpty(relay)) {
path = path + "?relay=" + relay;
}
String response = makeRequest(path, "GET", null);
AttachmentDescriptor descriptor = new Gson().fromJson(response, AttachmentDescriptor.class);
Log.w("PushServiceSocket", "Attachment: " + attachmentId + " is at: " + descriptor.getLocation());
downloadExternalFile(descriptor.getLocation(), destination);
}
public List<ContactTokenDetails> retrieveDirectory(Set<String> contactTokens)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
ContactTokenList contactTokenList = new ContactTokenList(new LinkedList<>(contactTokens));
String response = makeRequest(DIRECTORY_TOKENS_PATH, "PUT", new Gson().toJson(contactTokenList));
ContactTokenDetailsList activeTokens = new Gson().fromJson(response, ContactTokenDetailsList.class);
return activeTokens.getContacts();
}
public ContactTokenDetails getContactTokenDetails(String contactToken) throws IOException {
try {
String response = makeRequest(String.format(DIRECTORY_VERIFY_PATH, contactToken), "GET", null);
return new Gson().fromJson(response, ContactTokenDetails.class);
} catch (NotFoundException nfe) {
return null;
}
}
private void downloadExternalFile(String url, File localDestination)
throws IOException
{
URL downloadUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) downloadUrl.openConnection();
connection.setRequestProperty("Content-Type", "application/octet-stream");
connection.setRequestMethod("GET");
connection.setDoInput(true);
try {
if (connection.getResponseCode() != 200) {
throw new NonSuccessfulResponseCodeException("Bad response: " + connection.getResponseCode());
}
OutputStream output = new FileOutputStream(localDestination);
InputStream input = connection.getInputStream();
byte[] buffer = new byte[4096];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
output.close();
Log.w("PushServiceSocket", "Downloaded: " + url + " to: " + localDestination.getAbsolutePath());
} catch (IOException ioe) {
throw new PushNetworkException(ioe);
} finally {
connection.disconnect();
}
}
private void uploadAttachment(String method, String url, InputStream data, long dataSize, byte[] key)
throws IOException
{
URL uploadUrl = new URL(url);
HttpsURLConnection connection = (HttpsURLConnection) uploadUrl.openConnection();
connection.setDoOutput(true);
if (dataSize > 0) {
connection.setFixedLengthStreamingMode((int) AttachmentCipherOutputStream.getCiphertextLength(dataSize));
} else {
connection.setChunkedStreamingMode(0);
}
connection.setRequestMethod(method);
connection.setRequestProperty("Content-Type", "application/octet-stream");
connection.connect();
try {
OutputStream stream = connection.getOutputStream();
AttachmentCipherOutputStream out = new AttachmentCipherOutputStream(key, stream);
Util.copy(data, out);
out.flush();
if (connection.getResponseCode() != 200) {
throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage());
}
} finally {
connection.disconnect();
}
}
private String makeRequest(String urlFragment, String method, String body)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
HttpURLConnection connection = makeBaseRequest(urlFragment, method, body);
try {
String response = Util.readFully(connection.getInputStream());
connection.disconnect();
return response;
} catch (IOException ioe) {
throw new PushNetworkException(ioe);
}
}
private HttpURLConnection makeBaseRequest(String urlFragment, String method, String body)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
HttpURLConnection connection = getConnection(urlFragment, method, body);
int responseCode;
String responseMessage;
String response;
try {
responseCode = connection.getResponseCode();
responseMessage = connection.getResponseMessage();
} catch (IOException ioe) {
throw new PushNetworkException(ioe);
}
switch (responseCode) {
case 413:
connection.disconnect();
throw new RateLimitException("Rate limit exceeded: " + responseCode);
case 401:
case 403:
connection.disconnect();
throw new AuthorizationFailedException("Authorization failed!");
case 404:
connection.disconnect();
throw new NotFoundException("Not found");
case 409:
try {
response = Util.readFully(connection.getErrorStream());
} catch (IOException e) {
throw new PushNetworkException(e);
}
throw new MismatchedDevicesException(new Gson().fromJson(response, MismatchedDevices.class));
case 410:
try {
response = Util.readFully(connection.getErrorStream());
} catch (IOException e) {
throw new PushNetworkException(e);
}
throw new StaleDevicesException(new Gson().fromJson(response, StaleDevices.class));
case 417:
throw new ExpectationFailedException();
}
if (responseCode != 200 && responseCode != 204) {
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " +
responseMessage);
}
return connection;
}
private HttpURLConnection getConnection(String urlFragment, String method, String body)
throws PushNetworkException
{
try {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, trustManagers, null);
URL url = new URL(String.format("%s%s", serviceUrl, urlFragment));
Log.w("PushServiceSocket", "Push service URL: " + serviceUrl);
Log.w("PushServiceSocket", "Opening URL: " + url);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
if (ENFORCE_SSL) {
((HttpsURLConnection) connection).setSSLSocketFactory(context.getSocketFactory());
((HttpsURLConnection) connection).setHostnameVerifier(new StrictHostnameVerifier());
}
connection.setRequestMethod(method);
connection.setRequestProperty("Content-Type", "application/json");
if (password != null) {
connection.setRequestProperty("Authorization", getAuthorizationHeader());
}
if (body != null) {
connection.setDoOutput(true);
}
connection.connect();
if (body != null) {
Log.w("PushServiceSocket", method + " -- " + body);
OutputStream out = connection.getOutputStream();
out.write(body.getBytes());
out.close();
}
return connection;
} catch (IOException e) {
throw new PushNetworkException(e);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (KeyManagementException e) {
throw new AssertionError(e);
}
}
private String getAuthorizationHeader() {
try {
return "Basic " + Base64.encodeBytes((localNumber + ":" + password).getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
private TrustManager[] initializeTrustManager(TrustStore trustStore) {
try {
InputStream keyStoreInputStream = trustStore.getKeyStoreInputStream();
KeyStore keyStore = KeyStore.getInstance("BKS");
keyStore.load(keyStoreInputStream, trustStore.getKeyStorePassword().toCharArray());
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509");
trustManagerFactory.init(keyStore);
return BlacklistingTrustManager.createFor(trustManagerFactory.getTrustManagers());
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException kse) {
throw new AssertionError(kse);
}
}
private static class GcmRegistrationId {
private String gcmRegistrationId;
public GcmRegistrationId() {}
public GcmRegistrationId(String gcmRegistrationId) {
this.gcmRegistrationId = gcmRegistrationId;
}
}
private static class AttachmentDescriptor {
private long id;
private String location;
public long getId() {
return id;
}
public String getLocation() {
return location;
}
}
}

View File

@@ -1,75 +0,0 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.push;
import android.util.Log;
public class PushTransportDetails {
private final int messageVersion;
public PushTransportDetails(int messageVersion) {
this.messageVersion = messageVersion;
}
public byte[] getStrippedPaddingMessageBody(byte[] messageWithPadding) {
if (messageVersion < 2) throw new AssertionError("Unknown version: " + messageVersion);
else if (messageVersion == 2) return messageWithPadding;
int paddingStart = 0;
for (int i=messageWithPadding.length-1;i>=0;i--) {
if (messageWithPadding[i] == (byte)0x80) {
paddingStart = i;
break;
} else if (messageWithPadding[i] != (byte)0x00) {
Log.w("PushTransportDetails", "Padding byte is malformed, returning unstripped padding.");
return messageWithPadding;
}
}
byte[] strippedMessage = new byte[paddingStart];
System.arraycopy(messageWithPadding, 0, strippedMessage, 0, strippedMessage.length);
return strippedMessage;
}
public byte[] getPaddedMessageBody(byte[] messageBody) {
if (messageVersion < 2) throw new AssertionError("Unknown version: " + messageVersion);
else if (messageVersion == 2) return messageBody;
// NOTE: This is dumb. We have our own padding scheme, but so does the cipher.
// The +1 -1 here is to make sure the Cipher has room to add one padding byte,
// otherwise it'll add a full 16 extra bytes.
byte[] paddedMessage = new byte[getPaddedMessageLength(messageBody.length + 1) - 1];
System.arraycopy(messageBody, 0, paddedMessage, 0, messageBody.length);
paddedMessage[messageBody.length] = (byte)0x80;
return paddedMessage;
}
private int getPaddedMessageLength(int messageLength) {
int messageLengthWithTerminator = messageLength + 1;
int messagePartCount = messageLengthWithTerminator / 160;
if (messageLengthWithTerminator % 160 != 0) {
messagePartCount++;
}
return messagePartCount * 160;
}
}

View File

@@ -1,28 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.push;
import java.util.List;
public class StaleDevices {
private List<Integer> staleDevices;
public List<Integer> getStaleDevices() {
return staleDevices;
}
}

View File

@@ -1,33 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.push.exceptions;
import org.whispersystems.textsecure.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.textsecure.internal.push.MismatchedDevices;
public class MismatchedDevicesException extends NonSuccessfulResponseCodeException {
private final MismatchedDevices mismatchedDevices;
public MismatchedDevicesException(MismatchedDevices mismatchedDevices) {
this.mismatchedDevices = mismatchedDevices;
}
public MismatchedDevices getMismatchedDevices() {
return mismatchedDevices;
}
}

View File

@@ -1,33 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.push.exceptions;
import org.whispersystems.textsecure.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.textsecure.internal.push.StaleDevices;
public class StaleDevicesException extends NonSuccessfulResponseCodeException {
private final StaleDevices staleDevices;
public StaleDevicesException(StaleDevices staleDevices) {
this.staleDevices = staleDevices;
}
public StaleDevices getStaleDevices() {
return staleDevices;
}
}

View File

@@ -1,87 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.util;
import java.math.BigInteger;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.LinkedList;
import java.util.List;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
/**
* Trust manager that defers to a system X509 trust manager, and
* additionally rejects certificates if they have a blacklisted
* serial.
*
* @author Moxie Marlinspike
*/
public class BlacklistingTrustManager implements X509TrustManager {
private static final List<BigInteger> BLACKLIST = new LinkedList<BigInteger>() {{
add(new BigInteger("4098"));
}};
public static TrustManager[] createFor(TrustManager[] trustManagers) {
for (TrustManager trustManager : trustManagers) {
if (trustManager instanceof X509TrustManager) {
TrustManager[] results = new BlacklistingTrustManager[1];
results[0] = new BlacklistingTrustManager((X509TrustManager)trustManager);
return results;
}
}
throw new AssertionError("No X509 Trust Managers!");
}
private final X509TrustManager trustManager;
public BlacklistingTrustManager(X509TrustManager trustManager) {
this.trustManager = trustManager;
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException
{
trustManager.checkClientTrusted(chain, authType);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException
{
trustManager.checkServerTrusted(chain, authType);
for (X509Certificate certificate : chain) {
for (BigInteger blacklistedSerial : BLACKLIST) {
if (certificate.getSerialNumber().equals(blacklistedSerial)) {
throw new CertificateException("Blacklisted Serial: " + certificate.getSerialNumber());
}
}
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return trustManager.getAcceptedIssuers();
}
}

View File

@@ -1,138 +0,0 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.util;
import java.io.IOException;
/**
* Utility for generating hex dumps.
*/
public class Hex {
private final static int HEX_DIGITS_START = 10;
private final static int ASCII_TEXT_START = HEX_DIGITS_START + (16*2 + (16/2));
final static String EOL = System.getProperty("line.separator");
private final static char[] HEX_DIGITS = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};
public static String toString(byte[] bytes) {
return toString(bytes, 0, bytes.length);
}
public static String toString(byte[] bytes, int offset, int length) {
StringBuffer buf = new StringBuffer();
for (int i = 0; i < length; i++) {
appendHexChar(buf, bytes[offset + i]);
buf.append(' ');
}
return buf.toString();
}
public static String toStringCondensed(byte[] bytes) {
StringBuffer buf = new StringBuffer();
for (int i=0;i<bytes.length;i++) {
appendHexChar(buf, bytes[i]);
}
return buf.toString();
}
public static byte[] fromStringCondensed(String encoded) throws IOException {
final char[] data = encoded.toCharArray();
final int len = data.length;
if ((len & 0x01) != 0) {
throw new IOException("Odd number of characters.");
}
final byte[] out = new byte[len >> 1];
// two characters form the hex value.
for (int i = 0, j = 0; j < len; i++) {
int f = Character.digit(data[j], 16) << 4;
j++;
f = f | Character.digit(data[j], 16);
j++;
out[i] = (byte) (f & 0xFF);
}
return out;
}
public static String dump(byte[] bytes) {
return dump(bytes, 0, bytes.length);
}
public static String dump(byte[] bytes, int offset, int length) {
StringBuffer buf = new StringBuffer();
int lines = ((length - 1) / 16) + 1;
int lineOffset;
int lineLength;
for (int i = 0; i < lines; i++) {
lineOffset = (i * 16) + offset;
lineLength = Math.min(16, (length - (i * 16)));
appendDumpLine(buf, i, bytes, lineOffset, lineLength);
buf.append(EOL);
}
return buf.toString();
}
private static void appendDumpLine(StringBuffer buf, int line, byte[] bytes, int lineOffset, int lineLength) {
buf.append(HEX_DIGITS[(line >> 28) & 0xf]);
buf.append(HEX_DIGITS[(line >> 24) & 0xf]);
buf.append(HEX_DIGITS[(line >> 20) & 0xf]);
buf.append(HEX_DIGITS[(line >> 16) & 0xf]);
buf.append(HEX_DIGITS[(line >> 12) & 0xf]);
buf.append(HEX_DIGITS[(line >> 8) & 0xf]);
buf.append(HEX_DIGITS[(line >> 4) & 0xf]);
buf.append(HEX_DIGITS[(line ) & 0xf]);
buf.append(": ");
for (int i = 0; i < 16; i++) {
int idx = i + lineOffset;
if (i < lineLength) {
int b = bytes[idx];
appendHexChar(buf, b);
} else {
buf.append(" ");
}
if ((i % 2) == 1) {
buf.append(' ');
}
}
for (int i = 0; i < 16 && i < lineLength; i++) {
int idx = i + lineOffset;
int b = bytes[idx];
if (b >= 0x20 && b <= 0x7e) {
buf.append((char)b);
} else {
buf.append('.');
}
}
}
private static void appendHexChar(StringBuffer buf, int b) {
buf.append(HEX_DIGITS[(b >> 4) & 0xf]);
buf.append(HEX_DIGITS[b & 0xf]);
}
}

View File

@@ -1,92 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.internal.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public class Util {
public static byte[][] split(byte[] input, int firstLength, int secondLength) {
byte[][] parts = new byte[2][];
parts[0] = new byte[firstLength];
System.arraycopy(input, 0, parts[0], 0, firstLength);
parts[1] = new byte[secondLength];
System.arraycopy(input, firstLength, parts[1], 0, secondLength);
return parts;
}
public static boolean isEmpty(String value) {
return value == null || value.trim().length() == 0;
}
public static byte[] getSecretBytes(int size) {
try {
byte[] secret = new byte[size];
SecureRandom.getInstance("SHA1PRNG").nextBytes(secret);
return secret;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
public static String readFully(InputStream in) throws IOException {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int read;
while ((read = in.read(buffer)) != -1) {
bout.write(buffer, 0, read);
}
in.close();
return new String(bout.toByteArray());
}
public static void readFully(InputStream in, byte[] buffer) throws IOException {
int offset = 0;
for (;;) {
int read = in.read(buffer, offset, buffer.length - offset);
if (read + offset < buffer.length) offset += read;
else return;
}
}
public static void copy(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[4096];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
in.close();
out.close();
}
}

6
proguard.cfg Normal file
View File

@@ -0,0 +1,6 @@
-keepattributes **
-keep class !org.spongycastle.crypto.tls.**,!com.doomonafireball.betterpickers.**,!org.w3c.**,!android.support.v7.internal.view.menu.**,!org.spongycastle.asn1.**,!org.spongycastle.crypto.ec.**,!org.spongycastle.x509.**,!org.spongycastle.crypto.agreement.**,!org.spongycastle.crypto.ec.**,!org.spongycastle.math.**,!org.spongycastle.pqc.**,!org.spongycastle.jcajce.provider.asymmetric.**,!org.apache.http.**,** {*;}
-dontpreverify
-dontoptimize
-dontwarn **

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_enabled="false" android:color="@color/gray27"/>
<item android:color="@color/gray95"/>
</selector>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_enabled="false" android:color="@color/gray27"/>
<item android:color="@color/gray50"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Some files were not shown because too many files have changed in this diff Show More