Compare commits

...

343 Commits

Author SHA1 Message Date
Moxie Marlinspike
fbf02603ce Bump version to 2.0.4 2014-03-04 08:39:31 -08:00
Moxie Marlinspike
5caec4a146 Updated language translations. 2014-03-03 19:13:38 -08:00
Moxie Marlinspike
dc77c43435 Merge pull request #796 from mcginty/pending_messages
pending messages more accurately conveyed to user
2014-03-03 18:49:48 -08:00
Jake McGinty
827298d1a2 Merge pull request #975 from tinloaf/reloadcontactlist
Reload contact list after directory refresh // FREEBIE
2014-03-03 18:01:10 -08:00
Lukas Barth
48f5b932f7 Reload contact list after directory refresh // FREEBIE
I forgot that in my PR, it seems.
2014-03-03 21:18:38 +01:00
Jake McGinty
90169e9468 pending messages more accurately conveyed to user 2014-03-03 11:56:54 -08:00
Moxie Marlinspike
b5fe378bc9 Workaround for Android bug when swiped from recent tasks list.
Closes #168

https://code.google.com/p/android/issues/detail?id=53313
2014-03-03 11:45:05 -08:00
Moxie Marlinspike
0f53c9d170 Merge pull request #966 from backspace/add-video-sharing
Support videos from other apps. Fixes #949
2014-03-03 11:01:02 -08:00
Moxie Marlinspike
3474950830 Merge pull request #956 from tinloaf/refreshfromlist
Add possiblity to refresh the push directory directly from contact list
2014-03-03 10:09:32 -08:00
Lukas Barth
39ee363150 Add possiblity to refresh the push directory directly from the contact picking list. Fixes #835. Fixes #859.
Thanks benalbrecht for the icons.
2014-03-03 18:23:14 +01:00
Buck Doyle
411e3ceff6 Support videos from other apps. Fixes #949 2014-03-03 11:55:34 -05:00
Moxie Marlinspike
b7f8c3b3d3 Merge pull request #961 from cloudkicker/notification_preferences
Make notification preferences look normally
2014-03-03 08:53:00 -08:00
Moxie Marlinspike
3efa8e6899 Merge pull request #955 from benalbrecht/nocount
Use new icon from #905 for push message disable confirmation box
2014-03-03 08:33:10 -08:00
Vladislav Meshochkin
948f481530 Make notification preferences look normally 2014-03-03 19:22:51 +04:00
Benjamin Albrecht
716fdefa4c Use icon from #905 for push message disable confirmation box
//FREEBIE
2014-03-03 11:44:14 +01:00
Moxie Marlinspike
2dc893730a Auto-update every 12 hours instead.
// FREEBIE
2014-03-02 16:19:05 -08:00
Moxie Marlinspike
1af99ce155 Fix for periodic directory refresh.
// FREEBIE
2014-03-02 16:14:00 -08:00
Moxie Marlinspike
0850f1b0f1 Add libcurve25519 bins.
// FREEBIE
2014-03-02 16:06:37 -08:00
Florian Walch
1ddc45fd9c Build native code for multiple architectures.
* Include native libs without requiring intermediate .jar.
 * Fix build warnings; use latest build tools.
2014-03-02 16:06:37 -08:00
Moxie Marlinspike
977765c80f Merge pull request #941 from Wikinaut/add-new-string-ViewIdentityActivity_my_identity_fingerprint
add new string ViewIdentityActivity_my_identity_fingerprint
2014-03-02 15:50:18 -08:00
Thomas Gries
4b2d07ab35 add new string ViewIdentityActivity_my_identity_fingerprint 2014-03-03 00:45:42 +01:00
Jens Füllenbach
003ebe6364 Added the color white to the notification colors of the LED. 2014-03-02 14:11:31 -08:00
Benjamin Albrecht
2f7c005c23 Show confirmation box before disabling push messages 2014-03-02 14:09:54 -08:00
Moxie Marlinspike
367b481d07 Merge pull request #905 from benalbrecht/dialog_icons
Use modern icons in info and alert dialogs for ICS+
2014-03-02 14:01:52 -08:00
Moxie Marlinspike
5cd8c922d2 Merge pull request #648 from veeti/tweaks-and-polish
Assorted tweaks and polish
2014-03-02 14:01:20 -08:00
Moxie Marlinspike
a2fe8a9d5c Make default group avatar icon actually work. 2014-03-02 13:22:02 -08:00
Benedict Pregler
ee921a8f49 fixed some hardcoded strings 2014-03-02 13:22:02 -08:00
Benedict Pregler
e0394b4481 a little typo mistake 2014-03-02 13:22:02 -08:00
Veeti Paananen
de100f5be7 Add missing spaces 2014-03-02 09:40:45 +02:00
Veeti Paananen
a020a57be6 End conversation selection action mode if all deselected
This seems to be common convention in most apps.
2014-03-02 09:40:35 +02:00
Veeti Paananen
28f1a0a636 Replace the attachment dialog header icon with something modern 2014-03-02 09:40:35 +02:00
Veeti Paananen
5a807ffc28 Use unified touch highlight in import & export 2014-03-02 09:40:35 +02:00
Veeti Paananen
757cb1c846 Add touch highlight to contact photos 2014-03-02 09:40:35 +02:00
Veeti Paananen
4f066757e7 Use the touch highlight background for the emoji toggle 2014-03-02 09:40:35 +02:00
Veeti Paananen
633aa9b057 Fix the send button's touch highlight color
Introduce a generic touch highlight background drawable, and use the
proper shades of blue (and gray on v19+).
2014-03-02 09:40:35 +02:00
Jake McGinty
a5c26b2e16 Merge pull request #921 from liliakai/master
Add contributing.md // FREEBIE
2014-03-01 15:19:32 -08:00
lilia
ca561d76ff Add contributing.md // FREEBIE
This file is recognized by github and presented to users who are trying
to open issues and submit pull requests.

Hopefully this more prominent placement will help deter people from
opening issues/prs for translation fixes.
2014-03-01 15:11:21 -08:00
Jake McGinty
107d999ee7 make bithub price update // FREEBIE
thanks lilia
2014-03-01 11:57:46 -08:00
Benjamin Albrecht
0f6c7660cb Use modern icons in info and alert dialogs for ICS+ 2014-03-01 14:42:12 +01:00
Moxie Marlinspike
8ea4db03db Better support for local storage with passphrase disabled.
1) Never get into a state where messages aren't displayed
   unencrypted.

2) Fix bug where locked DB required launching twice.
2014-02-28 15:58:30 -08:00
Moxie Marlinspike
81ee9e31c5 Update behavior for incoming SMS path.
1) On KitKat, unencrypted SMS messages are never stored in
   TextSecure unless it is set as the system-wide default.

2) On KitKat, if TextSecure is set as the system-wide default,
   provide an option to change the default to a different app.

3) Don't store the TextSecure challenge on KitKat+ devices.
2014-02-28 13:40:35 -08:00
Moxie Marlinspike
a3e900ecbe Merge pull request #794 from SamWhited/issue788
Add option to disable screen security
2014-02-27 18:02:05 -08:00
Sam Whited
384fb3b2b5 Add option to disable screen security
See WhisperSystems/TextSecure#788
2014-02-27 19:00:39 -05:00
Moxie Marlinspike
d795aa30b3 Merge pull request #795 from benalbrecht/nocount
Don't show letter count when SMS mode is disabled
2014-02-27 14:12:27 -08:00
Jake McGinty
bac4d63312 Merge pull request #805 from wickedshimmy/fix-sender-details
Don't display sender in outgoing message details
2014-02-27 11:18:51 -08:00
Benjamin Albrecht
bf60f90019 Don't show letter count inside a push group or when SMS mode is disabled 2014-02-27 09:32:19 +01:00
Matt Enright
852ca2ac05 Don't display sender in outgoing message details
Because we don't know it. Fixes WhisperSystems#711.
2014-02-26 21:57:17 -05:00
Moxie Marlinspike
038bebfdbb Bump version to 2.0.3 // FREEBIE 2014-02-26 13:08:03 -08:00
Moxie Marlinspike
3b25b87aa8 Fix for NPE 2014-02-26 12:42:16 -08:00
Moxie Marlinspike
5a62856e46 Make SMS delivery reports optional. 2014-02-26 12:38:50 -08:00
Moxie Marlinspike
37a52df4e6 Fix bug that broke notifications for group messages. 2014-02-26 12:31:56 -08:00
Jake McGinty
d3148b6766 Merge pull request #764 from m0jo/master
Fix hpyerlinking recieved/sent date in messages
2014-02-26 09:46:42 -08:00
Tim Bücher
6fb85aff6d Fix hpyerlinking recieved/sent date in messages 2014-02-26 13:43:55 +01:00
Moxie Marlinspike
ed45067227 Bumping version to 2.0.2 // FREEBIE 2014-02-26 00:01:52 -08:00
Jake McGinty
0015711759 migrate logcat submit to preferences
// FREEBIE
2014-02-25 23:11:00 -08:00
Moxie Marlinspike
15390e477e Update translations.
// FREEBIE
2014-02-25 22:47:03 -08:00
Jake McGinty
a8c23413ba Merge pull request #745 from mcginty/logcat_submit
activity to submit logcat to pastebin
2014-02-25 22:20:34 -08:00
Jake McGinty
ce68429a9b activity to submit logcat to pastebin 2014-02-25 22:14:21 -08:00
Jake McGinty
1a9a88a5a1 Merge pull request #744 from backspace/change-sent-link-color
Make outgoing links readable
2014-02-25 21:23:41 -08:00
Moxie Marlinspike
7987362c25 Fix for bug where messages are stuck in pending state after upgrade. 2014-02-25 20:57:18 -08:00
Buck Doyle
5d42110d6c Make outgoing links readable 2014-02-25 22:46:49 -05:00
Moxie Marlinspike
dec7fd4c8a Fix for NPE with audio attachments. 2014-02-25 17:54:49 -08:00
Moxie Marlinspike
82df23dd41 Temporarily bring back end session. 2014-02-25 17:38:55 -08:00
Moxie Marlinspike
ce710b378f Fix for stalled retries. 2014-02-25 17:15:30 -08:00
Moxie Marlinspike
20fd881613 Display error code from server when already registered elsewhere. 2014-02-25 17:00:31 -08:00
Jake McGinty
5fa429b0d5 Merge pull request #676 from mcginty/emoji_density
Fix emoji density scaling issues
2014-02-25 16:28:43 -08:00
Jake McGinty
630dce04fc handle emoji density scaling more correctly 2014-02-25 16:24:33 -08:00
Moxie Marlinspike
0da1d8818e Merge pull request #729 from funk78/feature/use_getDeviceE164Number
FREEBIE reuse code
2014-02-25 15:15:26 -08:00
droidastic
c84285c639 FREEBIE reuse code 2014-02-25 23:56:18 +01:00
Moxie Marlinspike
5a525a2e58 Switch KCS priority to MIN on JB+. Eliminates icon in status bar! 2014-02-25 10:33:11 -08:00
Moxie Marlinspike
dda8a214a4 Merge pull request #668 from m0jo/master
Fix missing localozation strings for custom LED blink pattern.
2014-02-25 08:37:38 -08:00
Tim Bücher
bd167cbb17 Fix missing localozation strings for custom LED blink pattern. 2014-02-25 11:38:31 +01:00
Jake McGinty
a0aaa7d724 Explain why SMS fallback is disabled when it is 2014-02-25 01:23:22 -08:00
Moxie Marlinspike
25e03b3579 Bumping version to 2.0.1 2014-02-24 21:50:33 -08:00
Moxie Marlinspike
52ff4ecfd2 Fix to make emoji work in push groups. 2014-02-24 17:01:28 -08:00
Moxie Marlinspike
43c1576aab Fix for bad database migration. 2014-02-24 17:00:52 -08:00
Jake McGinty
23c607430d fix dark text input on dark background for conversation
// FREEBIE
2014-02-24 16:12:13 -08:00
Jake McGinty
829a92d371 contact selection reactive to dark theme
// FREEBIE
2014-02-24 14:43:38 -08:00
Moxie Marlinspike
559228af5b Fix for bug modifying immutable list. 2014-02-24 14:19:43 -08:00
Moxie Marlinspike
e8a0fac05b Fix for identity key mismatch on devices with V1 identities. 2014-02-24 14:19:17 -08:00
Moxie Marlinspike
7bb45d8a91 Bump version to 2.0 2014-02-24 08:58:33 -08:00
Moxie Marlinspike
fa0c783c64 Update language translations. 2014-02-24 02:17:15 -08:00
Moxie Marlinspike
377a332789 Tone down logging a bit. 2014-02-24 02:12:41 -08:00
Moxie Marlinspike
2ab8c77748 Correctly update group title. 2014-02-24 00:37:11 -08:00
Moxie Marlinspike
d5f0415907 Merge branch 'master' of github.com:WhisperSystems/TextSecure
Conflicts:
	src/org/thoughtcrime/securesms/GroupCreateActivity.java
	src/org/thoughtcrime/securesms/database/GroupDatabase.java
2014-02-24 00:23:40 -08:00
Moxie Marlinspike
5000957b99 Listen for group updates, fix group updates, and Recipient marshing 2014-02-24 00:19:54 -08:00
Jake McGinty
fc12f2cf8e disable subtitle for push groups
// FREEBIE
2014-02-23 23:59:13 -08:00
Jake McGinty
e3aed66ded avatar was getting nulled with any update, no more "unsetting"
// FREEBIE
2014-02-23 23:35:46 -08:00
Jake McGinty
f30304423d lose SD card dependency, fix NPEs
// FREEBIE
2014-02-23 23:35:41 -08:00
Jake McGinty
86b3de2a93 group updates do things
// FREEBIE
2014-02-23 21:18:08 -08:00
Jake McGinty
0ae1004142 Merge pull request #603 from WhisperSystems/idempotent_backgrounds
Fix bug with recycled ListItems.
2014-02-23 17:32:47 -08:00
Moxie Marlinspike
da344a0218 Fix bug with recycled ListItems. 2014-02-23 17:11:10 -08:00
Jake McGinty
aab34e491e fix bug where sdk < kitkat is unjustly punished 2014-02-23 16:04:09 -08:00
Moxie Marlinspike
e80de7de2b Fix bug where adding attachment duplicates recipient count. 2014-02-23 15:58:24 -08:00
Jake McGinty
3dd27ed59a push conversation messages are now blue
// FREEBIE
2014-02-23 15:29:07 -08:00
Moxie Marlinspike
ca2eecaedf Add advanced preference for refreshing push directory. 2014-02-23 15:12:40 -08:00
Moxie Marlinspike
e188f62d70 Shunt BCC messages directly to MMS transport. 2014-02-23 14:39:07 -08:00
Moxie Marlinspike
8093b14922 Be sure we don't let MMS out if the transport is disabled. 2014-02-23 14:38:41 -08:00
Moxie Marlinspike
636c50b3b8 Fix for creating non-push groups. 2014-02-23 14:38:03 -08:00
Moxie Marlinspike
9298dded15 Only disable inactive *push* group conversations. 2014-02-23 14:37:41 -08:00
Moxie Marlinspike
c36b5869fc Tweak create passphrase screen text. 2014-02-23 14:37:05 -08:00
Jake McGinty
46de25d658 conversation lock icon for dark theme
// FREEBIE
2014-02-22 22:22:39 -08:00
Jake McGinty
8794661433 fix "end session" inflation
// FREEBIE
2014-02-22 22:22:39 -08:00
Moxie Marlinspike
056dbd1a4e tweak text
//FREEBIE
2014-02-22 14:42:02 -08:00
Moxie Marlinspike
125a60290f Correctly handle key conflict resolution for incoming push. 2014-02-22 14:22:12 -08:00
Moxie Marlinspike
315cf2d8e4 Eliminate concept of group 'owner'. 2014-02-22 11:29:28 -08:00
Moxie Marlinspike
da57a689c1 Fill in some strings.
// FREEBIE
2014-02-22 11:10:30 -08:00
Moxie Marlinspike
9c9866e7ee Add 'leave group' functionality. Includes other bug fixes. 2014-02-22 10:54:43 -08:00
Moxie Marlinspike
a6e1d56cde Refactor group messaging protocol.
// FREEBIE
2014-02-21 17:51:52 -08:00
Jake McGinty
b855f8a163 fix single contact selection for contacts with multiple phones
// FREEBIE
2014-02-21 17:12:08 -08:00
Jake McGinty
7beab36c6a updates to conversation menu, and updating of group info
// FREEBIE
2014-02-21 11:33:31 -08:00
Jake McGinty
7667264789 add group photo placeholder assets (not currently used)
// FREEBIE
2014-02-20 23:09:28 -08:00
Moxie Marlinspike
ccd1691b22 Make 'push' status a type bit on both incoming and outgoing msgs. 2014-02-20 23:00:38 -08:00
Moxie Marlinspike
2026330f8a Remove dead code. 2014-02-20 18:27:43 -08:00
Moxie Marlinspike
3ca25de034 Fix for MMS decoding when no SIM is available. 2014-02-20 17:32:01 -08:00
Moxie Marlinspike
ff238a1ce9 Indicate a message was delivered via push in its delivery status. 2014-02-20 16:14:58 -08:00
Moxie Marlinspike
918f223149 Add preference to disable the SMS channel all together.
1) If the SMS fallback preference is disabled, no outgoing
   messages will succeed via the SMS transport.

2) If the SMS fallback preference is disabled, "mirroring" the
   SMS db state when not the default system SMS app is disabled.
2014-02-20 14:49:31 -08:00
Moxie Marlinspike
c5821f770a Don't prompt for auto-initiate if push registered (or active V2). 2014-02-20 12:55:02 -08:00
Moxie Marlinspike
5b22a7bdcb Create Curve25519 identities in the next release. 2014-02-20 12:46:51 -08:00
Moxie Marlinspike
2a74dc8d82 Update the conversation UI after aborting a V1 session. 2014-02-20 12:06:11 -08:00
Moxie Marlinspike
145e4335fc Only send round-trip key exchanges over SMS. 2014-02-20 12:06:11 -08:00
Moxie Marlinspike
1d4d9663db Fix for NPE 2014-02-20 12:06:11 -08:00
Jake McGinty
a0cbbd2d21 confirm session abort handler in ConversationItem
// FREEBIE
2014-02-20 11:42:15 -08:00
Jake McGinty
28b96204a9 make the menu titles a little less incorrect
// FREEBIE
2014-02-19 21:55:40 -08:00
Jake McGinty
c647820fb7 new default contact picture
// FREEBIE
2014-02-19 21:54:12 -08:00
Moxie Marlinspike
9614dc9055 Refactor group database model and flow.
1) Use existing DB types instead of adding new columns.

2) Store group attributes in message body, like everything else.
2014-02-19 21:07:47 -08:00
Jake McGinty
0cdc6fd87d embrace the darkness, don't open quickcontact for groups
// FREEBIE
2014-02-19 15:01:10 -08:00
Moxie Marlinspike
19dddd7adf Support for an 'end session' protocol message.
1) On the push side, this message is a flag in PushMessageContent.
   Any secure message with that flag will terminate the current
   sessin.

2) On the SMS side, there is an "end session" wire type and
   the convention that a message with this wire type must be
   secure and contain the string "TERMINATE."
2014-02-19 13:50:32 -08:00
Jake McGinty
0688dd0c2c repurpose ImageDivet for avatar, minor lint, fix bitmap decoding
// FREEBIE
2014-02-19 00:01:37 -08:00
Jake McGinty
649f037ed8 apply same fix used on DecryptingPartInputStream to AttachmentCipherInputStream // FREEBIE 2014-02-18 16:42:44 -08:00
Jake McGinty
9d9a0ec218 collection of fixes based on comments/small aesthetic stuff // FREEBIE 2014-02-18 16:42:44 -08:00
Jake McGinty
d19ab04bdd gradle wrapper 1.10, android plugin 0.8, android support 19.0.1 //
FREEBIE
2014-02-18 16:42:40 -08:00
Jake McGinty
e05bacd8c3 fixing avatar selection when some gallery apps won't return any extras
but still succeed // FREEBIE
2014-02-18 16:42:33 -08:00
Moxie Marlinspike
760c96171d Merge branch 'master' into groups 2014-02-18 13:53:35 -08:00
Moxie Marlinspike
3999171377 Introduce registration-time ID for detecting stale sessions.
1) At registration time, a client generates a random ID and
   transmits to the the server.

2) The server provides that registration ID to any client
   that requests a prekey.

3) Clients include that registration ID in any
   PreKeyWhisperMessage.

4) Clients include that registration ID in their sendMessage
   API call to the server.

5) The server verifies that the registration ID included in
   an API call is the same as the current registration ID
   for the destination device.  Otherwise, it notifies the
   sender that their session is stale.
