Compare commits

...

109 Commits

Author SHA1 Message Date
Moxie Marlinspike
e53bbe8453 Bumping version to 2.1.2
// FREEBIE
2014-08-01 16:14:02 -07:00
Moxie Marlinspike
ecb67cd84f Updated language translations.
// FREEBIE
2014-08-01 14:41:00 -07:00
Moxie Marlinspike
861d27279d Whoops, add GcmRegistrationService to Manifest.
// FREEBIE
2014-08-01 14:33:17 -07:00
Veeti Paananen
bef5b8f3e9 Occupy all vertical space for emoji grid
Fixes the recent emoji list leaving an empty gap at the bottom of the
drawer depending on the number of items shown.
2014-08-01 13:40:19 -07:00
Veeti Paananen
9e74b5c892 Remove gray placeholder squares while loading emoji 2014-08-01 13:40:19 -07:00
Veeti Paananen
3597915d17 Add a backspace key to the emoji drawer 2014-08-01 13:40:19 -07:00
Veeti Paananen
40ce0cebe0 Fix emoji backwards compatibility recents crash
The old emoji drawer stored emoji with a .png suffix. Replace it during
list deserialization.
2014-07-31 02:49:36 +03:00
jhertel
26d58047b5 Update BUILDING.md
Correction of a typo and a few style incongruences.

// FREEBIE
Closes #1750
2014-07-28 09:36:03 -07:00
Jake McGinty
7d688846f9 Move default SMS and system import to "reminders"
// FREEBIE
Closes #1730
2014-07-27 01:09:39 -07:00
McLoo
acc7c4c1c6 Null check for cipher text to prevent NPE on decryption
Fixes #1703
Closes #1728
// FREEBIE
2014-07-26 23:02:11 -07:00
Jake McGinty
530ad7bc86 new emoji drawer
// FREEBIE
Closes #1746
2014-07-26 13:35:03 -07:00
Jake McGinty
bea3c33223 disable passphrase creation on registration
// FREEBIE
Closes #1726
2014-07-25 17:46:50 -07:00
Moxie Marlinspike
9ef14a0f64 Upgrade to new GCM API. 2014-07-23 15:40:45 -07:00
Moxie Marlinspike
c632b32ff8 Bumping version to 2.1.1 2014-07-19 11:13:18 -07:00
Moxie Marlinspike
40698212bb Create a Curve25519 asymmetric master secret for users without.
Fixes #1701
2014-07-18 22:16:12 -07:00
Veeti Paananen
19ae5043cc Add number of messages to the notification number attribute
Although not used by stock Android, many custom ROM's (and possibly OEM
versions?) have a setting to display the "number" count of a notification
overlayed on the status bar icon. Add support for this.

Closes #1637
2014-07-16 11:50:42 -07:00
rymdhund
d1dd50e31c Add date to saved media filenames
Fixes #1689
Closes #1693
2014-07-16 11:46:18 -07:00
Jake McGinty
23a1c1c8fa Upgrade to latest Android gradle plugin
// FREEBIE
Closes #1660
2014-07-16 11:12:51 -07:00
Moxie Marlinspike
3a62a8b428 Bumping version to 2.1.0 2014-07-15 10:05:15 -07:00
Moxie Marlinspike
cfac27645b Update language translations.
// FREEBIE
2014-07-14 18:44:34 -07:00
Jake McGinty
f6e04d0f89 use latest android number as recipient number
Fixes #791
// FREEBIE
2014-07-14 16:22:15 -07:00
Jake McGinty
61d18f49ad Merge pull request #1678 from veeti/check-icon
Add xxhdpi check drawable for notifications
2014-07-07 16:09:44 -05:00
Veeti Paananen
f26f89d63d Add xxhdpi check drawable for notifications
Fixes blurry mark as read icon on Android Wear. FREEBIE.
2014-07-05 02:40:00 +03:00
Moxie Marlinspike
66aad852f8 Merge pull request #1642 from mcginty/canonical-bunk-addresses
don't try to load recipients for each filter text
2014-06-25 08:06:00 -07:00
Jake McGinty
da0eb5a779 no longer load a recipient for each filter text
// FREEBIE
2014-06-24 20:33:04 -07:00
Jake McGinty
a82d2dfc5c Revert "change out key cached icon to be more unique"
This reverts commit d6d76fa953.
2014-06-24 19:20:16 -07:00
McLoo
d429f9113b Replace XML serializer in plaintext export
Fixes #342

- using regex pattern/matcher to escape chars below 0x0020 and
  above 0xd7ff
- using String.Replace to escape XML entities
- changed XmlPullParser from Xml.newPullParser() to
  XmlPullParserFactory parser to fix import on GB
2014-06-24 13:02:36 -07:00
Moxie Marlinspike
8f85eb1822 Remove unused files.
Fixes #1522

// FREEBIE
2014-06-24 08:32:59 -07:00
Moxie Marlinspike
358c923891 Merge pull request #1630 from mcginty/remove-keys-list
remove ReviewIdentitiesActivity
2014-06-23 11:17:17 -07:00
Lukas Barth
2d9cd8eb52 Fixing race condition and other mistakes. Fixes #1603.
// FREEBIE
2014-06-23 11:16:37 -07:00
Moxie Marlinspike
db1d846833 Merge pull request #1631 from mcginty/disable-encrypted-export
temporarily disable encrypted backup
2014-06-23 07:18:50 -07:00
Jake McGinty
5121ab0eed temporarily disable encrypted backup
// FREEBIE
2014-06-22 16:40:02 -07:00
Jake McGinty
f63f95404e remove ReviewIdentitiesActivity
// FREEBIE
2014-06-22 16:21:30 -07:00
Jake McGinty
622d8975fc add section about submitting useful bug reports 2014-06-20 12:24:36 -07:00
Moxie Marlinspike
81365eff36 Merge pull request #1614 from mcginty/contact-list-security
move FLAG_SECURE to PassphraseRequiredMixin
2014-06-17 21:47:00 -07:00
Florian Walch
453610c39f Add Travis CI config.
//FREEBIE
2014-06-17 14:13:42 -07:00
Jake McGinty
5ce6dc954a move FLAG_SECURE to PassphraseRequiredMixin
Fixes #1402
// FREEBIE
2014-06-16 20:41:13 -07:00
Özgür Emir
c85a8bbb38 Always show the time of the received message. 2014-06-16 15:57:39 -07:00
agrajaghh
0f9a6e6296 add custom phone number type 2014-06-16 15:27:33 -07:00
phenx-de
d8cb893681 Fixes "subtitle is not updated when select all is pressed"
// FREEBIE
2014-06-16 09:33:35 -07:00
Michael Kaiser
1ad54e7b88 Fix more leaked service connections
PassphraseRequiredMixin might check for a bound service at a time where
the bind has been requested but the service connection has not been
established yet, and therefore fail to call unbindService, leading to a
leaked service connection. This fixes #1518.
2014-06-15 19:28:09 -07:00
Moxie Marlinspike
0d35e2bfa9 Fix the "Tap for X fallback" labels. 2014-06-13 17:48:56 -07:00
Moxie Marlinspike
983bf672cf Fix UI side of broken MMS fallback.
1) Actually tell the SendReceiveService to send the MMS if it is
   one.

2) Display the correct string (SMS vs MMS) in the fallback dialog.
2014-06-13 17:39:29 -07:00
Moxie Marlinspike
1c2e1a07f5 Fixes for outgoing SMS/MMS direct and fallback behavior.
1) Correct MMS fallback settings.

2) Prevent SMS/MMS messages from leaking out under certain
   circumstances when they shouldn't.
2014-06-13 17:15:46 -07:00
Moxie Marlinspike
2d739a324e Validate MMS delivery destination.
We can't depend on validated Recipients anymore, so this adds
parity to the validation the SMS transport does now.

Fixes #1592
2014-06-13 16:15:33 -07:00
Moxie Marlinspike
ba1055df8e Correct contextual send language.
1) Use "secure" and "insecure" vs "encrypted" and "unencrypted.

2) Use MMS instead of SMS where appropriate.

Fixes #1602
2014-06-13 15:24:38 -07:00
phenx-de
a54d20f3ef Add "%s selected" subtitle to Conversation List batch mode. 2014-06-13 09:35:36 -07:00
phenx-de
ea0fa58265 Add preview of encryption channel in compose text hint. 2014-06-12 16:32:31 -07:00
Moxie Marlinspike
359fe280e8 Fix for broken build (*ahem* @phenx-de *ahem*) =)
// FREEBIE
2014-06-12 16:27:51 -07:00
Jake McGinty
34e147838a use apply for preferences instead of commit
// FREEBIE
2014-06-12 14:45:51 -07:00
phenx-de
d8e6a93584 Use contextual action bar menu for conversation items. 2014-06-12 14:27:34 -07:00
phenx-de
5ae8a7a8c4 Improved the warning icon: Higher resolution, better size. 2014-06-12 14:24:08 -07:00
Moxie Marlinspike
0e6773b4b7 Remove directory refresh preference.
This is present on the contact screen now, so there's no longer
any need for it here.
2014-06-12 11:58:24 -07:00
McLoo
fb13d33e2e Show drafts emojified when Android version is below KitKat 2014-06-12 11:30:50 -07:00
Pascal Hartig
92fd8ededd Sort contacts case-insentively
This fixes the case sensitive ordering of contacts in the view
for creating new conversations.

Fix #1502
2014-06-12 10:56:46 -07:00
agrajaghh
8713a85beb Add notification for key change event.
Fixes #1460
2014-06-12 10:40:46 -07:00
Marek Wehmer
9b82411c3d Better share intent handling.
1) Guess mime type from share intent EXTRA_STREAM uri.

2) Always include EXTRA_TEXT (if present)
2014-06-12 10:23:56 -07:00
Jake McGinty
16764f74fe reorganize readme
// FREEBIE
2014-06-12 10:02:18 -07:00
Veeti Paananen
bd889d8fa9 Reword the very confusing screen security setting 2014-06-12 09:22:39 -07:00
Moxie Marlinspike
d51adab76b Use "date sent" as timestamp for push, "date received" for SMS.
The "sent time" is not reliable on SMS messages.  This switches
to using "sent time" by default for push messages, but "received
time" for SMS messages.
2014-06-12 08:59:54 -07:00
Chris V
b990202468 Allow passphrase unlock from "unlock" keyboard action. 2014-06-11 21:45:03 -07:00
Pascal Hartig
7208018097 Clear search when opening the drawer
Before this change opening the drawer while a filter was active
would hide the search bar but keep the conversation list filtered,
so there was no indication of an active filter.
2014-06-11 18:12:07 -07:00
Moxie Marlinspike
c719a48a2c Move media attachment long-click event to context menu.
Long-click on a media attachment will now bring up the normal
context menu for a ConversationItem long-click, but with the
addition of a "save attachment" option.

This allows users to long-click on messages with media in them
and still see the other contextual menu options.

// FREEBIE
2014-06-11 18:04:14 -07:00
Moxie Marlinspike
68747142d6 Add correct contextual menu options on 'Send' button.
[Send TextSecure message | Send unencrypted SMS | Send encrypted SMS]

// FREEBIE
2014-06-11 15:34:01 -07:00
Lukas Barth
7c9282f306 Cache circle cropped photos on Recipient. 2014-06-11 12:33:57 -07:00
Lukas Barth
fa3cb871d0 Use ACTION_OPEN_DOCUMENT for Android >= KitKat.
Fixes #926.

We have to do this, since with the new Storage Access Framework,
otherwise we can open the Uri only *once*. This would work well
unless someone saves a draft and goes back to the conversation -
then the Uri is opened again without the required permissions.

See:

https://developer.android.com/guide/topics/providers/document-provider.html#client

...for details.
2014-06-11 11:58:55 -07:00
Moxie Marlinspike
a19899a11f Merge pull request #1589 from jlund/cyanogen-error-message
Fixing a typo in the WhisperPush error message
2014-06-11 09:20:50 -07:00
Joshua Lund
667da3b2cf Fixing a typo in the WhisperPush error message
// FREEBIE
2014-06-10 20:56:45 -06:00
Moxie Marlinspike
1a86483b7f Merge pull request #1581 from mcginty/android-studio-060
Upgrade android plugin to stay compatible with latest Android Studio
2014-06-10 09:07:59 -07:00
Jake McGinty
de90222c95 Upgrade android plugin to stay compatible with latest Android Studio
// FREEBIE
2014-06-09 23:31:52 -07:00
Moxie Marlinspike
7cf84e904a Merge pull request #1573 from Jabro/master
Added APN Settings for T-Mobile UK
2014-06-09 09:44:50 -07:00
Jabro
12e92b9cdf Added APN Settings for T-Mobile UK
Fixes #1558
2014-06-09 12:43:19 +02:00
Moxie Marlinspike
4153c8dae9 No need to verify a local module.
// FREEBIE
2014-06-06 09:14:43 -07:00
Moxie Marlinspike
ef72702f0d Include gradle-witness plugin for verifying dependencies. 2014-06-05 10:19:28 -07:00
Moxie Marlinspike
f5e2010455 Merge pull request #1554 from thoughtbox/patch-8
Capitalisation according to Android guidelines
2014-06-05 09:06:19 -07:00
thoughtbox
bc769debe2 Capitalisation according to Android guidelines
Not all updated capitalisations were correct.
2014-06-05 09:24:31 +02:00
Moxie Marlinspike
df1c96a662 Do country code detection if we can't get the full number off SIM. 2014-06-03 19:16:27 -07:00
Corbin Souffrant
db356a0ec9 Fixed capitalization inconsistensies with Android guidelines.
Fixes #673
2014-06-03 18:51:21 -07:00
Michael Bennett
468eb3382c Add sorting by default phone number
Currently the order of numbers is times contacted -> displayName ->
phone type (mobile vs. home, etc.). This adds whether the number has
been saved as the default number for a contact to sort numbers belonging
to the same contact.