2014-02-18 12:51:23 -08:00
Jake McGinty
abce678cb4 received group messages aren't processed as "meta" group stuff, oops. 2014-02-17 22:20:43 -08:00
Jake McGinty
fb75d90edc fix bug where it opened wrong thread for TS group creates 2014-02-17 22:04:08 -08:00
Jake McGinty
25324a45b3 Show something for unnamed groups, allow us to have different default photos for groups vs. individuals 2014-02-17 21:42:30 -08:00
Jake McGinty
877ed8f59c Show TS group titles properly, hide MMS options 2014-02-17 20:25:40 -08:00
Jake McGinty
54b087c458 MMS should open conversation also 2014-02-17 20:19:54 -08:00
Jake McGinty
ca51ddac7f re-privatize getRecipientForNumber 2014-02-17 20:19:35 -08:00
Jake McGinty
e15ff6193f unbreak group insertion for new installs 2014-02-17 20:10:37 -08:00
Jake McGinty
e8251800ab gb icon for new group changed 2014-02-17 18:17:23 -08:00
Jake McGinty
93329df530 avatar selection on gb fixed, disable push groups if not push registered 2014-02-17 17:33:20 -08:00
Jake McGinty
948f888670 group ui task refactor, small bug fixes 2014-02-17 16:36:31 -08:00
Jake McGinty
c19ac8ec1e less hacky way of converting ContactData to Recipients 2014-02-17 16:23:29 -08:00
Jake McGinty
04327e9ed7 refresh conversation fragment to new thread when created 2014-02-17 15:48:09 -08:00
Jake McGinty
e81526e581 error messaging for group creation issues 2014-02-17 15:48:09 -08:00
Jake McGinty
4f87c1e52e better intent sequence for single contact selection 2014-02-17 15:48:08 -08:00
Jake McGinty
725e943842 fixed date floating in funky location 2014-02-17 15:48:08 -08:00
Moxie Marlinspike
9bb327db42 Handle SMS fallback preferences correctly, and fix directory sync. 2014-02-17 15:31:42 -08:00
Moxie Marlinspike
94b54a6d63 Fix build. 2014-02-17 13:22:37 -08:00
Moxie Marlinspike
fe03a22926 Make group creation exceptions more accurate. 2014-02-17 13:19:49 -08:00
Moxie Marlinspike
d8e1df9233 Check prekey identity on mismatched devices too. 2014-02-17 12:41:06 -08:00
Moxie Marlinspike
6b5753337c Putting this functionality in the hands of the user was a bad idea. 2014-02-17 12:37:34 -08:00
Moxie Marlinspike
18f3314cd2 Fix for no paradigm. 2014-02-17 11:54:39 -08:00
Moxie Marlinspike
b9f4fba98a Handle identity key mismatch on outgoing group messages.
Additionally, make the group creation process asynchronous.
2014-02-17 11:43:37 -08:00
Jake McGinty
5810062b25 group action conversation items, tweaks to single contact selection 2014-02-16 21:09:15 -08:00
Moxie Marlinspike
e7e5bc0884 Verify identity keys on outgoing messages.
If PreKeyEntity identity key doesn't match local DB, fail
outgoing message and queue "incoming" identity key update
message for manual user approval.
2014-02-16 15:23:49 -08:00
Moxie Marlinspike
e2989373cd All push groups are 'secure' ConversationActivities. 2014-02-15 18:40:08 -08:00
Moxie Marlinspike
4e703d5a00 Correct retry logic for SMS JB+ and for push groups. 2014-02-15 18:31:25 -08:00
Moxie Marlinspike
b79bc4c234 Fix for non-group threading issue. 2014-02-15 11:44:47 -08:00
Moxie Marlinspike
a305bb80e6 Correctly display group members 2014-02-15 11:30:20 -08:00
Jake McGinty
8e3aa94a05 group member name tags, group convo iconography, tweaks 2014-02-15 04:21:56 -08:00
Jake McGinty
b2fdd74a1c fix error indicator visibility/placement for sent messages 2014-02-14 17:10:05 -08:00
Moxie Marlinspike
7d3a2acb29 Fix build 2014-02-14 16:41:06 -08:00
Jake McGinty
556b0dec1d pretty up the interface/colors a bit. 2014-02-14 16:21:48 -08:00
Jake McGinty
9b41675f8f single contact selection 2014-02-14 16:21:48 -08:00
Moxie Marlinspike
9438973eac Switch to protobuf enums for push signal type. 2014-02-14 16:20:47 -08:00
Moxie Marlinspike
2f98622948 Fix group migration. 2014-02-14 16:18:18 -08:00
Moxie Marlinspike
067799be06 Display group actions and correctly handle group delivery. 2014-02-14 15:59:57 -08:00
Moxie Marlinspike
7c46f3cbf8 Fill in group creation actions 2014-02-13 17:10:20 -08:00
Jake McGinty
41aa53dd66 masterSecret 2014-02-13 12:36:31 -08:00
Jake McGinty
b47076cf1b kill wrong finish() for mms creation 2014-02-13 12:18:17 -08:00
Jake McGinty
bb4a4d33c9 handlePush and handleMms prototypes 2014-02-13 12:15:26 -08:00
Jake McGinty
c5d010c86f Quick progressbar sample for network operations during group create 2014-02-13 11:44:07 -08:00
Jake McGinty
fe55ac57d9 strings, strings, strings 2014-02-12 18:12:58 -08:00
Jake McGinty
01a438de09 basic messaging when non-push users are added 2014-02-12 17:17:33 -08:00
Jake McGinty
b4a4e629f1 alignment and string work 2014-02-12 12:05:18 -08:00
Jake McGinty
9cd5a67ec5 directory changes to use number instead of token, group ui progress 2014-02-12 11:35:04 -08:00
Moxie Marlinspike
fc3411da83 Merge pull request #593 from backspace/patch-1
Link BitHub image to donation page. Fixes #591
2014-02-10 21:57:10 -08:00
Buck Doyle
dc85b23a06 Link BitHub image to description page. Fixes #591 2014-02-11 00:50:09 -05:00
Moxie Marlinspike
0af473d880 Merge branch 'groups' of github.com:WhisperSystems/TextSecure into groups 2014-02-03 11:52:27 -08:00
Moxie Marlinspike
0ace469d74 Support for multi-device.
1) In addition to the Recipient interface, there is now
   RecipientDevice.  A Recipient can have multiple corresponding
   RecipientDevices.  All addressing is done to a Recipient, but
   crypto sessions and transport delivery are done to
   RecipientDevice.

2) The Push transport handles the discovery and session setup
   of additional Recipient devices.

3) Some internal rejiggering of Groups.
2014-02-02 19:38:06 -08:00
Jake McGinty
ed3151bdb5 backward compatible navigation drawer toggle // FREEBIE 2014-02-01 19:40:46 -08:00
Jake McGinty
6157a0df7a Merge pull request #572 from cketti/black_startup_theme
Use black theme without actionbar for RoutingActivity
2014-02-01 18:54:50 -08:00
Jake McGinty
5a75ff9299 Merge pull request #537 from veeti/registration
Registration activity fixes
2014-01-26 01:02:41 -08:00
Jake McGinty
9fd2c4753e ui wip 2014-01-25 23:39:56 -08:00
cketti
c8ef21774d Use black theme without actionbar for RoutingActivity
The theme from the manifest is used for the startup animation.
Previously light background with a dark actionbar and the
green icon was displayed during the startup animation, then
the theme switched to whatever is configured in settings.
Now a black background without actionbar is used for the
startup animation. This was chosen because a black "flash" when
using the light theme is visually less distracting than a white
flash when using the dark theme.
2014-01-26 06:26:30 +01:00
Veeti Paananen
fc458770ea Reduce margin between registration text 2014-01-20 15:31:09 +02:00
Veeti Paananen
0d2ce2528c Use equal margins in the registration view 2014-01-20 15:13:11 +02:00
Veeti Paananen
6bfae9ebc9 Fix registration activity being cut off on small screens or large fonts 2014-01-20 15:13:11 +02:00
Jake McGinty
2f01569e45 Revert "Don't stick thread IDs in the Intent extra data. Always mark all threads read from notification."
This reverts commit 91b52bed18.
2014-01-18 18:25:51 -08:00
Moxie Marlinspike
d51ccb8ce2 Update gradle docs //FREEBIE 2014-01-18 10:44:06 -08:00
Moxie Marlinspike
49daa45dca wip 2014-01-14 00:26:43 -08:00
Moxie Marlinspike
63b90bb57f Merge pull request #541 from mcginty/resource_crunch
pngcrush and removal of unused layout
2014-01-12 12:09:09 -08:00
Jake McGinty
8f7c9d45aa pngcrush and removal of unused layout // FREEBIE 2014-01-11 10:55:52 -10:00
Moxie Marlinspike
4851a555e7 Merge pull request #540 from WhisperSystems/lint
Fixes more lint errors
2014-01-11 10:52:32 -08:00
Ruben Pollan
a0dd1689da Use minSdkVersion 9
'Array.copyOfRange' depends on API level 9

// FREEBIE
2014-01-11 08:47:28 -10:00
Moxie Marlinspike
63d217bd80 Merge pull request #539 from WhisperSystems/apndefaults
Add apn settings for some providers
2014-01-11 10:01:17 -08:00
Ruben Pollan
6cdd2aff0d Suppress lint errors 2014-01-11 01:34:11 -10:00
Ruben Pollan
1e77c9904a VerifyKeysActivity don't exists anymore 2014-01-10 21:51:53 -10:00
Ruben Pollan
c134709009 Add apn settings for some providers 2014-01-10 21:03:45 -10:00
Moxie Marlinspike
4a93f7d012 Merge pull request #538 from WhisperSystems/lint
Merge Don't fail build if lint gives errors
2014-01-10 20:57:46 -08:00
Ruben Pollan
29cdfd4b96 Fix some translation errors 2014-01-10 16:22:30 -10:00
Ruben Pollan
a14897f8b5 Don't fail build if lint gives errors 2014-01-10 16:20:25 -10:00
Moxie Marlinspike
9b6d4e3696 Merge pull request #532 from mcginty/visual_refresh
WBoC Visual Refresh
2014-01-09 21:08:15 -08:00
Jake McGinty
28c1c5006b icon, contact list and conversation redesign 2014-01-09 19:06:41 -10:00
Moxie Marlinspike
8ca60a8746 Merge pull request #536 from bozsco/fix-#487
Merge Automatically show keyboard when new SMS is composed
2014-01-09 14:40:33 -08:00
bozsco
e9a8c7474c Automatically show keyboard when new SMS is composed 2014-01-09 16:52:11 -05:00
Moxie Marlinspike
75ce00cb90 Merge pull request #495 from breser/issue469
Merge Fix issue #469: Hide "Clear Passphrase" if passphrase is disabled.
2014-01-07 22:02:23 -08:00
Ben Reser
149671095b Fix issue #469: Hide "Clear Passphrase" if passphrase is disabled. 2014-01-07 21:47:28 -08:00
Moxie Marlinspike
c488947248 Merge pull request #453 from irv/master
Merge Fix issue #433
2014-01-07 20:08:53 -08:00
Moxie Marlinspike
04b2c3772a Support skipping password creation at setup 2014-01-07 16:27:00 -08:00
Moxie Marlinspike
a4a3b4a453 Update registration strings 2014-01-07 15:16:23 -08:00
Moxie Marlinspike
5c97b1e864 Twiddle the auto initiate 2014-01-06 20:03:16 -08:00
Moxie Marlinspike
dde2d2564a Fix rebase errors 2014-01-06 19:49:35 -08:00
Moxie Marlinspike
71664926e9 Better handling for unregistered users on outgoing message. 2014-01-06 14:38:41 -08:00
Moxie Marlinspike
3c3028c8e3 Typo 2014-01-06 14:38:15 -08:00
Moxie Marlinspike
842df1ac39 Remove receiver chains that are 5 old. 2014-01-06 14:38:15 -08:00
Moxie Marlinspike
ff0dff745c Let's order 3DHE by Alice/Bob roles. 2014-01-06 14:38:15 -08:00
Moxie Marlinspike
d4188c4a1c SHA256 attachment MACs. 2014-01-06 14:38:14 -08:00
Moxie Marlinspike
f002072f38 New attachment API 2014-01-06 14:38:14 -08:00
Moxie Marlinspike
72be77b47e Catch bad prekeys 2014-01-06 14:38:14 -08:00
Moxie Marlinspike
327ee4ff62 Remove unnecessary classes, up prekey limit to 100 2014-01-06 14:38:14 -08:00
Moxie Marlinspike
1ab4e7e9de Library accepts push connection certificate as argument. 2014-01-06 14:38:14 -08:00
Moxie Marlinspike
ff5ad4b85d Updated copyright 2014-01-06 14:37:52 -08:00
Moxie Marlinspike
ce5f3c5157 Validate phone numbers when formatting. 2014-01-06 14:37:52 -08:00
Moxie Marlinspike
f7b71e5e28 Add info to MessageKey HKDF, change order of RootKey derivation. 2014-01-06 14:37:52 -08:00
Moxie Marlinspike
44092a3eff Support for Axolotl protocol.
1) Split code into v1 and v2 message paths.

2) Do the Axolotl protocol for v2.

3) Switch all v2 entities to protobuf.
2014-01-06 14:37:52 -08:00
Moxie Marlinspike
dc73bc2a5c Having calling code specify push URL to library. 2014-01-06 14:37:52 -08:00
Moxie Marlinspike
f8dda5afd6 Missing comma 2014-01-06 14:37:51 -08:00
Moxie Marlinspike
4380b46a35 Correctly update MMS security status. 2014-01-06 14:37:51 -08:00
Moxie Marlinspike
7e926d08ac Better relay handling. 2014-01-06 14:37:51 -08:00
Moxie Marlinspike
fde8d32848 Subtle rebase conflicts... 2014-01-06 14:37:51 -08:00
Moxie Marlinspike
f10d6a7b0b Lets use strict wildcard matching. 2014-01-06 14:36:23 -08:00
Moxie Marlinspike
320cc84392 Update production endpoint. 2014-01-06 14:36:23 -08:00
Moxie Marlinspike
dadabdfaa8 Make UI responsive to UniversalTransport upgrades. 2014-01-06 14:36:23 -08:00
Moxie Marlinspike
07b7696937 Update 'DJB' type not to conlict with NIST type 0x04. 2014-01-06 14:36:23 -08:00
Moxie Marlinspike
c38a8aa699 Migrate to Curve25519.
1) Generate a Curve25519 identity key.

2) Use Curve25519 ephemerals and identities for v2 3DHE agreements.

3) Initiate v2 key exchange messages.

4) Accept v1 key exchange messages.

5) TOFU Curve25519 identities.
2014-01-06 14:36:22 -08:00
Moxie Marlinspike
a03fff8b24 Add HKDF support for new sessions. 2014-01-06 14:36:22 -08:00
Moxie Marlinspike
dbc070cd65 Refactor the ciphertext message parsing and building. 2014-01-06 14:36:22 -08:00
Moxie Marlinspike
6e640db39c 204 is ok 2014-01-06 14:36:21 -08:00
Moxie Marlinspike
44d59d0fd1 Correctly store destination SMS state. 2014-01-06 14:36:21 -08:00
Moxie Marlinspike
51b9affe90 Add federation support for attachment retrieval. 2014-01-06 14:36:21 -08:00
Moxie Marlinspike
246cd10454 Bundle e164 and relay into PushDestination 2014-01-06 14:36:21 -08:00
Moxie Marlinspike
ca3c82f581 Simply PushServiceSocket constructor. 2014-01-06 14:36:21 -08:00
Moxie Marlinspike
5b2caa0074 Make relay arguments consistent. 2014-01-06 14:36:21 -08:00
Moxie Marlinspike
fa073e0b46 Update gradle plugin version 2014-01-06 14:36:21 -08:00
Moxie Marlinspike
7fd9946275 Fix release push service url 2014-01-06 14:35:54 -08:00
Moxie Marlinspike
fa5ccc3f8a Support for server federation. 2014-01-06 14:35:54 -08:00
Moxie Marlinspike
25a2ad7289 Add directory refreshing at 24hr intervals. 2014-01-06 14:35:54 -08:00
Moxie Marlinspike
e7a21752d8 Switch code API to GET. 2014-01-06 14:35:54 -08:00
Moxie Marlinspike
b010c5194a Support publishing AARs to maven local 2014-01-06 14:35:54 -08:00
Moxie Marlinspike
eb4cc7f4e5 Add incoming message sources to active token dir. 2014-01-06 14:35:54 -08:00
Moxie Marlinspike
1ac32346c1 Graduate to a rebased and gradle world. 2014-01-06 14:35:54 -08:00
Moxie Marlinspike
2d083208cc Handle negative directory case and unlisted contacts. 2014-01-06 14:35:54 -08:00
Moxie Marlinspike
75cca3add1 Resign ourselves to a less sophisticated contact intersection method. 2014-01-06 14:35:53 -08:00
Moxie Marlinspike
073b1f69e3 Rollbacks, v2 sms-transport key exchanges, push identity conflicts.
1) Stop protocol rollbacks.

2) Handle v2 version key exchange messages.

3) Handle identity key conflicts on prekeybundle messages.
2014-01-06 14:35:53 -08:00
Moxie Marlinspike
5e6d39beea Move classes into library 2014-01-06 14:35:53 -08:00
Moxie Marlinspike
8f6590b738 Handle notifications and receiving push when locked. 2014-01-06 14:35:53 -08:00
Moxie Marlinspike
0dd36c64a4 Basic support for encrypted push-based attachments.
1) Move the attachment structures into the encrypted message body.

2) Encrypt attachments with symmetric keys transmitted in the
   encryptd attachment pointer structure.

3) Correctly handle asynchronous decryption and categorization of
   encrypted push messages.

TODO: Correct notification process and network/interruption
      retries.
2014-01-06 14:35:53 -08:00
Moxie Marlinspike
cddba2738f Make encoding/decoding more explicit. 2014-01-06 14:35:53 -08:00
Moxie Marlinspike
0cc5837d7f Support encrypted transport, properly handle multiple recipients.
1) Add encryption support for the transport layer.  This obscures
   metadata from the push messaging provider.

2) Better support the direction multiple destination messages is
   headed (one unique message per recipient).
2014-01-06 14:35:53 -08:00
Moxie Marlinspike
68ec0a3727 Add last resort key and signaling key. 2014-01-06 14:35:53 -08:00
Moxie Marlinspike
45e380a5bb move constants into library 2014-01-06 14:35:52 -08:00
Moxie Marlinspike
1bbcedabd4 Added SMS transport support for PreKeyBundle messages.
1) Added SMS transport support.

2) Keep track of whether a PreKeyBundle message has gotten
   a response, and send them as subsequent messages until
   one has been received.
2014-01-06 14:35:52 -08:00
Moxie Marlinspike
c3b8b62d32 Delete pre-key record when establishing agreement. 2014-01-06 14:35:52 -08:00
Moxie Marlinspike
43492b66c4 Rename EncryptedMessage 2014-01-06 14:35:52 -08:00
Moxie Marlinspike
90814be167 Fix up release parameters 2014-01-06 14:35:52 -08:00
Moxie Marlinspike
7f642666dd Basic support for prekeybundle message delivery and receipt. 2014-01-06 14:35:52 -08:00
Moxie Marlinspike
1cc2762656 Refactor relationship between SessionCipher and Message. 2014-01-06 14:35:52 -08:00
Moxie Marlinspike
d1969412fb Move PreKey ids to be Mediums, generate in circular buffer. 2014-01-06 14:35:51 -08:00
Moxie Marlinspike
edb89ee3e9 Encode prekey as full JSON rather than protobuf blobs.
1) Split prekey messages out into full JSON.

2) Add support for retrieving prekeys.
2014-01-06 14:35:51 -08:00
Moxie Marlinspike
499de2d2bf Move prekey jsonifcation into the push code, add identity key. 2014-01-06 14:35:51 -08:00
Moxie Marlinspike
b8f663b69c Move common crypto classes into TextSecureLibrary.
1) Move all the crypto classes from securesms.crypto.

2) Move all the crypto storage from securesms.database.keys

3) Replace the old imported BC code with spongycastle.
2014-01-06 14:35:51 -08:00
Moxie Marlinspike
2042ca6cb7 Generate "prekeys" at push registration time.
This generates a large number of key exchange messages and
registers them with the server during signup.
2014-01-06 14:34:47 -08:00
Moxie Marlinspike
cfb7b8fcba Consider unregistered if authorization fails.
This should only occur if another device has registered with the
same number, effectively making the current device unregistered.
2014-01-06 14:34:46 -08:00
Moxie Marlinspike
03ff55db97 Actually set the preference to unregistered 2014-01-06 14:33:53 -08:00
Moxie Marlinspike
0e899b93d4 Reset Release 2014-01-06 14:33:53 -08:00
Moxie Marlinspike
fb378a6e00 Change attachment retrieval interface 2014-01-06 14:33:53 -08:00
Moxie Marlinspike
9287d413ac Support for incoming attachments.
1) Refactored MMS layer to use abstracted types.

2) Added support for retrieving attachment IDs.
2014-01-06 14:33:52 -08:00
Moxie Marlinspike
4bb337a3a0 Revised support for outgoing attachments 2014-01-06 14:33:52 -08:00
Moxie Marlinspike
fd045f2354 Refactor MMS send/download to be synchronous.
1) Make the radio change a synchronous action with a timeout.

2) Move the send logic into an MmsTransport, in preparation for
   UniversalTransport composition.

3) Move the download logic into a synchronous receiver.
2014-01-06 14:33:52 -08:00
Moxie Marlinspike
53803630d4 Enable TextSecure universal transport. 2014-01-06 14:33:52 -08:00
Moxie Marlinspike
d7070e7ecf Inserted the Registration activity into initial setup flow.
1) Added push messaging toggle to preferences.

2) Added push messaging registration screen to setup flow.

3) Migrated rest of SharedPreferences accessors to
   TextSecurePreferences.
2014-01-06 14:30:03 -08:00
Moxie Marlinspike
52e5274a2f Add registration notification support. 2014-01-06 14:21:51 -08:00
Moxie Marlinspike
4c615546e8 update gitignore 2014-01-06 14:21:50 -08:00
Moxie Marlinspike
1d1492b15c Remove from VCS 2014-01-06 14:21:50 -08:00
Moxie Marlinspike
0ed9da9a2c Strings and slight cleanup 2014-01-06 14:21:50 -08:00
Moxie Marlinspike
26dadfcb7a Move trust store to raw resource from asset. 2014-01-06 14:21:50 -08:00
Moxie Marlinspike
1ac06312a0 Move directory and push service socket into library. 2014-01-06 14:21:50 -08:00
Moxie Marlinspike
a200d29514 Move most of Util into library 2014-01-06 14:21:50 -08:00
Moxie Marlinspike
21eee19380 Split into library project and add shared preferences layer of indirection. 2014-01-06 14:20:59 -08:00
Moxie Marlinspike
2539723410 Fix string 2014-01-06 14:19:06 -08:00
Moxie Marlinspike
0958c30400 Stringify 2014-01-06 14:19:06 -08:00
Moxie Marlinspike
7eef93c829 Stringify and clean layout 2014-01-06 14:19:05 -08:00
Moxie Marlinspike
3634ba0b55 Registration with voice verification. 2014-01-06 14:18:30 -08:00
Moxie Marlinspike
6ca029f64a Plugin 0.7, Gradle 1.9, AS 4.0 2014-01-06 14:13:12 -08:00
Moxie Marlinspike
7bf96ec7d7 Merge pull request #524 from thoughtbox/master
Merge Allow for "Enable Enter key" translation
2014-01-02 15:35:56 -08:00
thoughtbox
389fdaed32 Enter key string.xml references
To allow for translations
2014-01-02 18:58:54 +01:00
thoughtbox
8186694555 Change to string refs for Enter
To allow for translation
2014-01-02 18:53:50 +01:00
Moxie Marlinspike
b98d5ca244 Merge pull request #501 from veeti/notification
Improve password caching notification UI & UX
2014-01-01 13:51:26 -08:00
Veeti Paananen
eb1b762a76 Improve password caching notification UI & UX
On Jelly Bean and above:

- Use the standard notification style for a better and consistent visual
  appearance

- Use the JB notification actions API for the locking action

- Use a lower notification priority to prioritize other notifications
  over TextSecure

On ICS:

- Use the existing custom notification layout

Everywhere:

- Allow opening the app itself from the notification

- Simplify strings: don't talk about a "cached passphrase" but about the
  app being "unlocked"/"locked"
2013-12-22 04:48:39 +02:00
Moxie Marlinspike
ca92b4d904 Merge pull request #493 from niktrix/patch-1
set unread message count in notifications.
2013-12-18 13:21:10 -08:00
nicky g
a0c821f6ac set unread message count in notifications.
was not able to squash commit requesting new pull
2013-12-19 02:44:42 +05:30
Moxie Marlinspike
d260cdcc9f Add giffgaff APN defaults. // FREEBIE 2013-12-17 16:57:40 -08:00
Moxie Marlinspike
496d5e21d2 Add Slovenia APN defaults 2013-12-17 16:53:35 -08:00
Moxie Marlinspike
bba49fcb76 Merge pull request #446 from veeti/settings
Show LED and ringtone setting values in their summaries
2013-12-16 20:17:54 -08:00
Veeti Paananen
6eb094c87e Show LED and ringtone setting values in their summaries
As instructed in the Android design guidelines. Also removed unnatural
capitalization.
2013-12-17 03:13:22 +02:00
Moxie Marlinspike
23e8035edd Merge pull request #486 from jlund/strings-update
Incredibly minor cleanup of the strings file
2013-12-16 15:02:03 -08:00
Joshua Lund
0fed5593a8 Incredibly minor cleanup of the strings file 2013-12-16 15:11:17 -07:00
Moxie Marlinspike
60f5363da0 Merge pull request #485 from veeti/tabswipe
Add tab swiping to contacts picker
2013-12-16 12:50:41 -08:00
Veeti Paananen
bee964dcf8 Improve appearance of "no contacts" indicators by centering them.
Also remove pointless scroll views.
2013-12-16 22:27:10 +02:00
Veeti Paananen
f18324e2d6 Add tab swiping in the contact picker 2013-12-16 22:17:52 +02:00
Moxie Marlinspike
e7f4a6039c Merge pull request #479 from kyleconroy/new_icon
Add a new icon
2013-12-16 11:05:43 -08:00
Moxie Marlinspike
fccb4cc0cf Add bithub payout slug. 2013-12-16 09:58:56 -08:00
Andy Irving
91b52bed18 Don't stick thread IDs in the Intent extra data. Always mark all threads read from notification. 2013-12-14 16:55:24 +00:00
Kyle Conroy
f91cfe1c44 Update icon with more distinct drop shadow. Make the lock flat. 2013-12-13 11:33:02 -08:00
Kyle Conroy
c8422a8942 Lighten the drop shadow on the icon 2013-12-12 16:54:36 -08:00
Kyle Conroy
f74119a58c Add new icons 2013-12-12 16:30:14 -08:00
Moxie Marlinspike
ce4f935fa6 Bump version to 1.0.6 2013-12-07 11:47:14 -08:00
Moxie Marlinspike
ee0a84ec16 kn_IN 2013-12-07 11:31:08 -08:00
Moxie Marlinspike
7d931f5e6b Updated translations 2013-12-07 11:22:33 -08:00
Moxie Marlinspike
1740f9a7cd Merge pull request #443 from veeti/unlock
Clean up password unlock layout
2013-12-07 11:16:19 -08:00
Moxie Marlinspike
94913f34da Merge pull request #459 from thoughtbox/master
Add summary information to "Sound" setting in preferences.
2013-12-07 11:08:11 -08:00
Moxie Marlinspike
15284cd4d9 Merge pull request #460 from thejeshgn/patch-1
Added Kannada to languages
2013-12-07 11:05:02 -08:00
Moxie Marlinspike
7c2fb74af4 Fix for bug that could drop unencrypted MMS on KitKat. 2013-12-07 11:03:26 -08:00
Moxie Marlinspike
7489f3463a Change the way notifications work for non-default KitKat.
Messages that are not "secure" (encrypted or key exchange) are
automatically marked as read if TextSecure isn't the default
KitKat SMS app.

This change in functionality allows people who aren't using
TextSecure as a default SMS app on KitKat to still receive
notifications when they get incoming encrypted messages.
2013-12-07 11:03:25 -08:00
Thejesh GN
ed73bd57a1 Added Kannada to languages 2013-12-05 13:37:43 +05:30
Moxie Marlinspike
99d295abc5 Merge pull request #454 from veeti/highlights
Update card highlight style
2013-12-04 14:19:54 -08:00
thoughtbox
b56a4f9740 Update strings.xml to reference Sound summary in Settings. 2013-12-04 22:47:54 +01:00
thoughtbox
cc5981594f Add summary to "Sound" preference (1 of 2) 2013-12-04 22:46:30 +01:00
Veeti Paananen
37fb8fe445 Update card highlight style
Out with orange, in with blue and gray (on >= 4.4).
2013-12-04 01:19:53 +02:00
Andy Irving
7f44b029fe use PendingIntent.FLAG_UPDATE_CURRENT in calls to PendingIntent.getActivity() to avoid re-using the same extra data on every intent. 2013-12-03 21:48:16 +00:00
Moxie Marlinspike
b2bd19d787 Bumping version to 1.0.5 2013-12-03 08:30:15 -08:00
Moxie Marlinspike
c2b809c7c6 Fix for long multipart message bug on some ROMs. 2013-12-03 08:21:31 -08:00
Moxie Marlinspike
e0aa7fd359 Let unencrypted messages through. 2013-12-03 08:10:58 -08:00
Moxie Marlinspike
3a5f9748ff Merge pull request #449 from thoughtbox/patch-4
Update strings.xml
2013-12-03 07:56:18 -08:00
thoughtbox
33fd6414b8 Update strings.xml
Removed emoticon from QuickResponse and changed the wording slightly of the "default SMS app" setting.
2013-12-03 11:08:36 +01:00
Moxie Marlinspike
ce4b8701fb Merge pull request #445 from veeti/null
Fix crash when no SMS package is set
2013-12-02 23:23:29 -08:00
Veeti Paananen
10580a7f20 Fix crash when no SMS package is set 2013-12-03 09:06:16 +02:00
Veeti Paananen
89ae5ed4ed Clean up password unlock layout
Remove unnecessary and redundant nested layouts, fix the alignment of
the unlock button to the right edge of the screen, use cleaner margins
and replace the deprecated fill_parent sizes with match_parent.
2013-12-02 23:19:42 +02:00
967 changed files with 68240 additions and 19125 deletions

7
.gitignore vendored
View File

@@ -5,7 +5,7 @@ project.properties
bin/
gen/
.idea/
TextSecure.iml
*.iml
out
tests
lint.xml
@@ -17,6 +17,5 @@ build-log.xml
.gradle
build
signing.properties
gradle
gradlew
gradlew.bat
library/lib/
library/obj/

View File

@@ -1,6 +1,6 @@
[main]
host = https://www.transifex.com
lang_map = fr_CA:fr-rCA,pt_BR:pt-rBR,pt_PT:pt,zh_CN:zh-rCN,zh_HK:zh-rHK,zh_TW:zh-rTW,da_DK:da-rDK,de_DE:de,fr_FR:fr,es_ES:es,hu_HU:hu,sv_SE:sv-rSE,bg_BG:bg,el_GR:el,kn_IN:kn-rIN,cs_CZ:cs
lang_map = fr_CA:fr-rCA,pt_BR:pt-rBR,pt_PT:pt,zh_CN:zh-rCN,zh_HK:zh-rHK,zh_TW:zh-rTW,da_DK:da-rDK,de_DE:de,tr_TR:tr,fr_FR:fr,es_ES:es,hu_HU:hu,sv_SE:sv-rSE,bg_BG:bg,el_GR:el,kn_IN:kn-rIN,cs_CZ:cs
[textsecure-official.master]

View File

@@ -2,15 +2,15 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.thoughtcrime.securesms"
android:versionCode="60"
android:versionName="1.0.4">
android:versionCode="67"
android:versionName="2.0.4">
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="19"/>
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="19"/>
<permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"
android:label="Access to TextSecure Secrets"
android:protectionLevel="signature" />
android:protectionLevel="signature" />
<uses-permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"/>
<uses-permission android:name="android.permission.READ_PROFILE"/>
<uses-permission android:name="android.permission.WRITE_PROFILE"/>
@@ -39,14 +39,14 @@
<permission android:name="org.thoughtcrime.securesms.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="org.thoughtcrime.securesms.permission.C2D_MESSAGE" />
<application android:name="org.thoughtcrime.securesms.ApplicationListener"
android:icon="@drawable/icon"
android:label="@string/app_name"
android:theme="@style/TextSecure.LightTheme">
<activity android:name=".RoutingActivity"
android:theme="@style/NoAnimation.Theme.Sherlock.Light.DarkActionBar"
android:theme="@style/NoAnimation.Theme.BlackScreen"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
@@ -54,7 +54,7 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SENDTO"/>
<category android:name="android.intent.category.DEFAULT" />
@@ -63,17 +63,25 @@
<data android:scheme="mms" />
<data android:scheme="mmsto" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="audio/*" />
<data android:mimeType="image/*" />
<data android:mimeType="text/*" />
<data android:mimeType="video/*" />
</intent-filter>
</activity>
<activity android:name=".RegistrationProblemsActivity"
android:theme="@style/TextSecure.Light.Dialog"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".CountrySelectionActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ImportExportActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -85,16 +93,19 @@
<activity android:name=".MmsPreferencesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ConversationListActivity"
<activity android:name=".ConversationListActivity"
android:label="@string/app_name"
android:launchMode="singleTask"
android:uiOptions="splitActionBarWhenNarrow"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ConversationActivity"
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".GroupCreateActivity"
android:windowSoftInputMode="stateVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DatabaseMigrationActivity"
android:theme="@style/NoAnimation.Theme.Sherlock.Light.DarkActionBar"
android:launchMode="singleTask"
@@ -121,6 +132,17 @@
<activity android:name=".ContactSelectionActivity"
android:label="@string/AndroidManifest__select_contacts"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".SingleContactSelectionActivity"
android:label="@string/AndroidManifest__select_contact"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PushContactSelectionActivity"
android:label="@string/AndroidManifest__select_contacts"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".AutoInitiateActivity"
@@ -139,10 +161,6 @@
android:label="@string/AndroidManifest__change_passphrase"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".VerifyKeysActivity"
android:label="@string/AndroidManifest__verify_session"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".VerifyIdentityActivity"
android:label="@string/AndroidManifest__verify_identity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -166,10 +184,27 @@
<activity android:name=".RegistrationProgressActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".LogSubmitActivity"
android:label="@string/AndroidManifest__log_submit"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DummyActivity"
android:theme="@android:style/Theme.NoDisplay"
android:enabled="true"
android:allowTaskReparenting="true"
android:noHistory="true"
android:excludeFromRecents="true"
android:alwaysRetainTaskState="false"
android:stateNotNeeded="true"
android:clearTaskOnLaunch="true"
android:finishOnTaskLaunch="true" />
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:name=".service.KeyCachingService"/>
<service android:enabled="true" android:name=".service.SendReceiveService"/>
<service android:enabled="true" android:name=".service.RegistrationService"/>
<service android:enabled="true" android:name=".service.DirectoryRefreshService"/>
<service android:enabled="true" android:name=".gcm.GcmIntentService"/>
<service android:name=".service.QuickResponseService"
@@ -234,14 +269,6 @@
<data android:mimeType="application/vnd.wap.mms-message" />
</intent-filter>
</receiver>
<receiver android:name=".service.SystemStateListener"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
</intent-filter>
</receiver>
<receiver android:name=".notifications.MarkReadReceiver"
android:enabled="true"
@@ -255,6 +282,20 @@
android:grantUriPermissions="true"
android:authorities="org.thoughtcrime.provider.securesms" />
<receiver android:name=".service.RegistrationNotifier"
android:exported="false">
<intent-filter>
<action android:name="org.thoughtcrime.securesms.REGISTRATION_EVENT" />
</intent-filter>
</receiver>
<receiver android:name=".service.DirectoryRefreshListener">
<intent-filter>
<action android:name="org.whispersystems.whisperpush.DIRECTORY_REFRESH"/>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<uses-library android:name="android.test.runner" />
</application>

View File

@@ -2,8 +2,21 @@ Building TextSecure
===================
1. Ensure the 'Android Support Repository' is installed from the Android SDK manager.
1. Ensure gradle >= 1.8 is installed.
Execute Gradle:
gradle build
./gradlew build
Re-building native components
-----------------------------
Note: This step is optional; native components are contained as binaries (see [library/libs](library/libs)).
1. Ensure that the Android NDK is installed.
Execute ndk-build:
cd library
ndk-build
Afterwards, execute Gradle as above to re-create the APK.

View File

@@ -8,6 +8,11 @@ TextSecure is a replacement for the standard text messaging application, allowin
1. *Local Encryption* -- All text messages, regardless of destination, that are sent or received with TextSecure are stored in an encrypted database on your phone.
2. *Wire Encryption* -- When communicating with a recipient who is also using TextSecure, text messages are encrypted during transmission.
Current BitHub Payment For Commit:
=================
[![Current Price](https://bithub.herokuapp.com/v1/status/payment/commit/)](https://whispersystems.org/blog/bithub/)
Bug tracker
-----------
@@ -60,5 +65,6 @@ License
---------------------
Copyright 2011 Whisper Systems
Copyright 2013 Open WhisperSystems
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html

Binary file not shown.

BIN
artwork/ic_group_photo.psd Normal file

Binary file not shown.

31036
artwork/icon.ai Normal file

File diff suppressed because one or more lines are too long

BIN
artwork/icon_gigantic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -3,7 +3,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.6.+'
classpath 'com.android.tools.build:gradle:0.8.+'
}
}
@@ -21,18 +21,18 @@ repositories {
dependencies {
compile 'com.actionbarsherlock:actionbarsherlock:4.4.0@aar'
compile 'com.googlecode.libphonenumber:libphonenumber:5.3'
compile 'com.android.support:support-v4:18.0.0'
compile 'org.whispersystems:gson:2.1'
compile 'com.android.support:support-v4:19.0.1'
compile 'com.google.android.gcm:gcm-client:1.0.2'
compile project(':library')
}
android {
compileSdkVersion 19
buildToolsVersion '19.0.0'
buildToolsVersion '19.0.2'
defaultConfig {
minSdkVersion 8
minSdkVersion 9
targetSdkVersion 19
}
@@ -42,7 +42,7 @@ android {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
resources.srcDirs = ['src']
aild.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
@@ -58,6 +58,10 @@ android {
signingConfig signingConfigs.release
}
}
lintOptions {
abortOnError false
}
}
def Properties props = new Properties()

3
contributing.md Normal file
View File

@@ -0,0 +1,3 @@
##Translations
Please do not submit issues or pull requests for translation fixes. Anyone can update the translations in [Transifex](https://www.transifex.com/projects/p/textsecure-official/). Please submit your corrections there.

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#Sat Dec 21 23:48:05 PST 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=http\://services.gradle.org/distributions/gradle-1.10-bin.zip

164
gradlew vendored Executable file
View File

@@ -0,0 +1,164 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

90
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,90 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.whispersystems.textsecure"
android:versionCode="1"
android:versionName="0.1">
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="16"/>
<application />
</manifest>

58
library/build.gradle Normal file
View File

@@ -0,0 +1,58 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.8.+'
}
}
apply plugin: 'android-library'
apply plugin: 'maven'
repositories {
mavenCentral()
maven {
url "https://raw.github.com/whispersystems/maven/master/gson/releases/"
}
}
dependencies {
compile 'com.google.protobuf:protobuf-java:2.4.1'
compile 'com.madgag:sc-light-jdk15on:1.47.0.2'
compile 'com.googlecode.libphonenumber:libphonenumber:5.3'
compile 'org.whispersystems:gson:2.2.4'
}
android {
compileSdkVersion 19
buildToolsVersion '19.0.2'
android {
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
resources.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
jniLibs.srcDirs = ['libs']
}
}
}
}
version '0.1'
group 'org.whispersystems.textsecure'
archivesBaseName = 'textsecure-library'
uploadArchives {
repositories {
mavenDeployer {
repository(url: mavenLocal().getUrl())
}
}
}

92
library/build.xml Normal file
View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="library" default="help">
<!-- The local.properties file is created and updated by the 'android' tool.
It contains the path to the SDK. It should *NOT* be checked into
Version Control Systems. -->
<property file="local.properties"/>
<!-- The ant.properties file can be created by you. It is only edited by the
'android' tool to add properties to it.
This is the place to change some Ant specific build properties.
Here are some properties you may want to change/update:
source.dir
The name of the source directory. Default is 'src'.
out.dir
The name of the output directory. Default is 'bin'.
For other overridable properties, look at the beginning of the rules
files in the SDK, at tools/ant/build.xml
Properties related to the SDK location or the project target should
be updated using the 'android' tool with the 'update' action.
This file is an integral part of the build system for your
application and should be checked into Version Control Systems.
-->
<property file="ant.properties"/>
<!-- if sdk.dir was not set from one of the property file, then
get it from the ANDROID_HOME env var.
This must be done before we load project.properties since
the proguard config can use sdk.dir -->
<property environment="env"/>
<condition property="sdk.dir" value="${env.ANDROID_HOME}">
<isset property="env.ANDROID_HOME"/>
</condition>
<!-- The project.properties file is created and updated by the 'android'
tool, as well as ADT.
This contains project specific properties such as project target, and library
dependencies. Lower level build properties are stored in ant.properties
(or in .classpath for Eclipse projects).
This file is an integral part of the build system for your
application and should be checked into Version Control Systems. -->
<loadproperties srcFile="project.properties"/>
<!-- quick check on sdk.dir -->
<fail
message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
unless="sdk.dir"
/>
<!--
Import per project custom build rules if present at the root of the project.
This is the place to put custom intermediary targets such as:
-pre-build
-pre-compile
-post-compile (This is typically used for code obfuscation.
Compiled code location: ${out.classes.absolute.dir}
If this is not done in place, override ${out.dex.input.absolute.dir})
-post-package
-post-build
-pre-clean
-->
<import file="custom_rules.xml" optional="true"/>
<!-- Import the actual build file.
To customize existing targets, there are two options:
- Customize only one target:
- copy/paste the target into this file, *before* the
<import> task.
- customize it to your needs.
- Customize the whole content of build.xml
- copy/paste the content of the rules files (minus the top node)
into this file, replacing the <import> task.
- customize to your needs.
***********************
****** IMPORTANT ******
***********************
In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
in order to avoid having your file be overridden by tools such as "android update project"
-->
<!-- version-tag: 1 -->
<import file="${sdk.dir}/tools/ant/build.xml"/>
</project>

17
library/jni/Android.mk Normal file
View File