Fixes #580
2014-06-03 18:18:41 -07:00
Moxie Marlinspike
12d217991c Use dynamic PBE iteration count.
Fixes #184
Fixes #247
2014-06-03 17:59:11 -07:00
Ruben Pollan
5785860631 Support for multiple APN settings on the same provider 2014-06-03 16:24:20 -07:00
Moxie Marlinspike
addea8d340 Validate recipients at send time rather than when constructed.
Fixes #665
2014-06-03 14:58:19 -07:00
Moxie Marlinspike
59899b1caf Merge pull request #1550 from mcginty/email-send-fix
send email addresses as mms
2014-06-03 12:46:08 -07:00
Jake McGinty
829097d891 send email addresses as mms
// FREEBIE
2014-06-03 12:35:56 -07:00
Moxie Marlinspike
d95bb21065 More ideology.
// FREEBIE
2014-06-01 17:05:21 -07:00
Moxie Marlinspike
0fbe765447 Expand ideology.
//FREEBIE
2014-06-01 10:22:31 -07:00
Moxie Marlinspike
f190321e40 Add some ideology to contributing.md
//FREEBIE
2014-05-31 15:05:59 -07:00
Michael Kaiser
1cb4d479f1 Finish activity before restarting it
The current activity needs to be finished before calling startActivity.
Otherwise, activities with launchMode singleTask (ConversationListActivity)
will receive a new Intent instead of getting restarted. And in response
to the new Intent, they will run onResume once again and trigger a second restart.

Fixes #1292
2014-05-20 10:16:48 -07:00
McLoo
023d776e96 Reactivate a group if a contact gets readded
Fixes #723 //FREEBIE

Removes the own number from group on leaving, to receive a proper
re-added message
2014-05-19 13:18:28 -07:00
Jake McGinty
ce7b8ab75a new passphrase prompt activity
// FREEBIE
2014-05-19 12:16:42 -07:00
agrajaghh
82bb0c07e8 Fix AlertDialog Background on Android 2.3 2014-05-16 17:18:57 -07:00
Manuel
d8d5848dae Change draw selector to background 2014-05-16 11:43:47 -07:00
Jake McGinty
542e1984c1 sanely handle duplicate contacts in db
// FREEBIE
2014-05-16 09:16:20 -07:00
Jake McGinty
dff6997a65 don't call replace() on null formattedNumber
Fixes #1397
// FREEBIE
2014-05-16 09:13:50 -07:00
Moxie Marlinspike
5bfe64752e Merge pull request #1495 from mcginty/icon-cached
change out key cached icon to be more unique
2014-05-16 08:30:31 -07:00
Jake McGinty
d6d76fa953 change out key cached icon to be more unique
Fixes #651
// FREEBIE
2014-05-14 17:52:49 -07:00
Sebastian
03ecd79fe0 fix receiving utf-8 characters in multimedia push messages
Throw AssertionError instead of logging and trying to recover
2014-05-01 15:06:44 -07:00
Moxie Marlinspike
7a3d509ef4 Merge pull request #1437 from mcginty/mms-npe
prevent NPE in MMS logic
2014-05-01 13:05:01 -07:00
Jake McGinty
7a54f33f68 Merge pull request #1354 from agrajaghh/fix_empty_contact_filter
Fix empty contact filter not working properly
2014-05-01 14:18:25 -05:00
Jake McGinty
d4b4667d5a prevent NPE in MMS logic
Fixes #1434
// FREEBIE
2014-04-28 14:05:05 -07:00
thoughtbox
08d899e2e1 Slightly more verbose "no legacy support" message. 2014-04-28 11:02:48 -07:00
Jake McGinty
716519f4b8 Merge pull request #1383 from jocelynthode/master
Add time to messages when they are within the week
2014-04-27 19:19:49 -07:00
Moxie Marlinspike
02d3760b31 Merge pull request #1368 from mcginty/contact-select-header-dark
fix contact selection header theming
2014-04-24 18:47:13 -07:00
Jocelyn Thode
521fbc77c6 Add time to messages when they are within the week
//FREEBIE
2014-04-17 16:10:27 +02:00
Moxie Marlinspike
0574ec170a Display legacy message error when V1 message is received. 2014-04-16 11:47:51 -07:00
Moxie Marlinspike
cebad39422 Collapse some v2 interfaces now that there's no v1. 2014-04-16 11:47:51 -07:00
Moxie Marlinspike
1d07ca3e6f Remove V1 code. 2014-04-16 11:47:51 -07:00
Jake McGinty
7349378d8d fix contact selection header theming
Fixes #1343
// FREEBIE
2014-04-14 17:43:13 -07:00
agrajaghh
e9a50ce6c3 fix empty contact filter 2014-04-13 15:00:04 +02:00
381 changed files with 7437 additions and 7587 deletions

7
.travis.yml Normal file
View File

@@ -0,0 +1,7 @@
language: android
android:
components:
- platform-tools
- build-tools-19.1.0
- android-19
- extra-android-m2repository

View File

@@ -2,10 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.thoughtcrime.securesms"
android:versionCode="71"
android:versionName="2.0.8">
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="19"/>
android:versionCode="74"
android:versionName="2.1.2">
<permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"
android:label="Access to TextSecure Secrets"
@@ -33,7 +31,6 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<permission android:name="org.thoughtcrime.securesms.permission.C2D_MESSAGE"
@@ -45,6 +42,9 @@
android:label="@string/app_name"
android:theme="@style/TextSecure.LightTheme">
<meta-data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
<activity android:name=".RoutingActivity"
android:theme="@style/NoAnimation.Theme.BlackScreen"
android:launchMode="singleTask"
@@ -126,15 +126,15 @@
<activity android:name=".PassphraseCreateActivity"
android:label="@string/AndroidManifest__create_passphrase"
android:windowSoftInputMode="stateUnchanged"
android:theme="@style/NoAnimation.Theme.Sherlock.Light.DarkActionBar"
android:theme="@style/TextSecure.IntroTheme"
android:launchMode="singleTop"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PassphrasePromptActivity"
android:label="@string/AndroidManifest__enter_passphrase"
android:launchMode="singleTop"
android:theme="@style/NoAnimation.Theme.Sherlock.Light.DarkActionBar"
android:windowSoftInputMode="stateUnchanged"
android:theme="@style/TextSecure.IntroTheme"
android:windowSoftInputMode="stateAlwaysVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ContactSelectionActivity"
@@ -172,11 +172,7 @@
android:label="@string/AndroidManifest__verify_identity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ReviewIdentitiesActivity"
android:label="@string/AndroidManifest__manage_identity_keys"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ReceiveKeyActivity"
<activity android:name=".ReceiveKeyActivity"
android:label="@string/AndroidManifest__complete_key_exchange"
android:theme="@style/TextSecure.Light.Dialog"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -207,13 +203,13 @@
android:clearTaskOnLaunch="true"
android:finishOnTaskLaunch="true" />
<service android:enabled="true" android:name=".service.GcmRegistrationService"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:name=".service.KeyCachingService"/>
<service android:enabled="true" android:name=".service.SendReceiveService"/>
<service android:enabled="true" android:name=".service.RegistrationService"/>
<service android:enabled="true" android:name=".service.DirectoryRefreshService"/>
<service android:enabled="true" android:name=".service.PreKeyService"/>
<service android:enabled="true" android:name=".gcm.GcmIntentService"/>
<service android:name=".service.QuickResponseService"
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
@@ -240,7 +236,6 @@
<receiver android:name=".gcm.GcmBroadcastReceiver" android:permission="com.google.android.c2dm.permission.SEND" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
<category android:name="org.thoughtcrime.securesms" />
</intent-filter>
</receiver>
@@ -306,8 +301,4 @@
<uses-library android:name="android.test.runner" />
</application>
<instrumentation android:name="android.test.InstrumentationTestRunner"
android:targetPackage="org.thoughtcrime.securesms.tests" android:label="Tests for My App" />
</manifest>

View File