@@ -0,0 +1,17 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libcurve25519-donna
LOCAL_SRC_FILES := curve25519-donna.c
include $(BUILD_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libcurve25519
LOCAL_SRC_FILES := curve25519-donna-jni.c
LOCAL_STATIC_LIBRARIES := libcurve25519-donna
include $(BUILD_SHARED_LIBRARY)

View File

@@ -0,0 +1 @@
APP_ABI := armeabi armeabi-v7a x86

View File

@@ -0,0 +1,70 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <string.h>
#include <stdint.h>
#include <jni.h>
#include "curve25519-donna.h"
JNIEXPORT jbyteArray JNICALL Java_org_whispersystems_textsecure_crypto_ecc_Curve25519_generatePrivateKey
(JNIEnv *env, jclass clazz, jbyteArray random)
{
uint8_t* privateKey = (uint8_t*)(*env)->GetByteArrayElements(env, random, 0);
privateKey[0] &= 248;
privateKey[31] &= 127;
privateKey[31] |= 64;
(*env)->ReleaseByteArrayElements(env, random, privateKey, 0);
return random;
}
JNIEXPORT jbyteArray JNICALL Java_org_whispersystems_textsecure_crypto_ecc_Curve25519_generatePublicKey
(JNIEnv *env, jclass clazz, jbyteArray privateKey)
{
static const uint8_t basepoint[32] = {9};
jbyteArray publicKey = (*env)->NewByteArray(env, 32);
uint8_t* publicKeyBytes = (uint8_t*)(*env)->GetByteArrayElements(env, publicKey, 0);
uint8_t* privateKeyBytes = (uint8_t*)(*env)->GetByteArrayElements(env, privateKey, 0);
curve25519_donna(publicKeyBytes, privateKeyBytes, basepoint);
(*env)->ReleaseByteArrayElements(env, publicKey, publicKeyBytes, 0);
(*env)->ReleaseByteArrayElements(env, privateKey, privateKeyBytes, 0);
return publicKey;
}
JNIEXPORT jbyteArray JNICALL Java_org_whispersystems_textsecure_crypto_ecc_Curve25519_calculateAgreement
(JNIEnv *env, jclass clazz, jbyteArray privateKey, jbyteArray publicKey)
{
jbyteArray sharedKey = (*env)->NewByteArray(env, 32);
uint8_t* sharedKeyBytes = (uint8_t*)(*env)->GetByteArrayElements(env, sharedKey, 0);
uint8_t* privateKeyBytes = (uint8_t*)(*env)->GetByteArrayElements(env, privateKey, 0);
uint8_t* publicKeyBytes = (uint8_t*)(*env)->GetByteArrayElements(env, publicKey, 0);
curve25519_donna(sharedKeyBytes, privateKeyBytes, publicKeyBytes);
(*env)->ReleaseByteArrayElements(env, sharedKey, sharedKeyBytes, 0);
(*env)->ReleaseByteArrayElements(env, publicKey, publicKeyBytes, 0);
(*env)->ReleaseByteArrayElements(env, privateKey, privateKeyBytes, 0);
return sharedKey;
}

View File

@@ -0,0 +1,734 @@
/* Copyright 2008, Google Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* curve25519-donna: Curve25519 elliptic curve, public key function
*
* http://code.google.com/p/curve25519-donna/
*
* Adam Langley <agl@imperialviolet.org>
*
* Derived from public domain C code by Daniel J. Bernstein <djb@cr.yp.to>
*
* More information about curve25519 can be found here
* http://cr.yp.to/ecdh.html
*
* djb's sample implementation of curve25519 is written in a special assembly
* language called qhasm and uses the floating point registers.
*
* This is, almost, a clean room reimplementation from the curve25519 paper. It
* uses many of the tricks described therein. Only the crecip function is taken
* from the sample implementation.
*/
#include <string.h>
#include <stdint.h>
#ifdef _MSC_VER
#define inline __inline
#endif
typedef uint8_t u8;
typedef int32_t s32;
typedef int64_t limb;
/* Field element representation:
*
* Field elements are written as an array of signed, 64-bit limbs, least
* significant first. The value of the field element is:
* x[0] + 2^26·x[1] + x^51·x[2] + 2^102·x[3] + ...
*
* i.e. the limbs are 26, 25, 26, 25, ... bits wide.
*/
/* Sum two numbers: output += in */
static void fsum(limb *output, const limb *in) {
unsigned i;
for (i = 0; i < 10; i += 2) {
output[0+i] = (output[0+i] + in[0+i]);
output[1+i] = (output[1+i] + in[1+i]);
}
}
/* Find the difference of two numbers: output = in - output
* (note the order of the arguments!)
*/
static void fdifference(limb *output, const limb *in) {
unsigned i;
for (i = 0; i < 10; ++i) {
output[i] = (in[i] - output[i]);
}
}
/* Multiply a number by a scalar: output = in * scalar */
static void fscalar_product(limb *output, const limb *in, const limb scalar) {
unsigned i;
for (i = 0; i < 10; ++i) {
output[i] = in[i] * scalar;
}
}
/* Multiply two numbers: output = in2 * in
*
* output must be distinct to both inputs. The inputs are reduced coefficient
* form, the output is not.
*/
static void fproduct(limb *output, const limb *in2, const limb *in) {
output[0] = ((limb) ((s32) in2[0])) * ((s32) in[0]);
output[1] = ((limb) ((s32) in2[0])) * ((s32) in[1]) +
((limb) ((s32) in2[1])) * ((s32) in[0]);
output[2] = 2 * ((limb) ((s32) in2[1])) * ((s32) in[1]) +
((limb) ((s32) in2[0])) * ((s32) in[2]) +
((limb) ((s32) in2[2])) * ((s32) in[0]);
output[3] = ((limb) ((s32) in2[1])) * ((s32) in[2]) +
((limb) ((s32) in2[2])) * ((s32) in[1]) +
((limb) ((s32) in2[0])) * ((s32) in[3]) +
((limb) ((s32) in2[3])) * ((s32) in[0]);
output[4] = ((limb) ((s32) in2[2])) * ((s32) in[2]) +
2 * (((limb) ((s32) in2[1])) * ((s32) in[3]) +
((limb) ((s32) in2[3])) * ((s32) in[1])) +
((limb) ((s32) in2[0])) * ((s32) in[4]) +
((limb) ((s32) in2[4])) * ((s32) in[0]);
output[5] = ((limb) ((s32) in2[2])) * ((s32) in[3]) +
((limb) ((s32) in2[3])) * ((s32) in[2]) +
((limb) ((s32) in2[1])) * ((s32) in[4]) +
((limb) ((s32) in2[4])) * ((s32) in[1]) +
((limb) ((s32) in2[0])) * ((s32) in[5]) +
((limb) ((s32) in2[5])) * ((s32) in[0]);
output[6] = 2 * (((limb) ((s32) in2[3])) * ((s32) in[3]) +
((limb) ((s32) in2[1])) * ((s32) in[5]) +
((limb) ((s32) in2[5])) * ((s32) in[1])) +
((limb) ((s32) in2[2])) * ((s32) in[4]) +
((limb) ((s32) in2[4])) * ((s32) in[2]) +
((limb) ((s32) in2[0])) * ((s32) in[6]) +
((limb) ((s32) in2[6])) * ((s32) in[0]);
output[7] = ((limb) ((s32) in2[3])) * ((s32) in[4]) +
((limb) ((s32) in2[4])) * ((s32) in[3]) +
((limb) ((s32) in2[2])) * ((s32) in[5]) +
((limb) ((s32) in2[5])) * ((s32) in[2]) +
((limb) ((s32) in2[1])) * ((s32) in[6]) +
((limb) ((s32) in2[6])) * ((s32) in[1]) +
((limb) ((s32) in2[0])) * ((s32) in[7]) +
((limb) ((s32) in2[7])) * ((s32) in[0]);
output[8] = ((limb) ((s32) in2[4])) * ((s32) in[4]) +
2 * (((limb) ((s32) in2[3])) * ((s32) in[5]) +
((limb) ((s32) in2[5])) * ((s32) in[3]) +
((limb) ((s32) in2[1])) * ((s32) in[7]) +
((limb) ((s32) in2[7])) * ((s32) in[1])) +
((limb) ((s32) in2[2])) * ((s32) in[6]) +
((limb) ((s32) in2[6])) * ((s32) in[2]) +
((limb) ((s32) in2[0])) * ((s32) in[8]) +
((limb) ((s32) in2[8])) * ((s32) in[0]);
output[9] = ((limb) ((s32) in2[4])) * ((s32) in[5]) +
((limb) ((s32) in2[5])) * ((s32) in[4]) +
((limb) ((s32) in2[3])) * ((s32) in[6]) +
((limb) ((s32) in2[6])) * ((s32) in[3]) +
((limb) ((s32) in2[2])) * ((s32) in[7]) +
((limb) ((s32) in2[7])) * ((s32) in[2]) +
((limb) ((s32) in2[1])) * ((s32) in[8]) +
((limb) ((s32) in2[8])) * ((s32) in[1]) +
((limb) ((s32) in2[0])) * ((s32) in[9]) +
((limb) ((s32) in2[9])) * ((s32) in[0]);
output[10] = 2 * (((limb) ((s32) in2[5])) * ((s32) in[5]) +
((limb) ((s32) in2[3])) * ((s32) in[7]) +
((limb) ((s32) in2[7])) * ((s32) in[3]) +
((limb) ((s32) in2[1])) * ((s32) in[9]) +
((limb) ((s32) in2[9])) * ((s32) in[1])) +
((limb) ((s32) in2[4])) * ((s32) in[6]) +
((limb) ((s32) in2[6])) * ((s32) in[4]) +
((limb) ((s32) in2[2])) * ((s32) in[8]) +
((limb) ((s32) in2[8])) * ((s32) in[2]);
output[11] = ((limb) ((s32) in2[5])) * ((s32) in[6]) +
((limb) ((s32) in2[6])) * ((s32) in[5]) +
((limb) ((s32) in2[4])) * ((s32) in[7]) +
((limb) ((s32) in2[7])) * ((s32) in[4]) +
((limb) ((s32) in2[3])) * ((s32) in[8]) +
((limb) ((s32) in2[8])) * ((s32) in[3]) +
((limb) ((s32) in2[2])) * ((s32) in[9]) +
((limb) ((s32) in2[9])) * ((s32) in[2]);
output[12] = ((limb) ((s32) in2[6])) * ((s32) in[6]) +
2 * (((limb) ((s32) in2[5])) * ((s32) in[7]) +
((limb) ((s32) in2[7])) * ((s32) in[5]) +
((limb) ((s32) in2[3])) * ((s32) in[9]) +
((limb) ((s32) in2[9])) * ((s32) in[3])) +
((limb) ((s32) in2[4])) * ((s32) in[8]) +
((limb) ((s32) in2[8])) * ((s32) in[4]);
output[13] = ((limb) ((s32) in2[6])) * ((s32) in[7]) +
((limb) ((s32) in2[7])) * ((s32) in[6]) +
((limb) ((s32) in2[5])) * ((s32) in[8]) +
((limb) ((s32) in2[8])) * ((s32) in[5]) +
((limb) ((s32) in2[4])) * ((s32) in[9]) +
((limb) ((s32) in2[9])) * ((s32) in[4]);
output[14] = 2 * (((limb) ((s32) in2[7])) * ((s32) in[7]) +
((limb) ((s32) in2[5])) * ((s32) in[9]) +
((limb) ((s32) in2[9])) * ((s32) in[5])) +
((limb) ((s32) in2[6])) * ((s32) in[8]) +
((limb) ((s32) in2[8])) * ((s32) in[6]);
output[15] = ((limb) ((s32) in2[7])) * ((s32) in[8]) +
((limb) ((s32) in2[8])) * ((s32) in[7]) +
((limb) ((s32) in2[6])) * ((s32) in[9]) +
((limb) ((s32) in2[9])) * ((s32) in[6]);
output[16] = ((limb) ((s32) in2[8])) * ((s32) in[8]) +
2 * (((limb) ((s32) in2[7])) * ((s32) in[9]) +
((limb) ((s32) in2[9])) * ((s32) in[7]));
output[17] = ((limb) ((s32) in2[8])) * ((s32) in[9]) +
((limb) ((s32) in2[9])) * ((s32) in[8]);
output[18] = 2 * ((limb) ((s32) in2[9])) * ((s32) in[9]);
}
/* Reduce a long form to a short form by taking the input mod 2^255 - 19. */
static void freduce_degree(limb *output) {
/* Each of these shifts and adds ends up multiplying the value by 19. */
output[8] += output[18] << 4;
output[8] += output[18] << 1;
output[8] += output[18];
output[7] += output[17] << 4;
output[7] += output[17] << 1;
output[7] += output[17];
output[6] += output[16] << 4;
output[6] += output[16] << 1;
output[6] += output[16];
output[5] += output[15] << 4;
output[5] += output[15] << 1;
output[5] += output[15];
output[4] += output[14] << 4;
output[4] += output[14] << 1;
output[4] += output[14];
output[3] += output[13] << 4;
output[3] += output[13] << 1;
output[3] += output[13];
output[2] += output[12] << 4;
output[2] += output[12] << 1;
output[2] += output[12];
output[1] += output[11] << 4;
output[1] += output[11] << 1;
output[1] += output[11];
output[0] += output[10] << 4;
output[0] += output[10] << 1;
output[0] += output[10];
}
#if (-1 & 3) != 3
#error "This code only works on a two's complement system"
#endif
/* return v / 2^26, using only shifts and adds. */
static inline limb
div_by_2_26(const limb v)
{
/* High word of v; no shift needed*/
const uint32_t highword = (uint32_t) (((uint64_t) v) >> 32);
/* Set to all 1s if v was negative; else set to 0s. */
const int32_t sign = ((int32_t) highword) >> 31;
/* Set to 0x3ffffff if v was negative; else set to 0. */
const int32_t roundoff = ((uint32_t) sign) >> 6;
/* Should return v / (1<<26) */
return (v + roundoff) >> 26;
}
/* return v / (2^25), using only shifts and adds. */
static inline limb
div_by_2_25(const limb v)
{
/* High word of v; no shift needed*/
const uint32_t highword = (uint32_t) (((uint64_t) v) >> 32);
/* Set to all 1s if v was negative; else set to 0s. */
const int32_t sign = ((int32_t) highword) >> 31;
/* Set to 0x1ffffff if v was negative; else set to 0. */
const int32_t roundoff = ((uint32_t) sign) >> 7;
/* Should return v / (1<<25) */
return (v + roundoff) >> 25;
}
static inline s32
div_s32_by_2_25(const s32 v)
{
const s32 roundoff = ((uint32_t)(v >> 31)) >> 7;
return (v + roundoff) >> 25;
}
/* Reduce all coefficients of the short form input so that |x| < 2^26.
*
* On entry: |output[i]| < 2^62
*/
static void freduce_coefficients(limb *output) {
unsigned i;
output[10] = 0;
for (i = 0; i < 10; i += 2) {
limb over = div_by_2_26(output[i]);
output[i] -= over << 26;
output[i+1] += over;
over = div_by_2_25(output[i+1]);
output[i+1] -= over << 25;
output[i+2] += over;
}
/* Now |output[10]| < 2 ^ 38 and all other coefficients are reduced. */
output[0] += output[10] << 4;
output[0] += output[10] << 1;
output[0] += output[10];
output[10] = 0;
/* Now output[1..9] are reduced, and |output[0]| < 2^26 + 19 * 2^38
* So |over| will be no more than 77825 */
{
limb over = div_by_2_26(output[0]);
output[0] -= over << 26;
output[1] += over;
}
/* Now output[0,2..9] are reduced, and |output[1]| < 2^25 + 77825
* So |over| will be no more than 1. */
{
/* output[1] fits in 32 bits, so we can use div_s32_by_2_25 here. */
s32 over32 = div_s32_by_2_25((s32) output[1]);
output[1] -= over32 << 25;
output[2] += over32;
}
/* Finally, output[0,1,3..9] are reduced, and output[2] is "nearly reduced":
* we have |output[2]| <= 2^26. This is good enough for all of our math,
* but it will require an extra freduce_coefficients before fcontract. */
}
/* A helpful wrapper around fproduct: output = in * in2.
*
* output must be distinct to both inputs. The output is reduced degree and
* reduced coefficient.
*/
static void
fmul(limb *output, const limb *in, const limb *in2) {
limb t[19];
fproduct(t, in, in2);
freduce_degree(t);
freduce_coefficients(t);
memcpy(output, t, sizeof(limb) * 10);
}
static void fsquare_inner(limb *output, const limb *in) {
output[0] = ((limb) ((s32) in[0])) * ((s32) in[0]);
output[1] = 2 * ((limb) ((s32) in[0])) * ((s32) in[1]);
output[2] = 2 * (((limb) ((s32) in[1])) * ((s32) in[1]) +
((limb) ((s32) in[0])) * ((s32) in[2]));
output[3] = 2 * (((limb) ((s32) in[1])) * ((s32) in[2]) +
((limb) ((s32) in[0])) * ((s32) in[3]));
output[4] = ((limb) ((s32) in[2])) * ((s32) in[2]) +
4 * ((limb) ((s32) in[1])) * ((s32) in[3]) +
2 * ((limb) ((s32) in[0])) * ((s32) in[4]);
output[5] = 2 * (((limb) ((s32) in[2])) * ((s32) in[3]) +
((limb) ((s32) in[1])) * ((s32) in[4]) +
((limb) ((s32) in[0])) * ((s32) in[5]));
output[6] = 2 * (((limb) ((s32) in[3])) * ((s32) in[3]) +
((limb) ((s32) in[2])) * ((s32) in[4]) +
((limb) ((s32) in[0])) * ((s32) in[6]) +
2 * ((limb) ((s32) in[1])) * ((s32) in[5]));
output[7] = 2 * (((limb) ((s32) in[3])) * ((s32) in[4]) +
((limb) ((s32) in[2])) * ((s32) in[5]) +
((limb) ((s32) in[1])) * ((s32) in[6]) +
((limb) ((s32) in[0])) * ((s32) in[7]));
output[8] = ((limb) ((s32) in[4])) * ((s32) in[4]) +
2 * (((limb) ((s32) in[2])) * ((s32) in[6]) +
((limb) ((s32) in[0])) * ((s32) in[8]) +
2 * (((limb) ((s32) in[1])) * ((s32) in[7]) +
((limb) ((s32) in[3])) * ((s32) in[5])));
output[9] = 2 * (((limb) ((s32) in[4])) * ((s32) in[5]) +
((limb) ((s32) in[3])) * ((s32) in[6]) +
((limb) ((s32) in[2])) * ((s32) in[7]) +
((limb) ((s32) in[1])) * ((s32) in[8]) +
((limb) ((s32) in[0])) * ((s32) in[9]));
output[10] = 2 * (((limb) ((s32) in[5])) * ((s32) in[5]) +
((limb) ((s32) in[4])) * ((s32) in[6]) +
((limb) ((s32) in[2])) * ((s32) in[8]) +
2 * (((limb) ((s32) in[3])) * ((s32) in[7]) +
((limb) ((s32) in[1])) * ((s32) in[9])));
output[11] = 2 * (((limb) ((s32) in[5])) * ((s32) in[6]) +
((limb) ((s32) in[4])) * ((s32) in[7]) +
((limb) ((s32) in[3])) * ((s32) in[8]) +
((limb) ((s32) in[2])) * ((s32) in[9]));
output[12] = ((limb) ((s32) in[6])) * ((s32) in[6]) +
2 * (((limb) ((s32) in[4])) * ((s32) in[8]) +
2 * (((limb) ((s32) in[5])) * ((s32) in[7]) +
((limb) ((s32) in[3])) * ((s32) in[9])));
output[13] = 2 * (((limb) ((s32) in[6])) * ((s32) in[7]) +
((limb) ((s32) in[5])) * ((s32) in[8]) +
((limb) ((s32) in[4])) * ((s32) in[9]));
output[14] = 2 * (((limb) ((s32) in[7])) * ((s32) in[7]) +
((limb) ((s32) in[6])) * ((s32) in[8]) +
2 * ((limb) ((s32) in[5])) * ((s32) in[9]));
output[15] = 2 * (((limb) ((s32) in[7])) * ((s32) in[8]) +
((limb) ((s32) in[6])) * ((s32) in[9]));
output[16] = ((limb) ((s32) in[8])) * ((s32) in[8]) +
4 * ((limb) ((s32) in[7])) * ((s32) in[9]);
output[17] = 2 * ((limb) ((s32) in[8])) * ((s32) in[9]);
output[18] = 2 * ((limb) ((s32) in[9])) * ((s32) in[9]);
}
static void
fsquare(limb *output, const limb *in) {
limb t[19];
fsquare_inner(t, in);
freduce_degree(t);
freduce_coefficients(t);
memcpy(output, t, sizeof(limb) * 10);
}
/* Take a little-endian, 32-byte number and expand it into polynomial form */
static void
fexpand(limb *output, const u8 *input) {
#define F(n,start,shift,mask) \
output[n] = ((((limb) input[start + 0]) | \
((limb) input[start + 1]) << 8 | \
((limb) input[start + 2]) << 16 | \
((limb) input[start + 3]) << 24) >> shift) & mask;
F(0, 0, 0, 0x3ffffff);
F(1, 3, 2, 0x1ffffff);
F(2, 6, 3, 0x3ffffff);
F(3, 9, 5, 0x1ffffff);
F(4, 12, 6, 0x3ffffff);
F(5, 16, 0, 0x1ffffff);
F(6, 19, 1, 0x3ffffff);
F(7, 22, 3, 0x1ffffff);
F(8, 25, 4, 0x3ffffff);
F(9, 28, 6, 0x3ffffff);
#undef F
}
#if (-32 >> 1) != -16
#error "This code only works when >> does sign-extension on negative numbers"
#endif
/* Take a fully reduced polynomial form number and contract it into a
* little-endian, 32-byte array
*/
static void
fcontract(u8 *output, limb *input) {
int i;
int j;
for (j = 0; j < 2; ++j) {
for (i = 0; i < 9; ++i) {
if ((i & 1) == 1) {
/* This calculation is a time-invariant way to make input[i] positive
by borrowing from the next-larger limb.
*/
const s32 mask = (s32)(input[i]) >> 31;
const s32 carry = -(((s32)(input[i]) & mask) >> 25);
input[i] = (s32)(input[i]) + (carry << 25);
input[i+1] = (s32)(input[i+1]) - carry;
} else {
const s32 mask = (s32)(input[i]) >> 31;
const s32 carry = -(((s32)(input[i]) & mask) >> 26);
input[i] = (s32)(input[i]) + (carry << 26);
input[i+1] = (s32)(input[i+1]) - carry;
}
}
{
const s32 mask = (s32)(input[9]) >> 31;
const s32 carry = -(((s32)(input[9]) & mask) >> 25);
input[9] = (s32)(input[9]) + (carry << 25);
input[0] = (s32)(input[0]) - (carry * 19);
}
}
/* The first borrow-propagation pass above ended with every limb
except (possibly) input[0] non-negative.
Since each input limb except input[0] is decreased by at most 1
by a borrow-propagation pass, the second borrow-propagation pass
could only have wrapped around to decrease input[0] again if the
first pass left input[0] negative *and* input[1] through input[9]
were all zero. In that case, input[1] is now 2^25 - 1, and this
last borrow-propagation step will leave input[1] non-negative.
*/
{
const s32 mask = (s32)(input[0]) >> 31;
const s32 carry = -(((s32)(input[0]) & mask) >> 26);
input[0] = (s32)(input[0]) + (carry << 26);
input[1] = (s32)(input[1]) - carry;
}
/* Both passes through the above loop, plus the last 0-to-1 step, are
necessary: if input[9] is -1 and input[0] through input[8] are 0,
negative values will remain in the array until the end.
*/
input[1] <<= 2;
input[2] <<= 3;
input[3] <<= 5;
input[4] <<= 6;
input[6] <<= 1;
input[7] <<= 3;
input[8] <<= 4;
input[9] <<= 6;
#define F(i, s) \
output[s+0] |= input[i] & 0xff; \
output[s+1] = (input[i] >> 8) & 0xff; \
output[s+2] = (input[i] >> 16) & 0xff; \
output[s+3] = (input[i] >> 24) & 0xff;
output[0] = 0;
output[16] = 0;
F(0,0);
F(1,3);
F(2,6);
F(3,9);
F(4,12);
F(5,16);
F(6,19);
F(7,22);
F(8,25);
F(9,28);
#undef F
}
/* Input: Q, Q', Q-Q'
* Output: 2Q, Q+Q'
*
* x2 z3: long form
* x3 z3: long form
* x z: short form, destroyed
* xprime zprime: short form, destroyed
* qmqp: short form, preserved
*/
static void fmonty(limb *x2, limb *z2, /* output 2Q */
limb *x3, limb *z3, /* output Q + Q' */
limb *x, limb *z, /* input Q */
limb *xprime, limb *zprime, /* input Q' */
const limb *qmqp /* input Q - Q' */) {
limb origx[10], origxprime[10], zzz[19], xx[19], zz[19], xxprime[19],
zzprime[19], zzzprime[19], xxxprime[19];
memcpy(origx, x, 10 * sizeof(limb));
fsum(x, z);
fdifference(z, origx); // does x - z
memcpy(origxprime, xprime, sizeof(limb) * 10);
fsum(xprime, zprime);
fdifference(zprime, origxprime);
fproduct(xxprime, xprime, z);
fproduct(zzprime, x, zprime);
freduce_degree(xxprime);
freduce_coefficients(xxprime);
freduce_degree(zzprime);
freduce_coefficients(zzprime);
memcpy(origxprime, xxprime, sizeof(limb) * 10);
fsum(xxprime, zzprime);
fdifference(zzprime, origxprime);
fsquare(xxxprime, xxprime);
fsquare(zzzprime, zzprime);
fproduct(zzprime, zzzprime, qmqp);
freduce_degree(zzprime);
freduce_coefficients(zzprime);
memcpy(x3, xxxprime, sizeof(limb) * 10);
memcpy(z3, zzprime, sizeof(limb) * 10);
fsquare(xx, x);
fsquare(zz, z);
fproduct(x2, xx, zz);
freduce_degree(x2);
freduce_coefficients(x2);
fdifference(zz, xx); // does zz = xx - zz
memset(zzz + 10, 0, sizeof(limb) * 9);
fscalar_product(zzz, zz, 121665);
/* No need to call freduce_degree here:
fscalar_product doesn't increase the degree of its input. */
freduce_coefficients(zzz);
fsum(zzz, xx);
fproduct(z2, zz, zzz);
freduce_degree(z2);
freduce_coefficients(z2);
}
/* Conditionally swap two reduced-form limb arrays if 'iswap' is 1, but leave
* them unchanged if 'iswap' is 0. Runs in data-invariant time to avoid
* side-channel attacks.
*
* NOTE that this function requires that 'iswap' be 1 or 0; other values give
* wrong results. Also, the two limb arrays must be in reduced-coefficient,
* reduced-degree form: the values in a[10..19] or b[10..19] aren't swapped,
* and all all values in a[0..9],b[0..9] must have magnitude less than
* INT32_MAX.
*/
static void
swap_conditional(limb a[19], limb b[19], limb iswap) {
unsigned i;
const s32 swap = (s32) -iswap;
for (i = 0; i < 10; ++i) {
const s32 x = swap & ( ((s32)a[i]) ^ ((s32)b[i]) );
a[i] = ((s32)a[i]) ^ x;
b[i] = ((s32)b[i]) ^ x;
}
}
/* Calculates nQ where Q is the x-coordinate of a point on the curve
*
* resultx/resultz: the x coordinate of the resulting curve point (short form)
* n: a little endian, 32-byte number
* q: a point of the curve (short form)
*/
static void
cmult(limb *resultx, limb *resultz, const u8 *n, const limb *q) {
limb a[19] = {0}, b[19] = {1}, c[19] = {1}, d[19] = {0};
limb *nqpqx = a, *nqpqz = b, *nqx = c, *nqz = d, *t;
limb e[19] = {0}, f[19] = {1}, g[19] = {0}, h[19] = {1};
limb *nqpqx2 = e, *nqpqz2 = f, *nqx2 = g, *nqz2 = h;
unsigned i, j;
memcpy(nqpqx, q, sizeof(limb) * 10);
for (i = 0; i < 32; ++i) {
u8 byte = n[31 - i];
for (j = 0; j < 8; ++j) {
const limb bit = byte >> 7;
swap_conditional(nqx, nqpqx, bit);
swap_conditional(nqz, nqpqz, bit);
fmonty(nqx2, nqz2,
nqpqx2, nqpqz2,
nqx, nqz,
nqpqx, nqpqz,
q);
swap_conditional(nqx2, nqpqx2, bit);
swap_conditional(nqz2, nqpqz2, bit);
t = nqx;
nqx = nqx2;
nqx2 = t;
t = nqz;
nqz = nqz2;
nqz2 = t;
t = nqpqx;
nqpqx = nqpqx2;
nqpqx2 = t;
t = nqpqz;
nqpqz = nqpqz2;
nqpqz2 = t;
byte <<= 1;
}
}
memcpy(resultx, nqx, sizeof(limb) * 10);
memcpy(resultz, nqz, sizeof(limb) * 10);
}
// -----------------------------------------------------------------------------
// Shamelessly copied from djb's code
// -----------------------------------------------------------------------------
static void
crecip(limb *out, const limb *z) {
limb z2[10];
limb z9[10];
limb z11[10];
limb z2_5_0[10];
limb z2_10_0[10];
limb z2_20_0[10];
limb z2_50_0[10];
limb z2_100_0[10];
limb t0[10];
limb t1[10];
int i;
/* 2 */ fsquare(z2,z);
/* 4 */ fsquare(t1,z2);
/* 8 */ fsquare(t0,t1);
/* 9 */ fmul(z9,t0,z);
/* 11 */ fmul(z11,z9,z2);
/* 22 */ fsquare(t0,z11);
/* 2^5 - 2^0 = 31 */ fmul(z2_5_0,t0,z9);
/* 2^6 - 2^1 */ fsquare(t0,z2_5_0);
/* 2^7 - 2^2 */ fsquare(t1,t0);
/* 2^8 - 2^3 */ fsquare(t0,t1);
/* 2^9 - 2^4 */ fsquare(t1,t0);
/* 2^10 - 2^5 */ fsquare(t0,t1);
/* 2^10 - 2^0 */ fmul(z2_10_0,t0,z2_5_0);
/* 2^11 - 2^1 */ fsquare(t0,z2_10_0);
/* 2^12 - 2^2 */ fsquare(t1,t0);
/* 2^20 - 2^10 */ for (i = 2;i < 10;i += 2) { fsquare(t0,t1); fsquare(t1,t0); }
/* 2^20 - 2^0 */ fmul(z2_20_0,t1,z2_10_0);
/* 2^21 - 2^1 */ fsquare(t0,z2_20_0);
/* 2^22 - 2^2 */ fsquare(t1,t0);
/* 2^40 - 2^20 */ for (i = 2;i < 20;i += 2) { fsquare(t0,t1); fsquare(t1,t0); }
/* 2^40 - 2^0 */ fmul(t0,t1,z2_20_0);
/* 2^41 - 2^1 */ fsquare(t1,t0);
/* 2^42 - 2^2 */ fsquare(t0,t1);
/* 2^50 - 2^10 */ for (i = 2;i < 10;i += 2) { fsquare(t1,t0); fsquare(t0,t1); }
/* 2^50 - 2^0 */ fmul(z2_50_0,t0,z2_10_0);
/* 2^51 - 2^1 */ fsquare(t0,z2_50_0);
/* 2^52 - 2^2 */ fsquare(t1,t0);
/* 2^100 - 2^50 */ for (i = 2;i < 50;i += 2) { fsquare(t0,t1); fsquare(t1,t0); }
/* 2^100 - 2^0 */ fmul(z2_100_0,t1,z2_50_0);
/* 2^101 - 2^1 */ fsquare(t1,z2_100_0);
/* 2^102 - 2^2 */ fsquare(t0,t1);
/* 2^200 - 2^100 */ for (i = 2;i < 100;i += 2) { fsquare(t1,t0); fsquare(t0,t1); }
/* 2^200 - 2^0 */ fmul(t1,t0,z2_100_0);
/* 2^201 - 2^1 */ fsquare(t0,t1);
/* 2^202 - 2^2 */ fsquare(t1,t0);
/* 2^250 - 2^50 */ for (i = 2;i < 50;i += 2) { fsquare(t0,t1); fsquare(t1,t0); }
/* 2^250 - 2^0 */ fmul(t0,t1,z2_50_0);
/* 2^251 - 2^1 */ fsquare(t1,t0);
/* 2^252 - 2^2 */ fsquare(t0,t1);
/* 2^253 - 2^3 */ fsquare(t1,t0);
/* 2^254 - 2^4 */ fsquare(t0,t1);
/* 2^255 - 2^5 */ fsquare(t1,t0);
/* 2^255 - 21 */ fmul(out,t1,z11);
}
int curve25519_donna(u8 *, const u8 *, const u8 *);
int
curve25519_donna(u8 *mypublic, const u8 *secret, const u8 *basepoint) {
limb bp[10], x[10], z[11], zmone[10];
uint8_t e[32];
int i;
for (i = 0; i < 32; ++i) e[i] = secret[i];
e[0] &= 248;
e[31] &= 127;
e[31] |= 64;
fexpand(bp, basepoint);
cmult(x, z, e, bp);
crecip(zmone, z);
fmul(z, x, zmone);
freduce_coefficients(z);
fcontract(mypublic, z);
return 0;
}

View File

@@ -0,0 +1,6 @@
#ifndef CURVE25519_DONNA_H
#define CURVE25519_DONNA_H
extern int curve25519_donna(uint8_t *, const uint8_t *, const uint8_t *);
#endif

Binary file not shown.

Binary file not shown.

BIN
library/libs/x86/libcurve25519.so Executable file

Binary file not shown.

View File

@@ -0,0 +1,20 @@
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View File

@@ -0,0 +1,52 @@
package textsecure;
option java_package = "org.whispersystems.textsecure.push";
option java_outer_classname = "PushMessageProtos";
message IncomingPushMessageSignal {
enum Type {
UNKNOWN = 0;
CIPHERTEXT = 1;
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
PLAINTEXT = 4;
}
optional Type type = 1;
optional string source = 2;
optional uint32 sourceDevice = 7;
optional string relay = 3;
optional uint64 timestamp = 5;
optional bytes message = 6; // Contains an encrypted PushMessageContent
// repeated string destinations = 4; // No longer supported
}
message PushMessageContent {
message AttachmentPointer {
optional fixed64 id = 1;
optional string contentType = 2;
optional bytes key = 3;
}
message GroupContext {
enum Type {
UNKNOWN = 0;
UPDATE = 1;
DELIVER = 2;
QUIT = 3;
}
optional bytes id = 1;
optional Type type = 2;
optional string name = 3;
repeated string members = 4;
optional AttachmentPointer avatar = 5;
}
enum Flags {
END_SESSION = 1;
}
optional string body = 1;
repeated AttachmentPointer attachments = 2;
optional GroupContext group = 3;
optional uint32 flags = 4;
}

View File

@@ -0,0 +1,63 @@
package textsecure;
option java_package = "org.whispersystems.textsecure.storage";
option java_outer_classname = "StorageProtos";
message SessionStructure {
message Chain {
optional bytes senderEphemeral = 1;
optional bytes senderEphemeralPrivate = 2;
message ChainKey {
optional uint32 index = 1;
optional bytes key = 2;
}
optional ChainKey chainKey = 3;
message MessageKey {
optional uint32 index = 1;
optional bytes cipherKey = 2;
optional bytes macKey = 3;
}
repeated MessageKey messageKeys = 4;
}
message PendingKeyExchange {
optional uint32 sequence = 1;
optional bytes localBaseKey = 2;
optional bytes localBaseKeyPrivate = 3;
optional bytes localEphemeralKey = 4;
optional bytes localEphemeralKeyPrivate = 5;
optional bytes localIdentityKey = 7;
optional bytes localIdentityKeyPrivate = 8;
}
message PendingPreKey {
optional uint32 preKeyId = 1;
optional bytes baseKey = 2;
}
optional uint32 sessionVersion = 1;
optional bytes localIdentityPublic = 2;
optional bytes remoteIdentityPublic = 3;
optional bytes rootKey = 4;
optional uint32 previousCounter = 5;
optional Chain senderChain = 6;
repeated Chain receiverChains = 7;
optional PendingKeyExchange pendingKeyExchange = 8;
optional PendingPreKey pendingPreKey = 9;
optional uint32 remoteRegistrationId = 10;
optional uint32 localRegistrationId = 11;
}
message PreKeyRecordStructure {
optional uint32 id = 1;
optional bytes publicKey = 2;
optional bytes privateKey = 3;
}

View File

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

View File

@@ -0,0 +1,26 @@
package textsecure;
option java_package = "org.whispersystems.textsecure.crypto.protocol";
option java_outer_classname = "WhisperProtos";
message WhisperMessage {
optional bytes ephemeralKey = 1;
optional uint32 counter = 2;
optional uint32 previousCounter = 3;
optional bytes ciphertext = 4;
}
message PreKeyWhisperMessage {
optional uint32 registrationId = 5;
optional uint32 preKeyId = 1;
optional bytes baseKey = 2;
optional bytes identityKey = 3;
optional bytes message = 4; // WhisperMessage
}
message KeyExchangeMessage {
optional uint32 id = 1;
optional bytes baseKey = 2;
optional bytes ephemeralKey = 3;
optional bytes identityKey = 4;
}

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -0,0 +1,159 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.crypto;
import android.util.Log;
import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.Arrays;
/**
* Encrypts push attachments.
*
* @author Moxie Marlinspike
*/
public class AttachmentCipher {
static final int CIPHER_KEY_SIZE = 32;
static final int MAC_KEY_SIZE = 32;
private final SecretKeySpec cipherKey;
private final SecretKeySpec macKey;
private final Cipher cipher;
private final Mac mac;
public AttachmentCipher() {
this.cipherKey = initializeRandomCipherKey();
this.macKey = initializeRandomMacKey();
this.cipher = initializeCipher();
this.mac = initializeMac();
}
public AttachmentCipher(byte[] combinedKeyMaterial) {
byte[][] parts = Util.split(combinedKeyMaterial, CIPHER_KEY_SIZE, MAC_KEY_SIZE);
this.cipherKey = new SecretKeySpec(parts[0], "AES");
this.macKey = new SecretKeySpec(parts[1], "HmacSHA256");
this.cipher = initializeCipher();
this.mac = initializeMac();
}
public byte[] getCombinedKeyMaterial() {
return Util.combine(this.cipherKey.getEncoded(), this.macKey.getEncoded());
}
public byte[] encrypt(byte[] plaintext) {
try {
this.cipher.init(Cipher.ENCRYPT_MODE, this.cipherKey);
this.mac.init(this.macKey);
byte[] ciphertext = this.cipher.doFinal(plaintext);
byte[] iv = this.cipher.getIV();
byte[] mac = this.mac.doFinal(Util.combine(iv, ciphertext));
return Util.combine(iv, ciphertext, mac);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
public byte[] decrypt(byte[] ciphertext)
throws InvalidMacException, InvalidMessageException
{
try {
if (ciphertext.length <= cipher.getBlockSize() + mac.getMacLength()) {
throw new InvalidMessageException("Message too short!");
}
byte[][] ciphertextParts = Util.split(ciphertext,
this.cipher.getBlockSize(),
ciphertext.length - this.cipher.getBlockSize() - this.mac.getMacLength(),
this.mac.getMacLength());
this.mac.update(ciphertext, 0, ciphertext.length - mac.getMacLength());
byte[] ourMac = this.mac.doFinal();
if (!Arrays.equals(ourMac, ciphertextParts[2])) {
throw new InvalidMacException("Mac doesn't match!");
}
this.cipher.init(Cipher.DECRYPT_MODE, this.cipherKey,
new IvParameterSpec(ciphertextParts[0]));
return cipher.doFinal(ciphertextParts[1]);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
} catch (InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new InvalidMessageException(e);
} catch (ParseException e) {
throw new InvalidMessageException(e);
}
}
private Mac initializeMac() {
try {
Mac mac = Mac.getInstance("HmacSHA256");
return mac;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private Cipher initializeCipher() {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
return cipher;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (NoSuchPaddingException e) {
throw new AssertionError(e);
}
}
private SecretKeySpec initializeRandomCipherKey() {
byte[] key = new byte[CIPHER_KEY_SIZE];
Util.getSecureRandom().nextBytes(key);
return new SecretKeySpec(key, "AES");
}
private SecretKeySpec initializeRandomMacKey() {
byte[] key = new byte[MAC_KEY_SIZE];
Util.getSecureRandom().nextBytes(key);
return new SecretKeySpec(key, "HmacSHA256");
}
}

View File

@@ -0,0 +1,215 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.crypto;
import android.util.Log;
import org.whispersystems.textsecure.util.Util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Class for streaming an encrypted push attachment off disk.
*
* @author Moxie Marlinspike
*/
public class AttachmentCipherInputStream extends FileInputStream {
private static final int BLOCK_SIZE = 16;
private Cipher cipher;
private boolean done;
private long totalDataSize;
private long totalRead;
private byte[] overflowBuffer;
public AttachmentCipherInputStream(File file, byte[] combinedKeyMaterial)
throws IOException, InvalidMessageException
{
super(file);
try {
byte[][] parts = Util.split(combinedKeyMaterial,
AttachmentCipher.CIPHER_KEY_SIZE,
AttachmentCipher.MAC_KEY_SIZE);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(parts[1], "HmacSHA256"));
if (file.length() <= BLOCK_SIZE + mac.getMacLength()) {
throw new InvalidMessageException("Message shorter than crypto overhead!");
}
verifyMac(file, mac);
byte[] iv = new byte[BLOCK_SIZE];
readFully(iv);
this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(parts[0], "AES"), new IvParameterSpec(iv));
this.done = false;
this.totalRead = 0;
this.totalDataSize = file.length() - cipher.getBlockSize() - mac.getMacLength();
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
} catch (InvalidMacException e) {
throw new InvalidMessageException(e);
} catch (NoSuchPaddingException e) {
throw new AssertionError(e);
} catch (InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
}
}
@Override
public int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
@Override
public int read(byte[] buffer, int offset, int length) throws IOException {
if (totalRead != totalDataSize) return readIncremental(buffer, offset, length);
else if (!done) return readFinal(buffer, offset, length);
else return -1;
}
private int readFinal(byte[] buffer, int offset, int length) throws IOException {
try {
int flourish = cipher.doFinal(buffer, offset);
done = true;
return flourish;
} catch (IllegalBlockSizeException e) {
Log.w("EncryptingPartInputStream", e);
throw new IOException("Illegal block size exception!");
} catch (ShortBufferException e) {
Log.w("EncryptingPartInputStream", e);
throw new IOException("Short buffer exception!");
} catch (BadPaddingException e) {
Log.w("EncryptingPartInputStream", e);
throw new IOException("Bad padding exception!");
}
}
private int readIncremental(byte[] buffer, int offset, int length) throws IOException {
int readLength = 0;
if (null != overflowBuffer) {
if (overflowBuffer.length > length) {
System.arraycopy(overflowBuffer, 0, buffer, offset, length);
overflowBuffer = Arrays.copyOfRange(overflowBuffer, length, overflowBuffer.length);
return length;
} else if (overflowBuffer.length == length) {
System.arraycopy(overflowBuffer, 0, buffer, offset, length);
overflowBuffer = null;
return length;
} else {
System.arraycopy(overflowBuffer, 0, buffer, offset, overflowBuffer.length);
readLength += overflowBuffer.length;
offset += readLength;
length -= readLength;
overflowBuffer = null;
}
}
if (length + totalRead > totalDataSize)
length = (int)(totalDataSize - totalRead);
byte[] internalBuffer = new byte[length];
int read = super.read(internalBuffer, 0, internalBuffer.length <= cipher.getBlockSize() ? internalBuffer.length : internalBuffer.length - cipher.getBlockSize());
totalRead += read;
try {
int outputLen = cipher.getOutputSize(read);
if (outputLen <= length) {
readLength += cipher.update(internalBuffer, 0, read, buffer, offset);
return readLength;
}
byte[] transientBuffer = new byte[outputLen];
outputLen = cipher.update(internalBuffer, 0, read, transientBuffer, 0);
if (outputLen <= length) {
System.arraycopy(transientBuffer, 0, buffer, offset, outputLen);
readLength += outputLen;
} else {
System.arraycopy(transientBuffer, 0, buffer, offset, length);
overflowBuffer = Arrays.copyOfRange(transientBuffer, length, outputLen);
readLength += length;
}
return readLength;
} catch (ShortBufferException e) {
throw new AssertionError(e);
}
}
private void verifyMac(File file, Mac mac) throws FileNotFoundException, InvalidMacException {
try {
FileInputStream fin = new FileInputStream(file);
int remainingData = (int) file.length() - mac.getMacLength();
byte[] buffer = new byte[4096];
while (remainingData > 0) {
int read = fin.read(buffer, 0, Math.min(buffer.length, remainingData));
mac.update(buffer, 0, read);
remainingData -= read;
}
byte[] ourMac = mac.doFinal();
byte[] theirMac = new byte[mac.getMacLength()];
Util.readFully(fin, theirMac);
if (!Arrays.equals(ourMac, theirMac)) {
throw new InvalidMacException("MAC doesn't match!");
}
} catch (IOException e1) {
throw new InvalidMacException(e1);
}
}
private void readFully(byte[] buffer) throws IOException {
int offset = 0;
for (;;) {
int read = super.read(buffer, offset, buffer.length - offset);
if (read + offset < buffer.length) offset += read;
else return;
}
}
}

View File

@@ -1,5 +1,6 @@
/**
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,15 +15,16 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.math.ec.ECPoint;
import org.thoughtcrime.securesms.util.Hex;
package org.whispersystems.textsecure.crypto;
import android.os.Parcel;
import android.os.Parcelable;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util;
/**
* A class for representing an identity key.
*
@@ -45,15 +47,14 @@ public class IdentityKey implements Parcelable, SerializableKey {
}
};
public static final int SIZE = 1 + 33;
private static final int VERSION = 1;
private ECPublicKeyParameters publicKey;
public IdentityKey(ECPublicKeyParameters publicKey) {
public static final int NIST_SIZE = 1 + ECPublicKey.KEY_SIZE;
private ECPublicKey publicKey;
public IdentityKey(ECPublicKey publicKey) {
this.publicKey = publicKey;
}
public IdentityKey(Parcel in) throws InvalidKeyException {
int length = in.readInt();
byte[] serialized = new byte[length];
@@ -65,54 +66,45 @@ public class IdentityKey implements Parcelable, SerializableKey {
public IdentityKey(byte[] bytes, int offset) throws InvalidKeyException {
initializeFromSerialized(bytes, offset);
}
public ECPublicKeyParameters getPublicKeyParameters() {
return this.publicKey;
public ECPublicKey getPublicKey() {
return publicKey;
}
private void initializeFromSerialized(byte[] bytes, int offset) throws InvalidKeyException {
int version = bytes[offset] & 0xff;
if (version > VERSION)
throw new InvalidKeyException("Unsupported key version: " + version);
byte[] pointBytes = new byte[PublicKey.POINT_SIZE];
System.arraycopy(bytes, offset+1, pointBytes, 0, pointBytes.length);
ECPoint Q;
try {
Q = KeyUtil.decodePoint(pointBytes);
} catch (RuntimeException re) {
throw new InvalidKeyException(re);
if ((bytes[offset] & 0xff) == 1) {
this.publicKey = Curve.decodePoint(bytes, offset +1);
} else {
this.publicKey = Curve.decodePoint(bytes, offset);
}
this.publicKey = new ECPublicKeyParameters(Q, KeyUtil.domainParameters);
}
public byte[] serialize() {
byte[] encodedKey = KeyUtil.encodePoint(publicKey.getQ());
byte[] combined = new byte[1 + encodedKey.length];
combined[0] = (byte)VERSION;
System.arraycopy(encodedKey, 0, combined, 1, encodedKey.length);
return combined;
if (publicKey.getType() == Curve.NIST_TYPE) {
byte[] versionBytes = {0x01};
byte[] encodedKey = publicKey.serialize();
return Util.combine(versionBytes, encodedKey);
} else {
return publicKey.serialize();
}
}
public String getFingerprint() {
return Hex.toString(serialize());
return Hex.toString(publicKey.serialize());
}
@Override
public boolean equals(Object other) {
if (other == null) return false;
if (!(other instanceof IdentityKey)) return false;
return publicKey.getQ().equals(((IdentityKey)other).publicKey.getQ());
return publicKey.equals(((IdentityKey) other).getPublicKey());
}
@Override
public int hashCode() {
return publicKey.getQ().hashCode();
return publicKey.hashCode();
}
public int describeContents() {

View File

@@ -0,0 +1,43 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.crypto;
import org.whispersystems.textsecure.crypto.ecc.ECPrivateKey;
/**
* Holder for public and private identity key pair.
*
* @author Moxie Marlinspike
*/
public class IdentityKeyPair {
private final IdentityKey publicKey;
private final ECPrivateKey privateKey;
public IdentityKeyPair(IdentityKey publicKey, ECPrivateKey privateKey) {
this.publicKey = publicKey;
this.privateKey = privateKey;
}
public IdentityKey getPublicKey() {
return publicKey;
}
public ECPrivateKey getPrivateKey() {
return privateKey;
}
}

View File

@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
package org.whispersystems.textsecure.crypto;
public class InvalidKeyException extends Exception {

View File

@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
package org.whispersystems.textsecure.crypto;
public class InvalidMacException extends Exception {

View File

@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.util;
package org.whispersystems.textsecure.crypto;
public class InvalidMessageException extends Exception {

View File

@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
package org.whispersystems.textsecure.crypto;
public class InvalidVersionException extends Exception {

View File

@@ -1,5 +1,6 @@
/**
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,15 +15,15 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.thoughtcrime.securesms.util.Hex;
package org.whispersystems.textsecure.crypto;
import android.util.Log;
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
import org.whispersystems.textsecure.crypto.ecc.ECPrivateKey;
import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util;
/**
* Represents a session's active KeyPair.
*
@@ -31,15 +32,15 @@ import android.util.Log;
public class KeyPair {
private ECPrivateKeyParameters privateKey;
private PublicKey publicKey;
private PublicKey publicKey;
private ECPrivateKey privateKey;
private final MasterCipher masterCipher;
public KeyPair(int keyPairId, AsymmetricCipherKeyPair keyPair, MasterSecret masterSecret) {
public KeyPair(int keyPairId, ECKeyPair keyPair, MasterSecret masterSecret) {
this.masterCipher = new MasterCipher(masterSecret);
this.publicKey = new PublicKey(keyPairId, (ECPublicKeyParameters)keyPair.getPublic());
this.privateKey = (ECPrivateKeyParameters)keyPair.getPrivate();
this.publicKey = new PublicKey(keyPairId, keyPair.getPublicKey());
this.privateKey = keyPair.getPrivateKey();
}
public KeyPair(byte[] bytes, MasterCipher masterCipher) throws InvalidKeyException {
@@ -54,11 +55,11 @@ public class KeyPair {
public PublicKey getPublicKey() {
return publicKey;
}
public AsymmetricCipherKeyPair getKeyPair() {
return new AsymmetricCipherKeyPair(publicKey.getKey(), privateKey);
public ECPrivateKey getPrivateKey() {
return privateKey;
}
public byte[] toBytes() {
return serialize();
}
@@ -67,18 +68,14 @@ public class KeyPair {
this.publicKey = new PublicKey(bytes);
byte[] privateKeyBytes = new byte[bytes.length - PublicKey.KEY_SIZE];
System.arraycopy(bytes, PublicKey.KEY_SIZE, privateKeyBytes, 0, privateKeyBytes.length);
this.privateKey = masterCipher.decryptKey(privateKeyBytes);
this.privateKey = masterCipher.decryptKey(this.publicKey.getType(), privateKeyBytes);
}
public byte[] serialize() {
byte[] publicKeyBytes = publicKey.serialize();
Log.w("KeyPair", "Serialized public key bytes: " + Hex.toString(publicKeyBytes));
byte[] privateKeyBytes = masterCipher.encryptKey(privateKey);
byte[] combined = new byte[publicKeyBytes.length + privateKeyBytes.length];
System.arraycopy(publicKeyBytes, 0, combined, 0, publicKeyBytes.length);
System.arraycopy(privateKeyBytes, 0, combined, publicKeyBytes.length, privateKeyBytes.length);
return combined;
byte[] privateKeyBytes = masterCipher.encryptKey(privateKey);
return Util.combine(publicKeyBytes, privateKeyBytes);
}
}

View File

@@ -1,5 +1,6 @@
/**
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,10 +15,16 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
package org.whispersystems.textsecure.crypto;
import android.util.Log;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECPrivateKey;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.Hex;
import java.io.IOException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
@@ -32,13 +39,6 @@ import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.Hex;
import org.thoughtcrime.securesms.util.InvalidMessageException;
import android.util.Log;
/**
* Class that handles encryption for local storage.
*
@@ -70,13 +70,11 @@ public class MasterCipher {
throw new AssertionError(e);
}
}
public byte[] encryptKey(ECPrivateKeyParameters params) {
BigInteger d = params.getD();
byte[] dBytes = d.toByteArray();
return encryptBytes(dBytes);
public byte[] encryptKey(ECPrivateKey privateKey) {
return encryptBytes(privateKey.serialize());
}
public String encryptBody(String body) {
return encryptAndEncodeBytes(body.getBytes());
}
@@ -85,13 +83,13 @@ public class MasterCipher {
return new String(decodeAndDecryptBytes(body));
}
public ECPrivateKeyParameters decryptKey(byte[] key) {
public ECPrivateKey decryptKey(int type, byte[] key)
throws org.whispersystems.textsecure.crypto.InvalidKeyException
{
try {
BigInteger d = new BigInteger(decryptBytes(key));
return new ECPrivateKeyParameters(d, KeyUtil.domainParameters);
return Curve.decodePrivatePoint(type, decryptBytes(key));
} catch (InvalidMessageException ime) {
Log.w("bodycipher", ime);
return null; // XXX
throw new org.whispersystems.textsecure.crypto.InvalidKeyException(ime);
}
}

View File

@@ -14,14 +14,13 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
package org.whispersystems.textsecure.crypto;
import android.os.Parcel;
import android.os.Parcelable;
import org.bouncycastle.util.Arrays;
import javax.crypto.spec.SecretKeySpec;
import java.util.Arrays;
/**
* When a user first initializes TextSecure, a few secrets
@@ -72,7 +71,7 @@ public class MasterSecret implements Parcelable {
this.macKey = new SecretKeySpec(macKeyBytes, "HmacSHA1");
// SecretKeySpec does an internal copy in its constructor.
Arrays.fill(encryptionKeyBytes, (byte)0x00);
Arrays.fill(encryptionKeyBytes, (byte) 0x00);
Arrays.fill(macKeyBytes, (byte)0x00);
}

View File

@@ -0,0 +1,183 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.crypto;
import android.content.Context;
import android.util.Log;
import com.google.thoughtcrimegson.Gson;
import org.whispersystems.textsecure.crypto.ecc.Curve25519;
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
import org.whispersystems.textsecure.storage.InvalidKeyIdException;
import org.whispersystems.textsecure.storage.PreKeyRecord;
import org.whispersystems.textsecure.util.Medium;
import org.whispersystems.textsecure.util.Util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
public class PreKeyUtil {
public static final int BATCH_SIZE = 100;
public static List<PreKeyRecord> generatePreKeys(Context context, MasterSecret masterSecret) {
List<PreKeyRecord> records = new LinkedList<PreKeyRecord>();
int preKeyIdOffset = getNextPreKeyId(context);
for (int i=0;i<BATCH_SIZE;i++) {
int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
ECKeyPair keyPair = Curve25519.generateKeyPair();
PreKeyRecord record = new PreKeyRecord(context, masterSecret, preKeyId, keyPair);
record.save();
records.add(record);
}
setNextPreKeyId(context, (preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE);
return records;
}
public static PreKeyRecord generateLastResortKey(Context context, MasterSecret masterSecret) {
if (PreKeyRecord.hasRecord(context, Medium.MAX_VALUE)) {
try {
return new PreKeyRecord(context, masterSecret, Medium.MAX_VALUE);
} catch (InvalidKeyIdException e) {
Log.w("PreKeyUtil", e);
PreKeyRecord.delete(context, Medium.MAX_VALUE);
}
}
ECKeyPair keyPair = Curve25519.generateKeyPair();
PreKeyRecord record = new PreKeyRecord(context, masterSecret, Medium.MAX_VALUE, keyPair);
record.save();
return record;
}
public static List<PreKeyRecord> getPreKeys(Context context, MasterSecret masterSecret) {
List<PreKeyRecord> records = new LinkedList<PreKeyRecord>();
File directory = getPreKeysDirectory(context);
String[] keyRecordIds = directory.list();
Arrays.sort(keyRecordIds, new PreKeyRecordIdComparator());
for (String keyRecordId : keyRecordIds) {
try {
if (!keyRecordId.equals(PreKeyIndex.FILE_NAME) && Integer.parseInt(keyRecordId) != Medium.MAX_VALUE) {
records.add(new PreKeyRecord(context, masterSecret, Integer.parseInt(keyRecordId)));
}
} catch (InvalidKeyIdException e) {
Log.w("PreKeyUtil", e);
new File(getPreKeysDirectory(context), keyRecordId).delete();
} catch (NumberFormatException nfe) {
Log.w("PreKeyUtil", nfe);
new File(getPreKeysDirectory(context), keyRecordId).delete();
}
}
return records;
}
public static void clearPreKeys(Context context) {
File directory = getPreKeysDirectory(context);
String[] keyRecords = directory.list();
for (String keyRecord : keyRecords) {
new File(directory, keyRecord).delete();
}
}
private static void setNextPreKeyId(Context context, int id) {
try {
File nextFile = new File(getPreKeysDirectory(context), PreKeyIndex.FILE_NAME);
FileOutputStream fout = new FileOutputStream(nextFile);
fout.write(new Gson().toJson(new PreKeyIndex(id)).getBytes());
fout.close();
} catch (IOException e) {
Log.w("PreKeyUtil", e);
}
}
private static int getNextPreKeyId(Context context) {
try {
File nextFile = new File(getPreKeysDirectory(context), PreKeyIndex.FILE_NAME);
if (nextFile.exists()) {
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
} else {
InputStreamReader reader = new InputStreamReader(new FileInputStream(nextFile));
PreKeyIndex index = new Gson().fromJson(reader, PreKeyIndex.class);
reader.close();
return index.nextPreKeyId;
}
} catch (IOException e) {
Log.w("PreKeyUtil", e);
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
}
}
private static File getPreKeysDirectory(Context context) {
File directory = new File(context.getFilesDir(), PreKeyRecord.PREKEY_DIRECTORY);
if (!directory.exists())
directory.mkdirs();
return directory;
}
private static class PreKeyRecordIdComparator implements Comparator<String> {
@Override
public int compare(String lhs, String rhs) {
if (lhs.equals(PreKeyIndex.FILE_NAME)) return -1;
else if (rhs.equals(PreKeyIndex.FILE_NAME)) return 1;
try {
long lhsLong = Long.parseLong(lhs);
long rhsLong = Long.parseLong(rhs);
if (lhsLong < rhsLong) return -1;
else if (lhsLong > rhsLong) return 1;
else return 0;
} catch (NumberFormatException e) {
return 0;
}
}
}
private static class PreKeyIndex {
public static final String FILE_NAME = "index.dat";
private int nextPreKeyId;
public PreKeyIndex() {}
public PreKeyIndex(int nextPreKeyId) {
this.nextPreKeyId = nextPreKeyId;
}
}
}

View File

@@ -1,5 +1,6 @@
/**
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,23 +15,24 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
package org.whispersystems.textsecure.crypto;
import android.util.Log;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.util.Conversions;
import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.math.ec.ECPoint;
import org.thoughtcrime.securesms.util.Conversions;
import org.thoughtcrime.securesms.util.Hex;
import android.util.Log;
public class PublicKey {
public static final int POINT_SIZE = 33;
public static final int KEY_SIZE = 3 + POINT_SIZE;
private ECPublicKeyParameters publicKey;
public static final int KEY_SIZE = 3 + ECPublicKey.KEY_SIZE;
private final ECPublicKey publicKey;
private int id;
public PublicKey(PublicKey publicKey) {
@@ -40,35 +42,28 @@ public class PublicKey {
this.publicKey = publicKey.publicKey;
}
public PublicKey(int id, ECPublicKeyParameters publicKey) {
public PublicKey(int id, ECPublicKey publicKey) {
this.publicKey = publicKey;
this.id = id;
}
public PublicKey(byte[] bytes, int offset) throws InvalidKeyException {
Log.w("PublicKey", "PublicKey Length: " + (bytes.length - offset));
if ((bytes.length - offset) < KEY_SIZE)
throw new InvalidKeyException("Provided bytes are too short.");
this.id = Conversions.byteArrayToMedium(bytes, offset);
byte[] pointBytes = new byte[POINT_SIZE];
System.arraycopy(bytes, offset+3, pointBytes, 0, pointBytes.length);
ECPoint Q;
try {
Q = KeyUtil.decodePoint(pointBytes);
} catch (RuntimeException re) {
throw new InvalidKeyException(re);
}
this.publicKey = new ECPublicKeyParameters(Q, KeyUtil.domainParameters);
this.id = Conversions.byteArrayToMedium(bytes, offset);
this.publicKey = Curve.decodePoint(bytes, offset + 3);
}
public PublicKey(byte[] bytes) throws InvalidKeyException {
this(bytes, 0);
}
public int getType() {
return publicKey.getType();
}
public void setId(int id) {
this.id = id;
@@ -78,7 +73,7 @@ public class PublicKey {
return id;
}
public ECPublicKeyParameters getKey() {
public ECPublicKey getKey() {
return publicKey;
}
@@ -97,14 +92,11 @@ public class PublicKey {
}
public byte[] serialize() {
byte[] complete = new byte[KEY_SIZE];
byte[] serializedPoint = KeyUtil.encodePoint(publicKey.getQ());
byte[] keyIdBytes = Conversions.mediumToByteArray(id);
byte[] serializedPoint = publicKey.serialize();
Log.w("PublicKey", "Serializing public key point: " + Hex.toString(serializedPoint));
Conversions.mediumToByteArray(complete, 0, id);
System.arraycopy(serializedPoint, 0, complete, 3, serializedPoint.length);
return complete;
}
return Util.combine(keyIdBytes, serializedPoint);
}
}

View File

@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
package org.whispersystems.textsecure.crypto;
public interface SerializableKey {
public byte[] serialize();

View File

@@ -0,0 +1,48 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.crypto;
import android.content.Context;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.storage.SessionRecordV1;
import org.whispersystems.textsecure.storage.SessionRecordV2;
public abstract class SessionCipher {
protected static final Object SESSION_LOCK = new Object();
public abstract CiphertextMessage encrypt(byte[] paddedMessage);
public abstract byte[] decrypt(byte[] decodedMessage) throws InvalidMessageException;
public abstract int getRemoteRegistrationId();
public static SessionCipher createFor(Context context,
MasterSecret masterSecret,
RecipientDevice recipient)
{
if (SessionRecordV2.hasSession(context, masterSecret, recipient)) {
return new SessionCipherV2(context, masterSecret, recipient);
} else if (SessionRecordV1.hasSession(context, recipient.getRecipientId())) {
return new SessionCipherV1(context, masterSecret, recipient.getRecipient());
} else {
throw new AssertionError("Attempt to initialize cipher for non-existing session.");
}
}
}

View File

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

View File

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

View File

@@ -14,15 +14,15 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
package org.whispersystems.textsecure.crypto;
import java.io.IOException;
public interface TransportDetails {
public byte[] stripPaddedMessage(byte[] messageWithPadding);
public byte[] getStrippedPaddingMessageBody(byte[] messageWithPadding);
public byte[] getPaddedMessageBody(byte[] messageBody);
public byte[] encodeMessage(byte[] messageWithMac);
public byte[] decodeMessage(byte[] encodedMessageBytes) throws IOException;
public byte[] getEncodedMessage(byte[] messageWithMac);
public byte[] getDecodedMessage(byte[] encodedMessageBytes) throws IOException;
}

View File

@@ -0,0 +1,85 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.crypto.ecc;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
public class Curve {
public static final int NIST_TYPE = 0x02;
private static final int NIST_TYPE2 = 0x03;
public static final int DJB_TYPE = 0x05;
public static ECKeyPair generateKeyPairForType(int keyType) {
if (keyType == DJB_TYPE) {
return Curve25519.generateKeyPair();
} else if (keyType == NIST_TYPE || keyType == NIST_TYPE2) {
return CurveP256.generateKeyPair();
} else {
throw new AssertionError("Bad key type: " + keyType);
}
}
public static ECKeyPair generateKeyPairForSession(int messageVersion) {
if (messageVersion <= CiphertextMessage.LEGACY_VERSION) {
return generateKeyPairForType(NIST_TYPE);
} else {
return generateKeyPairForType(DJB_TYPE);
}
}
public static ECPublicKey decodePoint(byte[] bytes, int offset)
throws InvalidKeyException
{
int type = bytes[offset];
if (type == DJB_TYPE) {
return Curve25519.decodePoint(bytes, offset);
} else if (type == NIST_TYPE || type == NIST_TYPE2) {
return CurveP256.decodePoint(bytes, offset);
} else {
throw new InvalidKeyException("Unknown key type: " + type);
}
}
public static ECPrivateKey decodePrivatePoint(int type, byte[] bytes) {
if (type == DJB_TYPE) {
return new DjbECPrivateKey(bytes);
} else if (type == NIST_TYPE || type == NIST_TYPE2) {
return CurveP256.decodePrivatePoint(bytes);
} else {
throw new AssertionError("Bad key type: " + type);
}
}
public static byte[] calculateAgreement(ECPublicKey publicKey, ECPrivateKey privateKey)
throws InvalidKeyException
{
if (publicKey.getType() != privateKey.getType()) {
throw new InvalidKeyException("Public and private keys must be of the same type!");
}
if (publicKey.getType() == DJB_TYPE) {
return Curve25519.calculateAgreement(publicKey, privateKey);
} else if (publicKey.getType() == NIST_TYPE || publicKey.getType() == NIST_TYPE2) {
return CurveP256.calculateAgreement(publicKey, privateKey);
} else {
throw new InvalidKeyException("Unknown type: " + publicKey.getType());
}
}
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.crypto.ecc;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public class Curve25519 {
static {
System.loadLibrary("curve25519");
try {
random = SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private static final SecureRandom random;
private static native byte[] calculateAgreement(byte[] ourPrivate, byte[] theirPublic);
private static native byte[] generatePublicKey(byte[] privateKey);
private static native byte[] generatePrivateKey(byte[] random);
public static ECKeyPair generateKeyPair() {
byte[] privateKey = generatePrivateKey();
byte[] publicKey = generatePublicKey(privateKey);
return new ECKeyPair(new DjbECPublicKey(publicKey), new DjbECPrivateKey(privateKey));
}
static byte[] calculateAgreement(ECPublicKey publicKey, ECPrivateKey privateKey) {
return calculateAgreement(((DjbECPrivateKey)privateKey).getPrivateKey(),
((DjbECPublicKey)publicKey).getPublicKey());
}
static ECPublicKey decodePoint(byte[] encoded, int offset)
throws InvalidKeyException
{
int type = encoded[offset] & 0xFF;
byte[] keyBytes = new byte[32];
System.arraycopy(encoded, offset+1, keyBytes, 0, keyBytes.length);
if (type != Curve.DJB_TYPE) {
throw new InvalidKeyException("Bad key type: " + type);
}
return new DjbECPublicKey(keyBytes);
}
private static byte[] generatePrivateKey() {
byte[] privateKey = new byte[32];
random.nextBytes(privateKey);
return generatePrivateKey(privateKey);
}
}

View File

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

View File

@@ -1,6 +1,6 @@
/**
* Copyright (C) 2011 Whisper Systems
*
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
@@ -10,31 +10,32 @@
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
public class InvalidMessageException extends Exception {
package org.whispersystems.textsecure.crypto.ecc;
public InvalidMessageException() {
// TODO Auto-generated constructor stub
public class DjbECPrivateKey implements ECPrivateKey {
private final byte[] privateKey;
DjbECPrivateKey(byte[] privateKey) {
this.privateKey = privateKey;
}
public InvalidMessageException(String detailMessage) {
super(detailMessage);
// TODO Auto-generated constructor stub
@Override
public byte[] serialize() {
return privateKey;
}
public InvalidMessageException(Throwable throwable) {
super(throwable);
// TODO Auto-generated constructor stub
@Override
public int getType() {
return Curve.DJB_TYPE;
}
public InvalidMessageException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
// TODO Auto-generated constructor stub
public byte[] getPrivateKey() {
return privateKey;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,108 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.crypto.kdf;
import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class HKDF {
private static final int HASH_OUTPUT_SIZE = 32;
private static final int KEY_MATERIAL_SIZE = 64;
private static final int CIPHER_KEYS_OFFSET = 0;
private static final int MAC_KEYS_OFFSET = 32;
public DerivedSecrets deriveSecrets(byte[] inputKeyMaterial, byte[] info) {
byte[] salt = new byte[HASH_OUTPUT_SIZE];
return deriveSecrets(inputKeyMaterial, salt, info);
}
public DerivedSecrets deriveSecrets(byte[] inputKeyMaterial, byte[] salt, byte[] info) {
byte[] prk = extract(salt, inputKeyMaterial);
byte[] okm = expand(prk, info, KEY_MATERIAL_SIZE);
SecretKeySpec cipherKey = deriveCipherKey(okm);
SecretKeySpec macKey = deriveMacKey(okm);
return new DerivedSecrets(cipherKey, macKey);
}
private SecretKeySpec deriveCipherKey(byte[] okm) {
byte[] cipherKey = new byte[32];
System.arraycopy(okm, CIPHER_KEYS_OFFSET, cipherKey, 0, cipherKey.length);
return new SecretKeySpec(cipherKey, "AES");
}
private SecretKeySpec deriveMacKey(byte[] okm) {
byte[] macKey = new byte[32];
System.arraycopy(okm, MAC_KEYS_OFFSET, macKey, 0, macKey.length);
return new SecretKeySpec(macKey, "HmacSHA256");
}
private byte[] extract(byte[] salt, byte[] inputKeyMaterial) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(salt, "HmacSHA256"));
return mac.doFinal(inputKeyMaterial);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
private byte[] expand(byte[] prk, byte[] info, int outputSize) {
try {
int iterations = (int)Math.ceil((double)outputSize/(double)HASH_OUTPUT_SIZE);
byte[] mixin = new byte[0];
ByteArrayOutputStream results = new ByteArrayOutputStream();
for (int i=0;i<iterations;i++) {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(prk, "HmacSHA256"));
mac.update(mixin);
if (info != null) {
mac.update(info);
}
mac.update((byte)i);
byte[] stepResult = mac.doFinal();
results.write(stepResult, 0, stepResult.length);
mixin = stepResult;
}
return results.toByteArray();
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -0,0 +1,86 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.crypto.kdf;
import android.util.Log;
import org.whispersystems.textsecure.util.Conversions;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.spec.SecretKeySpec;
public class NKDF {
public static final int LEGACY_CIPHER_KEY_LENGTH = 16;
public static final int LEGACY_MAC_KEY_LENGTH = 20;
public DerivedSecrets deriveSecrets(byte[] sharedSecret, boolean isLowEnd)
{
SecretKeySpec cipherKey = deriveCipherSecret(isLowEnd, sharedSecret);
SecretKeySpec macKey = deriveMacSecret(cipherKey);
return new DerivedSecrets(cipherKey, macKey);
}
private SecretKeySpec deriveCipherSecret(boolean isLowEnd, byte[] sharedSecret) {
byte[] derivedBytes = deriveBytes(sharedSecret, LEGACY_CIPHER_KEY_LENGTH * 2);
byte[] cipherSecret = new byte[LEGACY_CIPHER_KEY_LENGTH];
if (isLowEnd) {
System.arraycopy(derivedBytes, LEGACY_CIPHER_KEY_LENGTH, cipherSecret, 0, LEGACY_CIPHER_KEY_LENGTH);
} else {
System.arraycopy(derivedBytes, 0, cipherSecret, 0, LEGACY_CIPHER_KEY_LENGTH);
}
return new SecretKeySpec(cipherSecret, "AES");
}
private SecretKeySpec deriveMacSecret(SecretKeySpec key) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] secret = md.digest(key.getEncoded());
return new SecretKeySpec(secret, "HmacSHA1");
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("SHA-1 Not Supported!",e);
}
}
private byte[] deriveBytes(byte[] seed, int bytesNeeded) {
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
Log.w("NKDF", e);
throw new IllegalArgumentException("SHA-256 Not Supported!");
}
int rounds = bytesNeeded / md.getDigestLength();
for (int i=1;i<=rounds;i++) {
byte[] roundBytes = Conversions.intToByteArray(i);
md.update(roundBytes);
md.update(seed);
}
return md.digest();
}
}

View File

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

View File

@@ -0,0 +1,112 @@
package org.whispersystems.textsecure.crypto.protocol;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.util.Conversions;
import org.whispersystems.textsecure.util.Util;
public class PreKeyWhisperMessage implements CiphertextMessage {
private final int version;
private final int registrationId;
private final int preKeyId;
private final ECPublicKey baseKey;
private final IdentityKey identityKey;
private final WhisperMessageV2 message;
private final byte[] serialized;
public PreKeyWhisperMessage(byte[] serialized)
throws InvalidMessageException, InvalidVersionException
{
try {
this.version = Conversions.lowBitsToInt(serialized[0]);
if (this.version > CiphertextMessage.CURRENT_VERSION) {
throw new InvalidVersionException("Unknown version: " + this.version);
}
WhisperProtos.PreKeyWhisperMessage preKeyWhisperMessage
= WhisperProtos.PreKeyWhisperMessage.parseFrom(ByteString.copyFrom(serialized, 1,
serialized.length-1));
if (!preKeyWhisperMessage.hasPreKeyId() ||
!preKeyWhisperMessage.hasBaseKey() ||
!preKeyWhisperMessage.hasIdentityKey() ||
!preKeyWhisperMessage.hasMessage())
{
throw new InvalidMessageException("Incomplete message.");
}
this.serialized = serialized;
this.registrationId = preKeyWhisperMessage.getRegistrationId();
this.preKeyId = preKeyWhisperMessage.getPreKeyId();
this.baseKey = Curve.decodePoint(preKeyWhisperMessage.getBaseKey().toByteArray(), 0);
this.identityKey = new IdentityKey(Curve.decodePoint(preKeyWhisperMessage.getIdentityKey().toByteArray(), 0));
this.message = new WhisperMessageV2(preKeyWhisperMessage.getMessage().toByteArray());
} catch (InvalidProtocolBufferException e) {
throw new InvalidMessageException(e);
} catch (InvalidKeyException e) {
throw new InvalidMessageException(e);
}
}
public PreKeyWhisperMessage(int registrationId, int preKeyId, ECPublicKey baseKey,
IdentityKey identityKey, WhisperMessageV2 message)
{
this.version = CiphertextMessage.CURRENT_VERSION;
this.registrationId = registrationId;
this.preKeyId = preKeyId;
this.baseKey = baseKey;
this.identityKey = identityKey;
this.message = message;
byte[] versionBytes = {Conversions.intsToByteHighAndLow(CURRENT_VERSION, this.version)};
byte[] messageBytes = WhisperProtos.PreKeyWhisperMessage.newBuilder()
.setPreKeyId(preKeyId)
.setBaseKey(ByteString.copyFrom(baseKey.serialize()))
.setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
.setMessage(ByteString.copyFrom(message.serialize()))
.setRegistrationId(registrationId)
.build().toByteArray();
this.serialized = Util.combine(versionBytes, messageBytes);
}
public IdentityKey getIdentityKey() {
return identityKey;
}
public int getRegistrationId() {
return registrationId;
}
public int getPreKeyId() {
return preKeyId;
}
public ECPublicKey getBaseKey() {
return baseKey;
}
public WhisperMessageV2 getWhisperMessage() {
return message;
}
@Override
public byte[] serialize() {
return serialized;
}
@Override
public int getType() {
return CiphertextMessage.PREKEY_WHISPER_TYPE;
}
}

View File

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

View File

@@ -0,0 +1,135 @@
package org.whispersystems.textsecure.crypto.protocol;
import android.util.Log;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.InvalidMessageException;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.protocol.WhisperProtos.WhisperMessage;
import org.whispersystems.textsecure.util.Conversions;
import org.whispersystems.textsecure.util.Hex;
import org.whispersystems.textsecure.util.Util;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.Arrays;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class WhisperMessageV2 implements CiphertextMessage {
private static final int MAC_LENGTH = 8;
private final ECPublicKey senderEphemeral;
private final int counter;
private final int previousCounter;
private final byte[] ciphertext;
private final byte[] serialized;
public WhisperMessageV2(byte[] serialized) throws InvalidMessageException {
try {
byte[][] messageParts = Util.split(serialized, 1, serialized.length - 1 - MAC_LENGTH, MAC_LENGTH);
byte version = messageParts[0][0];
byte[] message = messageParts[1];
byte[] mac = messageParts[2];
if (Conversions.highBitsToInt(version) != CURRENT_VERSION) {
throw new InvalidMessageException("Unknown version: " + Conversions.highBitsToInt(version));
}
WhisperMessage whisperMessage = WhisperMessage.parseFrom(message);
if (!whisperMessage.hasCiphertext() ||
!whisperMessage.hasCounter() ||
!whisperMessage.hasEphemeralKey())
{
throw new InvalidMessageException("Incomplete message.");
}
this.serialized = serialized;
this.senderEphemeral = Curve.decodePoint(whisperMessage.getEphemeralKey().toByteArray(), 0);
this.counter = whisperMessage.getCounter();
this.previousCounter = whisperMessage.getPreviousCounter();
this.ciphertext = whisperMessage.getCiphertext().toByteArray();
} catch (InvalidProtocolBufferException e) {
throw new InvalidMessageException(e);
} catch (InvalidKeyException e) {
throw new InvalidMessageException(e);
} catch (ParseException e) {
throw new InvalidMessageException(e);
}
}
public WhisperMessageV2(SecretKeySpec macKey, ECPublicKey senderEphemeral,
int counter, int previousCounter, byte[] ciphertext)
{
byte[] version = {Conversions.intsToByteHighAndLow(CURRENT_VERSION, CURRENT_VERSION)};
byte[] message = WhisperMessage.newBuilder()
.setEphemeralKey(ByteString.copyFrom(senderEphemeral.serialize()))
.setCounter(counter)
.setPreviousCounter(previousCounter)
.setCiphertext(ByteString.copyFrom(ciphertext))
.build().toByteArray();
byte[] mac = getMac(macKey, Util.combine(version, message));
this.serialized = Util.combine(version, message, mac);
this.senderEphemeral = senderEphemeral;
this.counter = counter;
this.previousCounter = previousCounter;
this.ciphertext = ciphertext;
}
public ECPublicKey getSenderEphemeral() {
return senderEphemeral;
}
public int getCounter() {
return counter;
}
public byte[] getBody() {
return ciphertext;
}
public void verifyMac(SecretKeySpec macKey)
throws InvalidMessageException
{
byte[][] parts = Util.split(serialized, serialized.length - MAC_LENGTH, MAC_LENGTH);
byte[] ourMac = getMac(macKey, parts[0]);
byte[] theirMac = parts[1];
if (!Arrays.equals(ourMac, theirMac)) {
throw new InvalidMessageException("Bad Mac!");
}
}
private byte[] getMac(SecretKeySpec macKey, byte[] serialized) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(macKey);
byte[] fullMac = mac.doFinal(serialized);
return Util.trim(fullMac, MAC_LENGTH);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (java.security.InvalidKeyException e) {
throw new AssertionError(e);
}
}
@Override
public byte[] serialize() {
return serialized;
}
@Override
public int getType() {
return CiphertextMessage.CURRENT_WHISPER_TYPE;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
package org.whispersystems.textsecure.crypto.ratchet;
import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets;
import org.whispersystems.textsecure.crypto.kdf.HKDF;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class ChainKey {
private static final byte[] MESSAGE_KEY_SEED = {0x01};
private static final byte[] CHAIN_KEY_SEED = {0x02};
private final byte[] key;
private final int index;
public ChainKey(byte[] key, int index) {
this.key = key;
this.index = index;
}
public byte[] getKey() {
return key;
}
public int getIndex() {
return index;
}
public ChainKey getNextChainKey() {
byte[] nextKey = getBaseMaterial(CHAIN_KEY_SEED);
return new ChainKey(nextKey, index + 1);
}
public MessageKeys getMessageKeys() {
HKDF kdf = new HKDF();
byte[] inputKeyMaterial = getBaseMaterial(MESSAGE_KEY_SEED);
DerivedSecrets keyMaterial = kdf.deriveSecrets(inputKeyMaterial, "WhisperMessageKeys".getBytes());
return new MessageKeys(keyMaterial.getCipherKey(), keyMaterial.getMacKey(), index);
}
private byte[] getBaseMaterial(byte[] seed) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(seed);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -0,0 +1,28 @@
package org.whispersystems.textsecure.crypto.ratchet;
import javax.crypto.spec.SecretKeySpec;
public class MessageKeys {
private final SecretKeySpec cipherKey;
private final SecretKeySpec macKey;
private final int counter;
public MessageKeys(SecretKeySpec cipherKey, SecretKeySpec macKey, int counter) {
this.cipherKey = cipherKey;
this.macKey = macKey;
this.counter = counter;
}
public SecretKeySpec getCipherKey() {
return cipherKey;
}
public SecretKeySpec getMacKey() {
return macKey;
}
public int getCounter() {
return counter;
}
}

View File

@@ -0,0 +1,121 @@
package org.whispersystems.textsecure.crypto.ratchet;
import android.util.Pair;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.IdentityKeyPair;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets;
import org.whispersystems.textsecure.crypto.kdf.HKDF;
import org.whispersystems.textsecure.storage.SessionRecordV2;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class RatchetingSession {
public static void initializeSession(SessionRecordV2 sessionRecord,
ECKeyPair ourBaseKey,
ECPublicKey theirBaseKey,
ECKeyPair ourEphemeralKey,
ECPublicKey theirEphemeralKey,
IdentityKeyPair ourIdentityKey,
IdentityKey theirIdentityKey)
throws InvalidKeyException
{
if (isAlice(ourBaseKey.getPublicKey(), theirBaseKey, ourEphemeralKey.getPublicKey(), theirEphemeralKey)) {
initializeSessionAsAlice(sessionRecord, ourBaseKey, theirBaseKey, theirEphemeralKey,
ourIdentityKey, theirIdentityKey);
} else {
initializeSessionAsBob(sessionRecord, ourBaseKey, theirBaseKey,
ourEphemeralKey, ourIdentityKey, theirIdentityKey);
}
}
private static void initializeSessionAsAlice(SessionRecordV2 sessionRecord,
ECKeyPair ourBaseKey, ECPublicKey theirBaseKey,
ECPublicKey theirEphemeralKey,
IdentityKeyPair ourIdentityKey,
IdentityKey theirIdentityKey)
throws InvalidKeyException
{
sessionRecord.setRemoteIdentityKey(theirIdentityKey);
sessionRecord.setLocalIdentityKey(ourIdentityKey.getPublicKey());
ECKeyPair sendingKey = Curve.generateKeyPairForType(ourIdentityKey.getPublicKey().getPublicKey().getType());
Pair<RootKey, ChainKey> receivingChain = calculate3DHE(true, ourBaseKey, theirBaseKey, ourIdentityKey, theirIdentityKey);
Pair<RootKey, ChainKey> sendingChain = receivingChain.first.createChain(theirEphemeralKey, sendingKey);
sessionRecord.addReceiverChain(theirEphemeralKey, receivingChain.second);
sessionRecord.setSenderChain(sendingKey, sendingChain.second);
sessionRecord.setRootKey(sendingChain.first);
}
private static void initializeSessionAsBob(SessionRecordV2 sessionRecord,
ECKeyPair ourBaseKey, ECPublicKey theirBaseKey,
ECKeyPair ourEphemeralKey,
IdentityKeyPair ourIdentityKey,
IdentityKey theirIdentityKey)
throws InvalidKeyException
{
sessionRecord.setRemoteIdentityKey(theirIdentityKey);
sessionRecord.setLocalIdentityKey(ourIdentityKey.getPublicKey());
Pair<RootKey, ChainKey> sendingChain = calculate3DHE(false, ourBaseKey, theirBaseKey,
ourIdentityKey, theirIdentityKey);
sessionRecord.setSenderChain(ourEphemeralKey, sendingChain.second);
sessionRecord.setRootKey(sendingChain.first);
}
private static Pair<RootKey, ChainKey> calculate3DHE(boolean isAlice,
ECKeyPair ourEphemeral, ECPublicKey theirEphemeral,
IdentityKeyPair ourIdentity, IdentityKey theirIdentity)
throws InvalidKeyException
{
try {
ByteArrayOutputStream secrets = new ByteArrayOutputStream();
if (isAlice) {
secrets.write(Curve.calculateAgreement(theirEphemeral, ourIdentity.getPrivateKey()));
secrets.write(Curve.calculateAgreement(theirIdentity.getPublicKey(), ourEphemeral.getPrivateKey()));
} else {
secrets.write(Curve.calculateAgreement(theirIdentity.getPublicKey(), ourEphemeral.getPrivateKey()));
secrets.write(Curve.calculateAgreement(theirEphemeral, ourIdentity.getPrivateKey()));
}
secrets.write(Curve.calculateAgreement(theirEphemeral, ourEphemeral.getPrivateKey()));
DerivedSecrets derivedSecrets = new HKDF().deriveSecrets(secrets.toByteArray(),
"WhisperText".getBytes());
return new Pair<RootKey, ChainKey>(new RootKey(derivedSecrets.getCipherKey().getEncoded()),
new ChainKey(derivedSecrets.getMacKey().getEncoded(), 0));
} catch (IOException e) {
throw new AssertionError(e);
}
}
private static boolean isAlice(ECPublicKey ourBaseKey, ECPublicKey theirBaseKey,
ECPublicKey ourEphemeralKey, ECPublicKey theirEphemeralKey)
{
if (ourEphemeralKey.equals(ourBaseKey)) {
return false;
}
if (theirEphemeralKey.equals(theirBaseKey)) {
return true;
}
return isLowEnd(ourBaseKey, theirBaseKey);
}
private static boolean isLowEnd(ECPublicKey ourKey, ECPublicKey theirKey) {
return ourKey.compareTo(theirKey) < 0;
}
}

View File

@@ -0,0 +1,38 @@
package org.whispersystems.textsecure.crypto.ratchet;
import android.util.Pair;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets;
import org.whispersystems.textsecure.crypto.kdf.HKDF;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class RootKey {
private final byte[] key;
public RootKey(byte[] key) {
this.key = key;
}
public byte[] getKeyBytes() {
return key;
}
public Pair<RootKey, ChainKey> createChain(ECPublicKey theirEphemeral, ECKeyPair ourEphemeral)
throws InvalidKeyException
{
HKDF kdf = new HKDF();
byte[] sharedSecret = Curve.calculateAgreement(theirEphemeral, ourEphemeral.getPrivateKey());
DerivedSecrets keys = kdf.deriveSecrets(sharedSecret, key, "WhisperRatchet".getBytes());
RootKey newRootKey = new RootKey(keys.getCipherKey().getEncoded());
ChainKey newChainKey = new ChainKey(keys.getMacKey().getEncoded(), 0);
return new Pair<RootKey, ChainKey>(newRootKey, newChainKey);
}
}

View File

@@ -15,9 +15,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.directory;
package org.whispersystems.textsecure.directory;
import org.thoughtcrime.securesms.util.Conversions;
import org.whispersystems.textsecure.util.Conversions;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

View File

@@ -0,0 +1,255 @@
package org.whispersystems.textsecure.directory;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.util.Log;
import org.whispersystems.textsecure.push.ContactTokenDetails;
import org.whispersystems.textsecure.util.InvalidNumberException;
import org.whispersystems.textsecure.util.PhoneNumberFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Directory {
private static final int INTRODUCED_CHANGE_FROM_TOKEN_TO_E164_NUMBER = 2;
private static final String DATABASE_NAME = "whisper_directory.db";
private static final int DATABASE_VERSION = 2;
private static final String TABLE_NAME = "directory";
private static final String ID = "_id";
private static final String NUMBER = "number";
private static final String REGISTERED = "registered";
private static final String RELAY = "relay";
private static final String SUPPORTS_SMS = "supports_sms";
private static final String TIMESTAMP = "timestamp";
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY, " +
NUMBER + " TEXT UNIQUE, " +
REGISTERED + " INTEGER, " +
RELAY + " TEXT, " +
SUPPORTS_SMS + " INTEGER, " +
TIMESTAMP + " INTEGER);";
private static final Object instanceLock = new Object();
private static volatile Directory instance;
public static Directory getInstance(Context context) {
if (instance == null) {
synchronized (instanceLock) {
if (instance == null) {
instance = new Directory(context);
}
}
}
return instance;
}
private final DatabaseHelper databaseHelper;
private final Context context;
private Directory(Context context) {
this.context = context;
this.databaseHelper = new DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
}
public boolean isSmsFallbackSupported(String e164number) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = db.query(TABLE_NAME, new String[] {SUPPORTS_SMS}, NUMBER + " = ?",
new String[]{e164number}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
return cursor.getInt(0) == 1;
} else {
return false;
}
} finally {
if (cursor != null)
cursor.close();
}
}
public boolean isActiveNumber(String e164number) throws NotInDirectoryException {
if (e164number == null || e164number.length() == 0) {
return false;
}
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = db.query(TABLE_NAME,
new String[]{REGISTERED}, NUMBER + " = ?",
new String[] {e164number}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
return cursor.getInt(0) == 1;
} else {
throw new NotInDirectoryException();
}
} finally {
if (cursor != null)
cursor.close();
}
}
public String getRelay(String e164number) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, null, NUMBER + " = ?", new String[]{e164number}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
return cursor.getString(cursor.getColumnIndexOrThrow(RELAY));
}
return null;
} finally {
if (cursor != null)
cursor.close();
}
}
public void setNumber(ContactTokenDetails token, boolean active) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(NUMBER, token.getNumber());
values.put(RELAY, token.getRelay());
values.put(REGISTERED, active ? 1 : 0);
values.put(SUPPORTS_SMS, token.isSupportsSms() ? 1 : 0);
values.put(TIMESTAMP, System.currentTimeMillis());
db.replace(TABLE_NAME, null, values);
}
public void setNumbers(List<ContactTokenDetails> activeTokens, Collection<String> inactiveTokens) {
long timestamp = System.currentTimeMillis();
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();
try {
for (ContactTokenDetails token : activeTokens) {
Log.w("Directory", "Adding active token: " + token);
ContentValues values = new ContentValues();
values.put(NUMBER, token.getNumber());
values.put(REGISTERED, 1);
values.put(TIMESTAMP, timestamp);
values.put(RELAY, token.getRelay());
values.put(SUPPORTS_SMS, token.isSupportsSms() ? 1 : 0);
db.replace(TABLE_NAME, null, values);
}
for (String token : inactiveTokens) {
ContentValues values = new ContentValues();
values.put(NUMBER, token);
values.put(REGISTERED, 0);
values.put(TIMESTAMP, timestamp);
db.replace(TABLE_NAME, null, values);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public Set<String> getPushEligibleContactNumbers(String localNumber) {
final Uri uri = Phone.CONTENT_URI;
final Set<String> results = new HashSet<String>();
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(uri, new String[] {Phone.NUMBER}, null, null, null);
while (cursor != null && cursor.moveToNext()) {
final String rawNumber = cursor.getString(0);
if (rawNumber != null) {
try {
final String e164Number = PhoneNumberFormatter.formatNumber(rawNumber, localNumber);
results.add(e164Number);
} catch (InvalidNumberException e) {
Log.w("Directory", "Invalid number: " + rawNumber);
}
}
}
if (cursor != null)
cursor.close();
final SQLiteDatabase readableDb = databaseHelper.getReadableDatabase();
if (readableDb != null) {
cursor = readableDb.query(TABLE_NAME, new String[]{NUMBER},
null, null, null, null, null);
while (cursor != null && cursor.moveToNext()) {
results.add(cursor.getString(0));
}
}
return results;
} finally {
if (cursor != null)
cursor.close();
}
}
public List<String> getActiveNumbers() {
final List<String> results = new ArrayList<String>();
Cursor cursor = null;
try {
cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[]{NUMBER},
REGISTERED + " = 1", null, null, null, null);
while (cursor != null && cursor.moveToNext()) {
results.add(cursor.getString(0));
}
return results;
} finally {
if (cursor != null)
cursor.close();
}
}
private class DatabaseHelper extends SQLiteOpenHelper {
public DatabaseHelper(Context context, String name,
SQLiteDatabase.CursorFactory factory,
int version)
{
super(context, name, factory, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion < INTRODUCED_CHANGE_FROM_TOKEN_TO_E164_NUMBER) {
db.execSQL("DROP TABLE directory;");
db.execSQL("CREATE TABLE directory ( _id INTEGER PRIMARY KEY, " +
"number TEXT UNIQUE, " +
"registered INTEGER, " +
"relay TEXT, " +
"supports_sms INTEGER, " +
"timestamp INTEGER);");
}
}
}
}

View File

@@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.directory;
package org.whispersystems.textsecure.directory;
public class DirectoryDescriptor {
private String version;

View File

@@ -0,0 +1,4 @@
package org.whispersystems.textsecure.directory;
public class NotInDirectoryException extends Throwable {
}

View File

@@ -15,7 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.directory;
package org.whispersystems.textsecure.directory;
import android.content.Context;
import android.util.Log;
@@ -23,7 +23,6 @@ import android.util.Log;
import com.google.thoughtcrimegson.Gson;
import com.google.thoughtcrimegson.JsonParseException;
import com.google.thoughtcrimegson.annotations.SerializedName;
import org.thoughtcrime.securesms.util.PhoneNumberFormatter;
import java.io.BufferedReader;
import java.io.File;
@@ -33,6 +32,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.List;
import java.util.zip.GZIPInputStream;
/**
* Handles providing lookups, serializing, and deserializing the RedPhone directory.
@@ -75,14 +76,57 @@ public class NumberFilter {
if (bloomFilter == null) return false;
else if (number == null || number.length() == 0) return false;
return new BloomFilter(bloomFilter, hashCount).contains(PhoneNumberFormatter.formatNumber(context, number));
return new BloomFilter(bloomFilter, hashCount).contains(number);
} catch (IOException ioe) {
Log.w("NumberFilter", ioe);
return false;
}
}
public synchronized void update(File bloomFilter, long capacity, int hashCount, String version)
public synchronized boolean containsNumbers(List<String> numbers) {
try {
if (bloomFilter == null) return false;
if (numbers == null || numbers.size() == 0) return false;
BloomFilter filter = new BloomFilter(bloomFilter, hashCount);
for (String number : numbers) {
if (!filter.contains(number)) {
return false;
}
}
return true;
} catch (IOException ioe) {
Log.w("NumberFilter", ioe);
return false;
}
}
public synchronized void update(DirectoryDescriptor descriptor, File compressedData) {
try {
File uncompressed = File.createTempFile("directory", ".dat", context.getFilesDir());
FileInputStream fin = new FileInputStream (compressedData);
GZIPInputStream gin = new GZIPInputStream(fin);
FileOutputStream out = new FileOutputStream(uncompressed);
byte[] buffer = new byte[4096];
int read;
while ((read = gin.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
out.close();
compressedData.delete();
update(uncompressed, descriptor.getCapacity(), descriptor.getHashCount(), descriptor.getVersion());
} catch (IOException ioe) {
Log.w("NumberFilter", ioe);
}
}
private synchronized void update(File bloomFilter, long capacity, int hashCount, String version)
{
if (this.bloomFilter != null)
this.bloomFilter.delete();

View File

@@ -0,0 +1,28 @@
package org.whispersystems.textsecure.push;
public class AccountAttributes {
private String signalingKey;
private boolean supportsSms;
private int registrationId;
public AccountAttributes(String signalingKey, boolean supportsSms, int registrationId) {
this.signalingKey = signalingKey;
this.supportsSms = supportsSms;
this.registrationId = registrationId;
}
public AccountAttributes() {}
public String getSignalingKey() {
return signalingKey;
}
public boolean isSupportsSms() {
return supportsSms;
}
public int getRegistrationId() {
return registrationId;
}
}

View File

@@ -0,0 +1,9 @@
package org.whispersystems.textsecure.push;
import java.io.IOException;
public class AuthorizationFailedException extends IOException {
public AuthorizationFailedException(String s) {
super(s);
}
}

View File

@@ -0,0 +1,33 @@
package org.whispersystems.textsecure.push;
import com.google.thoughtcrimegson.Gson;
public class ContactTokenDetails {
private String token;
private String relay;
private String number;
private boolean supportsSms;
public ContactTokenDetails() {}
public String getToken() {
return token;
}
public String getRelay() {
return relay;
}
public boolean isSupportsSms() {
return supportsSms;
}
public void setNumber(String number) {
this.number = number;
}
public String getNumber() {
return number;
}
}

View File

@@ -0,0 +1,14 @@
package org.whispersystems.textsecure.push;
import java.util.List;
public class ContactTokenDetailsList {
private List<ContactTokenDetails> contacts;
public ContactTokenDetailsList() {}
public List<ContactTokenDetails> getContacts() {
return contacts;
}
}

View File

@@ -0,0 +1,18 @@
package org.whispersystems.textsecure.push;
import java.util.List;
public class ContactTokenList {
private List<String> contacts;
public ContactTokenList(List<String> contacts) {
this.contacts = contacts;
}
public ContactTokenList() {}
public List<String> getContacts() {
return contacts;
}
}

View File

@@ -0,0 +1,6 @@
package org.whispersystems.textsecure.push;
import java.io.IOException;
public class ExpectationFailedException extends IOException {
}

View File

@@ -0,0 +1,136 @@
package org.whispersystems.textsecure.push;
import android.util.Log;
import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal;
import org.whispersystems.textsecure.util.Hex;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class IncomingEncryptedPushMessage {
private static final int SUPPORTED_VERSION = 1;
private static final int CIPHER_KEY_SIZE = 32;
private static final int MAC_KEY_SIZE = 20;
private static final int MAC_SIZE = 10;
private static final int VERSION_OFFSET = 0;
private static final int VERSION_LENGTH = 1;
private static final int IV_OFFSET = VERSION_OFFSET + VERSION_LENGTH;
private static final int IV_LENGTH = 16;
private static final int CIPHERTEXT_OFFSET = IV_OFFSET + IV_LENGTH;
private final IncomingPushMessage incomingPushMessage;
public IncomingEncryptedPushMessage(String message, String signalingKey)
throws IOException, InvalidVersionException
{
byte[] ciphertext = Base64.decode(message);
if (ciphertext.length < VERSION_LENGTH || ciphertext[VERSION_OFFSET] != SUPPORTED_VERSION)
throw new InvalidVersionException("Unsupported version!");
SecretKeySpec cipherKey = getCipherKey(signalingKey);
SecretKeySpec macKey = getMacKey(signalingKey);
verifyMac(ciphertext, macKey);
byte[] plaintext = getPlaintext(ciphertext, cipherKey);
IncomingPushMessageSignal signal = IncomingPushMessageSignal.parseFrom(plaintext);
this.incomingPushMessage = new IncomingPushMessage(signal);
}
public IncomingPushMessage getIncomingPushMessage() {
return incomingPushMessage;
}
private byte[] getPlaintext(byte[] ciphertext, SecretKeySpec cipherKey) throws IOException {
try {
byte[] ivBytes = new byte[IV_LENGTH];
System.arraycopy(ciphertext, IV_OFFSET, ivBytes, 0, ivBytes.length);
IvParameterSpec iv = new IvParameterSpec(ivBytes);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, cipherKey, iv);
return cipher.doFinal(ciphertext, CIPHERTEXT_OFFSET,
ciphertext.length - VERSION_LENGTH - IV_LENGTH - MAC_SIZE);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (NoSuchPaddingException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
} catch (InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
} catch (IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
Log.w("IncomingEncryptedPushMessage", e);
throw new IOException("Bad padding?");
}
}
private void verifyMac(byte[] ciphertext, SecretKeySpec macKey) throws IOException {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(macKey);
if (ciphertext.length < MAC_SIZE + 1)
throw new IOException("Invalid MAC!");
mac.update(ciphertext, 0, ciphertext.length - MAC_SIZE);
byte[] ourMacFull = mac.doFinal();
byte[] ourMacBytes = new byte[MAC_SIZE];
System.arraycopy(ourMacFull, 0, ourMacBytes, 0, ourMacBytes.length);
byte[] theirMacBytes = new byte[MAC_SIZE];
System.arraycopy(ciphertext, ciphertext.length-MAC_SIZE, theirMacBytes, 0, theirMacBytes.length);
Log.w("IncomingEncryptedPushMessage", "Our MAC: " + Hex.toString(ourMacBytes));
Log.w("IncomingEncryptedPushMessage", "Thr MAC: " + Hex.toString(theirMacBytes));
if (!Arrays.equals(ourMacBytes, theirMacBytes)) {
throw new IOException("Invalid MAC compare!");
}
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
private SecretKeySpec getCipherKey(String signalingKey) throws IOException {
byte[] signalingKeyBytes = Base64.decode(signalingKey);
byte[] cipherKey = new byte[CIPHER_KEY_SIZE];
System.arraycopy(signalingKeyBytes, 0, cipherKey, 0, cipherKey.length);
return new SecretKeySpec(cipherKey, "AES");
}
private SecretKeySpec getMacKey(String signalingKey) throws IOException {
byte[] signalingKeyBytes = Base64.decode(signalingKey);
byte[] macKey = new byte[MAC_KEY_SIZE];
System.arraycopy(signalingKeyBytes, CIPHER_KEY_SIZE, macKey, 0, macKey.length);
return new SecretKeySpec(macKey, "HmacSHA256");
}
}

View File

@@ -0,0 +1,141 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.push;
import android.os.Parcel;
import android.os.Parcelable;
import org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal;
public class IncomingPushMessage implements Parcelable {
public static final Parcelable.Creator<IncomingPushMessage> CREATOR = new Parcelable.Creator<IncomingPushMessage>() {
@Override
public IncomingPushMessage createFromParcel(Parcel in) {
return new IncomingPushMessage(in);
}
@Override
public IncomingPushMessage[] newArray(int size) {
return new IncomingPushMessage[size];
}
};
private int type;
private String source;
private int sourceDevice;
private byte[] message;
private long timestamp;
private String relay;
private IncomingPushMessage(IncomingPushMessage message, byte[] body) {
this.type = message.type;
this.source = message.source;
this.sourceDevice = message.sourceDevice;
this.timestamp = message.timestamp;
this.relay = message.relay;
this.message = body;
}
public IncomingPushMessage(IncomingPushMessageSignal signal) {
this.type = signal.getType().getNumber();
this.source = signal.getSource();
this.sourceDevice = signal.getSourceDevice();
this.message = signal.getMessage().toByteArray();
this.timestamp = signal.getTimestamp();
this.relay = signal.getRelay();
}
public IncomingPushMessage(Parcel in) {
this.type = in.readInt();
this.source = in.readString();
this.sourceDevice = in.readInt();
if (in.readInt() == 1) {
this.relay = in.readString();
}
this.message = new byte[in.readInt()];
in.readByteArray(this.message);
this.timestamp = in.readLong();
}
public IncomingPushMessage(int type, String source, int sourceDevice,
byte[] body, long timestamp)
{
this.type = type;
this.source = source;
this.sourceDevice = sourceDevice;
this.message = body;
this.timestamp = timestamp;
}
public String getRelay() {
return relay;
}
public long getTimestampMillis() {
return timestamp;
}
public String getSource() {
return source;
}
public int getSourceDevice() {
return sourceDevice;
}
public byte[] getBody() {
return message;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(type);
dest.writeString(source);
dest.writeInt(sourceDevice);
dest.writeInt(relay == null ? 0 : 1);
if (relay != null) {
dest.writeString(relay);
}
dest.writeInt(message.length);
dest.writeByteArray(message);
dest.writeLong(timestamp);
}
public IncomingPushMessage withBody(byte[] body) {
return new IncomingPushMessage(this, body);
}
public int getType() {
return type;
}
public boolean isSecureMessage() {
return getType() == IncomingPushMessageSignal.Type.CIPHERTEXT_VALUE;
}
public boolean isPreKeyBundle() {
return getType() == IncomingPushMessageSignal.Type.PREKEY_BUNDLE_VALUE;
}
}

View File

@@ -0,0 +1,17 @@
package org.whispersystems.textsecure.push;
import java.util.List;
public class MismatchedDevices {
private List<Integer> missingDevices;
private List<Integer> extraDevices;
public List<Integer> getMissingDevices() {
return missingDevices;
}
public List<Integer> getExtraDevices() {
return extraDevices;
}
}

View File

@@ -0,0 +1,16 @@
package org.whispersystems.textsecure.push;
import java.io.IOException;
public class MismatchedDevicesException extends IOException {
private final MismatchedDevices mismatchedDevices;
public MismatchedDevicesException(MismatchedDevices mismatchedDevices) {
this.mismatchedDevices = mismatchedDevices;
}
public MismatchedDevices getMismatchedDevices() {
return mismatchedDevices;
}
}

View File

@@ -0,0 +1,9 @@
package org.whispersystems.textsecure.push;
import java.io.IOException;
public class NotFoundException extends IOException {
public NotFoundException(String s) {
super(s);
}
}

View File

@@ -0,0 +1,50 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.push;
import org.whispersystems.textsecure.util.Base64;
public class OutgoingPushMessage {
private int type;
private int destinationDeviceId;
private int destinationRegistrationId;
private String body;
public OutgoingPushMessage(PushAddress address, PushBody body) {
this.type = body.getType();
this.destinationDeviceId = address.getDeviceId();
this.destinationRegistrationId = body.getRemoteRegistrationId();
this.body = Base64.encodeBytes(body.getBody());
}
public int getDestinationDeviceId() {
return destinationDeviceId;
}
public String getBody() {
return body;
}
public int getType() {
return type;
}
public int getDestinationRegistrationId() {
return destinationRegistrationId;
}
}

View File

@@ -0,0 +1,31 @@
package org.whispersystems.textsecure.push;
import java.util.LinkedList;
import java.util.List;
public class OutgoingPushMessageList {
private String destination;
private String relay;
private List<OutgoingPushMessage> messages;
public OutgoingPushMessageList(String destination, String relay, List<OutgoingPushMessage> messages) {
this.destination = destination;
this.relay = relay;
this.messages = messages;
}
public String getDestination() {
return destination;
}
public List<OutgoingPushMessage> getMessages() {
return messages;
}
public String getRelay() {
return relay;
}
}

View File

@@ -0,0 +1,126 @@
package org.whispersystems.textsecure.push;
import com.google.thoughtcrimegson.GsonBuilder;
import com.google.thoughtcrimegson.JsonDeserializationContext;
import com.google.thoughtcrimegson.JsonDeserializer;
import com.google.thoughtcrimegson.JsonElement;
import com.google.thoughtcrimegson.JsonParseException;
import com.google.thoughtcrimegson.JsonPrimitive;
import com.google.thoughtcrimegson.JsonSerializationContext;
import com.google.thoughtcrimegson.JsonSerializer;
import com.google.thoughtcrimegson.annotations.Expose;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.ecc.Curve;
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
import org.whispersystems.textsecure.util.Base64;
import java.io.IOException;
import java.lang.reflect.Type;
public class PreKeyEntity {
@Expose(serialize = false)
private int deviceId;
private int keyId;
private ECPublicKey publicKey;
private IdentityKey identityKey;
private int registrationId;
public PreKeyEntity(int keyId, ECPublicKey publicKey, IdentityKey identityKey) {
this.keyId = keyId;
this.publicKey = publicKey;
this.identityKey = identityKey;
this.registrationId = registrationId;
}
public int getDeviceId() {
return deviceId;
}
public int getKeyId() {
return keyId;
}
public ECPublicKey getPublicKey() {
return publicKey;
}
public IdentityKey getIdentityKey() {
return identityKey;
}
public int getRegistrationId() {
return registrationId;
}
public static String toJson(PreKeyEntity entity) {
return getBuilder().create().toJson(entity);
}
public static PreKeyEntity fromJson(String encoded) {
return getBuilder().create().fromJson(encoded, PreKeyEntity.class);
}
public static GsonBuilder getBuilder() {
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(ECPublicKey.class, new ECPublicKeyJsonAdapter());
builder.registerTypeAdapter(IdentityKey.class, new IdentityKeyJsonAdapter());
return builder;
}
private static class ECPublicKeyJsonAdapter
implements JsonSerializer<ECPublicKey>, JsonDeserializer<ECPublicKey>
{
@Override
public JsonElement serialize(ECPublicKey preKeyPublic, Type type,
JsonSerializationContext jsonSerializationContext)
{
return new JsonPrimitive(Base64.encodeBytesWithoutPadding(preKeyPublic.serialize()));
}
@Override
public ECPublicKey deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext jsonDeserializationContext)
throws JsonParseException
{
try {
return Curve.decodePoint(Base64.decodeWithoutPadding(jsonElement.getAsJsonPrimitive().getAsString()), 0);
} catch (InvalidKeyException e) {
throw new JsonParseException(e);
} catch (IOException e) {
throw new JsonParseException(e);
}
}
}
private static class IdentityKeyJsonAdapter
implements JsonSerializer<IdentityKey>, JsonDeserializer<IdentityKey>
{
@Override
public JsonElement serialize(IdentityKey identityKey, Type type,
JsonSerializationContext jsonSerializationContext)
{
return new JsonPrimitive(Base64.encodeBytesWithoutPadding(identityKey.serialize()));
}
@Override
public IdentityKey deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext jsonDeserializationContext)
throws JsonParseException
{
try {
return new IdentityKey(Base64.decodeWithoutPadding(jsonElement.getAsJsonPrimitive().getAsString()), 0);
} catch (InvalidKeyException e) {
throw new JsonParseException(e);
} catch (IOException e) {
throw new JsonParseException(e);
}
}
}
}

View File

@@ -0,0 +1,30 @@
package org.whispersystems.textsecure.push;
import java.util.List;
public class PreKeyList {
private PreKeyEntity lastResortKey;
private List<PreKeyEntity> keys;
public PreKeyList(PreKeyEntity lastResortKey, List<PreKeyEntity> keys) {
this.keys = keys;
this.lastResortKey = lastResortKey;
}
public List<PreKeyEntity> getKeys() {
return keys;
}
public static String toJson(PreKeyList entity) {
return PreKeyEntity.getBuilder().create().toJson(entity);
}
public static PreKeyList fromJson(String serialized) {
return PreKeyEntity.getBuilder().create().fromJson(serialized, PreKeyList.class);
}
public PreKeyEntity getLastResortKey() {
return lastResortKey;
}
}

View File

@@ -0,0 +1,32 @@
package org.whispersystems.textsecure.push;
import android.content.Context;
import org.whispersystems.textsecure.directory.Directory;
import org.whispersystems.textsecure.storage.RecipientDevice;
public class PushAddress extends RecipientDevice {
private final String e164number;
private final String relay;
private PushAddress(long recipientId, String e164number, int deviceId, String relay) {
super(recipientId, deviceId);
this.e164number = e164number;
this.relay = relay;
}
public String getNumber() {
return e164number;
}
public String getRelay() {
return relay;
}
public static PushAddress create(Context context, long recipientId, String e164number, int deviceId) {
String relay = Directory.getInstance(context).getRelay(e164number);
return new PushAddress(recipientId, e164number, deviceId, relay);
}
}

View File

@@ -0,0 +1,21 @@
package org.whispersystems.textsecure.push;
public class PushAttachmentData {
private final String contentType;
private final byte[] data;
public PushAttachmentData(String contentType, byte[] data) {
this.contentType = contentType;
this.data = data;
}
public String getContentType() {
return contentType;
}
public byte[] getData() {
return data;
}
}

View File

@@ -0,0 +1,63 @@
package org.whispersystems.textsecure.push;
import android.os.Parcel;
import android.os.Parcelable;
public class PushAttachmentPointer implements Parcelable {
public static final Parcelable.Creator<PushAttachmentPointer> CREATOR = new Parcelable.Creator<PushAttachmentPointer>() {
@Override
public PushAttachmentPointer createFromParcel(Parcel in) {
return new PushAttachmentPointer(in);
}
@Override
public PushAttachmentPointer[] newArray(int size) {
return new PushAttachmentPointer[size];
}
};
private final String contentType;
private final long id;
private final byte[] key;
public PushAttachmentPointer(String contentType, long id, byte[] key) {
this.contentType = contentType;
this.id = id;
this.key = key;
}
public PushAttachmentPointer(Parcel in) {
this.contentType = in.readString();
this.id = in.readLong();
int keyLength = in.readInt();
this.key = new byte[keyLength];
in.readByteArray(this.key);
}
public String getContentType() {
return contentType;
}
public long getId() {
return id;
}
public byte[] getKey() {
return key;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(contentType);
dest.writeLong(id);
dest.writeInt(this.key.length);
dest.writeByteArray(this.key);
}
}

View File

@@ -0,0 +1,26 @@
package org.whispersystems.textsecure.push;
public class PushBody {
private final int type;
private final int remoteRegistrationId;
private final byte[] body;
public PushBody(int type, int remoteRegistrationId, byte[] body) {
this.type = type;
this.remoteRegistrationId = remoteRegistrationId;
this.body = body;
}
public int getType() {
return type;
}
public byte[] getBody() {
return body;
}
public int getRemoteRegistrationId() {
return remoteRegistrationId;
}
}

File diff suppressed because it is too large Load Diff

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