@@ -45,12 +45,12 @@ Setting up a development environment
[Android Studio](https://developer.android.com/sdk/installing/studio.html) is the recommended development environment.
1. Install Android Studio
1. Install Android Studio.
2. Make sure the "Android Support Repository" is installed in the Android Studio SDK.
3. Make sure the latest "Android SDK build-tools" is installed in the Android Studio SDK.
4. Create a new Android Studio project. from the Quickstart pannel (use File > Close Project to see it), choose "Checkout from Version Control" then "git".
5. Paste the URL for the TextSecure project when prompted (https://github.com/WhisperSystems/TextSecure.git)
6. Android studio should detect the presence of a project file and ask you wethere to open it. Click "yes".
5. Paste the URL for the TextSecure project when prompted (https://github.com/WhisperSystems/TextSecure.git).
6. Android studio should detect the presence of a project file and ask you whether to open it. Click "yes".
7. Default config options should be good enough.
8. Project initialisation and build should proceed.

View File

@@ -1,73 +1,73 @@
TextSecure
=================
# TextSecure [![Build Status](https://travis-ci.org/WhisperSystems/TextSecure.svg?branch=master)](https://travis-ci.org/WhisperSystems/TextSecure)
A secure text messaging application for Android.
TextSecure is a messaging app for easy private communicate with friends.
TextSecure is a replacement for the standard text messaging application, allowing you to send and receive text messages as normal. Additionally, TextSecure provides:
TextSecure can use either data (WiFi/3G/4G) or SMS to communicate securely, and all TextSecure
messages can also be encrypted locally on your device.
1. *Local Encryption* -- All text messages, regardless of destination, that are sent or received with TextSecure are stored in an encrypted database on your phone.
2. *Wire Encryption* -- When communicating with a recipient who is also using TextSecure, text messages are encrypted during transmission.
Currently available on the Play store.
Current BitHub Payment For Commit:
=================
[![Current Price](https://bithub.herokuapp.com/v1/status/payment/commit/)](https://whispersystems.org/blog/bithub/)
*[![Play Store Badge](https://developer.android.com/images/brand/en_app_rgb_wo_60.png)](https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms)*
Building and contributing code
==============================
Instructions on how to build TextSecure, as well as on how to setup an IDE to modify it can be found in the "BUILDING.md" file.
Bug tracker
-----------
Have a bug? Please create an issue here on GitHub!
## Contributing Bug reports
We use GitHub for bug tracking. Please search the existing issues for your bug and create a new one if the issue is not yet tracked!
https://github.com/WhisperSystems/TextSecure/issues
## Contributing Translations
Interested in helping to translate TextSecure? Contribute here:
Documentation
-------------
https://www.transifex.com/projects/p/textsecure-official/
Looking for documentation? Check out the wiki!
## Contributing Code
Instructions on how to setup your development environment and build TextSecure can be found in [BUILDING.md](https://github.com/WhisperSystems/TextSecure/blob/master/BUILDING.md).
https://github.com/WhisperSystems/TextSecure/wiki
If you're new to the TextSecure codebase, we recommend going through our issues and picking out a simple bug to fix (check the "easy" label in our issues) in order to get yourself familiar.
Mailing list
------------
For larger changes and feature ideas, we ask that you propose it on the mailing list for a high-level discussion before implementation.
Have a question? Ask on our mailing list!
This repository is set up with [BitHub](https://whispersystems.org/blog/bithub/), so you can make money for committing to TextSecure. The current BitHub price for an accepted pull request is:
[![Current BitHub Price](https://bithub.herokuapp.com/v1/status/payment/commit/)](https://whispersystems.org/blog/bithub/)
## Contributing Ideas
Have something you want to say about Open Whisper Systems projects or want to be part of the conversation? Get involved in the mailing list!
whispersystems@lists.riseup.net
https://lists.riseup.net/www/info/whispersystems
Translation
------------
## Contributing Funds
[![Donate](https://coinbase.com/assets/buttons/donation_large-6ec72b1a9eec516944e50a22aca7db35.png)](https://whispersystems.org/blog/bithub/)
Interested in helping to translate TextSecure? Contribute here:
You can add funds to BitHub to directly help further development efforts.
https://www.transifex.com/projects/p/textsecure-official/
Help
====
## Support
For troubleshooting and questions, please visit our support center!
Downloads
------------
http://support.whispersystems.org/
TextSecure can be downloaded from the Play Store here:
## Documentation
Looking for documentation? Check out the wiki!
https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms
https://github.com/WhisperSystems/TextSecure/wiki
Cryptography Notice
------------
# Legal things
## Cryptography Notice
This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software.
BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted.
This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software.
BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted.
See <http://www.wassenaar.org/> for more information.
The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms.
The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms.
The form and manner of this distribution makes it eligible for export under the License Exception ENC Technology Software Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Section 740.13) for both object code and source code.
License
---------------------
## License
Copyright 2011 Whisper Systems
Copyright 2013 Open WhisperSystems
Copyright 2013-2014 Open Whisper Systems
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html

View File

@@ -0,0 +1,118 @@
package org.thoughtcrime.securesms.database;
import android.test.InstrumentationTestCase;
import static org.fest.assertions.api.Assertions.assertThat;
public class CanonicalAddressDatabaseTest extends InstrumentationTestCase {
private static final String AMBIGUOUS_NUMBER = "222-3333";
private static final String SPECIFIC_NUMBER = "+49 444 222 3333";
private static final String EMAIL = "a@b.fom";
private static final String SIMILAR_EMAIL = "a@b.com";
private static final String GROUP = "__textsecure_group__!000111222333";
private static final String SIMILAR_GROUP = "__textsecure_group__!100111222333";
private static final String ALPHA = "T-Mobile";
private static final String SIMILAR_ALPHA = "T-Mobila";
private CanonicalAddressDatabase db;
public void setUp() throws Exception {
super.setUp();
this.db = CanonicalAddressDatabase.getInstance(getInstrumentation().getTargetContext());
}
public void tearDown() throws Exception {
}
/**
* Throw two equivalent numbers (one without locale info, one with full info) at the canonical
* address db and see that the caching and DB operations work properly in revealing the right
* addresses. This is run twice to ensure cache logic is hit.
*
* @throws Exception
*/
public void testNumberAddressUpdates() throws Exception {
final long id = db.getCanonicalAddressId(AMBIGUOUS_NUMBER);
assertThat(db.getAddressFromId(id)).isEqualTo(AMBIGUOUS_NUMBER);
assertThat(db.getCanonicalAddressId(SPECIFIC_NUMBER)).isEqualTo(id);
assertThat(db.getAddressFromId(id)).isEqualTo(SPECIFIC_NUMBER);
assertThat(db.getCanonicalAddressId(AMBIGUOUS_NUMBER)).isEqualTo(id);
assertThat(db.getCanonicalAddressId(AMBIGUOUS_NUMBER)).isEqualTo(id);
assertThat(db.getAddressFromId(id)).isEqualTo(AMBIGUOUS_NUMBER);
assertThat(db.getCanonicalAddressId(SPECIFIC_NUMBER)).isEqualTo(id);
assertThat(db.getAddressFromId(id)).isEqualTo(SPECIFIC_NUMBER);
assertThat(db.getCanonicalAddressId(AMBIGUOUS_NUMBER)).isEqualTo(id);
}
public void testSimilarNumbers() throws Exception {
assertThat(db.getCanonicalAddressId("This is a phone number 222-333-444"))
.isNotEqualTo(db.getCanonicalAddressId("222-333-4444"));
assertThat(db.getCanonicalAddressId("222-333-444"))
.isNotEqualTo(db.getCanonicalAddressId("222-333-4444"));
assertThat(db.getCanonicalAddressId("222-333-44"))
.isNotEqualTo(db.getCanonicalAddressId("222-333-4444"));
assertThat(db.getCanonicalAddressId("222-333-4"))
.isNotEqualTo(db.getCanonicalAddressId("222-333-4444"));
assertThat(db.getCanonicalAddressId("+49 222-333-4444"))
.isNotEqualTo(db.getCanonicalAddressId("+1 222-333-4444"));
assertThat(db.getCanonicalAddressId("1 222-333-4444"))
.isEqualTo(db.getCanonicalAddressId("222-333-4444"));
assertThat(db.getCanonicalAddressId("1 (222) 333-4444"))
.isEqualTo(db.getCanonicalAddressId("222-333-4444"));
assertThat(db.getCanonicalAddressId("+12223334444"))
.isEqualTo(db.getCanonicalAddressId("222-333-4444"));
assertThat(db.getCanonicalAddressId("+1 (222) 333.4444"))
.isEqualTo(db.getCanonicalAddressId("222-333-4444"));
assertThat(db.getCanonicalAddressId("+49 (222) 333.4444"))
.isEqualTo(db.getCanonicalAddressId("222-333-4444"));
}
public void testEmailAddresses() throws Exception {
final long emailId = db.getCanonicalAddressId(EMAIL);
final long similarEmailId = db.getCanonicalAddressId(SIMILAR_EMAIL);
assertThat(emailId).isNotEqualTo(similarEmailId);
assertThat(db.getAddressFromId(emailId)).isEqualTo(EMAIL);
assertThat(db.getAddressFromId(similarEmailId)).isEqualTo(SIMILAR_EMAIL);
}
public void testGroups() throws Exception {
final long groupId = db.getCanonicalAddressId(GROUP);
final long similarGroupId = db.getCanonicalAddressId(SIMILAR_GROUP);
assertThat(groupId).isNotEqualTo(similarGroupId);
assertThat(db.getAddressFromId(groupId)).isEqualTo(GROUP);
assertThat(db.getAddressFromId(similarGroupId)).isEqualTo(SIMILAR_GROUP);
}
public void testAlpha() throws Exception {
final long id = db.getCanonicalAddressId(ALPHA);
final long similarId = db.getCanonicalAddressId(SIMILAR_ALPHA);
assertThat(id).isNotEqualTo(similarId);
assertThat(db.getAddressFromId(id)).isEqualTo(ALPHA);
assertThat(db.getAddressFromId(similarId)).isEqualTo(SIMILAR_ALPHA);
}
public void testIsNumber() throws Exception {
assertThat(CanonicalAddressDatabase.isNumberAddress("+495556666777")).isTrue();
assertThat(CanonicalAddressDatabase.isNumberAddress("(222) 333-4444")).isTrue();
assertThat(CanonicalAddressDatabase.isNumberAddress("1 (222) 333-4444")).isTrue();
assertThat(CanonicalAddressDatabase.isNumberAddress("T-Mobile123")).isTrue();
assertThat(CanonicalAddressDatabase.isNumberAddress("333-4444")).isTrue();
assertThat(CanonicalAddressDatabase.isNumberAddress("12345")).isTrue();
assertThat(CanonicalAddressDatabase.isNumberAddress("T-Mobile")).isFalse();
assertThat(CanonicalAddressDatabase.isNumberAddress("T-Mobile1")).isFalse();
assertThat(CanonicalAddressDatabase.isNumberAddress("Wherever bank")).isFalse();
assertThat(CanonicalAddressDatabase.isNumberAddress("__textsecure_group__!afafafafafaf")).isFalse();
assertThat(CanonicalAddressDatabase.isNumberAddress("email@domain.com")).isFalse();
}
}

View File

@@ -0,0 +1,33 @@
package org.thoughtcrime.securesms.util;
import android.test.AndroidTestCase;
import junit.framework.AssertionFailedError;
import org.whispersystems.textsecure.util.InvalidNumberException;
import org.whispersystems.textsecure.util.PhoneNumberFormatter;
import static org.fest.assertions.api.Assertions.assertThat;
public class PhoneNumberFormatterTest extends AndroidTestCase {
private static final String LOCAL_NUMBER = "+15555555555";
public void testFormatNumberE164() throws Exception, InvalidNumberException {
assertThat(PhoneNumberFormatter.formatNumber("(555) 555-5555", LOCAL_NUMBER)).isEqualTo(LOCAL_NUMBER);
assertThat(PhoneNumberFormatter.formatNumber("555-5555", LOCAL_NUMBER)).isEqualTo(LOCAL_NUMBER);
assertThat(PhoneNumberFormatter.formatNumber("(123) 555-5555", LOCAL_NUMBER)).isNotEqualTo(LOCAL_NUMBER);
}
public void testFormatNumberEmail() throws Exception {
try {
PhoneNumberFormatter.formatNumber("person@domain.com", LOCAL_NUMBER);
throw new AssertionFailedError("should have thrown on email");
} catch (InvalidNumberException ine) {
// success
}
}
@Override
public void setUp() throws Exception {
super.setUp();
}
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

BIN
assets/emoji_0_wrapped.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

BIN
assets/emoji_1_wrapped.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 KiB

BIN
assets/emoji_2_wrapped.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 KiB

BIN
assets/emoji_3_wrapped.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

BIN
assets/emoji_4_wrapped.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

View File

@@ -3,11 +3,13 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.9.+'
classpath 'com.android.tools.build:gradle:0.12.+'
classpath files('libs/gradle-witness.jar')
}
}
apply plugin: 'android'
apply plugin: 'com.android.application'
apply plugin: 'witness'
repositories {
mavenCentral()
@@ -21,16 +23,32 @@ repositories {
dependencies {
compile 'com.actionbarsherlock:actionbarsherlock:4.4.0@aar'
compile 'com.android.support:support-v4:19.0.1'
compile 'com.google.android.gcm:gcm-client:1.0.2'
compile 'com.android.support:support-v4:19.1.0'
compile 'se.emilsjolander:stickylistheaders:2.2.0'
compile 'com.google.android.gms:play-services:5.0.77'
compile 'com.astuetz:pagerslidingtabstrip:1.0.1'
androidTestCompile 'com.squareup:fest-android:1.0.8'
compile project(':library')
}
dependencyVerification {
verify = [
'com.actionbarsherlock:actionbarsherlock:5ab04d74101f70024b222e3ff9c87bee151ec43331b4a2134b6cc08cf8565819',
'com.android.support:support-v4:3f40fa7b3a4ead01ce15dce9453b061646e7fe2e7c51cb75ca01ee1e77037f3f',
'se.emilsjolander:stickylistheaders:89146b46c96fea0e40200474a2625cda10fe94891e4128f53cdb42375091b9b6',
'com.astuetz:pagerslidingtabstrip:f1641396732c7132a7abb837e482e5ee2b0ebb8d10813fc52bbaec2c15c184c2',
'com.google.protobuf:protobuf-java:ad9769a22989e688a46af4d3accc348cc501ced22118033230542bc916e33f0b',
'com.madgag:sc-light-jdk15on:931f39d351429fb96c2f749e7ecb1a256a8ebbf5edca7995c9cc085b94d1841d',
'com.googlecode.libphonenumber:libphonenumber:eba17eae81dd622ea89a00a3a8c025b2f25d342e0d9644c5b62e16f15687c3ab',
'org.whispersystems:gson:08f4f7498455d1539c9233e5aac18e9b1805815ef29221572996508eb512fe51',
]
}
android {
compileSdkVersion 19
buildToolsVersion '19.0.2'
buildToolsVersion '19.1.0'
defaultConfig {
minSdkVersion 9
@@ -48,6 +66,12 @@ android {
res.srcDirs = ['res']
assets.srcDirs = ['assets']
}
androidTest {
java.srcDirs = ['androidTest']
resources.srcDirs = ['androidTest']
aidl.srcDirs = ['androidTest']
renderscript.srcDirs = ['androidTest']
}
}
}

View File

@@ -1,3 +1,30 @@
##Translations
Please do not submit issues or pull requests for translation fixes. Anyone can update the translations in [Transifex](https://www.transifex.com/projects/p/textsecure-official/). Please submit your corrections there.
## Submitting useful bug reports
1. Search our issues first to make sure this is not a duplicate.
1. Read the [Submitting useful bug reports guide](https://github.com/WhisperSystems/TextSecure/wiki/Submitting-useful-bug-reports) before posting a bug.
## Development Ideology
Truths which we believe to be self-evident:
1. **The answer is not more options.** If you feel compelled to add a
preference that's exposed to the user, it's very possible you've made
a wrong turn somewhere.
1. **The user doesn't know what a key is.** We need to minimize the points
at which a user is exposed to this sort of terminology as extremely as
possible.
1. **There are no power users.** The idea that some users "understand"
concepts better than others has proven to be, for the most part, false.
If anything, "power users" are more dangerous than the rest, and we
should avoid exposing dangerous functionality to them.
1. **If it's "like PGP," it's wrong.** PGP is our spirit guide for what
not to do.
1. **It's an asynchronous world.** Be wary of anything that is
anti-asynchronous: ACKs, protocol confirmations, or any protocol-level
"advisory" message.
1. **There is no such thing as time.** Protocol ideas that require synchronized
clocks are doomed to failure.

View File

@@ -1,6 +1,6 @@
#Mon Mar 10 23:44:05 PDT 2014
#Mon Jun 09 23:26:49 PDT 2014
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=http\://services.gradle.org/distributions/gradle-1.11-all.zip
distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-all.zip

View File

@@ -4,7 +4,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:0.9.+'
classpath 'com.android.tools.build:gradle:0.12.+'
}
}
@@ -21,13 +21,13 @@ repositories {
dependencies {
compile 'com.google.protobuf:protobuf-java:2.4.1'
compile 'com.madgag:sc-light-jdk15on:1.47.0.2'
compile 'com.googlecode.libphonenumber:libphonenumber:5.3'
compile 'com.googlecode.libphonenumber:libphonenumber:6.1'
compile 'org.whispersystems:gson:2.2.4'
}
android {
compileSdkVersion 19
buildToolsVersion '19.0.2'
buildToolsVersion '19.1.0'
android {
sourceSets {

View File

@@ -23,7 +23,6 @@ import android.os.Parcelable;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util;
/**
* A class for representing an identity key.
@@ -80,14 +79,7 @@ public class IdentityKey implements Parcelable, SerializableKey {
}
public byte[] serialize() {
if (publicKey.getType() == Curve.NIST_TYPE) {
byte[] versionBytes = {0x01};
byte[] encodedKey = publicKey.serialize();
return Util.combine(versionBytes, encodedKey);
} else {
return publicKey.serialize();
}
return publicKey.serialize();
}
public String getFingerprint() {

View File

@@ -1,81 +0,0 @@
/**
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.crypto;
import android.util.Log;
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
import org.whispersystems.textsecure.crypto.ecc.ECPrivateKey;
import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util;
/**
* Represents a session's active KeyPair.
*
* @author Moxie Marlinspike
*/
public class KeyPair {
private PublicKey publicKey;
private ECPrivateKey privateKey;
private final MasterCipher masterCipher;
public KeyPair(int keyPairId, ECKeyPair keyPair, MasterSecret masterSecret) {
this.masterCipher = new MasterCipher(masterSecret);
this.publicKey = new PublicKey(keyPairId, keyPair.getPublicKey());
this.privateKey = keyPair.getPrivateKey();
}
public KeyPair(byte[] bytes, MasterCipher masterCipher) throws InvalidKeyException {
this.masterCipher = masterCipher;
deserialize(bytes);
}
public int getId() {
return publicKey.getId();
}
public PublicKey getPublicKey() {
return publicKey;
}
public ECPrivateKey getPrivateKey() {
return privateKey;
}
public byte[] toBytes() {
return serialize();
}
private void deserialize(byte[] bytes) throws InvalidKeyException {
this.publicKey = new PublicKey(bytes);
byte[] privateKeyBytes = new byte[bytes.length - PublicKey.KEY_SIZE];
System.arraycopy(bytes, PublicKey.KEY_SIZE, privateKeyBytes, 0, privateKeyBytes.length);
this.privateKey = masterCipher.decryptKey(this.publicKey.getType(), privateKeyBytes);
}
public byte[] serialize() {
byte[] publicKeyBytes = publicKey.serialize();
Log.w("KeyPair", "Serialized public key bytes: " + Hex.toString(publicKeyBytes));
byte[] privateKeyBytes = masterCipher.encryptKey(privateKey);
return Util.combine(publicKeyBytes, privateKeyBytes);
}
}

View File

@@ -0,0 +1,7 @@
package org.whispersystems.textsecure.crypto;
public class LegacyMessageException extends Exception {
public LegacyMessageException(String s) {
super(s);
}
}

View File

@@ -83,11 +83,11 @@ public class MasterCipher {
return new String(decodeAndDecryptBytes(body));
}
public ECPrivateKey decryptKey(int type, byte[] key)
public ECPrivateKey decryptKey(byte[] key)
throws org.whispersystems.textsecure.crypto.InvalidKeyException
{
try {
return Curve.decodePrivatePoint(type, decryptBytes(key));
return Curve.decodePrivatePoint(decryptBytes(key));
} catch (InvalidMessageException ime) {
throw new org.whispersystems.textsecure.crypto.InvalidKeyException(ime);
}

View File

@@ -18,31 +18,258 @@ package org.whispersystems.textsecure.crypto;
import android.content.Context;
import android.util.Log;
import android.util.Pair;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.crypto.protocol.PreKeyWhisperMessage;
import org.whispersystems.textsecure.crypto.protocol.WhisperMessage;
import org.whispersystems.textsecure.crypto.ratchet.ChainKey;
import org.whispersystems.textsecure.crypto.ratchet.MessageKeys;
import org.whispersystems.textsecure.crypto.ratchet.RootKey;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.storage.SessionRecordV1;
import org.whispersystems.textsecure.storage.SessionRecordV2;
import org.whispersystems.textsecure.storage.SessionState;
import org.whispersystems.textsecure.util.Conversions;
public abstract class SessionCipher {
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
protected static final Object SESSION_LOCK = new Object();
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public abstract CiphertextMessage encrypt(byte[] paddedMessage);
public abstract byte[] decrypt(byte[] decodedMessage) throws InvalidMessageException, DuplicateMessageException;
public abstract int getRemoteRegistrationId();
public class SessionCipher {
private static final Object SESSION_LOCK = new Object();
private final Context context;
private final MasterSecret masterSecret;
private final RecipientDevice recipient;
public static SessionCipher createFor(Context context,
MasterSecret masterSecret,
RecipientDevice recipient)
{
if (SessionRecordV2.hasSession(context, masterSecret, recipient)) {
return new SessionCipherV2(context, masterSecret, recipient);
} else if (SessionRecordV1.hasSession(context, recipient.getRecipientId())) {
return new SessionCipherV1(context, masterSecret, recipient.getRecipient());
return new SessionCipher(context, masterSecret, recipient);
} else {
throw new AssertionError("Attempt to initialize cipher for non-existing session.");
}
}
private SessionCipher(Context context, MasterSecret masterSecret, RecipientDevice recipient) {
this.recipient = recipient;
this.masterSecret = masterSecret;
this.context = context;
}
public CiphertextMessage encrypt(byte[] paddedMessage) {
synchronized (SESSION_LOCK) {
SessionRecordV2 sessionRecord = getSessionRecord();
SessionState sessionState = sessionRecord.getSessionState();
ChainKey chainKey = sessionState.getSenderChainKey();
MessageKeys messageKeys = chainKey.getMessageKeys();
ECPublicKey senderEphemeral = sessionState.getSenderEphemeral();
int previousCounter = sessionState.getPreviousCounter();
byte[] ciphertextBody = getCiphertext(messageKeys, paddedMessage);
CiphertextMessage ciphertextMessage = new WhisperMessage(messageKeys.getMacKey(),
senderEphemeral, chainKey.getIndex(),
previousCounter, ciphertextBody);
if (sessionState.hasPendingPreKey()) {
Pair<Integer, ECPublicKey> pendingPreKey = sessionState.getPendingPreKey();
int localRegistrationId = sessionState.getLocalRegistrationId();
ciphertextMessage = new PreKeyWhisperMessage(localRegistrationId, pendingPreKey.first,
pendingPreKey.second,
sessionState.getLocalIdentityKey(),
(WhisperMessage) ciphertextMessage);
}
sessionState.setSenderChainKey(chainKey.getNextChainKey());
sessionRecord.save();
return ciphertextMessage;
}
}
public byte[] decrypt(byte[] decodedMessage)
throws InvalidMessageException, DuplicateMessageException, LegacyMessageException
{
synchronized (SESSION_LOCK) {
SessionRecordV2 sessionRecord = getSessionRecord();
SessionState sessionState = sessionRecord.getSessionState();
List<SessionState> previousStates = sessionRecord.getPreviousSessions();
try {
byte[] plaintext = decrypt(sessionState, decodedMessage);
sessionRecord.save();
return plaintext;
} catch (InvalidMessageException e) {
Log.w("SessionCipherV2", e);
}
for (SessionState previousState : previousStates) {
try {
Log.w("SessionCipherV2", "Attempting decrypt on previous state...");
byte[] plaintext = decrypt(previousState, decodedMessage);
sessionRecord.save();
return plaintext;
} catch (InvalidMessageException e) {
Log.w("SessionCipherV2", e);
}
}
throw new InvalidMessageException("No valid sessions.");
}
}
public byte[] decrypt(SessionState sessionState, byte[] decodedMessage)
throws InvalidMessageException, DuplicateMessageException, LegacyMessageException
{
if (!sessionState.hasSenderChain()) {
throw new InvalidMessageException("Uninitialized session!");
}
WhisperMessage ciphertextMessage = new WhisperMessage(decodedMessage);
ECPublicKey theirEphemeral = ciphertextMessage.getSenderEphemeral();
int counter = ciphertextMessage.getCounter();
ChainKey chainKey = getOrCreateChainKey(sessionState, theirEphemeral);
MessageKeys messageKeys = getOrCreateMessageKeys(sessionState, theirEphemeral,
chainKey, counter);
ciphertextMessage.verifyMac(messageKeys.getMacKey());
byte[] plaintext = getPlaintext(messageKeys, ciphertextMessage.getBody());
sessionState.clearPendingPreKey();
return plaintext;
}
public int getRemoteRegistrationId() {
synchronized (SESSION_LOCK) {
SessionRecordV2 sessionRecord = getSessionRecord();
return sessionRecord.getSessionState().getRemoteRegistrationId();
}
}
private ChainKey getOrCreateChainKey(SessionState sessionState, ECPublicKey theirEphemeral)
throws InvalidMessageException
{
try {
if (sessionState.hasReceiverChain(theirEphemeral)) {
return sessionState.getReceiverChainKey(theirEphemeral);
} else {
RootKey rootKey = sessionState.getRootKey();
ECKeyPair ourEphemeral = sessionState.getSenderEphemeralPair();
Pair<RootKey, ChainKey> receiverChain = rootKey.createChain(theirEphemeral, ourEphemeral);
ECKeyPair ourNewEphemeral = Curve.generateKeyPair(true);
Pair<RootKey, ChainKey> senderChain = receiverChain.first.createChain(theirEphemeral, ourNewEphemeral);
sessionState.setRootKey(senderChain.first);
sessionState.addReceiverChain(theirEphemeral, receiverChain.second);
sessionState.setPreviousCounter(sessionState.getSenderChainKey().getIndex()-1);
sessionState.setSenderChain(ourNewEphemeral, senderChain.second);
return receiverChain.second;
}
} catch (InvalidKeyException e) {
throw new InvalidMessageException(e);
}
}
private MessageKeys getOrCreateMessageKeys(SessionState sessionState,
ECPublicKey theirEphemeral,
ChainKey chainKey, int counter)
throws InvalidMessageException, DuplicateMessageException
{
if (chainKey.getIndex() > counter) {
if (sessionState.hasMessageKeys(theirEphemeral, counter)) {
return sessionState.removeMessageKeys(theirEphemeral, counter);
} else {
throw new DuplicateMessageException("Received message with old counter: " +
chainKey.getIndex() + " , " + counter);
}
}
if (chainKey.getIndex() - counter > 2000) {
throw new InvalidMessageException("Over 2000 messages into the future!");
}
while (chainKey.getIndex() < counter) {
MessageKeys messageKeys = chainKey.getMessageKeys();
sessionState.setMessageKeys(theirEphemeral, messageKeys);
chainKey = chainKey.getNextChainKey();
}
sessionState.setReceiverChainKey(theirEphemeral, chainKey.getNextChainKey());
return chainKey.getMessageKeys();
}
private byte[] getCiphertext(MessageKeys messageKeys, byte[] plaintext) {
try {
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE,
messageKeys.getCipherKey(),
messageKeys.getCounter());
return cipher.doFinal(plaintext);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new AssertionError(e);
}
}
private byte[] getPlaintext(MessageKeys messageKeys, byte[] cipherText) {
try {
Cipher cipher = getCipher(Cipher.DECRYPT_MODE,
messageKeys.getCipherKey(),
messageKeys.getCounter());
return cipher.doFinal(cipherText);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new AssertionError(e);
}
}
private Cipher getCipher(int mode, SecretKeySpec key, int counter) {
try {
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
byte[] ivBytes = new byte[16];
Conversions.intToByteArray(ivBytes, 0, counter);
IvParameterSpec iv = new IvParameterSpec(ivBytes);
cipher.init(mode, key, iv);
return cipher;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (NoSuchPaddingException e) {
throw new AssertionError(e);
} catch (java.security.InvalidKeyException e) {
throw new AssertionError(e);
} catch (InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
}
}
private SessionRecordV2 getSessionRecord() {
return new SessionRecordV2(context, masterSecret, recipient);
}
}

View File

@@ -1,332 +0,0 @@
package org.whispersystems.textsecure.crypto;
import android.content.Context;
import android.util.Log;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets;
import org.whispersystems.textsecure.crypto.kdf.NKDF;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.crypto.protocol.WhisperMessageV1;
import org.whispersystems.textsecure.storage.CanonicalRecipient;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.storage.InvalidKeyIdException;
import org.whispersystems.textsecure.storage.LocalKeyRecord;
import org.whispersystems.textsecure.storage.RemoteKeyRecord;
import org.whispersystems.textsecure.storage.SessionKey;
import org.whispersystems.textsecure.storage.SessionRecordV1;
import org.whispersystems.textsecure.util.Conversions;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class SessionCipherV1 extends SessionCipher {
private final Context context;
private final MasterSecret masterSecret;
private final CanonicalRecipient recipient;
public SessionCipherV1(Context context,
MasterSecret masterSecret,
CanonicalRecipient recipient)
{
this.context = context;
this.masterSecret = masterSecret;
this.recipient = recipient;
}
public CiphertextMessage encrypt(byte[] paddedMessageBody) {
synchronized (SESSION_LOCK) {
SessionCipherContext encryptionContext = getEncryptionContext();
byte[] cipherText = getCiphertext(paddedMessageBody,
encryptionContext.getSessionKey().getCipherKey(),
encryptionContext.getSessionRecord().getCounter());
encryptionContext.getSessionRecord().setSessionKey(encryptionContext.getSessionKey());
encryptionContext.getSessionRecord().incrementCounter();
encryptionContext.getSessionRecord().save();
return new WhisperMessageV1(encryptionContext, cipherText);
}
}
public byte[] decrypt(byte[] decodedCiphertext) throws InvalidMessageException {
synchronized (SESSION_LOCK) {
WhisperMessageV1 message = new WhisperMessageV1(decodedCiphertext);
SessionCipherContext decryptionContext = getDecryptionContext(message);
message.verifyMac(decryptionContext);
byte[] plaintextWithPadding = getPlaintext(message.getBody(),
decryptionContext.getSessionKey().getCipherKey(),
decryptionContext.getCounter());
decryptionContext.getRemoteKeyRecord().updateCurrentRemoteKey(decryptionContext.getNextKey());
decryptionContext.getRemoteKeyRecord().save();
decryptionContext.getLocalKeyRecord().advanceKeyIfNecessary(decryptionContext.getRecipientKeyId());
decryptionContext.getLocalKeyRecord().save();
decryptionContext.getSessionRecord().setSessionKey(decryptionContext.getSessionKey());
decryptionContext.getSessionRecord().save();
return plaintextWithPadding;
}
}
@Override
public int getRemoteRegistrationId() {
return 0;
}
private SessionCipherContext getEncryptionContext() {
try {
KeyRecords records = getKeyRecords(context, masterSecret, recipient);
int localKeyId = records.getLocalKeyRecord().getCurrentKeyPair().getId();
int remoteKeyId = records.getRemoteKeyRecord().getCurrentRemoteKey().getId();
int sessionVersion = records.getSessionRecord().getSessionVersion();
SessionKey sessionKey = getSessionKey(masterSecret, Cipher.ENCRYPT_MODE,
records, localKeyId, remoteKeyId);
PublicKey nextKey = records.getLocalKeyRecord().getNextKeyPair().getPublicKey();
int counter = records.getSessionRecord().getCounter();
return new SessionCipherContext(records, sessionKey, localKeyId, remoteKeyId,
nextKey, counter, sessionVersion);
} catch (InvalidKeyIdException e) {
throw new IllegalArgumentException(e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException(e);
}
}
public SessionCipherContext getDecryptionContext(WhisperMessageV1 message)
throws InvalidMessageException
{
try {
KeyRecords records = getKeyRecords(context, masterSecret, recipient);
int messageVersion = message.getCurrentVersion();
int recipientKeyId = message.getReceiverKeyId();
int senderKeyId = message.getSenderKeyId();
PublicKey nextKey = new PublicKey(message.getNextKeyBytes());
int counter = message.getCounter();
if (messageVersion < records.getSessionRecord().getSessionVersion()) {
throw new InvalidMessageException("Message version: " + messageVersion +
" but negotiated session version: " +
records.getSessionRecord().getSessionVersion());
}
SessionKey sessionKey = getSessionKey(masterSecret, Cipher.DECRYPT_MODE,
records, recipientKeyId, senderKeyId);
return new SessionCipherContext(records, sessionKey, senderKeyId,
recipientKeyId, nextKey, counter,
messageVersion);
} catch (InvalidKeyIdException e) {
throw new InvalidMessageException(e);
} catch (InvalidKeyException e) {
throw new InvalidMessageException(e);
}
}
private byte[] getCiphertext(byte[] message, SecretKeySpec key, int counter) {
try {
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, key, counter);
return cipher.doFinal(message);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new AssertionError(e);
}
}
private byte[] getPlaintext(byte[] cipherText, SecretKeySpec key, int counter) {
try {
Cipher cipher = getCipher(Cipher.DECRYPT_MODE, key, counter);
return cipher.doFinal(cipherText);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new AssertionError(e);
}
}
private Cipher getCipher(int mode, SecretKeySpec key, int counter) {
try {
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
byte[] ivBytes = new byte[16];
Conversions.mediumToByteArray(ivBytes, 0, counter);
IvParameterSpec iv = new IvParameterSpec(ivBytes);
cipher.init(mode, key, iv);
return cipher;
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("AES Not Supported!");
} catch (NoSuchPaddingException e) {
throw new IllegalArgumentException("NoPadding Not Supported!");
} catch (java.security.InvalidKeyException e) {
Log.w("SessionCipher", e);
throw new IllegalArgumentException("Invaid Key?");
} catch (InvalidAlgorithmParameterException e) {
Log.w("SessionCipher", e);
throw new IllegalArgumentException("Bad IV?");
}
}
private SessionKey getSessionKey(MasterSecret masterSecret, int mode,
KeyRecords records,
int localKeyId, int remoteKeyId)
throws InvalidKeyIdException, InvalidKeyException
{
Log.w("SessionCipher", "Getting session key for local: " + localKeyId + " remote: " + remoteKeyId);
SessionKey sessionKey = records.getSessionRecord().getSessionKey(mode, localKeyId, remoteKeyId);
if (sessionKey != null)
return sessionKey;
DerivedSecrets derivedSecrets = calculateSharedSecret(mode, records, localKeyId, remoteKeyId);
return new SessionKey(mode, localKeyId, remoteKeyId, derivedSecrets.getCipherKey(),
derivedSecrets.getMacKey(), masterSecret);
}
private DerivedSecrets calculateSharedSecret(int mode, KeyRecords records,
int localKeyId, int remoteKeyId)
throws InvalidKeyIdException, InvalidKeyException
{
NKDF kdf = new NKDF();
KeyPair localKeyPair = records.getLocalKeyRecord().getKeyPairForId(localKeyId);
ECPublicKey remoteKey = records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getKey();
byte[] sharedSecret = Curve.calculateAgreement(remoteKey, localKeyPair.getPrivateKey());
boolean isLowEnd = isLowEnd(records, localKeyId, remoteKeyId);
isLowEnd = (mode == Cipher.ENCRYPT_MODE ? isLowEnd : !isLowEnd);
return kdf.deriveSecrets(sharedSecret, isLowEnd);
}
private boolean isLowEnd(KeyRecords records, int localKeyId, int remoteKeyId)
throws InvalidKeyIdException
{
ECPublicKey localPublic = records.getLocalKeyRecord().getKeyPairForId(localKeyId).getPublicKey().getKey();
ECPublicKey remotePublic = records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getKey();
return localPublic.compareTo(remotePublic) < 0;
}
private KeyRecords getKeyRecords(Context context, MasterSecret masterSecret,
CanonicalRecipient recipient)
{
LocalKeyRecord localKeyRecord = new LocalKeyRecord(context, masterSecret, recipient);
RemoteKeyRecord remoteKeyRecord = new RemoteKeyRecord(context, recipient);
SessionRecordV1 sessionRecord = new SessionRecordV1(context, masterSecret, recipient);
return new KeyRecords(localKeyRecord, remoteKeyRecord, sessionRecord);
}
private static class KeyRecords {
private final LocalKeyRecord localKeyRecord;
private final RemoteKeyRecord remoteKeyRecord;
private final SessionRecordV1 sessionRecord;
public KeyRecords(LocalKeyRecord localKeyRecord,
RemoteKeyRecord remoteKeyRecord,
SessionRecordV1 sessionRecord)
{
this.localKeyRecord = localKeyRecord;
this.remoteKeyRecord = remoteKeyRecord;
this.sessionRecord = sessionRecord;
}
private LocalKeyRecord getLocalKeyRecord() {
return localKeyRecord;
}
private RemoteKeyRecord getRemoteKeyRecord() {
return remoteKeyRecord;
}
private SessionRecordV1 getSessionRecord() {
return sessionRecord;
}
}
public static class SessionCipherContext {
private final LocalKeyRecord localKeyRecord;
private final RemoteKeyRecord remoteKeyRecord;
private final SessionRecordV1 sessionRecord;
private final SessionKey sessionKey;
private final int senderKeyId;
private final int recipientKeyId;
private final PublicKey nextKey;
private final int counter;
private final int messageVersion;
public SessionCipherContext(KeyRecords records,
SessionKey sessionKey,
int senderKeyId,
int receiverKeyId,
PublicKey nextKey,
int counter,
int messageVersion)
{
this.localKeyRecord = records.getLocalKeyRecord();
this.remoteKeyRecord = records.getRemoteKeyRecord();
this.sessionRecord = records.getSessionRecord();
this.sessionKey = sessionKey;
this.senderKeyId = senderKeyId;
this.recipientKeyId = receiverKeyId;
this.nextKey = nextKey;
this.counter = counter;
this.messageVersion = messageVersion;
}
public LocalKeyRecord getLocalKeyRecord() {
return localKeyRecord;
}
public RemoteKeyRecord getRemoteKeyRecord() {
return remoteKeyRecord;
}
public SessionRecordV1 getSessionRecord() {
return sessionRecord;
}
public SessionKey getSessionKey() {
return sessionKey;
}
public PublicKey getNextKey() {
return nextKey;
}
public int getCounter() {
return counter;
}
public int getSenderKeyId() {
return senderKeyId;
}
public int getRecipientKeyId() {
return recipientKeyId;
}
public int getMessageVersion() {
return messageVersion;
}
}
}

View File

@@ -1,252 +0,0 @@
package org.whispersystems.textsecure.crypto;
import android.content.Context;
import android.util.Log;
import android.util.Pair;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.crypto.protocol.PreKeyWhisperMessage;
import org.whispersystems.textsecure.crypto.protocol.WhisperMessageV2;
import org.whispersystems.textsecure.crypto.ratchet.ChainKey;
import org.whispersystems.textsecure.crypto.ratchet.MessageKeys;
import org.whispersystems.textsecure.crypto.ratchet.RootKey;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.storage.SessionRecordV2;
import org.whispersystems.textsecure.storage.SessionState;
import org.whispersystems.textsecure.util.Conversions;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class SessionCipherV2 extends SessionCipher {
private final Context context;
private final MasterSecret masterSecret;
private final RecipientDevice recipient;
public SessionCipherV2(Context context,
MasterSecret masterSecret,
RecipientDevice recipient)
{
this.context = context;
this.masterSecret = masterSecret;
this.recipient = recipient;
}
@Override
public CiphertextMessage encrypt(byte[] paddedMessage) {
synchronized (SESSION_LOCK) {
SessionRecordV2 sessionRecord = getSessionRecord();
SessionState sessionState = sessionRecord.getSessionState();
ChainKey chainKey = sessionState.getSenderChainKey();
MessageKeys messageKeys = chainKey.getMessageKeys();
ECPublicKey senderEphemeral = sessionState.getSenderEphemeral();
int previousCounter = sessionState.getPreviousCounter();
byte[] ciphertextBody = getCiphertext(messageKeys, paddedMessage);
CiphertextMessage ciphertextMessage = new WhisperMessageV2(messageKeys.getMacKey(),
senderEphemeral, chainKey.getIndex(),
previousCounter, ciphertextBody);
if (sessionState.hasPendingPreKey()) {
Pair<Integer, ECPublicKey> pendingPreKey = sessionState.getPendingPreKey();
int localRegistrationId = sessionState.getLocalRegistrationId();
ciphertextMessage = new PreKeyWhisperMessage(localRegistrationId, pendingPreKey.first,
pendingPreKey.second,
sessionState.getLocalIdentityKey(),
(WhisperMessageV2) ciphertextMessage);
}
sessionState.setSenderChainKey(chainKey.getNextChainKey());
sessionRecord.save();
return ciphertextMessage;
}
}
@Override
public byte[] decrypt(byte[] decodedMessage)
throws InvalidMessageException, DuplicateMessageException
{
synchronized (SESSION_LOCK) {
SessionRecordV2 sessionRecord = getSessionRecord();
SessionState sessionState = sessionRecord.getSessionState();
List<SessionState> previousStates = sessionRecord.getPreviousSessions();
try {
byte[] plaintext = decrypt(sessionState, decodedMessage);
sessionRecord.save();
return plaintext;
} catch (InvalidMessageException e) {
Log.w("SessionCipherV2", e);
}
for (SessionState previousState : previousStates) {
try {
Log.w("SessionCipherV2", "Attempting decrypt on previous state...");
byte[] plaintext = decrypt(previousState, decodedMessage);
sessionRecord.save();
return plaintext;
} catch (InvalidMessageException e) {
Log.w("SessionCipherV2", e);
}
}
throw new InvalidMessageException("No valid sessions.");
}
}
public byte[] decrypt(SessionState sessionState, byte[] decodedMessage)
throws InvalidMessageException, DuplicateMessageException
{
if (!sessionState.hasSenderChain()) {
throw new InvalidMessageException("Uninitialized session!");
}
WhisperMessageV2 ciphertextMessage = new WhisperMessageV2(decodedMessage);
ECPublicKey theirEphemeral = ciphertextMessage.getSenderEphemeral();
int counter = ciphertextMessage.getCounter();
ChainKey chainKey = getOrCreateChainKey(sessionState, theirEphemeral);
MessageKeys messageKeys = getOrCreateMessageKeys(sessionState, theirEphemeral,
chainKey, counter);
ciphertextMessage.verifyMac(messageKeys.getMacKey());
byte[] plaintext = getPlaintext(messageKeys, ciphertextMessage.getBody());
sessionState.clearPendingPreKey();
return plaintext;
}
@Override
public int getRemoteRegistrationId() {
synchronized (SESSION_LOCK) {
SessionRecordV2 sessionRecord = getSessionRecord();
return sessionRecord.getSessionState().getRemoteRegistrationId();
}
}
private ChainKey getOrCreateChainKey(SessionState sessionState, ECPublicKey theirEphemeral)
throws InvalidMessageException
{
try {
if (sessionState.hasReceiverChain(theirEphemeral)) {
return sessionState.getReceiverChainKey(theirEphemeral);
} else {
RootKey rootKey = sessionState.getRootKey();
ECKeyPair ourEphemeral = sessionState.getSenderEphemeralPair();
Pair<RootKey, ChainKey> receiverChain = rootKey.createChain(theirEphemeral, ourEphemeral);
ECKeyPair ourNewEphemeral = Curve.generateKeyPairForType(Curve.DJB_TYPE, true);
Pair<RootKey, ChainKey> senderChain = receiverChain.first.createChain(theirEphemeral, ourNewEphemeral);
sessionState.setRootKey(senderChain.first);
sessionState.addReceiverChain(theirEphemeral, receiverChain.second);
sessionState.setPreviousCounter(sessionState.getSenderChainKey().getIndex()-1);
sessionState.setSenderChain(ourNewEphemeral, senderChain.second);
return receiverChain.second;
}
} catch (InvalidKeyException e) {
throw new InvalidMessageException(e);
}
}
private MessageKeys getOrCreateMessageKeys(SessionState sessionState,
ECPublicKey theirEphemeral,
ChainKey chainKey, int counter)
throws InvalidMessageException, DuplicateMessageException
{
if (chainKey.getIndex() > counter) {
if (sessionState.hasMessageKeys(theirEphemeral, counter)) {
return sessionState.removeMessageKeys(theirEphemeral, counter);
} else {
throw new DuplicateMessageException("Received message with old counter: " +
chainKey.getIndex() + " , " + counter);
}
}
if (chainKey.getIndex() - counter > 2000) {
throw new InvalidMessageException("Over 2000 messages into the future!");
}
while (chainKey.getIndex() < counter) {
MessageKeys messageKeys = chainKey.getMessageKeys();
sessionState.setMessageKeys(theirEphemeral, messageKeys);
chainKey = chainKey.getNextChainKey();
}
sessionState.setReceiverChainKey(theirEphemeral, chainKey.getNextChainKey());
return chainKey.getMessageKeys();
}
private byte[] getCiphertext(MessageKeys messageKeys, byte[] plaintext) {
try {
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE,
messageKeys.getCipherKey(),
messageKeys.getCounter());
return cipher.doFinal(plaintext);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new AssertionError(e);
}
}
private byte[] getPlaintext(MessageKeys messageKeys, byte[] cipherText) {
try {
Cipher cipher = getCipher(Cipher.DECRYPT_MODE,
messageKeys.getCipherKey(),
messageKeys.getCounter());
return cipher.doFinal(cipherText);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new AssertionError(e);
}
}
private Cipher getCipher(int mode, SecretKeySpec key, int counter) {
try {
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
byte[] ivBytes = new byte[16];
Conversions.intToByteArray(ivBytes, 0, counter);
IvParameterSpec iv = new IvParameterSpec(ivBytes);
cipher.init(mode, key, iv);
return cipher;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (NoSuchPaddingException e) {
throw new AssertionError(e);
} catch (java.security.InvalidKeyException e) {
throw new AssertionError(e);
} catch (InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
}
}
private SessionRecordV2 getSessionRecord() {
return new SessionRecordV2(context, masterSecret, recipient);
}
}

View File

@@ -21,26 +21,10 @@ import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
public class Curve {
public static final int NIST_TYPE = 0x02;
private static final int NIST_TYPE2 = 0x03;
public static final int DJB_TYPE = 0x05;
public static ECKeyPair generateKeyPairForType(int keyType, boolean ephemeral) {
if (keyType == DJB_TYPE) {
return Curve25519.generateKeyPair(ephemeral);
} else if (keyType == NIST_TYPE || keyType == NIST_TYPE2) {
return CurveP256.generateKeyPair();
} else {
throw new AssertionError("Bad key type: " + keyType);
}
}
public static ECKeyPair generateKeyPairForSession(int messageVersion, boolean ephemeral) {
if (messageVersion <= CiphertextMessage.LEGACY_VERSION) {
return generateKeyPairForType(NIST_TYPE, ephemeral);
} else {
return generateKeyPairForType(DJB_TYPE, ephemeral);
}
public static ECKeyPair generateKeyPair(boolean ephemeral) {
return Curve25519.generateKeyPair(ephemeral);
}
public static ECPublicKey decodePoint(byte[] bytes, int offset)
@@ -50,21 +34,13 @@ public class Curve {
if (type == DJB_TYPE) {
return Curve25519.decodePoint(bytes, offset);
} else if (type == NIST_TYPE || type == NIST_TYPE2) {
return CurveP256.decodePoint(bytes, offset);
} else {
throw new InvalidKeyException("Unknown key type: " + type);
}
}
public static ECPrivateKey decodePrivatePoint(int type, byte[] bytes) {
if (type == DJB_TYPE) {
return new DjbECPrivateKey(bytes);
} else if (type == NIST_TYPE || type == NIST_TYPE2) {
return CurveP256.decodePrivatePoint(bytes);
} else {
throw new AssertionError("Bad key type: " + type);
}
public static ECPrivateKey decodePrivatePoint(byte[] bytes) {
return new DjbECPrivateKey(bytes);
}
public static byte[] calculateAgreement(ECPublicKey publicKey, ECPrivateKey privateKey)
@@ -76,8 +52,6 @@ public class Curve {
if (publicKey.getType() == DJB_TYPE) {
return Curve25519.calculateAgreement(publicKey, privateKey);
} else if (publicKey.getType() == NIST_TYPE || publicKey.getType() == NIST_TYPE2) {
return CurveP256.calculateAgreement(publicKey, privateKey);
} else {
throw new InvalidKeyException("Unknown type: " + publicKey.getType());
}

View File

@@ -1,122 +0,0 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.crypto.ecc;
import android.util.Log;
import org.spongycastle.crypto.AsymmetricCipherKeyPair;
import org.spongycastle.crypto.agreement.ECDHBasicAgreement;
import org.spongycastle.crypto.generators.ECKeyPairGenerator;
import org.spongycastle.crypto.params.ECDomainParameters;
import org.spongycastle.crypto.params.ECKeyGenerationParameters;
import org.spongycastle.crypto.params.ECPrivateKeyParameters;
import org.spongycastle.crypto.params.ECPublicKeyParameters;
import org.spongycastle.math.ec.ECCurve;
import org.spongycastle.math.ec.ECFieldElement;
import org.spongycastle.math.ec.ECPoint;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public class CurveP256 {
private static final BigInteger q = new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", 16);
private static final BigInteger a = new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", 16);
private static final BigInteger b = new BigInteger("5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", 16);
private static final BigInteger n = new BigInteger("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", 16);
private static final ECFieldElement x = new ECFieldElement.Fp(q, new BigInteger("6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296", 16));
private static final ECFieldElement y = new ECFieldElement.Fp(q, new BigInteger("4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5", 16));
private static final ECCurve curve = new ECCurve.Fp(q, a, b);
private static final ECPoint g = new ECPoint.Fp(curve, x, y, true);
private static final ECDomainParameters domainParameters = new ECDomainParameters(curve, g, n);
public static final int P256_POINT_SIZE = 33;
static byte[] encodePoint(ECPoint point) {
synchronized (curve) {
return point.getEncoded();
}
}
static ECPublicKey decodePoint(byte[] encoded, int offset)
throws InvalidKeyException
{
byte[] pointBytes = new byte[P256_POINT_SIZE];
System.arraycopy(encoded, offset, pointBytes, 0, pointBytes.length);
synchronized (curve) {
ECPoint Q;
try {
Q = curve.decodePoint(pointBytes);
} catch (RuntimeException re) {
throw new InvalidKeyException(re);
}
return new NistECPublicKey(new ECPublicKeyParameters(Q, domainParameters));
}
}
static ECPrivateKey decodePrivatePoint(byte[] encoded) {
BigInteger d = new BigInteger(encoded);
return new NistECPrivateKey(new ECPrivateKeyParameters(d, domainParameters));
}
static byte[] calculateAgreement(ECPublicKey publicKey, ECPrivateKey privateKey) {
ECDHBasicAgreement agreement = new ECDHBasicAgreement();
agreement.init(((NistECPrivateKey)privateKey).getParameters());
synchronized (curve) {
return agreement.calculateAgreement(((NistECPublicKey)publicKey).getParameters()).toByteArray();
}
}
public static ECKeyPair generateKeyPair() {
try {
synchronized (curve) {
ECKeyGenerationParameters keyParamters = new ECKeyGenerationParameters(domainParameters, SecureRandom.getInstance("SHA1PRNG"));
ECKeyPairGenerator generator = new ECKeyPairGenerator();
generator.init(keyParamters);
AsymmetricCipherKeyPair keyPair = generator.generateKeyPair();
keyPair = cloneKeyPairWithPointCompression(keyPair);
return new ECKeyPair(new NistECPublicKey((ECPublicKeyParameters)keyPair.getPublic()),
new NistECPrivateKey((ECPrivateKeyParameters)keyPair.getPrivate()));
}
} catch (NoSuchAlgorithmException nsae) {
Log.w("CurveP256", nsae);
throw new AssertionError(nsae);
}
}
// This is dumb, but the ECPublicKeys that the generator makes by default don't have point compression
// turned on, and there's no setter. Great.
private static AsymmetricCipherKeyPair cloneKeyPairWithPointCompression(AsymmetricCipherKeyPair keyPair) {
ECPublicKeyParameters publicKey = (ECPublicKeyParameters)keyPair.getPublic();
ECPoint q = publicKey.getQ();
return new AsymmetricCipherKeyPair(new ECPublicKeyParameters(new ECPoint.Fp(q.getCurve(), q.getX(), q.getY(), true),
publicKey.getParameters()), keyPair.getPrivate());
}
}

View File

@@ -1,43 +0,0 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.crypto.ecc;
import org.spongycastle.crypto.params.ECPrivateKeyParameters;
public class NistECPrivateKey implements ECPrivateKey {
private final ECPrivateKeyParameters privateKey;
public NistECPrivateKey(ECPrivateKeyParameters privateKey) {
this.privateKey = privateKey;
}
@Override
public byte[] serialize() {
return privateKey.getD().toByteArray();
}
@Override
public int getType() {
return Curve.NIST_TYPE;
}
public ECPrivateKeyParameters getParameters() {
return privateKey;
}
}

View File

@@ -1,63 +0,0 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.crypto.ecc;
import org.spongycastle.crypto.params.ECPublicKeyParameters;
public class NistECPublicKey implements ECPublicKey {
private final ECPublicKeyParameters publicKey;
NistECPublicKey(ECPublicKeyParameters publicKey) {
this.publicKey = publicKey;
}
@Override
public byte[] serialize() {
return CurveP256.encodePoint(publicKey.getQ());
}
@Override
public int getType() {
return Curve.NIST_TYPE;
}
@Override
public boolean equals(Object other) {
if (other == null) return false;
if (!(other instanceof NistECPublicKey)) return false;
NistECPublicKey that = (NistECPublicKey)other;
return publicKey.getQ().equals(that.publicKey.getQ());
}
@Override
public int hashCode() {
return publicKey.getQ().hashCode();
}
@Override
public int compareTo(ECPublicKey another) {
return publicKey.getQ().getX().toBigInteger()
.compareTo(((NistECPublicKey) another).publicKey.getQ().getX().toBigInteger());
}
public ECPublicKeyParameters getParameters() {
return publicKey;
}
}

View File

@@ -2,15 +2,14 @@ package org.whispersystems.textsecure.crypto.protocol;
public interface CiphertextMessage {
public static final int LEGACY_VERSION = 1;
public static final int CURRENT_VERSION = 2;
public static final int UNSUPPORTED_VERSION = 1;
public static final int CURRENT_VERSION = 2;
public static final int LEGACY_WHISPER_TYPE = 1;
public static final int CURRENT_WHISPER_TYPE = 2;
public static final int PREKEY_WHISPER_TYPE = 3;
public static final int WHISPER_TYPE = 2;
public static final int PREKEY_TYPE = 3;
// This should be the worst case (worse than V2). So not always accurate, but good enough for padding.
public static final int ENCRYPTED_MESSAGE_OVERHEAD = WhisperMessageV1.ENCRYPTED_MESSAGE_OVERHEAD;
public static final int ENCRYPTED_MESSAGE_OVERHEAD = 53;
public byte[] serialize();
public int getType();

View File

@@ -7,6 +7,7 @@ import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.crypto.LegacyMessageException;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.util.Conversions;
@@ -14,19 +15,19 @@ import org.whispersystems.textsecure.util.Util;
public class PreKeyWhisperMessage implements CiphertextMessage {
private final int version;
private final int registrationId;
private final int preKeyId;
private final ECPublicKey baseKey;
private final IdentityKey identityKey;
private final WhisperMessageV2 message;
private final byte[] serialized;
private final int version;
private final int registrationId;
private final int preKeyId;
private final ECPublicKey baseKey;
private final IdentityKey identityKey;
private final WhisperMessage message;
private final byte[] serialized;
public PreKeyWhisperMessage(byte[] serialized)
throws InvalidMessageException, InvalidVersionException
{
try {
this.version = Conversions.lowBitsToInt(serialized[0]);
this.version = Conversions.highBitsToInt(serialized[0]);
if (this.version > CiphertextMessage.CURRENT_VERSION) {
throw new InvalidVersionException("Unknown version: " + this.version);
@@ -49,16 +50,18 @@ public class PreKeyWhisperMessage implements CiphertextMessage {
this.preKeyId = preKeyWhisperMessage.getPreKeyId();
this.baseKey = Curve.decodePoint(preKeyWhisperMessage.getBaseKey().toByteArray(), 0);
this.identityKey = new IdentityKey(Curve.decodePoint(preKeyWhisperMessage.getIdentityKey().toByteArray(), 0));
this.message = new WhisperMessageV2(preKeyWhisperMessage.getMessage().toByteArray());
this.message = new WhisperMessage(preKeyWhisperMessage.getMessage().toByteArray());
} catch (InvalidProtocolBufferException e) {
throw new InvalidMessageException(e);
} catch (InvalidKeyException e) {
throw new InvalidMessageException(e);
} catch (LegacyMessageException e) {
throw new InvalidMessageException(e);
}
}
public PreKeyWhisperMessage(int registrationId, int preKeyId, ECPublicKey baseKey,
IdentityKey identityKey, WhisperMessageV2 message)
IdentityKey identityKey, WhisperMessage message)
{
this.version = CiphertextMessage.CURRENT_VERSION;
this.registrationId = registrationId;
@@ -95,7 +98,7 @@ public class PreKeyWhisperMessage implements CiphertextMessage {
return baseKey;
}
public WhisperMessageV2 getWhisperMessage() {
public WhisperMessage getWhisperMessage() {
return message;
}
@@ -106,7 +109,7 @@ public class PreKeyWhisperMessage implements CiphertextMessage {
@Override
public int getType() {
return CiphertextMessage.PREKEY_WHISPER_TYPE;
return CiphertextMessage.PREKEY_TYPE;
}
}

View File

@@ -1,27 +1,24 @@
package org.whispersystems.textsecure.crypto.protocol;
import android.util.Log;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.LegacyMessageException;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.protocol.WhisperProtos.WhisperMessage;
import org.whispersystems.textsecure.util.Conversions;
import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.Arrays;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class WhisperMessageV2 implements CiphertextMessage {
public class WhisperMessage implements CiphertextMessage {
private static final int MAC_LENGTH = 8;
@@ -31,18 +28,22 @@ public class WhisperMessageV2 implements CiphertextMessage {
private final byte[] ciphertext;
private final byte[] serialized;
public WhisperMessageV2(byte[] serialized) throws InvalidMessageException {
public WhisperMessage(byte[] serialized) throws InvalidMessageException, LegacyMessageException {
try {
byte[][] messageParts = Util.split(serialized, 1, serialized.length - 1 - MAC_LENGTH, MAC_LENGTH);
byte version = messageParts[0][0];
byte[] message = messageParts[1];
byte[] mac = messageParts[2];
if (Conversions.highBitsToInt(version) <= CiphertextMessage.UNSUPPORTED_VERSION) {
throw new LegacyMessageException("Legacy message: " + Conversions.highBitsToInt(version));
}
if (Conversions.highBitsToInt(version) != CURRENT_VERSION) {
throw new InvalidMessageException("Unknown version: " + Conversions.highBitsToInt(version));
}
WhisperMessage whisperMessage = WhisperMessage.parseFrom(message);
WhisperProtos.WhisperMessage whisperMessage = WhisperProtos.WhisperMessage.parseFrom(message);
if (!whisperMessage.hasCiphertext() ||
!whisperMessage.hasCounter() ||
@@ -65,11 +66,11 @@ public class WhisperMessageV2 implements CiphertextMessage {
}
}
public WhisperMessageV2(SecretKeySpec macKey, ECPublicKey senderEphemeral,
int counter, int previousCounter, byte[] ciphertext)
public WhisperMessage(SecretKeySpec macKey, ECPublicKey senderEphemeral,
int counter, int previousCounter, byte[] ciphertext)
{
byte[] version = {Conversions.intsToByteHighAndLow(CURRENT_VERSION, CURRENT_VERSION)};
byte[] message = WhisperMessage.newBuilder()
byte[] message = WhisperProtos.WhisperMessage.newBuilder()
.setEphemeralKey(ByteString.copyFrom(senderEphemeral.serialize()))
.setCounter(counter)
.setPreviousCounter(previousCounter)
@@ -103,7 +104,7 @@ public class WhisperMessageV2 implements CiphertextMessage {
byte[] ourMac = getMac(macKey, parts[0]);
byte[] theirMac = parts[1];
if (!Arrays.equals(ourMac, theirMac)) {
if (!MessageDigest.isEqual(ourMac, theirMac)) {
throw new InvalidMessageException("Bad Mac!");
}
}
@@ -129,7 +130,12 @@ public class WhisperMessageV2 implements CiphertextMessage {
@Override
public int getType() {
return CiphertextMessage.CURRENT_WHISPER_TYPE;
return CiphertextMessage.WHISPER_TYPE;
}
public static boolean isLegacy(byte[] message) {
return message != null && message.length >= 1 &&
Conversions.highBitsToInt(message[0]) <= CiphertextMessage.UNSUPPORTED_VERSION;
}
}

View File

@@ -1,187 +0,0 @@
package org.whispersystems.textsecure.crypto.protocol;
import android.util.Log;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.PublicKey;
import org.whispersystems.textsecure.crypto.SessionCipherV1;
import org.whispersystems.textsecure.util.Conversions;
import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class WhisperMessageV1 implements CiphertextMessage{
private static final int VERSION_LENGTH = 1;
private static final int SENDER_KEY_ID_LENGTH = 3;
private static final int RECEIVER_KEY_ID_LENGTH = 3;
private static final int NEXT_KEY_LENGTH = PublicKey.KEY_SIZE;
private static final int COUNTER_LENGTH = 3;
private static final int HEADER_LENGTH = VERSION_LENGTH + SENDER_KEY_ID_LENGTH +
RECEIVER_KEY_ID_LENGTH + COUNTER_LENGTH +
NEXT_KEY_LENGTH;
private static final int MAC_LENGTH = 10;
private static final int VERSION_OFFSET = 0;
private static final int SENDER_KEY_ID_OFFSET = VERSION_OFFSET + VERSION_LENGTH;
private static final int RECEIVER_KEY_ID_OFFSET = SENDER_KEY_ID_OFFSET + SENDER_KEY_ID_LENGTH;
private static final int NEXT_KEY_OFFSET = RECEIVER_KEY_ID_OFFSET + RECEIVER_KEY_ID_LENGTH;
private static final int COUNTER_OFFSET = NEXT_KEY_OFFSET + NEXT_KEY_LENGTH;
private static final int BODY_OFFSET = COUNTER_OFFSET + COUNTER_LENGTH;
static final int ENCRYPTED_MESSAGE_OVERHEAD = HEADER_LENGTH + MAC_LENGTH;
private final byte[] ciphertext;
public WhisperMessageV1(SessionCipherV1.SessionCipherContext sessionContext,
byte[] ciphertextBody)
{
this.ciphertext = new byte[HEADER_LENGTH + ciphertextBody.length + MAC_LENGTH];
setVersion(sessionContext.getMessageVersion(), CURRENT_VERSION);
setSenderKeyId(sessionContext.getSenderKeyId());
setReceiverKeyId(sessionContext.getRecipientKeyId());
setNextKeyBytes(sessionContext.getNextKey().serialize());
setCounter(sessionContext.getCounter());
setBody(ciphertextBody);
setMac(calculateMac(sessionContext.getSessionKey().getMacKey(),
ciphertext, 0, ciphertext.length - MAC_LENGTH));
}
public WhisperMessageV1(byte[] ciphertext) throws InvalidMessageException {
this.ciphertext = ciphertext;
if (ciphertext.length < HEADER_LENGTH) {
throw new InvalidMessageException("Not long enough for ciphertext header!");
}
if (getCurrentVersion() > LEGACY_VERSION) {
throw new InvalidMessageException("Received non-legacy version: " + getCurrentVersion());
}
}
public void setVersion(int current, int supported) {
ciphertext[VERSION_OFFSET] = Conversions.intsToByteHighAndLow(current, supported);
}
public int getCurrentVersion() {
return Conversions.highBitsToInt(ciphertext[VERSION_OFFSET]);
}
public int getSupportedVersion() {
return Conversions.lowBitsToInt(ciphertext[VERSION_OFFSET]);
}
public void setSenderKeyId(int senderKeyId) {
Conversions.mediumToByteArray(ciphertext, SENDER_KEY_ID_OFFSET, senderKeyId);
}
public int getSenderKeyId() {
return Conversions.byteArrayToMedium(ciphertext, SENDER_KEY_ID_OFFSET);
}
public void setReceiverKeyId(int receiverKeyId) {
Conversions.mediumToByteArray(ciphertext, RECEIVER_KEY_ID_OFFSET, receiverKeyId);
}
public int getReceiverKeyId() {
return Conversions.byteArrayToMedium(ciphertext, RECEIVER_KEY_ID_OFFSET);
}
public void setNextKeyBytes(byte[] nextKey) {
assert(nextKey.length == NEXT_KEY_LENGTH);
System.arraycopy(nextKey, 0, ciphertext, NEXT_KEY_OFFSET, nextKey.length);
}
public byte[] getNextKeyBytes() {
byte[] nextKeyBytes = new byte[NEXT_KEY_LENGTH];
System.arraycopy(ciphertext, NEXT_KEY_OFFSET, nextKeyBytes, 0, nextKeyBytes.length);
return nextKeyBytes;
}
public void setCounter(int counter) {
Conversions.mediumToByteArray(ciphertext, COUNTER_OFFSET, counter);
}
public int getCounter() {
return Conversions.byteArrayToMedium(ciphertext, COUNTER_OFFSET);
}
public void setBody(byte[] body) {
System.arraycopy(body, 0, ciphertext, BODY_OFFSET, body.length);
}
public byte[] getBody() {
byte[] body = new byte[ciphertext.length - HEADER_LENGTH - MAC_LENGTH];
System.arraycopy(ciphertext, BODY_OFFSET, body, 0, body.length);
return body;
}
public void setMac(byte[] mac) {
System.arraycopy(mac, 0, ciphertext, ciphertext.length-mac.length, mac.length);
}
public byte[] getMac() {
byte[] mac = new byte[MAC_LENGTH];
System.arraycopy(ciphertext, ciphertext.length-mac.length, mac, 0, mac.length);
return mac;
}
@Override
public byte[] serialize() {
return ciphertext;
}
@Override
public int getType() {
return CiphertextMessage.LEGACY_WHISPER_TYPE;
}
public void verifyMac(SessionCipherV1.SessionCipherContext sessionContext)
throws InvalidMessageException
{
verifyMac(sessionContext.getSessionKey().getMacKey(),
this.ciphertext, 0, this.ciphertext.length - MAC_LENGTH, getMac());
}
private byte[] calculateMac(SecretKeySpec macKey, byte[] message, int offset, int length) {
try {
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(macKey);
mac.update(message, offset, length);
byte[] macBytes = mac.doFinal();
return Util.trim(macBytes, MAC_LENGTH);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException(e);
}
}
private void verifyMac(SecretKeySpec macKey, byte[] message, int offset, int length,
byte[] receivedMac)
throws InvalidMessageException
{
byte[] localMac = calculateMac(macKey, message, offset, length);
Log.w("WhisperMessageV1", "Local Mac: " + Hex.toString(localMac));
Log.w("WhisperMessageV1", "Remot Mac: " + Hex.toString(receivedMac));
if (!Arrays.equals(localMac, receivedMac)) {
throw new InvalidMessageException("MAC on message does not match calculated MAC.");
}
}
}

View File

@@ -45,7 +45,7 @@ public class RatchetingSession {
sessionState.setRemoteIdentityKey(theirIdentityKey);
sessionState.setLocalIdentityKey(ourIdentityKey.getPublicKey());
ECKeyPair sendingKey = Curve.generateKeyPairForType(ourIdentityKey.getPublicKey().getPublicKey().getType(), true);
ECKeyPair sendingKey = Curve.generateKeyPair(true);
Pair<RootKey, ChainKey> receivingChain = calculate3DHE(true, ourBaseKey, theirBaseKey, ourIdentityKey, theirIdentityKey);
Pair<RootKey, ChainKey> sendingChain = receivingChain.first.createChain(theirEphemeralKey, sendingKey);

View File

@@ -18,135 +18,14 @@
package org.whispersystems.textsecure.storage;
import android.content.Context;
import android.util.Log;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.KeyPair;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.util.Medium;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
public class LocalKeyRecord extends Record {
private static final Object FILE_LOCK = new Object();
private KeyPair localCurrentKeyPair;
private KeyPair localNextKeyPair;
private final MasterCipher masterCipher;
private final MasterSecret masterSecret;
public LocalKeyRecord(Context context, MasterSecret masterSecret, CanonicalRecipient recipient) {
super(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
this.masterSecret = masterSecret;
this.masterCipher = new MasterCipher(masterSecret);
loadData();
}
public static boolean hasRecord(Context context, CanonicalRecipient recipient) {
Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(recipient));
return Record.hasRecord(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
}
public class LocalKeyRecord {
public static void delete(Context context, CanonicalRecipient recipient) {
Record.delete(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
Record.delete(context, Record.SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
}
private static String getFileNameForRecipient(CanonicalRecipient recipient) {
return recipient.getRecipientId() + "-local";
}
public void advanceKeyIfNecessary(int keyId) {
Log.w("LocalKeyRecord", "Remote client acknowledges receiving key id: " + keyId);
if (keyId == localNextKeyPair.getId()) {
int keyType = this.localNextKeyPair.getPublicKey().getType();
this.localCurrentKeyPair = this.localNextKeyPair;
this.localNextKeyPair = new KeyPair((this.localNextKeyPair.getId()+1) % Medium.MAX_VALUE,
Curve.generateKeyPairForType(keyType, true),
masterSecret);
}
}
public void setCurrentKeyPair(KeyPair localCurrentKeyPair) {
this.localCurrentKeyPair = localCurrentKeyPair;
}
public void setNextKeyPair(KeyPair localNextKeyPair) {
this.localNextKeyPair = localNextKeyPair;
}
public KeyPair getCurrentKeyPair() {
return this.localCurrentKeyPair;
}
public KeyPair getNextKeyPair() {
return this.localNextKeyPair;
}
public KeyPair getKeyPairForId(int id) throws InvalidKeyIdException {
if (this.localCurrentKeyPair.getId() == id) return this.localCurrentKeyPair;
else if (this.localNextKeyPair.getId() == id) return this.localNextKeyPair;
else throw new InvalidKeyIdException("No local key for ID: " + id);
}
public void save() {
synchronized (FILE_LOCK) {
try {
RandomAccessFile file = openRandomAccessFile();
FileChannel out = file.getChannel();
out.position(0);
writeKeyPair(localCurrentKeyPair, out);
writeKeyPair(localNextKeyPair, out);
out.force(true);
out.truncate(out.position());
out.close();
file.close();
} catch (IOException ioe) {
Log.w("keyrecord", ioe);
// XXX
}
}
}
private void loadData() {
Log.w("LocalKeyRecord", "Loading local key record...");
synchronized (FILE_LOCK) {
try {
FileInputStream in = this.openInputStream();
localCurrentKeyPair = readKeyPair(in, masterCipher);
localNextKeyPair = readKeyPair(in, masterCipher);
in.close();
} catch (FileNotFoundException e) {
Log.w("LocalKeyRecord", "No local keypair set found.");
} catch (IOException ioe) {
Log.w("keyrecord", ioe);
// XXX
} catch (InvalidKeyException ike) {
Log.w("LocalKeyRecord", ike);
}
}
}
private void writeKeyPair(KeyPair keyPair, FileChannel out) throws IOException {
byte[] keyPairBytes = keyPair.toBytes();
writeBlob(keyPairBytes, out);
}
private KeyPair readKeyPair(FileInputStream in, MasterCipher masterCipher)
throws IOException, InvalidKeyException
{
byte[] keyPairBytes = readBlob(in);
return new KeyPair(keyPairBytes, masterCipher);
}
}

View File

@@ -60,7 +60,7 @@ public class PreKeyRecord extends Record {
public ECKeyPair getKeyPair() {
try {
ECPublicKey publicKey = Curve.decodePoint(this.structure.getPublicKey().toByteArray(), 0);
ECPrivateKey privateKey = Curve.decodePrivatePoint(publicKey.getType(), this.structure.getPrivateKey().toByteArray());
ECPrivateKey privateKey = Curve.decodePrivatePoint(this.structure.getPrivateKey().toByteArray());
return new ECKeyPair(publicKey, privateKey);
} catch (InvalidKeyException e) {

View File

@@ -37,126 +37,13 @@ import java.nio.channels.FileChannel;
* @author Moxie Marlinspike
*/
public class RemoteKeyRecord extends Record {
private static final Object FILE_LOCK = new Object();
private PublicKey remoteKeyCurrent;
private PublicKey remoteKeyLast;
public RemoteKeyRecord(Context context, CanonicalRecipient recipient) {
super(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
loadData();
}
public class RemoteKeyRecord {
public static void delete(Context context, CanonicalRecipient recipient) {
delete(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
}
public static boolean hasRecord(Context context, CanonicalRecipient recipient) {
Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(recipient));
return hasRecord(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
Record.delete(context, Record.SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
}
private static String getFileNameForRecipient(CanonicalRecipient recipient) {
return recipient.getRecipientId() + "-remote";
}
public void updateCurrentRemoteKey(PublicKey remoteKey) {
Log.w("RemoteKeyRecord", "Updating current remote key: " + remoteKey.getId());
if (isWrappingGreaterThan(remoteKey.getId(), remoteKeyCurrent.getId())) {
this.remoteKeyLast = this.remoteKeyCurrent;
this.remoteKeyCurrent = remoteKey;
}
}
public void setCurrentRemoteKey(PublicKey remoteKeyCurrent) {
this.remoteKeyCurrent = remoteKeyCurrent;
}
public void setLastRemoteKey(PublicKey remoteKeyLast) {
this.remoteKeyLast = remoteKeyLast;
}
public PublicKey getCurrentRemoteKey() {
return this.remoteKeyCurrent;
}
public PublicKey getLastRemoteKey() {
return this.remoteKeyLast;
}
public PublicKey getKeyForId(int id) throws InvalidKeyIdException {
if (this.remoteKeyCurrent.getId() == id) return this.remoteKeyCurrent;
else if (this.remoteKeyLast.getId() == id) return this.remoteKeyLast;
else throw new InvalidKeyIdException("No remote key for ID: " + id);
}
public void save() {
Log.w("RemoteKeyRecord", "Saving remote key record for recipient: " + this.address);
synchronized (FILE_LOCK) {
try {
RandomAccessFile file = openRandomAccessFile();
FileChannel out = file.getChannel();
Log.w("RemoteKeyRecord", "Opened file of size: " + out.size());
out.position(0);
writeKey(remoteKeyCurrent, out);
writeKey(remoteKeyLast, out);
out.truncate(out.position());
out.close();
file.close();
} catch (IOException ioe) {
Log.w("keyrecord", ioe);
// XXX
}
}
}
private boolean isWrappingGreaterThan(int receivedValue, int currentValue) {
if (receivedValue > currentValue) {
return true;
}
if (receivedValue == currentValue) {
return false;
}
int gap = (receivedValue - currentValue) + Medium.MAX_VALUE;
return (gap >= 0) && (gap < 5);
}
private void loadData() {
Log.w("RemoteKeyRecord", "Loading remote key record for recipient: " + this.address);
synchronized (FILE_LOCK) {
try {
FileInputStream in = this.openInputStream();
remoteKeyCurrent = readKey(in);
remoteKeyLast = readKey(in);
in.close();
} catch (FileNotFoundException e) {
Log.w("RemoteKeyRecord", "No remote keys found.");
} catch (IOException ioe) {
Log.w("keyrecord", ioe);
// XXX
}
}
}
private void writeKey(PublicKey key, FileChannel out) throws IOException {
byte[] keyBytes = key.serialize();
Log.w("RemoteKeyRecord", "Serializing remote key bytes: " + Hex.toString(keyBytes));
writeBlob(keyBytes, out);
}
private PublicKey readKey(FileInputStream in) throws IOException {
try {
byte[] keyBytes = readBlob(in);
return new PublicKey(keyBytes);
} catch (InvalidKeyException ike) {
throw new AssertionError(ike);
}
}
}

View File

@@ -5,7 +5,6 @@ import android.util.Log;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
/**
* Helper class for generating key pairs and calculating ECDH agreements.
@@ -32,7 +31,8 @@ public class Session {
CanonicalRecipient recipient)
{
Log.w("Session", "Checking session...");
return hasV1Session(context, recipient) || hasV2Session(context, masterSecret, recipient);
return SessionRecordV2.hasSession(context, masterSecret, recipient.getRecipientId(),
RecipientDevice.DEFAULT_DEVICE_ID);
}
public static boolean hasEncryptCapableSession(Context context,
@@ -50,30 +50,8 @@ public class Session {
CanonicalRecipient recipient,
RecipientDevice device)
{
return
hasV1Session(context, recipient) ||
(hasV2Session(context, masterSecret, recipient) &&
!SessionRecordV2.needsRefresh(context, masterSecret, device));
}
public static boolean hasRemoteIdentityKey(Context context,
MasterSecret masterSecret,
CanonicalRecipient recipient)
{
return (hasV2Session(context, masterSecret, recipient) || (hasV1Session(context, recipient) &&
new SessionRecordV1(context, masterSecret, recipient).getIdentityKey() != null));
}
private static boolean hasV2Session(Context context, MasterSecret masterSecret,
CanonicalRecipient recipient)
{
return SessionRecordV2.hasSession(context, masterSecret, recipient.getRecipientId(),
RecipientDevice.DEFAULT_DEVICE_ID);
}
private static boolean hasV1Session(Context context, CanonicalRecipient recipient) {
return SessionRecordV1.hasSession(context, recipient) &&
RemoteKeyRecord.hasRecord(context, recipient) &&
LocalKeyRecord.hasRecord(context, recipient);
return hasSession(context, masterSecret, recipient) &&
!SessionRecordV2.needsRefresh(context, masterSecret, device);
}
public static IdentityKey getRemoteIdentityKey(Context context, MasterSecret masterSecret,
@@ -92,25 +70,8 @@ public class Session {
return new SessionRecordV2(context, masterSecret, recipientId,
RecipientDevice.DEFAULT_DEVICE_ID).getSessionState()
.getRemoteIdentityKey();
} else if (SessionRecordV1.hasSession(context, recipientId)) {
return new SessionRecordV1(context, masterSecret, recipientId).getIdentityKey();
} else {
return null;
}
}
public static int getSessionVersion(Context context, MasterSecret masterSecret,
CanonicalRecipient recipient)
{
if (SessionRecordV2.hasSession(context, masterSecret,
recipient.getRecipientId(),
RecipientDevice.DEFAULT_DEVICE_ID))
{
return CiphertextMessage.CURRENT_VERSION;
} else if (SessionRecordV1.hasSession(context, recipient)) {
return new SessionRecordV1(context, masterSecret, recipient).getSessionVersion();
}
return 0;
}
}

View File

@@ -1,18 +1,6 @@
package org.whispersystems.textsecure.storage;
import android.content.Context;
import android.util.Log;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.MasterSecret;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
/**
* A disk record representing a current session.
@@ -20,208 +8,8 @@ import java.nio.channels.FileChannel;
* @author Moxie Marlinspike
*/
public class SessionRecordV1 extends Record {
private static final int CURRENT_VERSION_MARKER = 0X55555556;
private static final int[] VALID_VERSION_MARKERS = {CURRENT_VERSION_MARKER, 0X55555555};
private static final Object FILE_LOCK = new Object();
private int counter;
private byte[] localFingerprint;
private byte[] remoteFingerprint;
private int currentSessionVersion;
private IdentityKey identityKey;
private SessionKey sessionKeyRecord;
private boolean verifiedSessionKey;
private final MasterSecret masterSecret;
public SessionRecordV1(Context context, MasterSecret masterSecret, CanonicalRecipient recipient) {
this(context, masterSecret, recipient.getRecipientId());
}
public SessionRecordV1(Context context, MasterSecret masterSecret, long recipientId) {
super(context, SESSIONS_DIRECTORY, recipientId+"");
this.masterSecret = masterSecret;
this.currentSessionVersion = 31337;
loadData();
}
public class SessionRecordV1 {
public static void delete(Context context, CanonicalRecipient recipient) {
delete(context, SESSIONS_DIRECTORY, recipient.getRecipientId() + "");
Record.delete(context, Record.SESSIONS_DIRECTORY, recipient.getRecipientId() + "");
}
public static boolean hasSession(Context context, CanonicalRecipient recipient) {
return hasSession(context, recipient.getRecipientId());
}
public static boolean hasSession(Context context, long recipientId) {
Log.w("SessionRecordV1", "Checking: " + recipientId);
return hasRecord(context, SESSIONS_DIRECTORY, recipientId+"");
}
public void setSessionKey(SessionKey sessionKeyRecord) {
this.sessionKeyRecord = sessionKeyRecord;
}
public void setSessionId(byte[] localFingerprint, byte[] remoteFingerprint) {
this.localFingerprint = localFingerprint;
this.remoteFingerprint = remoteFingerprint;
}
public void setIdentityKey(IdentityKey identityKey) {
this.identityKey = identityKey;
}
public int getSessionVersion() {
return (currentSessionVersion == 31337 ? 0 : currentSessionVersion);
}
public void setSessionVersion(int sessionVersion) {
this.currentSessionVersion = sessionVersion;
}
public int getCounter() {
return this.counter;
}
public void incrementCounter() {
this.counter++;
}
public byte[] getLocalFingerprint() {
return this.localFingerprint;
}
public byte[] getRemoteFingerprint() {
return this.remoteFingerprint;
}
public IdentityKey getIdentityKey() {
return this.identityKey;
}
public boolean isVerifiedSession() {
return this.verifiedSessionKey;
}
private void writeIdentityKey(FileChannel out) throws IOException {
if (identityKey == null) writeBlob(new byte[0], out);
else writeBlob(identityKey.serialize(), out);
}
private boolean isValidVersionMarker(int versionMarker) {
for (int VALID_VERSION_MARKER : VALID_VERSION_MARKERS)
if (versionMarker == VALID_VERSION_MARKER)
return true;
return false;
}
private void readIdentityKey(FileInputStream in) throws IOException {
try {
byte[] blob = readBlob(in);
if (blob.length == 0) this.identityKey = null;
else this.identityKey = new IdentityKey(blob, 0);
} catch (InvalidKeyException ike) {
throw new AssertionError(ike);
}
}
public void save() {
synchronized (FILE_LOCK) {
try {
RandomAccessFile file = openRandomAccessFile();
FileChannel out = file.getChannel();
out.position(0);
writeInteger(CURRENT_VERSION_MARKER, out);
writeInteger(counter, out);
writeBlob(localFingerprint, out);
writeBlob(remoteFingerprint, out);
writeInteger(currentSessionVersion, out);
writeIdentityKey(out);
writeInteger(verifiedSessionKey ? 1 : 0, out);
if (sessionKeyRecord != null)
writeBlob(sessionKeyRecord.serialize(), out);
out.truncate(out.position());
file.close();
} catch (IOException ioe) {
throw new IllegalArgumentException(ioe);
}
}
}
private void loadData() {
synchronized (FILE_LOCK) {
try {
FileInputStream in = this.openInputStream();
int versionMarker = readInteger(in);
// Sigh, always put a version number on everything.
if (!isValidVersionMarker(versionMarker)) {
this.counter = versionMarker;
this.localFingerprint = readBlob(in);
this.remoteFingerprint = readBlob(in);
this.currentSessionVersion = 31337;
if (in.available() != 0) {
try {
this.sessionKeyRecord = new SessionKey(readBlob(in), masterSecret);
} catch (InvalidMessageException e) {
Log.w("SessionRecord", e);
this.sessionKeyRecord = null;
}
}
in.close();
} else {
this.counter = readInteger(in);
this.localFingerprint = readBlob (in);
this.remoteFingerprint = readBlob (in);
this.currentSessionVersion = readInteger(in);
if (versionMarker >= 0X55555556) {
readIdentityKey(in);
this.verifiedSessionKey = (readInteger(in) == 1);
}
if (in.available() != 0) {
try {
this.sessionKeyRecord = new SessionKey(readBlob(in), masterSecret);
} catch (InvalidMessageException e) {
Log.w("SessionRecord", e);
this.sessionKeyRecord = null;
}
}
in.close();
}
} catch (FileNotFoundException e) {
Log.w("SessionRecord", "No session information found.");
// XXX
} catch (IOException ioe) {
Log.w("keyrecord", ioe);
// XXX
}
}
}
public SessionKey getSessionKey(int mode, int localKeyId, int remoteKeyId) {
if (this.sessionKeyRecord == null) return null;
if ((this.sessionKeyRecord.getLocalKeyId() == localKeyId) &&
(this.sessionKeyRecord.getRemoteKeyId() == remoteKeyId) &&
(this.sessionKeyRecord.getMode() == mode))
{
return this.sessionKeyRecord;
}
return null;
}
}

View File

@@ -121,9 +121,8 @@ public class SessionState {
}
public ECKeyPair getSenderEphemeralPair() {
ECPublicKey publicKey = getSenderEphemeral();
ECPrivateKey privateKey = Curve.decodePrivatePoint(publicKey.getType(),
sessionStructure.getSenderChain()
ECPublicKey publicKey = getSenderEphemeral();
ECPrivateKey privateKey = Curve.decodePrivatePoint(sessionStructure.getSenderChain()
.getSenderEphemeralPrivate()
.toByteArray());
@@ -342,8 +341,7 @@ public class SessionState {
ECPublicKey publicKey = Curve.decodePoint(sessionStructure.getPendingKeyExchange()
.getLocalBaseKey().toByteArray(), 0);
ECPrivateKey privateKey = Curve.decodePrivatePoint(publicKey.getType(),
sessionStructure.getPendingKeyExchange()
ECPrivateKey privateKey = Curve.decodePrivatePoint(sessionStructure.getPendingKeyExchange()
.getLocalBaseKeyPrivate()
.toByteArray());
@@ -354,8 +352,7 @@ public class SessionState {
ECPublicKey publicKey = Curve.decodePoint(sessionStructure.getPendingKeyExchange()
.getLocalEphemeralKey().toByteArray(), 0);
ECPrivateKey privateKey = Curve.decodePrivatePoint(publicKey.getType(),
sessionStructure.getPendingKeyExchange()
ECPrivateKey privateKey = Curve.decodePrivatePoint(sessionStructure.getPendingKeyExchange()
.getLocalEphemeralKeyPrivate()
.toByteArray());
@@ -366,8 +363,7 @@ public class SessionState {
IdentityKey publicKey = new IdentityKey(sessionStructure.getPendingKeyExchange()
.getLocalIdentityKey().toByteArray(), 0);
ECPrivateKey privateKey = Curve.decodePrivatePoint(publicKey.getPublicKey().getType(),
sessionStructure.getPendingKeyExchange()
ECPrivateKey privateKey = Curve.decodePrivatePoint(sessionStructure.getPendingKeyExchange()
.getLocalIdentityKeyPrivate()
.toByteArray());

View File

@@ -1,7 +1,13 @@
package org.whispersystems.textsecure.util;
import android.content.Context;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.DrawableContainer;
import android.graphics.drawable.StateListDrawable;
import android.telephony.TelephonyManager;
import android.view.View;
import android.widget.EditText;
import java.io.ByteArrayOutputStream;
@@ -201,4 +207,17 @@ public class Util {
throw new AssertionError(e);
}
}
/*
* source: http://stackoverflow.com/a/9500334
*/
public static void fixBackgroundRepeat(Drawable bg) {
if (bg != null) {
if (bg instanceof BitmapDrawable) {
BitmapDrawable bmp = (BitmapDrawable) bg;
bmp.mutate();
bmp.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
}
}
}
}

BIN
libs/gradle-witness.jar Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 949 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 910 B

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